diff --git a/.github/workflows/e2e_archgw.yml b/.github/workflows/e2e_archgw.yml index a59954c9..97e2492c 100644 --- a/.github/workflows/e2e_archgw.yml +++ b/.github/workflows/e2e_archgw.yml @@ -30,7 +30,7 @@ jobs: - name: build arch docker image run: | - cd ../../ && docker build -f arch/Dockerfile . -t katanemo/archgw -t katanemo/archgw:0.3.18 -t katanemo/archgw:latest + cd ../../ && docker build -f arch/Dockerfile . -t katanemo/archgw -t katanemo/archgw:0.3.22 -t katanemo/archgw:latest - name: start archgw env: diff --git a/.github/workflows/e2e_model_server.yml b/.github/workflows/e2e_model_server.yml deleted file mode 100644 index 2c4a3c73..00000000 --- a/.github/workflows/e2e_model_server.yml +++ /dev/null @@ -1,46 +0,0 @@ -name: e2e model server tests - -on: - push: - branches: - - main - pull_request: - -jobs: - e2e_model_server_tests: - runs-on: ubuntu-latest-m - strategy: - fail-fast: false - matrix: - python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] - defaults: - run: - working-directory: ./tests/modelserver - - steps: - - name: Checkout code - uses: actions/checkout@v3 - - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python-version }} - cache: "pip" # auto-caches based on requirements files - - - name: install poetry - run: | - export POETRY_VERSION=2.2.1 - curl -sSL https://install.python-poetry.org | python3 - - export PATH="$HOME/.local/bin:$PATH" - - - name: install model server and start it - run: | - cd ../../model_server/ && poetry install && poetry run archgw_modelserver start - - - name: install test dependencies - run: | - poetry install - - - name: run tests - run: | - poetry run pytest diff --git a/.github/workflows/e2e_test_currency_convert.yml b/.github/workflows/e2e_test_currency_convert.yml index 8b15cb7e..05be3d84 100644 --- a/.github/workflows/e2e_test_currency_convert.yml +++ b/.github/workflows/e2e_test_currency_convert.yml @@ -24,7 +24,7 @@ jobs: - name: build arch docker image run: | - docker build -f arch/Dockerfile . -t katanemo/archgw -t katanemo/archgw:0.3.18 + docker build -f arch/Dockerfile . -t katanemo/archgw -t katanemo/archgw:0.3.22 - name: install poetry run: | @@ -40,11 +40,10 @@ jobs: curl --location --remote-name https://github.com/Orange-OpenSource/hurl/releases/download/4.0.0/hurl_4.0.0_amd64.deb sudo dpkg -i hurl_4.0.0_amd64.deb - - name: install model server, arch gateway and test dependencies + - name: install arch gateway and test dependencies run: | source venv/bin/activate - cd model_server/ && echo "installing model server" && poetry install - cd ../arch/tools && echo "installing archgw cli" && poetry install + cd arch/tools && echo "installing archgw cli" && poetry install cd ../../demos/shared/test_runner && echo "installing test dependencies" && poetry install - name: run demo tests diff --git a/.github/workflows/e2e_test_preference_based_routing.yml b/.github/workflows/e2e_test_preference_based_routing.yml index 9e0bcb13..3f921f60 100644 --- a/.github/workflows/e2e_test_preference_based_routing.yml +++ b/.github/workflows/e2e_test_preference_based_routing.yml @@ -24,7 +24,7 @@ jobs: - name: build arch docker image run: | - docker build -f arch/Dockerfile . -t katanemo/archgw -t katanemo/archgw:0.3.18 + docker build -f arch/Dockerfile . -t katanemo/archgw -t katanemo/archgw:0.3.22 - name: install poetry run: | @@ -40,11 +40,10 @@ jobs: curl --location --remote-name https://github.com/Orange-OpenSource/hurl/releases/download/4.0.0/hurl_4.0.0_amd64.deb sudo dpkg -i hurl_4.0.0_amd64.deb - - name: install model server, arch gateway and test dependencies + - name: install arch gateway and test dependencies run: | source venv/bin/activate - cd model_server/ && echo "installing model server" && poetry install - cd ../arch/tools && echo "installing archgw cli" && poetry install + cd arch/tools && echo "installing archgw cli" && poetry install cd ../../demos/shared/test_runner && echo "installing test dependencies" && poetry install - name: run demo tests diff --git a/.github/workflows/e2e_tests.yml b/.github/workflows/e2e_tests.yml index 5750ca74..be601264 100644 --- a/.github/workflows/e2e_tests.yml +++ b/.github/workflows/e2e_tests.yml @@ -14,6 +14,32 @@ jobs: - name: Checkout code uses: actions/checkout@v3 + # --- Disk inspection & cleanup section (added to free space on GitHub runner) --- + - name: Check disk usage before cleanup + run: | + echo "=== Disk usage before cleanup ===" + df -h + echo "=== Repo size ===" + du -sh . + + - name: Free disk space on runner + run: | + echo "=== Cleaning preinstalled SDKs and toolchains to free space ===" + sudo rm -rf /usr/share/dotnet + sudo rm -rf /usr/local/lib/android + sudo rm -rf /opt/ghc + # If you still hit disk issues, uncomment this to free more space. + # It just removes cached tool versions; setup-python will re-download what it needs. + # sudo rm -rf /opt/hostedtoolcache || true + + echo "=== Docker cleanup (before our builds/compose) ===" + docker system prune -af || true + docker volume prune -f || true + + echo "=== Disk usage after cleanup ===" + df -h + # --- End disk cleanup section --- + - name: Set up Python uses: actions/setup-python@v4 with: @@ -33,6 +59,7 @@ jobs: ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} AZURE_API_KEY: ${{ secrets.AZURE_API_KEY }} AWS_BEARER_TOKEN_BEDROCK: ${{ secrets.AWS_BEARER_TOKEN_BEDROCK }} + GROK_API_KEY : ${{ secrets.GROK_API_KEY }} run: | python -mvenv venv source venv/bin/activate && cd tests/e2e && bash run_e2e_tests.sh diff --git a/.github/workflows/model-server-tests.yml b/.github/workflows/model-server-tests.yml deleted file mode 100644 index 976bea4c..00000000 --- a/.github/workflows/model-server-tests.yml +++ /dev/null @@ -1,45 +0,0 @@ -name: model server tests - -on: - push: - branches: - - main # Run tests on pushes to the main branch - pull_request: - branches: - - main # Run tests on pull requests to the main branch - -jobs: - test: - runs-on: ubuntu-latest - - steps: - # Step 1: Check out the code from your repository - - name: Checkout code - uses: actions/checkout@v3 - - # Step 2: Set up Python (specify the version) - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: "3.12" - - # Step 3: Install Poetry - - name: Install Poetry - run: | - export POETRY_VERSION=2.2.1 - curl -sSL https://install.python-poetry.org | python3 - - export PATH="$HOME/.local/bin:$PATH" - - # Step 4: Install dependencies using Poetry - - name: Install dependencies - run: | - cd model_server - poetry install - - # Step 5: Set PYTHONPATH and run tests - - name: Run model server tests with pytest - env: - PYTHONPATH: model_server # Ensure the app's path is available - run: | - cd model_server - poetry run pytest diff --git a/.github/workflows/rust_tests.yml b/.github/workflows/rust_tests.yml index 9837531d..e75a6b60 100644 --- a/.github/workflows/rust_tests.yml +++ b/.github/workflows/rust_tests.yml @@ -29,3 +29,6 @@ jobs: - name: Run unit tests run: cargo test --lib + + - name: Run trace integration tests + run: cargo test -p common --features trace-collection traces::tests::trace_integration_test diff --git a/.github/workflows/validate_arch_config.yml b/.github/workflows/validate_arch_config.yml index 08bf9045..19b95980 100644 --- a/.github/workflows/validate_arch_config.yml +++ b/.github/workflows/validate_arch_config.yml @@ -24,7 +24,7 @@ jobs: - name: build arch docker image run: | - docker build -f arch/Dockerfile . -t katanemo/archgw -t katanemo/archgw:0.3.18 + docker build -f arch/Dockerfile . -t katanemo/archgw -t katanemo/archgw:0.3.22 - name: validate arch config run: | diff --git a/.gitignore b/.gitignore index d3557d74..08abcc90 100644 --- a/.gitignore +++ b/.gitignore @@ -111,11 +111,6 @@ venv.bak/ arch/tools/config arch/tools/build -# Archgw - model_server -model_server/venv_model_server -model_server/build -model_server/dist - # Archgw - Docs docs/build/ diff --git a/README.md b/README.md index 6f3217fa..ed2c118b 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ _Arch is a models-native (edge and service) proxy server for agents._

# About The Latest Release: -[0.3.18] [Preference-aware multi LLM routing for Claude Code 2.0](demos/use_cases/claude_code_router/README.md)
high-level network architecture for ArchGW +[0.3.20] [Preference-aware multi LLM routing for Claude Code 2.0](demos/use_cases/claude_code_router/README.md)
high-level network architecture for ArchGW # Overview @@ -87,7 +87,7 @@ Arch's CLI allows you to manage and interact with the Arch gateway efficiently. ```console $ python3.12 -m venv venv $ source venv/bin/activate # On Windows, use: venv\Scripts\activate -$ pip install archgw==0.3.18 +$ pip install archgw==0.3.22 ``` ### Use Arch as a LLM Router @@ -276,7 +276,7 @@ endpoints: ```sh $ archgw up arch_config.yaml -2024-12-05 16:56:27,979 - cli.main - INFO - Starting archgw cli version: 0.3.18 +2024-12-05 16:56:27,979 - cli.main - INFO - Starting archgw cli version: 0.3.22 2024-12-05 16:56:28,485 - cli.utils - INFO - Schema validation successful! 2024-12-05 16:56:28,485 - cli.main - INFO - Starting arch model server and arch gateway 2024-12-05 16:56:51,647 - cli.core - INFO - Container is healthy! diff --git a/arch/arch_config_schema.yaml b/arch/arch_config_schema.yaml index f481b389..61c24edb 100644 --- a/arch/arch_config_schema.yaml +++ b/arch/arch_config_schema.yaml @@ -14,6 +14,38 @@ properties: type: array items: type: object + properties: + id: + type: string + url: + type: string + additionalProperties: false + required: + - id + - url + filters: + type: array + items: + type: object + properties: + id: + type: string + url: + type: string + type: + type: string + enum: + - mcp + transport: + type: string + enum: + - streamable-http + tool: + type: string + additionalProperties: false + required: + - id + - url listeners: oneOf: - type: array @@ -331,6 +363,31 @@ properties: model: type: string additionalProperties: false + state_storage: + type: object + properties: + type: + type: string + enum: + - memory + - postgres + connection_string: + type: string + description: Required when type is postgres. Supports environment variable substitution using $VAR or ${VAR} syntax. + additionalProperties: false + required: + - type + # Note: connection_string is conditionally required based on type + # If type is 'postgres', connection_string must be provided + # If type is 'memory', connection_string is not needed + allOf: + - if: + properties: + type: + const: postgres + then: + required: + - connection_string prompt_guards: type: object properties: diff --git a/arch/docker-compose.dev.yaml b/arch/docker-compose.dev.yaml index 0b52d057..118b5ac3 100644 --- a/arch/docker-compose.dev.yaml +++ b/arch/docker-compose.dev.yaml @@ -22,4 +22,3 @@ services: - OPENAI_API_KEY=${OPENAI_API_KEY:?error} - MISTRAL_API_KEY=${MISTRAL_API_KEY:?error} - OTEL_TRACING_HTTP_ENDPOINT=http://host.docker.internal:4318/v1/traces - - MODEL_SERVER_PORT=${MODEL_SERVER_PORT:-51000} diff --git a/arch/envoy.template.yaml b/arch/envoy.template.yaml index ae9d0fbc..4ead29c2 100644 --- a/arch/envoy.template.yaml +++ b/arch/envoy.template.yaml @@ -51,11 +51,11 @@ static_resources: envoy_grpc: cluster_name: opentelemetry_collector timeout: 0.250s - service_name: archgw(inbound) + service_name: plano(inbound) random_sampling: value: {{ arch_tracing.random_sampling }} {% endif %} - stat_prefix: ingress_traffic + stat_prefix: plano(inbound) codec_type: AUTO scheme_header_transformation: scheme_to_overwrite: https @@ -95,21 +95,6 @@ static_resources: - name: envoy.filters.network.http_connection_manager typed_config: "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager - {% if "random_sampling" in arch_tracing and arch_tracing["random_sampling"] > 0 %} - generate_request_id: true - tracing: - provider: - name: envoy.tracers.opentelemetry - typed_config: - "@type": type.googleapis.com/envoy.config.trace.v3.OpenTelemetryConfig - grpc_service: - envoy_grpc: - cluster_name: opentelemetry_collector - timeout: 0.250s - service_name: ingress_traffic - random_sampling: - value: {{ arch_tracing.random_sampling }} - {% endif %} stat_prefix: ingress_traffic codec_type: AUTO scheme_header_transformation: @@ -221,7 +206,7 @@ static_resources: - name: outbound_api_traffic address: socket_address: - address: 0.0.0.0 + address: 127.0.0.1 port_value: 11000 traffic_direction: OUTBOUND filter_chains: @@ -229,21 +214,21 @@ static_resources: - name: envoy.filters.network.http_connection_manager typed_config: "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager - {% if "random_sampling" in arch_tracing and arch_tracing["random_sampling"] > 0 %} - generate_request_id: true - tracing: - provider: - name: envoy.tracers.opentelemetry - typed_config: - "@type": type.googleapis.com/envoy.config.trace.v3.OpenTelemetryConfig - grpc_service: - envoy_grpc: - cluster_name: opentelemetry_collector - timeout: 0.250s - service_name: outbound_api_traffic - random_sampling: - value: {{ arch_tracing.random_sampling }} - {% endif %} + # {% if "random_sampling" in arch_tracing and arch_tracing["random_sampling"] > 0 %} + # generate_request_id: true + # tracing: + # provider: + # name: envoy.tracers.opentelemetry + # typed_config: + # "@type": type.googleapis.com/envoy.config.trace.v3.OpenTelemetryConfig + # grpc_service: + # envoy_grpc: + # cluster_name: opentelemetry_collector + # timeout: 0.250s + # service_name: tools + # random_sampling: + # value: {{ arch_tracing.random_sampling }} + # {% endif %} stat_prefix: outbound_api_traffic codec_type: AUTO scheme_header_transformation: @@ -262,19 +247,16 @@ static_resources: domains: - "*" routes: - {% for internal_cluster in ["arch_fc", "model_server"] %} - match: prefix: "/" headers: - name: "x-arch-upstream" string_match: - exact: {{ internal_cluster }} + exact: bright_staff route: auto_host_rewrite: true - cluster: {{ internal_cluster }} + cluster: bright_staff timeout: 300s - {% endfor %} - {% for cluster_name, cluster in arch_clusters.items() %} - match: prefix: "/" @@ -317,7 +299,7 @@ static_resources: envoy_grpc: cluster_name: opentelemetry_collector timeout: 0.250s - service_name: arch_gateway + service_name: plano(inbound) random_sampling: value: {{ arch_tracing.random_sampling }} {% endif %} @@ -416,7 +398,7 @@ static_resources: envoy_grpc: cluster_name: opentelemetry_collector timeout: 0.250s - service_name: archgw(outbound) + service_name: plano(outbound) random_sampling: value: {{ arch_tracing.random_sampling }} {% endif %} @@ -487,6 +469,50 @@ static_resources: typed_config: "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router +{% if "random_sampling" in arch_tracing and arch_tracing["random_sampling"] > 0 %} + - name: otel_collector_proxy + address: + socket_address: + address: 127.0.0.1 + port_value: 9903 + traffic_direction: OUTBOUND + filter_chains: + - filters: + - name: envoy.filters.network.http_connection_manager + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + stat_prefix: otel_proxy + codec_type: AUTO + access_log: + - name: envoy.access_loggers.file + typed_config: + "@type": type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog + path: "/var/log/access_otel.log" + format: | + [%START_TIME%] "%REQ(:METHOD)% %REQ(X-ENVOY-ORIGINAL-PATH?:PATH)% %PROTOCOL%" %RESPONSE_CODE% %RESPONSE_FLAGS% %BYTES_RECEIVED% %BYTES_SENT% %DURATION% %RESP(X-ENVOY-UPSTREAM-SERVICE-TIME)% "%REQ(X-FORWARDED-FOR)%" "%REQ(USER-AGENT)%" "%REQ(X-REQUEST-ID)%" "%REQ(:AUTHORITY)%" "%UPSTREAM_HOST%" "%UPSTREAM_CLUSTER%" + route_config: + name: otel_route + virtual_hosts: + - name: otel_backend + domains: ["*"] + routes: + - match: + prefix: "/v1/traces" + route: + cluster: opentelemetry_collector_http + timeout: 5s + retry_policy: + retry_on: "5xx,connect-failure,refused-stream,reset" + num_retries: 3 + per_try_timeout: 2s + host_selection_retry_max_attempts: 5 + retriable_status_codes: [500, 502, 503, 504] + http_filters: + - name: envoy.filters.http.router + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router +{% endif %} + - name: egress_traffic_llm address: socket_address: @@ -599,7 +625,7 @@ static_resources: clusters: - name: arch - connect_timeout: 0.5s + connect_timeout: 5s type: LOGICAL_DNS dns_lookup_family: V4_ONLY lb_policy: ROUND_ROBIN @@ -868,24 +894,6 @@ static_resources: tls_params: tls_minimum_protocol_version: TLSv1_2 tls_maximum_protocol_version: TLSv1_3 - - {% for internal_cluster in ["arch_fc", "model_server"] %} - - name: {{ internal_cluster }} - connect_timeout: 0.5s - type: STRICT_DNS - dns_lookup_family: V4_ONLY - lb_policy: ROUND_ROBIN - load_assignment: - cluster_name: {{ internal_cluster }} - endpoints: - - lb_endpoints: - - endpoint: - address: - socket_address: - address: host.docker.internal - port_value: 51000 - hostname: {{ internal_cluster }} - {% endfor %} - name: mistral_7b_instruct connect_timeout: 0.5s type: STRICT_DNS @@ -1035,7 +1043,6 @@ static_resources: port_value: 12001 hostname: arch_listener_llm - {% if "random_sampling" in arch_tracing and arch_tracing["random_sampling"] > 0 %} - name: opentelemetry_collector type: STRICT_DNS @@ -1069,4 +1076,19 @@ static_resources: socket_address: address: host.docker.internal port_value: 4318 + # Circuit breaker configuration to prevent overwhelming OTEL collector + circuit_breakers: + thresholds: + - priority: DEFAULT + max_connections: 100 + max_pending_requests: 100 + max_requests: 100 + max_retries: 3 + # Health checking and outlier detection + outlier_detection: + consecutive_5xx: 5 + interval: 10s + base_ejection_time: 30s + max_ejection_percent: 50 + enforcing_consecutive_5xx: 100 {% endif %} diff --git a/arch/supervisord.conf b/arch/supervisord.conf index 2b715a1e..9761f779 100644 --- a/arch/supervisord.conf +++ b/arch/supervisord.conf @@ -2,7 +2,7 @@ nodaemon=true [program:brightstaff] -command=sh -c "RUST_LOG=info /app/brightstaff 2>&1 | tee /var/log/brightstaff.log | while IFS= read -r line; do echo '[brightstaff]' \"$line\"; done" +command=sh -c "envsubst < /app/arch_config_rendered.yaml > /app/arch_config_rendered.env_sub.yaml && RUST_LOG=debug ARCH_CONFIG_PATH_RENDERED=/app/arch_config_rendered.env_sub.yaml /app/brightstaff 2>&1 | tee /var/log/brightstaff.log | while IFS= read -r line; do echo '[brightstaff]' \"$line\"; done" stdout_logfile=/dev/stdout redirect_stderr=true stdout_logfile_maxbytes=0 diff --git a/arch/tools/README.md b/arch/tools/README.md index 5efd5e40..c86934a8 100644 --- a/arch/tools/README.md +++ b/arch/tools/README.md @@ -19,7 +19,7 @@ source venv/bin/activate ### Step 3: Run the build script ```bash -pip install archgw==0.3.18 +pip install archgw==0.3.22 ``` ## Uninstall Instructions: archgw CLI @@ -56,15 +56,8 @@ poetry install archgw build ``` -### Step 5: download models -This will help download models so model_server can load faster. This should be done once. - -```bash -archgw download-models -``` - ### Logs -`archgw` command can also view logs from gateway and model_server. Use following command to view logs, +`archgw` command can also view logs from the gateway. Use following command to view logs, ```bash archgw logs --follow diff --git a/arch/tools/cli/config_generator.py b/arch/tools/cli/config_generator.py index ead0a351..3acdb932 100644 --- a/arch/tools/cli/config_generator.py +++ b/arch/tools/cli/config_generator.py @@ -13,10 +13,10 @@ SUPPORTED_PROVIDERS_WITH_BASE_URL = [ "ollama", "qwen", "amazon_bedrock", + "arch", ] SUPPORTED_PROVIDERS_WITHOUT_BASE_URL = [ - "arch", "deepseek", "groq", "mistral", @@ -101,8 +101,17 @@ def validate_and_render_schema(): # Process agents section and convert to endpoints agents = config_yaml.get("agents", []) - for agent in agents: + filters = config_yaml.get("filters", []) + agents_combined = agents + filters + agent_id_keys = set() + + for agent in agents_combined: agent_id = agent.get("id") + if agent_id in agent_id_keys: + raise Exception( + f"Duplicate agent id {agent_id}, please provide unique id for each agent" + ) + agent_id_keys.add(agent_id) agent_endpoint = agent.get("url") if agent_id and agent_endpoint: @@ -304,6 +313,16 @@ def validate_and_render_schema(): } ) + # Always add arch-function model provider if not already defined + if "arch-function" not in model_provider_name_set: + updated_model_providers.append( + { + "name": "arch-function", + "provider_interface": "arch", + "model": "Arch-Function", + } + ) + config_yaml["model_providers"] = deepcopy(updated_model_providers) listeners_with_provider = 0 diff --git a/arch/tools/cli/consts.py b/arch/tools/cli/consts.py index ffff31aa..b768e037 100644 --- a/arch/tools/cli/consts.py +++ b/arch/tools/cli/consts.py @@ -1,13 +1,5 @@ import os - -KATANEMO_DOCKERHUB_REPO = "katanemo/archgw" -KATANEMO_LOCAL_MODEL_LIST = [ - "katanemo/Arch-Guard", -] SERVICE_NAME_ARCHGW = "archgw" -SERVICE_NAME_MODEL_SERVER = "model_server" -SERVICE_ALL = "all" -MODEL_SERVER_LOG_FILE = "~/archgw_logs/modelserver.log" ARCHGW_DOCKER_NAME = "archgw" -ARCHGW_DOCKER_IMAGE = os.getenv("ARCHGW_DOCKER_IMAGE", "katanemo/archgw:0.3.18") +ARCHGW_DOCKER_IMAGE = os.getenv("ARCHGW_DOCKER_IMAGE", "katanemo/archgw:0.3.22") diff --git a/arch/tools/cli/core.py b/arch/tools/cli/core.py index 94b5adf7..ec1d354a 100644 --- a/arch/tools/cli/core.py +++ b/arch/tools/cli/core.py @@ -9,9 +9,7 @@ from cli.utils import convert_legacy_listeners, getLogger from cli.consts import ( ARCHGW_DOCKER_IMAGE, ARCHGW_DOCKER_NAME, - KATANEMO_LOCAL_MODEL_LIST, ) -from huggingface_hub import snapshot_download import subprocess from cli.docker_cli import ( docker_container_status, @@ -144,49 +142,6 @@ def stop_docker_container(service=ARCHGW_DOCKER_NAME): log.info(f"Failed to shut down services: {str(e)}") -def download_models_from_hf(): - for model in KATANEMO_LOCAL_MODEL_LIST: - log.info(f"Downloading model: {model}") - snapshot_download(repo_id=model) - - -def start_arch_modelserver(foreground): - """ - Start the model server. This assumes that the archgw_modelserver package is installed locally - - """ - try: - log.info("archgw_modelserver restart") - if foreground: - subprocess.run( - ["archgw_modelserver", "start", "--foreground"], - check=True, - ) - else: - subprocess.run( - ["archgw_modelserver", "start"], - check=True, - ) - except subprocess.CalledProcessError as e: - log.info(f"Failed to start model_server. Please check archgw_modelserver logs") - sys.exit(1) - - -def stop_arch_modelserver(): - """ - Stop the model server. This assumes that the archgw_modelserver package is installed locally - - """ - try: - subprocess.run( - ["archgw_modelserver", "stop"], - check=True, - ) - except subprocess.CalledProcessError as e: - log.info(f"Failed to start model_server. Please check archgw_modelserver logs") - sys.exit(1) - - def start_cli_agent(arch_config_file=None, settings_json="{}"): """Start a CLI client connected to Arch.""" diff --git a/arch/tools/cli/main.py b/arch/tools/cli/main.py index de3ed20f..35bb34eb 100644 --- a/arch/tools/cli/main.py +++ b/arch/tools/cli/main.py @@ -20,20 +20,14 @@ from cli.utils import ( find_config_file, ) from cli.core import ( - start_arch_modelserver, - stop_arch_modelserver, start_arch, stop_docker_container, - download_models_from_hf, start_cli_agent, ) from cli.consts import ( ARCHGW_DOCKER_IMAGE, ARCHGW_DOCKER_NAME, - KATANEMO_DOCKERHUB_REPO, SERVICE_NAME_ARCHGW, - SERVICE_NAME_MODEL_SERVER, - SERVICE_ALL, ) log = getLogger(__name__) @@ -47,9 +41,8 @@ logo = r""" """ -# Command to build archgw and model_server Docker images +# Command to build archgw Docker images ARCHGW_DOCKERFILE = "./arch/Dockerfile" -MODEL_SERVER_BUILD_FILE = "./model_server/pyproject.toml" def get_version(): @@ -60,18 +53,6 @@ def get_version(): return "version not found" -def verify_service_name(service): - """Verify if the service name is valid.""" - if service not in [ - SERVICE_NAME_ARCHGW, - SERVICE_NAME_MODEL_SERVER, - SERVICE_ALL, - ]: - print(f"Error: Invalid service {service}. Exiting") - sys.exit(1) - return True - - @click.group(invoke_without_command=True) @click.option("--version", is_flag=True, help="Show the archgw cli version and exit.") @click.pass_context @@ -89,17 +70,11 @@ def main(ctx, version): @click.command() -@click.option( - "--service", - default=SERVICE_ALL, - help="Optional parameter to specify which service to build. Options are model_server, archgw", -) -def build(service): +def build(): """Build Arch from source. Must be in root of cloned repo.""" - verify_service_name(service) # Check if /arch/Dockerfile exists - if service == SERVICE_NAME_ARCHGW or service == SERVICE_ALL: + if os.path.exists(ARCHGW_DOCKERFILE): if os.path.exists(ARCHGW_DOCKERFILE): click.echo("Building archgw image...") try: @@ -110,8 +85,6 @@ def build(service): "-f", ARCHGW_DOCKERFILE, "-t", - f"{KATANEMO_DOCKERHUB_REPO}:latest", - "-t", f"{ARCHGW_DOCKER_IMAGE}", ".", "--add-host=host.docker.internal:host-gateway", @@ -128,57 +101,20 @@ def build(service): click.echo("archgw image built successfully.") - """Install the model server dependencies using Poetry.""" - if service == SERVICE_NAME_MODEL_SERVER or service == SERVICE_ALL: - # Check if pyproject.toml exists - if os.path.exists(MODEL_SERVER_BUILD_FILE): - click.echo("Installing model server dependencies with Poetry...") - try: - subprocess.run( - ["poetry", "install", "--no-cache"], - cwd=os.path.dirname(MODEL_SERVER_BUILD_FILE), - check=True, - ) - click.echo("Model server dependencies installed successfully.") - except subprocess.CalledProcessError as e: - click.echo(f"Error installing model server dependencies: {e}") - sys.exit(1) - else: - click.echo(f"Error: pyproject.toml not found in {MODEL_SERVER_BUILD_FILE}") - sys.exit(1) - @click.command() @click.argument("file", required=False) # Optional file argument @click.option( "--path", default=".", help="Path to the directory containing arch_config.yaml" ) -@click.option( - "--service", - default=SERVICE_ALL, - help="Service to start. Options are model_server, archgw.", -) @click.option( "--foreground", default=False, help="Run Arch in the foreground. Default is False", is_flag=True, ) -def up(file, path, service, foreground): +def up(file, path, foreground): """Starts Arch.""" - verify_service_name(service) - - if service == SERVICE_ALL and foreground: - # foreground can only be specified when starting individual services - log.info("foreground flag is only supported for individual services. Exiting.") - sys.exit(1) - - if service == SERVICE_NAME_MODEL_SERVER: - log.info("Download models from HuggingFace...") - download_models_from_hf() - start_arch_modelserver(foreground) - return - # Use the utility function to find config file arch_config_file = find_config_file(path, file) @@ -202,7 +138,6 @@ def up(file, path, service, foreground): # Set the ARCH_CONFIG_FILE environment variable env_stage = { "OTEL_TRACING_HTTP_ENDPOINT": "http://host.docker.internal:4318/v1/traces", - "MODEL_SERVER_PORT": os.getenv("MODEL_SERVER_PORT", "51000"), } env = os.environ.copy() # Remove PATH variable if present @@ -242,40 +177,13 @@ def up(file, path, service, foreground): env_stage[access_key] = env_file_dict[access_key] env.update(env_stage) - - if service == SERVICE_NAME_ARCHGW: - start_arch(arch_config_file, env, foreground=foreground) - else: - # Check if ingress_traffic listener is configured before starting model_server - if has_ingress_listener(arch_config_file): - download_models_from_hf() - start_arch_modelserver(foreground) - else: - log.info( - "Skipping model_server startup: no ingress_traffic listener configured in arch_config.yaml" - ) - - start_arch(arch_config_file, env, foreground=foreground) + start_arch(arch_config_file, env, foreground=foreground) @click.command() -@click.option( - "--service", - default=SERVICE_ALL, - help="Service to down. Options are all, model_server, archgw. Default is all", -) -def down(service): +def down(): """Stops Arch.""" - - verify_service_name(service) - - if service == SERVICE_NAME_MODEL_SERVER: - stop_arch_modelserver() - elif service == SERVICE_NAME_ARCHGW: - stop_docker_container() - else: - stop_arch_modelserver() - stop_docker_container(SERVICE_NAME_ARCHGW) + stop_docker_container() @click.command() @@ -303,7 +211,7 @@ def generate_prompt_targets(file): @click.command() @click.option( "--debug", - help="For detailed debug logs to trace calls from archgw <> model_server <> api_server, etc", + help="For detailed debug logs to trace calls from archgw <> api_server, etc", is_flag=True, ) @click.option("--follow", help="Follow the logs", is_flag=True) diff --git a/arch/tools/cli/utils.py b/arch/tools/cli/utils.py index 2f29b16e..ef0a37ea 100644 --- a/arch/tools/cli/utils.py +++ b/arch/tools/cli/utils.py @@ -57,6 +57,10 @@ def convert_legacy_listeners( "timeout": "30s", } + # Handle None case + if listeners is None: + return [llm_gateway_listener], llm_gateway_listener, prompt_gateway_listener + if isinstance(listeners, dict): # legacy listeners # check if type is array or object @@ -148,6 +152,24 @@ def get_llm_provider_access_keys(arch_config_file): if access_key is not None: access_key_list.append(access_key) + # Extract environment variables from state_storage.connection_string + state_storage = arch_config_yaml.get("state_storage_v1_responses") + if state_storage: + connection_string = state_storage.get("connection_string") + if connection_string and isinstance(connection_string, str): + # Extract all $VAR and ${VAR} patterns from connection string + import re + + # Match both $VAR and ${VAR} patterns + pattern = r"\$\{?([A-Z_][A-Z0-9_]*)\}?" + matches = re.findall(pattern, connection_string) + for var in matches: + access_key_list.append(f"${var}") + else: + raise ValueError( + "Invalid connection string received in state_storage_v1_responses" + ) + return access_key_list diff --git a/arch/tools/poetry.lock b/arch/tools/poetry.lock index d04e6209..bf08343b 100644 --- a/arch/tools/poetry.lock +++ b/arch/tools/poetry.lock @@ -1,118 +1,4 @@ -# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. - -[[package]] -name = "accelerate" -version = "1.11.0" -description = "Accelerate" -optional = false -python-versions = ">=3.10.0" -groups = ["main"] -files = [ - {file = "accelerate-1.11.0-py3-none-any.whl", hash = "sha256:a628fa6beb069b8e549460fc449135d5bd8d73e7a11fd09f0bc9fc4ace7f06f1"}, - {file = "accelerate-1.11.0.tar.gz", hash = "sha256:bb1caf2597b4cd632b917b5000c591d10730bb024a79746f1ee205bba80bd229"}, -] - -[package.dependencies] -huggingface_hub = ">=0.21.0" -numpy = ">=1.17" -packaging = ">=20.0" -psutil = "*" -pyyaml = "*" -safetensors = ">=0.4.3" -torch = ">=2.0.0" - -[package.extras] -deepspeed = ["deepspeed"] -dev = ["bitsandbytes", "datasets", "diffusers", "evaluate", "parameterized", "pytest (>=7.2.0)", "pytest-order", "pytest-subtests", "pytest-xdist", "rich", "ruff (==0.13.1)", "scikit-learn", "scipy", "timm", "torchdata (>=0.8.0)", "torchpippy (>=0.2.0)", "tqdm", "transformers"] -quality = ["ruff (==0.13.1)"] -rich = ["rich"] -sagemaker = ["sagemaker"] -test-dev = ["bitsandbytes", "datasets", "diffusers", "evaluate", "scikit-learn", "scipy", "timm", "torchdata (>=0.8.0)", "torchpippy (>=0.2.0)", "tqdm", "transformers"] -test-fp8 = ["torchao"] -test-prod = ["parameterized", "pytest (>=7.2.0)", "pytest-order", "pytest-subtests", "pytest-xdist"] -test-trackers = ["comet-ml", "dvclive", "matplotlib", "swanlab[dashboard]", "tensorboard", "trackio", "wandb"] -testing = ["bitsandbytes", "datasets", "diffusers", "evaluate", "parameterized", "pytest (>=7.2.0)", "pytest-order", "pytest-subtests", "pytest-xdist", "scikit-learn", "scipy", "timm", "torchdata (>=0.8.0)", "torchpippy (>=0.2.0)", "tqdm", "transformers"] - -[[package]] -name = "annotated-types" -version = "0.7.0" -description = "Reusable constraint types to use with typing.Annotated" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, - {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, -] - -[[package]] -name = "anyio" -version = "4.11.0" -description = "High-level concurrency and networking framework on top of asyncio or Trio" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc"}, - {file = "anyio-4.11.0.tar.gz", hash = "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4"}, -] - -[package.dependencies] -exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} -idna = ">=2.8" -sniffio = ">=1.1" -typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} - -[package.extras] -trio = ["trio (>=0.31.0)"] - -[[package]] -name = "archgw-modelserver" -version = "0.3.18" -description = "A model server for serving models" -optional = false -python-versions = ">=3.10" -groups = ["main"] -files = [] -develop = true - -[package.dependencies] -accelerate = ">=1.0.0,<2.0.0" -dateparser = "*" -fastapi = "0.115.0" -httpx = "0.27.2" -openai = ">=1.50.2,<2.0.0" -opentelemetry-api = ">=1.28.0,<2.0.0" -opentelemetry-exporter-otlp = ">=1.28.0,<2.0.0" -opentelemetry-instrumentation-fastapi = ">=0.49b0,<1.0" -opentelemetry-sdk = ">=1.28.0,<2.0.0" -overrides = ">=7.7.0,<8.0.0" -pydantic = ">=2.10.1,<3.0.0" -torch = ">=2.6.0" -transformers = ">=4.37.0,<5.0.0" -uvicorn = "0.31.0" - -[package.source] -type = "directory" -url = "../../model_server" - -[[package]] -name = "asgiref" -version = "3.10.0" -description = "ASGI specs, helper code, and adapters" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "asgiref-3.10.0-py3-none-any.whl", hash = "sha256:aef8a81283a34d0ab31630c9b7dfe70c812c95eba78171367ca8745e88124734"}, - {file = "asgiref-3.10.0.tar.gz", hash = "sha256:d89f2d8cd8b56dada7d52fa7dc8075baa08fb836560710d38c292a7a3f78c04e"}, -] - -[package.dependencies] -typing_extensions = {version = ">=4", markers = "python_version < \"3.11\""} - -[package.extras] -tests = ["mypy (>=1.14.0)", "pytest", "pytest-asyncio"] +# This file is automatically @generated by Poetry 2.1.2 and should not be changed by hand. [[package]] name = "attrs" @@ -128,14 +14,14 @@ files = [ [[package]] name = "certifi" -version = "2025.10.5" +version = "2025.11.12" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.7" groups = ["main"] files = [ - {file = "certifi-2025.10.5-py3-none-any.whl", hash = "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de"}, - {file = "certifi-2025.10.5.tar.gz", hash = "sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43"}, + {file = "certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b"}, + {file = "certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316"}, ] [[package]] @@ -289,52 +175,17 @@ files = [ ] markers = {main = "platform_system == \"Windows\"", dev = "sys_platform == \"win32\""} -[[package]] -name = "dateparser" -version = "1.2.2" -description = "Date parsing library designed to parse dates from HTML pages" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "dateparser-1.2.2-py3-none-any.whl", hash = "sha256:5a5d7211a09013499867547023a2a0c91d5a27d15dd4dbcea676ea9fe66f2482"}, - {file = "dateparser-1.2.2.tar.gz", hash = "sha256:986316f17cb8cdc23ea8ce563027c5ef12fc725b6fb1d137c14ca08777c5ecf7"}, -] - -[package.dependencies] -python-dateutil = ">=2.7.0" -pytz = ">=2024.2" -regex = ">=2024.9.11" -tzlocal = ">=0.2" - -[package.extras] -calendars = ["convertdate (>=2.2.1)", "hijridate"] -fasttext = ["fasttext (>=0.9.1)", "numpy (>=1.19.3,<2)"] -langdetect = ["langdetect (>=1.0.0)"] - -[[package]] -name = "distro" -version = "1.9.0" -description = "Distro - an OS platform information API" -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2"}, - {file = "distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed"}, -] - [[package]] name = "exceptiongroup" -version = "1.3.0" +version = "1.3.1" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" -groups = ["main", "dev"] +groups = ["dev"] markers = "python_version == \"3.10\"" files = [ - {file = "exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10"}, - {file = "exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88"}, + {file = "exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598"}, + {file = "exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219"}, ] [package.dependencies] @@ -343,309 +194,6 @@ typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""} [package.extras] test = ["pytest (>=6)"] -[[package]] -name = "fastapi" -version = "0.115.0" -description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "fastapi-0.115.0-py3-none-any.whl", hash = "sha256:17ea427674467486e997206a5ab25760f6b09e069f099b96f5b55a32fb6f1631"}, - {file = "fastapi-0.115.0.tar.gz", hash = "sha256:f93b4ca3529a8ebc6fc3fcf710e5efa8de3df9b41570958abf1d97d843138004"}, -] - -[package.dependencies] -pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0" -starlette = ">=0.37.2,<0.39.0" -typing-extensions = ">=4.8.0" - -[package.extras] -all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.7)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] -standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "jinja2 (>=2.11.2)", "python-multipart (>=0.0.7)", "uvicorn[standard] (>=0.12.0)"] - -[[package]] -name = "filelock" -version = "3.20.0" -description = "A platform independent file lock." -optional = false -python-versions = ">=3.10" -groups = ["main"] -files = [ - {file = "filelock-3.20.0-py3-none-any.whl", hash = "sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2"}, - {file = "filelock-3.20.0.tar.gz", hash = "sha256:711e943b4ec6be42e1d4e6690b48dc175c822967466bb31c0c293f34334c13f4"}, -] - -[[package]] -name = "fsspec" -version = "2025.9.0" -description = "File-system specification" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "fsspec-2025.9.0-py3-none-any.whl", hash = "sha256:530dc2a2af60a414a832059574df4a6e10cce927f6f4a78209390fe38955cfb7"}, - {file = "fsspec-2025.9.0.tar.gz", hash = "sha256:19fd429483d25d28b65ec68f9f4adc16c17ea2c7c7bf54ec61360d478fb19c19"}, -] - -[package.extras] -abfs = ["adlfs"] -adl = ["adlfs"] -arrow = ["pyarrow (>=1)"] -dask = ["dask", "distributed"] -dev = ["pre-commit", "ruff (>=0.5)"] -doc = ["numpydoc", "sphinx", "sphinx-design", "sphinx-rtd-theme", "yarl"] -dropbox = ["dropbox", "dropboxdrivefs", "requests"] -full = ["adlfs", "aiohttp (!=4.0.0a0,!=4.0.0a1)", "dask", "distributed", "dropbox", "dropboxdrivefs", "fusepy", "gcsfs", "libarchive-c", "ocifs", "panel", "paramiko", "pyarrow (>=1)", "pygit2", "requests", "s3fs", "smbprotocol", "tqdm"] -fuse = ["fusepy"] -gcs = ["gcsfs"] -git = ["pygit2"] -github = ["requests"] -gs = ["gcsfs"] -gui = ["panel"] -hdfs = ["pyarrow (>=1)"] -http = ["aiohttp (!=4.0.0a0,!=4.0.0a1)"] -libarchive = ["libarchive-c"] -oci = ["ocifs"] -s3 = ["s3fs"] -sftp = ["paramiko"] -smb = ["smbprotocol"] -ssh = ["paramiko"] -test = ["aiohttp (!=4.0.0a0,!=4.0.0a1)", "numpy", "pytest", "pytest-asyncio (!=0.22.0)", "pytest-benchmark", "pytest-cov", "pytest-mock", "pytest-recording", "pytest-rerunfailures", "requests"] -test-downstream = ["aiobotocore (>=2.5.4,<3.0.0)", "dask[dataframe,test]", "moto[server] (>4,<5)", "pytest-timeout", "xarray"] -test-full = ["adlfs", "aiohttp (!=4.0.0a0,!=4.0.0a1)", "cloudpickle", "dask", "distributed", "dropbox", "dropboxdrivefs", "fastparquet", "fusepy", "gcsfs", "jinja2", "kerchunk", "libarchive-c", "lz4", "notebook", "numpy", "ocifs", "pandas", "panel", "paramiko", "pyarrow", "pyarrow (>=1)", "pyftpdlib", "pygit2", "pytest", "pytest-asyncio (!=0.22.0)", "pytest-benchmark", "pytest-cov", "pytest-mock", "pytest-recording", "pytest-rerunfailures", "python-snappy", "requests", "smbprotocol", "tqdm", "urllib3", "zarr", "zstandard ; python_version < \"3.14\""] -tqdm = ["tqdm"] - -[[package]] -name = "googleapis-common-protos" -version = "1.71.0" -description = "Common protobufs used in Google APIs" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "googleapis_common_protos-1.71.0-py3-none-any.whl", hash = "sha256:59034a1d849dc4d18971997a72ac56246570afdd17f9369a0ff68218d50ab78c"}, - {file = "googleapis_common_protos-1.71.0.tar.gz", hash = "sha256:1aec01e574e29da63c80ba9f7bbf1ccfaacf1da877f23609fe236ca7c72a2e2e"}, -] - -[package.dependencies] -protobuf = ">=3.20.2,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<7.0.0" - -[package.extras] -grpc = ["grpcio (>=1.44.0,<2.0.0)"] - -[[package]] -name = "grpcio" -version = "1.76.0" -description = "HTTP/2-based RPC framework" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "grpcio-1.76.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:65a20de41e85648e00305c1bb09a3598f840422e522277641145a32d42dcefcc"}, - {file = "grpcio-1.76.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:40ad3afe81676fd9ec6d9d406eda00933f218038433980aa19d401490e46ecde"}, - {file = "grpcio-1.76.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:035d90bc79eaa4bed83f524331d55e35820725c9fbb00ffa1904d5550ed7ede3"}, - {file = "grpcio-1.76.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:4215d3a102bd95e2e11b5395c78562967959824156af11fa93d18fdd18050990"}, - {file = "grpcio-1.76.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:49ce47231818806067aea3324d4bf13825b658ad662d3b25fada0bdad9b8a6af"}, - {file = "grpcio-1.76.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8cc3309d8e08fd79089e13ed4819d0af72aa935dd8f435a195fd152796752ff2"}, - {file = "grpcio-1.76.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:971fd5a1d6e62e00d945423a567e42eb1fa678ba89072832185ca836a94daaa6"}, - {file = "grpcio-1.76.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9d9adda641db7207e800a7f089068f6f645959f2df27e870ee81d44701dd9db3"}, - {file = "grpcio-1.76.0-cp310-cp310-win32.whl", hash = "sha256:063065249d9e7e0782d03d2bca50787f53bd0fb89a67de9a7b521c4a01f1989b"}, - {file = "grpcio-1.76.0-cp310-cp310-win_amd64.whl", hash = "sha256:a6ae758eb08088d36812dd5d9af7a9859c05b1e0f714470ea243694b49278e7b"}, - {file = "grpcio-1.76.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:2e1743fbd7f5fa713a1b0a8ac8ebabf0ec980b5d8809ec358d488e273b9cf02a"}, - {file = "grpcio-1.76.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:a8c2cf1209497cf659a667d7dea88985e834c24b7c3b605e6254cbb5076d985c"}, - {file = "grpcio-1.76.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:08caea849a9d3c71a542827d6df9d5a69067b0a1efbea8a855633ff5d9571465"}, - {file = "grpcio-1.76.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:f0e34c2079d47ae9f6188211db9e777c619a21d4faba6977774e8fa43b085e48"}, - {file = "grpcio-1.76.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8843114c0cfce61b40ad48df65abcfc00d4dba82eae8718fab5352390848c5da"}, - {file = "grpcio-1.76.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8eddfb4d203a237da6f3cc8a540dad0517d274b5a1e9e636fd8d2c79b5c1d397"}, - {file = "grpcio-1.76.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:32483fe2aab2c3794101c2a159070584e5db11d0aa091b2c0ea9c4fc43d0d749"}, - {file = "grpcio-1.76.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dcfe41187da8992c5f40aa8c5ec086fa3672834d2be57a32384c08d5a05b4c00"}, - {file = "grpcio-1.76.0-cp311-cp311-win32.whl", hash = "sha256:2107b0c024d1b35f4083f11245c0e23846ae64d02f40b2b226684840260ed054"}, - {file = "grpcio-1.76.0-cp311-cp311-win_amd64.whl", hash = "sha256:522175aba7af9113c48ec10cc471b9b9bd4f6ceb36aeb4544a8e2c80ed9d252d"}, - {file = "grpcio-1.76.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:81fd9652b37b36f16138611c7e884eb82e0cec137c40d3ef7c3f9b3ed00f6ed8"}, - {file = "grpcio-1.76.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:04bbe1bfe3a68bbfd4e52402ab7d4eb59d72d02647ae2042204326cf4bbad280"}, - {file = "grpcio-1.76.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d388087771c837cdb6515539f43b9d4bf0b0f23593a24054ac16f7a960be16f4"}, - {file = "grpcio-1.76.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:9f8f757bebaaea112c00dba718fc0d3260052ce714e25804a03f93f5d1c6cc11"}, - {file = "grpcio-1.76.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:980a846182ce88c4f2f7e2c22c56aefd515daeb36149d1c897f83cf57999e0b6"}, - {file = "grpcio-1.76.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f92f88e6c033db65a5ae3d97905c8fea9c725b63e28d5a75cb73b49bda5024d8"}, - {file = "grpcio-1.76.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4baf3cbe2f0be3289eb68ac8ae771156971848bb8aaff60bad42005539431980"}, - {file = "grpcio-1.76.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:615ba64c208aaceb5ec83bfdce7728b80bfeb8be97562944836a7a0a9647d882"}, - {file = "grpcio-1.76.0-cp312-cp312-win32.whl", hash = "sha256:45d59a649a82df5718fd9527ce775fd66d1af35e6d31abdcdc906a49c6822958"}, - {file = "grpcio-1.76.0-cp312-cp312-win_amd64.whl", hash = "sha256:c088e7a90b6017307f423efbb9d1ba97a22aa2170876223f9709e9d1de0b5347"}, - {file = "grpcio-1.76.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:26ef06c73eb53267c2b319f43e6634c7556ea37672029241a056629af27c10e2"}, - {file = "grpcio-1.76.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:45e0111e73f43f735d70786557dc38141185072d7ff8dc1829d6a77ac1471468"}, - {file = "grpcio-1.76.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:83d57312a58dcfe2a3a0f9d1389b299438909a02db60e2f2ea2ae2d8034909d3"}, - {file = "grpcio-1.76.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:3e2a27c89eb9ac3d81ec8835e12414d73536c6e620355d65102503064a4ed6eb"}, - {file = "grpcio-1.76.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61f69297cba3950a524f61c7c8ee12e55c486cb5f7db47ff9dcee33da6f0d3ae"}, - {file = "grpcio-1.76.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6a15c17af8839b6801d554263c546c69c4d7718ad4321e3166175b37eaacca77"}, - {file = "grpcio-1.76.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:25a18e9810fbc7e7f03ec2516addc116a957f8cbb8cbc95ccc80faa072743d03"}, - {file = "grpcio-1.76.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:931091142fd8cc14edccc0845a79248bc155425eee9a98b2db2ea4f00a235a42"}, - {file = "grpcio-1.76.0-cp313-cp313-win32.whl", hash = "sha256:5e8571632780e08526f118f74170ad8d50fb0a48c23a746bef2a6ebade3abd6f"}, - {file = "grpcio-1.76.0-cp313-cp313-win_amd64.whl", hash = "sha256:f9f7bd5faab55f47231ad8dba7787866b69f5e93bc306e3915606779bbfb4ba8"}, - {file = "grpcio-1.76.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:ff8a59ea85a1f2191a0ffcc61298c571bc566332f82e5f5be1b83c9d8e668a62"}, - {file = "grpcio-1.76.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:06c3d6b076e7b593905d04fdba6a0525711b3466f43b3400266f04ff735de0cd"}, - {file = "grpcio-1.76.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fd5ef5932f6475c436c4a55e4336ebbe47bd3272be04964a03d316bbf4afbcbc"}, - {file = "grpcio-1.76.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b331680e46239e090f5b3cead313cc772f6caa7d0fc8de349337563125361a4a"}, - {file = "grpcio-1.76.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2229ae655ec4e8999599469559e97630185fdd53ae1e8997d147b7c9b2b72cba"}, - {file = "grpcio-1.76.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:490fa6d203992c47c7b9e4a9d39003a0c2bcc1c9aa3c058730884bbbb0ee9f09"}, - {file = "grpcio-1.76.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:479496325ce554792dba6548fae3df31a72cef7bad71ca2e12b0e58f9b336bfc"}, - {file = "grpcio-1.76.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1c9b93f79f48b03ada57ea24725d83a30284a012ec27eab2cf7e50a550cbbbcc"}, - {file = "grpcio-1.76.0-cp314-cp314-win32.whl", hash = "sha256:747fa73efa9b8b1488a95d0ba1039c8e2dca0f741612d80415b1e1c560febf4e"}, - {file = "grpcio-1.76.0-cp314-cp314-win_amd64.whl", hash = "sha256:922fa70ba549fce362d2e2871ab542082d66e2aaf0c19480ea453905b01f384e"}, - {file = "grpcio-1.76.0-cp39-cp39-linux_armv7l.whl", hash = "sha256:8ebe63ee5f8fa4296b1b8cfc743f870d10e902ca18afc65c68cf46fd39bb0783"}, - {file = "grpcio-1.76.0-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:3bf0f392c0b806905ed174dcd8bdd5e418a40d5567a05615a030a5aeddea692d"}, - {file = "grpcio-1.76.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0b7604868b38c1bfd5cf72d768aedd7db41d78cb6a4a18585e33fb0f9f2363fd"}, - {file = "grpcio-1.76.0-cp39-cp39-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:e6d1db20594d9daba22f90da738b1a0441a7427552cc6e2e3d1297aeddc00378"}, - {file = "grpcio-1.76.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d099566accf23d21037f18a2a63d323075bebace807742e4b0ac210971d4dd70"}, - {file = "grpcio-1.76.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:ebea5cc3aa8ea72e04df9913492f9a96d9348db876f9dda3ad729cfedf7ac416"}, - {file = "grpcio-1.76.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:0c37db8606c258e2ee0c56b78c62fc9dee0e901b5dbdcf816c2dd4ad652b8b0c"}, - {file = "grpcio-1.76.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:ebebf83299b0cb1721a8859ea98f3a77811e35dce7609c5c963b9ad90728f886"}, - {file = "grpcio-1.76.0-cp39-cp39-win32.whl", hash = "sha256:0aaa82d0813fd4c8e589fac9b65d7dd88702555f702fb10417f96e2a2a6d4c0f"}, - {file = "grpcio-1.76.0-cp39-cp39-win_amd64.whl", hash = "sha256:acab0277c40eff7143c2323190ea57b9ee5fd353d8190ee9652369fae735668a"}, - {file = "grpcio-1.76.0.tar.gz", hash = "sha256:7be78388d6da1a25c0d5ec506523db58b18be22d9c37d8d3a32c08be4987bd73"}, -] - -[package.dependencies] -typing-extensions = ">=4.12,<5.0" - -[package.extras] -protobuf = ["grpcio-tools (>=1.76.0)"] - -[[package]] -name = "h11" -version = "0.16.0" -description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"}, - {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, -] - -[[package]] -name = "hf-xet" -version = "1.2.0" -description = "Fast transfer of large files with the Hugging Face Hub." -optional = false -python-versions = ">=3.8" -groups = ["main"] -markers = "platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"arm64\" or platform_machine == \"aarch64\"" -files = [ - {file = "hf_xet-1.2.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:ceeefcd1b7aed4956ae8499e2199607765fbd1c60510752003b6cc0b8413b649"}, - {file = "hf_xet-1.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b70218dd548e9840224df5638fdc94bd033552963cfa97f9170829381179c813"}, - {file = "hf_xet-1.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d40b18769bb9a8bc82a9ede575ce1a44c75eb80e7375a01d76259089529b5dc"}, - {file = "hf_xet-1.2.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:cd3a6027d59cfb60177c12d6424e31f4b5ff13d8e3a1247b3a584bf8977e6df5"}, - {file = "hf_xet-1.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6de1fc44f58f6dd937956c8d304d8c2dea264c80680bcfa61ca4a15e7b76780f"}, - {file = "hf_xet-1.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f182f264ed2acd566c514e45da9f2119110e48a87a327ca271027904c70c5832"}, - {file = "hf_xet-1.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:293a7a3787e5c95d7be1857358a9130694a9c6021de3f27fa233f37267174382"}, - {file = "hf_xet-1.2.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:10bfab528b968c70e062607f663e21e34e2bba349e8038db546646875495179e"}, - {file = "hf_xet-1.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2a212e842647b02eb6a911187dc878e79c4aa0aa397e88dd3b26761676e8c1f8"}, - {file = "hf_xet-1.2.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:30e06daccb3a7d4c065f34fc26c14c74f4653069bb2b194e7f18f17cbe9939c0"}, - {file = "hf_xet-1.2.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:29c8fc913a529ec0a91867ce3d119ac1aac966e098cf49501800c870328cc090"}, - {file = "hf_xet-1.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e159cbfcfbb29f920db2c09ed8b660eb894640d284f102ada929b6e3dc410a"}, - {file = "hf_xet-1.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9c91d5ae931510107f148874e9e2de8a16052b6f1b3ca3c1b12f15ccb491390f"}, - {file = "hf_xet-1.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:210d577732b519ac6ede149d2f2f34049d44e8622bf14eb3d63bbcd2d4b332dc"}, - {file = "hf_xet-1.2.0-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:46740d4ac024a7ca9b22bebf77460ff43332868b661186a8e46c227fdae01848"}, - {file = "hf_xet-1.2.0-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:27df617a076420d8845bea087f59303da8be17ed7ec0cd7ee3b9b9f579dff0e4"}, - {file = "hf_xet-1.2.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3651fd5bfe0281951b988c0facbe726aa5e347b103a675f49a3fa8144c7968fd"}, - {file = "hf_xet-1.2.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d06fa97c8562fb3ee7a378dd9b51e343bc5bc8190254202c9771029152f5e08c"}, - {file = "hf_xet-1.2.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4c1428c9ae73ec0939410ec73023c4f842927f39db09b063b9482dac5a3bb737"}, - {file = "hf_xet-1.2.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a55558084c16b09b5ed32ab9ed38421e2d87cf3f1f89815764d1177081b99865"}, - {file = "hf_xet-1.2.0-cp37-abi3-win_amd64.whl", hash = "sha256:e6584a52253f72c9f52f9e549d5895ca7a471608495c4ecaa6cc73dba2b24d69"}, - {file = "hf_xet-1.2.0.tar.gz", hash = "sha256:a8c27070ca547293b6890c4bf389f713f80e8c478631432962bb7f4bc0bd7d7f"}, -] - -[package.extras] -tests = ["pytest"] - -[[package]] -name = "httpcore" -version = "1.0.9" -description = "A minimal low-level HTTP client." -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"}, - {file = "httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"}, -] - -[package.dependencies] -certifi = "*" -h11 = ">=0.16" - -[package.extras] -asyncio = ["anyio (>=4.0,<5.0)"] -http2 = ["h2 (>=3,<5)"] -socks = ["socksio (==1.*)"] -trio = ["trio (>=0.22.0,<1.0)"] - -[[package]] -name = "httpx" -version = "0.27.2" -description = "The next generation HTTP client." -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0"}, - {file = "httpx-0.27.2.tar.gz", hash = "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2"}, -] - -[package.dependencies] -anyio = "*" -certifi = "*" -httpcore = "==1.*" -idna = "*" -sniffio = "*" - -[package.extras] -brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi ; platform_python_implementation != \"CPython\""] -cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] -http2 = ["h2 (>=3,<5)"] -socks = ["socksio (==1.*)"] -zstd = ["zstandard (>=0.18.0)"] - -[[package]] -name = "huggingface-hub" -version = "0.36.0" -description = "Client library to download and publish models, datasets and other repos on the huggingface.co hub" -optional = false -python-versions = ">=3.8.0" -groups = ["main"] -files = [ - {file = "huggingface_hub-0.36.0-py3-none-any.whl", hash = "sha256:7bcc9ad17d5b3f07b57c78e79d527102d08313caa278a641993acddcb894548d"}, - {file = "huggingface_hub-0.36.0.tar.gz", hash = "sha256:47b3f0e2539c39bf5cde015d63b72ec49baff67b6931c3d97f3f84532e2b8d25"}, -] - -[package.dependencies] -filelock = "*" -fsspec = ">=2023.5.0" -hf-xet = {version = ">=1.1.3,<2.0.0", markers = "platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"arm64\" or platform_machine == \"aarch64\""} -packaging = ">=20.9" -pyyaml = ">=5.1" -requests = "*" -tqdm = ">=4.42.1" -typing-extensions = ">=3.7.4.3" - -[package.extras] -all = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "authlib (>=1.3.2)", "fastapi", "gradio (>=4.0.0)", "httpx", "itsdangerous", "jedi", "libcst (>=1.4.0)", "mypy (==1.15.0) ; python_version >= \"3.9\"", "mypy (>=1.14.1,<1.15.0) ; python_version == \"3.8\"", "numpy", "pytest (>=8.1.1,<8.2.2)", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-mock", "pytest-rerunfailures (<16.0)", "pytest-vcr", "pytest-xdist", "ruff (>=0.9.0)", "soundfile", "ty", "types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)", "urllib3 (<2.0)"] -cli = ["InquirerPy (==0.3.4)"] -dev = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "authlib (>=1.3.2)", "fastapi", "gradio (>=4.0.0)", "httpx", "itsdangerous", "jedi", "libcst (>=1.4.0)", "mypy (==1.15.0) ; python_version >= \"3.9\"", "mypy (>=1.14.1,<1.15.0) ; python_version == \"3.8\"", "numpy", "pytest (>=8.1.1,<8.2.2)", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-mock", "pytest-rerunfailures (<16.0)", "pytest-vcr", "pytest-xdist", "ruff (>=0.9.0)", "soundfile", "ty", "types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)", "urllib3 (<2.0)"] -fastai = ["fastai (>=2.4)", "fastcore (>=1.3.27)", "toml"] -hf-transfer = ["hf-transfer (>=0.1.4)"] -hf-xet = ["hf-xet (>=1.1.2,<2.0.0)"] -inference = ["aiohttp"] -mcp = ["aiohttp", "mcp (>=1.8.0)", "typer"] -oauth = ["authlib (>=1.3.2)", "fastapi", "httpx", "itsdangerous"] -quality = ["libcst (>=1.4.0)", "mypy (==1.15.0) ; python_version >= \"3.9\"", "mypy (>=1.14.1,<1.15.0) ; python_version == \"3.8\"", "ruff (>=0.9.0)", "ty"] -tensorflow = ["graphviz", "pydot", "tensorflow"] -tensorflow-testing = ["keras (<3.0)", "tensorflow"] -testing = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "authlib (>=1.3.2)", "fastapi", "gradio (>=4.0.0)", "httpx", "itsdangerous", "jedi", "numpy", "pytest (>=8.1.1,<8.2.2)", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-mock", "pytest-rerunfailures (<16.0)", "pytest-vcr", "pytest-xdist", "soundfile", "urllib3 (<2.0)"] -torch = ["safetensors[torch]", "torch"] -typing = ["types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)"] - [[package]] name = "idna" version = "3.11" @@ -661,30 +209,6 @@ files = [ [package.extras] all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] -[[package]] -name = "importlib-metadata" -version = "8.7.0" -description = "Read metadata from Python packages" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd"}, - {file = "importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000"}, -] - -[package.dependencies] -zipp = ">=3.20" - -[package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -enabler = ["pytest-enabler (>=2.2)"] -perf = ["ipython"] -test = ["flufl.flake8", "importlib_resources (>=1.3) ; python_version < \"3.9\"", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] -type = ["pytest-mypy"] - [[package]] name = "iniconfig" version = "2.3.0" @@ -715,118 +239,6 @@ MarkupSafe = ">=2.0" [package.extras] i18n = ["Babel (>=2.7)"] -[[package]] -name = "jiter" -version = "0.11.1" -description = "Fast iterable JSON parser." -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "jiter-0.11.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:ed58841a491bbbf3f7c55a6b68fff568439ab73b2cce27ace0e169057b5851df"}, - {file = "jiter-0.11.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:499beb9b2d7e51d61095a8de39ebcab1d1778f2a74085f8305a969f6cee9f3e4"}, - {file = "jiter-0.11.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b87b2821795e28cc990939b68ce7a038edea680a24910bd68a79d54ff3f03c02"}, - {file = "jiter-0.11.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:83f6fa494d8bba14ab100417c80e70d32d737e805cb85be2052d771c76fcd1f8"}, - {file = "jiter-0.11.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5fbc6aea1daa2ec6f5ed465f0c5e7b0607175062ceebbea5ca70dd5ddab58083"}, - {file = "jiter-0.11.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:302288e2edc43174bb2db838e94688d724f9aad26c5fb9a74f7a5fb427452a6a"}, - {file = "jiter-0.11.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85db563fe3b367bb568af5d29dea4d4066d923b8e01f3417d25ebecd958de815"}, - {file = "jiter-0.11.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f1c1ba2b6b22f775444ef53bc2d5778396d3520abc7b2e1da8eb0c27cb3ffb10"}, - {file = "jiter-0.11.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:523be464b14f8fd0cc78da6964b87b5515a056427a2579f9085ce30197a1b54a"}, - {file = "jiter-0.11.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:25b99b3f04cd2a38fefb22e822e35eb203a2cd37d680dbbc0c0ba966918af336"}, - {file = "jiter-0.11.1-cp310-cp310-win32.whl", hash = "sha256:47a79e90545a596bb9104109777894033347b11180d4751a216afef14072dbe7"}, - {file = "jiter-0.11.1-cp310-cp310-win_amd64.whl", hash = "sha256:cace75621ae9bd66878bf69fbd4dfc1a28ef8661e0c2d0eb72d3d6f1268eddf5"}, - {file = "jiter-0.11.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:9b0088ff3c374ce8ce0168523ec8e97122ebb788f950cf7bb8e39c7dc6a876a2"}, - {file = "jiter-0.11.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:74433962dd3c3090655e02e461267095d6c84f0741c7827de11022ef8d7ff661"}, - {file = "jiter-0.11.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6d98030e345e6546df2cc2c08309c502466c66c4747b043f1a0d415fada862b8"}, - {file = "jiter-0.11.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1d6db0b2e788db46bec2cf729a88b6dd36959af2abd9fa2312dfba5acdd96dcb"}, - {file = "jiter-0.11.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55678fbbda261eafe7289165dd2ddd0e922df5f9a1ae46d7c79a5a15242bd7d1"}, - {file = "jiter-0.11.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6a6b74fae8e40497653b52ce6ca0f1b13457af769af6fb9c1113efc8b5b4d9be"}, - {file = "jiter-0.11.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a55a453f8b035eb4f7852a79a065d616b7971a17f5e37a9296b4b38d3b619e4"}, - {file = "jiter-0.11.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2638148099022e6bdb3f42904289cd2e403609356fb06eb36ddec2d50958bc29"}, - {file = "jiter-0.11.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:252490567a5d990986f83b95a5f1ca1bf205ebd27b3e9e93bb7c2592380e29b9"}, - {file = "jiter-0.11.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d431d52b0ca2436eea6195f0f48528202100c7deda354cb7aac0a302167594d5"}, - {file = "jiter-0.11.1-cp311-cp311-win32.whl", hash = "sha256:db6f41e40f8bae20c86cb574b48c4fd9f28ee1c71cb044e9ec12e78ab757ba3a"}, - {file = "jiter-0.11.1-cp311-cp311-win_amd64.whl", hash = "sha256:0cc407b8e6cdff01b06bb80f61225c8b090c3df108ebade5e0c3c10993735b19"}, - {file = "jiter-0.11.1-cp311-cp311-win_arm64.whl", hash = "sha256:fe04ea475392a91896d1936367854d346724a1045a247e5d1c196410473b8869"}, - {file = "jiter-0.11.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:c92148eec91052538ce6823dfca9525f5cfc8b622d7f07e9891a280f61b8c96c"}, - {file = "jiter-0.11.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ecd4da91b5415f183a6be8f7158d127bdd9e6a3174138293c0d48d6ea2f2009d"}, - {file = "jiter-0.11.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7e3ac25c00b9275684d47aa42febaa90a9958e19fd1726c4ecf755fbe5e553b"}, - {file = "jiter-0.11.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:57d7305c0a841858f866cd459cd9303f73883fb5e097257f3d4a3920722c69d4"}, - {file = "jiter-0.11.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e86fa10e117dce22c547f31dd6d2a9a222707d54853d8de4e9a2279d2c97f239"}, - {file = "jiter-0.11.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ae5ef1d48aec7e01ee8420155d901bb1d192998fa811a65ebb82c043ee186711"}, - {file = "jiter-0.11.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb68e7bf65c990531ad8715e57d50195daf7c8e6f1509e617b4e692af1108939"}, - {file = "jiter-0.11.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:43b30c8154ded5845fa454ef954ee67bfccce629b2dea7d01f795b42bc2bda54"}, - {file = "jiter-0.11.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:586cafbd9dd1f3ce6a22b4a085eaa6be578e47ba9b18e198d4333e598a91db2d"}, - {file = "jiter-0.11.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:677cc2517d437a83bb30019fd4cf7cad74b465914c56ecac3440d597ac135250"}, - {file = "jiter-0.11.1-cp312-cp312-win32.whl", hash = "sha256:fa992af648fcee2b850a3286a35f62bbbaeddbb6dbda19a00d8fbc846a947b6e"}, - {file = "jiter-0.11.1-cp312-cp312-win_amd64.whl", hash = "sha256:88b5cae9fa51efeb3d4bd4e52bfd4c85ccc9cac44282e2a9640893a042ba4d87"}, - {file = "jiter-0.11.1-cp312-cp312-win_arm64.whl", hash = "sha256:9a6cae1ab335551917f882f2c3c1efe7617b71b4c02381e4382a8fc80a02588c"}, - {file = "jiter-0.11.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:71b6a920a5550f057d49d0e8bcc60945a8da998019e83f01adf110e226267663"}, - {file = "jiter-0.11.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0b3de72e925388453a5171be83379549300db01284f04d2a6f244d1d8de36f94"}, - {file = "jiter-0.11.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc19dd65a2bd3d9c044c5b4ebf657ca1e6003a97c0fc10f555aa4f7fb9821c00"}, - {file = "jiter-0.11.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d58faaa936743cd1464540562f60b7ce4fd927e695e8bc31b3da5b914baa9abd"}, - {file = "jiter-0.11.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:902640c3103625317291cb73773413b4d71847cdf9383ba65528745ff89f1d14"}, - {file = "jiter-0.11.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:30405f726e4c2ed487b176c09f8b877a957f535d60c1bf194abb8dadedb5836f"}, - {file = "jiter-0.11.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3217f61728b0baadd2551844870f65219ac4a1285d5e1a4abddff3d51fdabe96"}, - {file = "jiter-0.11.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b1364cc90c03a8196f35f396f84029f12abe925415049204446db86598c8b72c"}, - {file = "jiter-0.11.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:53a54bf8e873820ab186b2dca9f6c3303f00d65ae5e7b7d6bda1b95aa472d646"}, - {file = "jiter-0.11.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:7e29aca023627b0e0c2392d4248f6414d566ff3974fa08ff2ac8dbb96dfee92a"}, - {file = "jiter-0.11.1-cp313-cp313-win32.whl", hash = "sha256:f153e31d8bca11363751e875c0a70b3d25160ecbaee7b51e457f14498fb39d8b"}, - {file = "jiter-0.11.1-cp313-cp313-win_amd64.whl", hash = "sha256:f773f84080b667c69c4ea0403fc67bb08b07e2b7ce1ef335dea5868451e60fed"}, - {file = "jiter-0.11.1-cp313-cp313-win_arm64.whl", hash = "sha256:635ecd45c04e4c340d2187bcb1cea204c7cc9d32c1364d251564bf42e0e39c2d"}, - {file = "jiter-0.11.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d892b184da4d94d94ddb4031296931c74ec8b325513a541ebfd6dfb9ae89904b"}, - {file = "jiter-0.11.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa22c223a3041dacb2fcd37c70dfd648b44662b4a48e242592f95bda5ab09d58"}, - {file = "jiter-0.11.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:330e8e6a11ad4980cd66a0f4a3e0e2e0f646c911ce047014f984841924729789"}, - {file = "jiter-0.11.1-cp313-cp313t-win_amd64.whl", hash = "sha256:09e2e386ebf298547ca3a3704b729471f7ec666c2906c5c26c1a915ea24741ec"}, - {file = "jiter-0.11.1-cp313-cp313t-win_arm64.whl", hash = "sha256:fe4a431c291157e11cee7c34627990ea75e8d153894365a3bc84b7a959d23ca8"}, - {file = "jiter-0.11.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:0fa1f70da7a8a9713ff8e5f75ec3f90c0c870be6d526aa95e7c906f6a1c8c676"}, - {file = "jiter-0.11.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:569ee559e5046a42feb6828c55307cf20fe43308e3ae0d8e9e4f8d8634d99944"}, - {file = "jiter-0.11.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f69955fa1d92e81987f092b233f0be49d4c937da107b7f7dcf56306f1d3fcce9"}, - {file = "jiter-0.11.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:090f4c9d4a825e0fcbd0a2647c9a88a0f366b75654d982d95a9590745ff0c48d"}, - {file = "jiter-0.11.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bbf3d8cedf9e9d825233e0dcac28ff15c47b7c5512fdfe2e25fd5bbb6e6b0cee"}, - {file = "jiter-0.11.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2aa9b1958f9c30d3d1a558b75f0626733c60eb9b7774a86b34d88060be1e67fe"}, - {file = "jiter-0.11.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e42d1ca16590b768c5e7d723055acd2633908baacb3628dd430842e2e035aa90"}, - {file = "jiter-0.11.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5db4c2486a023820b701a17aec9c5a6173c5ba4393f26662f032f2de9c848b0f"}, - {file = "jiter-0.11.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:4573b78777ccfac954859a6eff45cbd9d281d80c8af049d0f1a3d9fc323d5c3a"}, - {file = "jiter-0.11.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:7593ac6f40831d7961cb67633c39b9fef6689a211d7919e958f45710504f52d3"}, - {file = "jiter-0.11.1-cp314-cp314-win32.whl", hash = "sha256:87202ec6ff9626ff5f9351507def98fcf0df60e9a146308e8ab221432228f4ea"}, - {file = "jiter-0.11.1-cp314-cp314-win_amd64.whl", hash = "sha256:a5dd268f6531a182c89d0dd9a3f8848e86e92dfff4201b77a18e6b98aa59798c"}, - {file = "jiter-0.11.1-cp314-cp314-win_arm64.whl", hash = "sha256:5d761f863f912a44748a21b5c4979c04252588ded8d1d2760976d2e42cd8d991"}, - {file = "jiter-0.11.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2cc5a3965285ddc33e0cab933e96b640bc9ba5940cea27ebbbf6695e72d6511c"}, - {file = "jiter-0.11.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b572b3636a784c2768b2342f36a23078c8d3aa6d8a30745398b1bab58a6f1a8"}, - {file = "jiter-0.11.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ad93e3d67a981f96596d65d2298fe8d1aa649deb5374a2fb6a434410ee11915e"}, - {file = "jiter-0.11.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a83097ce379e202dcc3fe3fc71a16d523d1ee9192c8e4e854158f96b3efe3f2f"}, - {file = "jiter-0.11.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7042c51e7fbeca65631eb0c332f90c0c082eab04334e7ccc28a8588e8e2804d9"}, - {file = "jiter-0.11.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a68d679c0e47649a61df591660507608adc2652442de7ec8276538ac46abe08"}, - {file = "jiter-0.11.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a1b0da75dbf4b6ec0b3c9e604d1ee8beaf15bc046fff7180f7d89e3cdbd3bb51"}, - {file = "jiter-0.11.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:69dd514bf0fa31c62147d6002e5ca2b3e7ef5894f5ac6f0a19752385f4e89437"}, - {file = "jiter-0.11.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:bb31ac0b339efa24c0ca606febd8b77ef11c58d09af1b5f2be4c99e907b11111"}, - {file = "jiter-0.11.1-cp314-cp314t-win32.whl", hash = "sha256:b2ce0d6156a1d3ad41da3eec63b17e03e296b78b0e0da660876fccfada86d2f7"}, - {file = "jiter-0.11.1-cp314-cp314t-win_amd64.whl", hash = "sha256:f4db07d127b54c4a2d43b4cf05ff0193e4f73e0dd90c74037e16df0b29f666e1"}, - {file = "jiter-0.11.1-cp314-cp314t-win_arm64.whl", hash = "sha256:28e4fdf2d7ebfc935523e50d1efa3970043cfaa161674fe66f9642409d001dfe"}, - {file = "jiter-0.11.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:baa99c8db49467527658bb479857344daf0a14dff909b7f6714579ac439d1253"}, - {file = "jiter-0.11.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:860fe55fa3b01ad0edf2adde1098247ff5c303d0121f9ce028c03d4f88c69502"}, - {file = "jiter-0.11.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:173dd349d99b6feaf5a25a6fbcaf3489a6f947708d808240587a23df711c67db"}, - {file = "jiter-0.11.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:14ac1dca837514cc946a6ac2c4995d9695303ecc754af70a3163d057d1a444ab"}, - {file = "jiter-0.11.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:69af47de5f93a231d5b85f7372d3284a5be8edb4cc758f006ec5a1406965ac5e"}, - {file = "jiter-0.11.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:685f8b3abd3bbd3e06e4dfe2429ff87fd5d7a782701151af99b1fcbd80e31b2b"}, - {file = "jiter-0.11.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d04afa2d4e5526e54ae8a58feea953b1844bf6e3526bc589f9de68e86d0ea01"}, - {file = "jiter-0.11.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1e92b927259035b50d8e11a8fdfe0ebd014d883e4552d37881643fa289a4bcf1"}, - {file = "jiter-0.11.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e7bd8be4fad8d4c5558b7801770cd2da6c072919c6f247cc5336edb143f25304"}, - {file = "jiter-0.11.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:121381a77a3c85987f3eba0d30ceaca9116f7463bedeec2fa79b2e7286b89b60"}, - {file = "jiter-0.11.1-cp39-cp39-win32.whl", hash = "sha256:160225407f6dfabdf9be1b44e22f06bc293a78a28ffa4347054698bd712dad06"}, - {file = "jiter-0.11.1-cp39-cp39-win_amd64.whl", hash = "sha256:028e0d59bcdfa1079f8df886cdaefc6f515c27a5288dec956999260c7e4a7cfd"}, - {file = "jiter-0.11.1-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:e642b5270e61dd02265866398707f90e365b5db2eb65a4f30c789d826682e1f6"}, - {file = "jiter-0.11.1-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:464ba6d000585e4e2fd1e891f31f1231f497273414f5019e27c00a4b8f7a24ad"}, - {file = "jiter-0.11.1-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:055568693ab35e0bf3a171b03bb40b2dcb10352359e0ab9b5ed0da2bf1eb6f6f"}, - {file = "jiter-0.11.1-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e0c69ea798d08a915ba4478113efa9e694971e410056392f4526d796f136d3fa"}, - {file = "jiter-0.11.1-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:0d4d6993edc83cf75e8c6828a8d6ce40a09ee87e38c7bfba6924f39e1337e21d"}, - {file = "jiter-0.11.1-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:f78d151c83a87a6cf5461d5ee55bc730dd9ae227377ac6f115b922989b95f838"}, - {file = "jiter-0.11.1-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9022974781155cd5521d5cb10997a03ee5e31e8454c9d999dcdccd253f2353f"}, - {file = "jiter-0.11.1-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18c77aaa9117510d5bdc6a946baf21b1f0cfa58ef04d31c8d016f206f2118960"}, - {file = "jiter-0.11.1.tar.gz", hash = "sha256:849dcfc76481c0ea0099391235b7ca97d7279e0fa4c86005457ac7c88e8b76dc"}, -] - [[package]] name = "jsonschema" version = "4.25.1" @@ -963,922 +375,13 @@ files = [ {file = "markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698"}, ] -[[package]] -name = "mpmath" -version = "1.3.0" -description = "Python library for arbitrary-precision floating-point arithmetic" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c"}, - {file = "mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f"}, -] - -[package.extras] -develop = ["codecov", "pycodestyle", "pytest (>=4.6)", "pytest-cov", "wheel"] -docs = ["sphinx"] -gmpy = ["gmpy2 (>=2.1.0a4) ; platform_python_implementation != \"PyPy\""] -tests = ["pytest (>=4.6)"] - -[[package]] -name = "networkx" -version = "3.4.2" -description = "Python package for creating and manipulating graphs and networks" -optional = false -python-versions = ">=3.10" -groups = ["main"] -markers = "python_version < \"3.13\"" -files = [ - {file = "networkx-3.4.2-py3-none-any.whl", hash = "sha256:df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f"}, - {file = "networkx-3.4.2.tar.gz", hash = "sha256:307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1"}, -] - -[package.extras] -default = ["matplotlib (>=3.7)", "numpy (>=1.24)", "pandas (>=2.0)", "scipy (>=1.10,!=1.11.0,!=1.11.1)"] -developer = ["changelist (==0.5)", "mypy (>=1.1)", "pre-commit (>=3.2)", "rtoml"] -doc = ["intersphinx-registry", "myst-nb (>=1.1)", "numpydoc (>=1.8.0)", "pillow (>=9.4)", "pydata-sphinx-theme (>=0.15)", "sphinx (>=7.3)", "sphinx-gallery (>=0.16)", "texext (>=0.6.7)"] -example = ["cairocffi (>=1.7)", "contextily (>=1.6)", "igraph (>=0.11)", "momepy (>=0.7.2)", "osmnx (>=1.9)", "scikit-learn (>=1.5)", "seaborn (>=0.13)"] -extra = ["lxml (>=4.6)", "pydot (>=3.0.1)", "pygraphviz (>=1.14)", "sympy (>=1.10)"] -test = ["pytest (>=7.2)", "pytest-cov (>=4.0)"] - -[[package]] -name = "networkx" -version = "3.5" -description = "Python package for creating and manipulating graphs and networks" -optional = false -python-versions = ">=3.11" -groups = ["main"] -markers = "python_version >= \"3.13\"" -files = [ - {file = "networkx-3.5-py3-none-any.whl", hash = "sha256:0030d386a9a06dee3565298b4a734b68589749a544acbb6c412dc9e2489ec6ec"}, - {file = "networkx-3.5.tar.gz", hash = "sha256:d4c6f9cf81f52d69230866796b82afbccdec3db7ae4fbd1b65ea750feed50037"}, -] - -[package.extras] -default = ["matplotlib (>=3.8)", "numpy (>=1.25)", "pandas (>=2.0)", "scipy (>=1.11.2)"] -developer = ["mypy (>=1.15)", "pre-commit (>=4.1)"] -doc = ["intersphinx-registry", "myst-nb (>=1.1)", "numpydoc (>=1.8.0)", "pillow (>=10)", "pydata-sphinx-theme (>=0.16)", "sphinx (>=8.0)", "sphinx-gallery (>=0.18)", "texext (>=0.6.7)"] -example = ["cairocffi (>=1.7)", "contextily (>=1.6)", "igraph (>=0.11)", "momepy (>=0.7.2)", "osmnx (>=2.0.0)", "scikit-learn (>=1.5)", "seaborn (>=0.13)"] -extra = ["lxml (>=4.6)", "pydot (>=3.0.1)", "pygraphviz (>=1.14)", "sympy (>=1.10)"] -test = ["pytest (>=7.2)", "pytest-cov (>=4.0)", "pytest-xdist (>=3.0)"] -test-extras = ["pytest-mpl", "pytest-randomly"] - -[[package]] -name = "numpy" -version = "2.2.6" -description = "Fundamental package for array computing in Python" -optional = false -python-versions = ">=3.10" -groups = ["main"] -markers = "python_version < \"3.13\"" -files = [ - {file = "numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb"}, - {file = "numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90"}, - {file = "numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163"}, - {file = "numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf"}, - {file = "numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83"}, - {file = "numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915"}, - {file = "numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680"}, - {file = "numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289"}, - {file = "numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d"}, - {file = "numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3"}, - {file = "numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae"}, - {file = "numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a"}, - {file = "numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42"}, - {file = "numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491"}, - {file = "numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a"}, - {file = "numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf"}, - {file = "numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1"}, - {file = "numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab"}, - {file = "numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47"}, - {file = "numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303"}, - {file = "numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff"}, - {file = "numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c"}, - {file = "numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3"}, - {file = "numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282"}, - {file = "numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87"}, - {file = "numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249"}, - {file = "numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49"}, - {file = "numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de"}, - {file = "numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4"}, - {file = "numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2"}, - {file = "numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84"}, - {file = "numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b"}, - {file = "numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d"}, - {file = "numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566"}, - {file = "numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f"}, - {file = "numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f"}, - {file = "numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868"}, - {file = "numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d"}, - {file = "numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd"}, - {file = "numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c"}, - {file = "numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6"}, - {file = "numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda"}, - {file = "numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40"}, - {file = "numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8"}, - {file = "numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f"}, - {file = "numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa"}, - {file = "numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571"}, - {file = "numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1"}, - {file = "numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff"}, - {file = "numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06"}, - {file = "numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d"}, - {file = "numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db"}, - {file = "numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543"}, - {file = "numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00"}, - {file = "numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd"}, -] - -[[package]] -name = "numpy" -version = "2.3.4" -description = "Fundamental package for array computing in Python" -optional = false -python-versions = ">=3.11" -groups = ["main"] -markers = "python_version >= \"3.13\"" -files = [ - {file = "numpy-2.3.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e78aecd2800b32e8347ce49316d3eaf04aed849cd5b38e0af39f829a4e59f5eb"}, - {file = "numpy-2.3.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7fd09cc5d65bda1e79432859c40978010622112e9194e581e3415a3eccc7f43f"}, - {file = "numpy-2.3.4-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:1b219560ae2c1de48ead517d085bc2d05b9433f8e49d0955c82e8cd37bd7bf36"}, - {file = "numpy-2.3.4-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:bafa7d87d4c99752d07815ed7a2c0964f8ab311eb8168f41b910bd01d15b6032"}, - {file = "numpy-2.3.4-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36dc13af226aeab72b7abad501d370d606326a0029b9f435eacb3b8c94b8a8b7"}, - {file = "numpy-2.3.4-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7b2f9a18b5ff9824a6af80de4f37f4ec3c2aab05ef08f51c77a093f5b89adda"}, - {file = "numpy-2.3.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9984bd645a8db6ca15d850ff996856d8762c51a2239225288f08f9050ca240a0"}, - {file = "numpy-2.3.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:64c5825affc76942973a70acf438a8ab618dbd692b84cd5ec40a0a0509edc09a"}, - {file = "numpy-2.3.4-cp311-cp311-win32.whl", hash = "sha256:ed759bf7a70342f7817d88376eb7142fab9fef8320d6019ef87fae05a99874e1"}, - {file = "numpy-2.3.4-cp311-cp311-win_amd64.whl", hash = "sha256:faba246fb30ea2a526c2e9645f61612341de1a83fb1e0c5edf4ddda5a9c10996"}, - {file = "numpy-2.3.4-cp311-cp311-win_arm64.whl", hash = "sha256:4c01835e718bcebe80394fd0ac66c07cbb90147ebbdad3dcecd3f25de2ae7e2c"}, - {file = "numpy-2.3.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ef1b5a3e808bc40827b5fa2c8196151a4c5abe110e1726949d7abddfe5c7ae11"}, - {file = "numpy-2.3.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c2f91f496a87235c6aaf6d3f3d89b17dba64996abadccb289f48456cff931ca9"}, - {file = "numpy-2.3.4-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:f77e5b3d3da652b474cc80a14084927a5e86a5eccf54ca8ca5cbd697bf7f2667"}, - {file = "numpy-2.3.4-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:8ab1c5f5ee40d6e01cbe96de5863e39b215a4d24e7d007cad56c7184fdf4aeef"}, - {file = "numpy-2.3.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77b84453f3adcb994ddbd0d1c5d11db2d6bda1a2b7fd5ac5bd4649d6f5dc682e"}, - {file = "numpy-2.3.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4121c5beb58a7f9e6dfdee612cb24f4df5cd4db6e8261d7f4d7450a997a65d6a"}, - {file = "numpy-2.3.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:65611ecbb00ac9846efe04db15cbe6186f562f6bb7e5e05f077e53a599225d16"}, - {file = "numpy-2.3.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dabc42f9c6577bcc13001b8810d300fe814b4cfbe8a92c873f269484594f9786"}, - {file = "numpy-2.3.4-cp312-cp312-win32.whl", hash = "sha256:a49d797192a8d950ca59ee2d0337a4d804f713bb5c3c50e8db26d49666e351dc"}, - {file = "numpy-2.3.4-cp312-cp312-win_amd64.whl", hash = "sha256:985f1e46358f06c2a09921e8921e2c98168ed4ae12ccd6e5e87a4f1857923f32"}, - {file = "numpy-2.3.4-cp312-cp312-win_arm64.whl", hash = "sha256:4635239814149e06e2cb9db3dd584b2fa64316c96f10656983b8026a82e6e4db"}, - {file = "numpy-2.3.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c090d4860032b857d94144d1a9976b8e36709e40386db289aaf6672de2a81966"}, - {file = "numpy-2.3.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a13fc473b6db0be619e45f11f9e81260f7302f8d180c49a22b6e6120022596b3"}, - {file = "numpy-2.3.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:3634093d0b428e6c32c3a69b78e554f0cd20ee420dcad5a9f3b2a63762ce4197"}, - {file = "numpy-2.3.4-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:043885b4f7e6e232d7df4f51ffdef8c36320ee9d5f227b380ea636722c7ed12e"}, - {file = "numpy-2.3.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4ee6a571d1e4f0ea6d5f22d6e5fbd6ed1dc2b18542848e1e7301bd190500c9d7"}, - {file = "numpy-2.3.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fc8a63918b04b8571789688b2780ab2b4a33ab44bfe8ccea36d3eba51228c953"}, - {file = "numpy-2.3.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:40cc556d5abbc54aabe2b1ae287042d7bdb80c08edede19f0c0afb36ae586f37"}, - {file = "numpy-2.3.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ecb63014bb7f4ce653f8be7f1df8cbc6093a5a2811211770f6606cc92b5a78fd"}, - {file = "numpy-2.3.4-cp313-cp313-win32.whl", hash = "sha256:e8370eb6925bb8c1c4264fec52b0384b44f675f191df91cbe0140ec9f0955646"}, - {file = "numpy-2.3.4-cp313-cp313-win_amd64.whl", hash = "sha256:56209416e81a7893036eea03abcb91c130643eb14233b2515c90dcac963fe99d"}, - {file = "numpy-2.3.4-cp313-cp313-win_arm64.whl", hash = "sha256:a700a4031bc0fd6936e78a752eefb79092cecad2599ea9c8039c548bc097f9bc"}, - {file = "numpy-2.3.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:86966db35c4040fdca64f0816a1c1dd8dbd027d90fca5a57e00e1ca4cd41b879"}, - {file = "numpy-2.3.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:838f045478638b26c375ee96ea89464d38428c69170360b23a1a50fa4baa3562"}, - {file = "numpy-2.3.4-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d7315ed1dab0286adca467377c8381cd748f3dc92235f22a7dfc42745644a96a"}, - {file = "numpy-2.3.4-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:84f01a4d18b2cc4ade1814a08e5f3c907b079c847051d720fad15ce37aa930b6"}, - {file = "numpy-2.3.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:817e719a868f0dacde4abdfc5c1910b301877970195db9ab6a5e2c4bd5b121f7"}, - {file = "numpy-2.3.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85e071da78d92a214212cacea81c6da557cab307f2c34b5f85b628e94803f9c0"}, - {file = "numpy-2.3.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2ec646892819370cf3558f518797f16597b4e4669894a2ba712caccc9da53f1f"}, - {file = "numpy-2.3.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:035796aaaddfe2f9664b9a9372f089cfc88bd795a67bd1bfe15e6e770934cf64"}, - {file = "numpy-2.3.4-cp313-cp313t-win32.whl", hash = "sha256:fea80f4f4cf83b54c3a051f2f727870ee51e22f0248d3114b8e755d160b38cfb"}, - {file = "numpy-2.3.4-cp313-cp313t-win_amd64.whl", hash = "sha256:15eea9f306b98e0be91eb344a94c0e630689ef302e10c2ce5f7e11905c704f9c"}, - {file = "numpy-2.3.4-cp313-cp313t-win_arm64.whl", hash = "sha256:b6c231c9c2fadbae4011ca5e7e83e12dc4a5072f1a1d85a0a7b3ed754d145a40"}, - {file = "numpy-2.3.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:81c3e6d8c97295a7360d367f9f8553973651b76907988bb6066376bc2252f24e"}, - {file = "numpy-2.3.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7c26b0b2bf58009ed1f38a641f3db4be8d960a417ca96d14e5b06df1506d41ff"}, - {file = "numpy-2.3.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:62b2198c438058a20b6704351b35a1d7db881812d8512d67a69c9de1f18ca05f"}, - {file = "numpy-2.3.4-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:9d729d60f8d53a7361707f4b68a9663c968882dd4f09e0d58c044c8bf5faee7b"}, - {file = "numpy-2.3.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd0c630cf256b0a7fd9d0a11c9413b42fef5101219ce6ed5a09624f5a65392c7"}, - {file = "numpy-2.3.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5e081bc082825f8b139f9e9fe42942cb4054524598aaeb177ff476cc76d09d2"}, - {file = "numpy-2.3.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:15fb27364ed84114438fff8aaf998c9e19adbeba08c0b75409f8c452a8692c52"}, - {file = "numpy-2.3.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:85d9fb2d8cd998c84d13a79a09cc0c1091648e848e4e6249b0ccd7f6b487fa26"}, - {file = "numpy-2.3.4-cp314-cp314-win32.whl", hash = "sha256:e73d63fd04e3a9d6bc187f5455d81abfad05660b212c8804bf3b407e984cd2bc"}, - {file = "numpy-2.3.4-cp314-cp314-win_amd64.whl", hash = "sha256:3da3491cee49cf16157e70f607c03a217ea6647b1cea4819c4f48e53d49139b9"}, - {file = "numpy-2.3.4-cp314-cp314-win_arm64.whl", hash = "sha256:6d9cd732068e8288dbe2717177320723ccec4fb064123f0caf9bbd90ab5be868"}, - {file = "numpy-2.3.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:22758999b256b595cf0b1d102b133bb61866ba5ceecf15f759623b64c020c9ec"}, - {file = "numpy-2.3.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9cb177bc55b010b19798dc5497d540dea67fd13a8d9e882b2dae71de0cf09eb3"}, - {file = "numpy-2.3.4-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:0f2bcc76f1e05e5ab58893407c63d90b2029908fa41f9f1cc51eecce936c3365"}, - {file = "numpy-2.3.4-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:8dc20bde86802df2ed8397a08d793da0ad7a5fd4ea3ac85d757bf5dd4ad7c252"}, - {file = "numpy-2.3.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e199c087e2aa71c8f9ce1cb7a8e10677dc12457e7cc1be4798632da37c3e86e"}, - {file = "numpy-2.3.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85597b2d25ddf655495e2363fe044b0ae999b75bc4d630dc0d886484b03a5eb0"}, - {file = "numpy-2.3.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:04a69abe45b49c5955923cf2c407843d1c85013b424ae8a560bba16c92fe44a0"}, - {file = "numpy-2.3.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e1708fac43ef8b419c975926ce1eaf793b0c13b7356cfab6ab0dc34c0a02ac0f"}, - {file = "numpy-2.3.4-cp314-cp314t-win32.whl", hash = "sha256:863e3b5f4d9915aaf1b8ec79ae560ad21f0b8d5e3adc31e73126491bb86dee1d"}, - {file = "numpy-2.3.4-cp314-cp314t-win_amd64.whl", hash = "sha256:962064de37b9aef801d33bc579690f8bfe6c5e70e29b61783f60bcba838a14d6"}, - {file = "numpy-2.3.4-cp314-cp314t-win_arm64.whl", hash = "sha256:8b5a9a39c45d852b62693d9b3f3e0fe052541f804296ff401a72a1b60edafb29"}, - {file = "numpy-2.3.4-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:6e274603039f924c0fe5cb73438fa9246699c78a6df1bd3decef9ae592ae1c05"}, - {file = "numpy-2.3.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d149aee5c72176d9ddbc6803aef9c0f6d2ceeea7626574fc68518da5476fa346"}, - {file = "numpy-2.3.4-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:6d34ed9db9e6395bb6cd33286035f73a59b058169733a9db9f85e650b88df37e"}, - {file = "numpy-2.3.4-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:fdebe771ca06bb8d6abce84e51dca9f7921fe6ad34a0c914541b063e9a68928b"}, - {file = "numpy-2.3.4-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:957e92defe6c08211eb77902253b14fe5b480ebc5112bc741fd5e9cd0608f847"}, - {file = "numpy-2.3.4-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13b9062e4f5c7ee5c7e5be96f29ba71bc5a37fed3d1d77c37390ae00724d296d"}, - {file = "numpy-2.3.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:81b3a59793523e552c4a96109dde028aa4448ae06ccac5a76ff6532a85558a7f"}, - {file = "numpy-2.3.4.tar.gz", hash = "sha256:a7d018bfedb375a8d979ac758b120ba846a7fe764911a64465fd87b8729f4a6a"}, -] - -[[package]] -name = "nvidia-cublas-cu12" -version = "12.6.4.1" -description = "CUBLAS native runtime libraries" -optional = false -python-versions = ">=3" -groups = ["main"] -markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and python_version >= \"3.13\"" -files = [ - {file = "nvidia_cublas_cu12-12.6.4.1-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:08ed2686e9875d01b58e3cb379c6896df8e76c75e0d4a7f7dace3d7b6d9ef8eb"}, - {file = "nvidia_cublas_cu12-12.6.4.1-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:235f728d6e2a409eddf1df58d5b0921cf80cfa9e72b9f2775ccb7b4a87984668"}, - {file = "nvidia_cublas_cu12-12.6.4.1-py3-none-win_amd64.whl", hash = "sha256:9e4fa264f4d8a4eb0cdbd34beadc029f453b3bafae02401e999cf3d5a5af75f8"}, -] - -[[package]] -name = "nvidia-cublas-cu12" -version = "12.8.4.1" -description = "CUBLAS native runtime libraries" -optional = false -python-versions = ">=3" -groups = ["main"] -markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and python_version < \"3.13\"" -files = [ - {file = "nvidia_cublas_cu12-12.8.4.1-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:b86f6dd8935884615a0683b663891d43781b819ac4f2ba2b0c9604676af346d0"}, - {file = "nvidia_cublas_cu12-12.8.4.1-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:8ac4e771d5a348c551b2a426eda6193c19aa630236b418086020df5ba9667142"}, - {file = "nvidia_cublas_cu12-12.8.4.1-py3-none-win_amd64.whl", hash = "sha256:47e9b82132fa8d2b4944e708049229601448aaad7e6f296f630f2d1a32de35af"}, -] - -[[package]] -name = "nvidia-cuda-cupti-cu12" -version = "12.6.80" -description = "CUDA profiling tools runtime libs." -optional = false -python-versions = ">=3" -groups = ["main"] -markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and python_version >= \"3.13\"" -files = [ - {file = "nvidia_cuda_cupti_cu12-12.6.80-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:166ee35a3ff1587f2490364f90eeeb8da06cd867bd5b701bf7f9a02b78bc63fc"}, - {file = "nvidia_cuda_cupti_cu12-12.6.80-py3-none-manylinux2014_aarch64.whl", hash = "sha256:358b4a1d35370353d52e12f0a7d1769fc01ff74a191689d3870b2123156184c4"}, - {file = "nvidia_cuda_cupti_cu12-12.6.80-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6768bad6cab4f19e8292125e5f1ac8aa7d1718704012a0e3272a6f61c4bce132"}, - {file = "nvidia_cuda_cupti_cu12-12.6.80-py3-none-manylinux2014_x86_64.whl", hash = "sha256:a3eff6cdfcc6a4c35db968a06fcadb061cbc7d6dde548609a941ff8701b98b73"}, - {file = "nvidia_cuda_cupti_cu12-12.6.80-py3-none-win_amd64.whl", hash = "sha256:bbe6ae76e83ce5251b56e8c8e61a964f757175682bbad058b170b136266ab00a"}, -] - -[[package]] -name = "nvidia-cuda-cupti-cu12" -version = "12.8.90" -description = "CUDA profiling tools runtime libs." -optional = false -python-versions = ">=3" -groups = ["main"] -markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and python_version < \"3.13\"" -files = [ - {file = "nvidia_cuda_cupti_cu12-12.8.90-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4412396548808ddfed3f17a467b104ba7751e6b58678a4b840675c56d21cf7ed"}, - {file = "nvidia_cuda_cupti_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ea0cb07ebda26bb9b29ba82cda34849e73c166c18162d3913575b0c9db9a6182"}, - {file = "nvidia_cuda_cupti_cu12-12.8.90-py3-none-win_amd64.whl", hash = "sha256:bb479dcdf7e6d4f8b0b01b115260399bf34154a1a2e9fe11c85c517d87efd98e"}, -] - -[[package]] -name = "nvidia-cuda-nvrtc-cu12" -version = "12.6.77" -description = "NVRTC native runtime libraries" -optional = false -python-versions = ">=3" -groups = ["main"] -markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and python_version >= \"3.13\"" -files = [ - {file = "nvidia_cuda_nvrtc_cu12-12.6.77-py3-none-manylinux2014_aarch64.whl", hash = "sha256:5847f1d6e5b757f1d2b3991a01082a44aad6f10ab3c5c0213fa3e25bddc25a13"}, - {file = "nvidia_cuda_nvrtc_cu12-12.6.77-py3-none-manylinux2014_x86_64.whl", hash = "sha256:35b0cc6ee3a9636d5409133e79273ce1f3fd087abb0532d2d2e8fff1fe9efc53"}, - {file = "nvidia_cuda_nvrtc_cu12-12.6.77-py3-none-win_amd64.whl", hash = "sha256:f7007dbd914c56bd80ea31bc43e8e149da38f68158f423ba845fc3292684e45a"}, -] - -[[package]] -name = "nvidia-cuda-nvrtc-cu12" -version = "12.8.93" -description = "NVRTC native runtime libraries" -optional = false -python-versions = ">=3" -groups = ["main"] -markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and python_version < \"3.13\"" -files = [ - {file = "nvidia_cuda_nvrtc_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:a7756528852ef889772a84c6cd89d41dfa74667e24cca16bb31f8f061e3e9994"}, - {file = "nvidia_cuda_nvrtc_cu12-12.8.93-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fc1fec1e1637854b4c0a65fb9a8346b51dd9ee69e61ebaccc82058441f15bce8"}, - {file = "nvidia_cuda_nvrtc_cu12-12.8.93-py3-none-win_amd64.whl", hash = "sha256:7a4b6b2904850fe78e0bd179c4b655c404d4bb799ef03ddc60804247099ae909"}, -] - -[[package]] -name = "nvidia-cuda-runtime-cu12" -version = "12.6.77" -description = "CUDA Runtime native Libraries" -optional = false -python-versions = ">=3" -groups = ["main"] -markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and python_version >= \"3.13\"" -files = [ - {file = "nvidia_cuda_runtime_cu12-12.6.77-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6116fad3e049e04791c0256a9778c16237837c08b27ed8c8401e2e45de8d60cd"}, - {file = "nvidia_cuda_runtime_cu12-12.6.77-py3-none-manylinux2014_aarch64.whl", hash = "sha256:d461264ecb429c84c8879a7153499ddc7b19b5f8d84c204307491989a365588e"}, - {file = "nvidia_cuda_runtime_cu12-12.6.77-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ba3b56a4f896141e25e19ab287cd71e52a6a0f4b29d0d31609f60e3b4d5219b7"}, - {file = "nvidia_cuda_runtime_cu12-12.6.77-py3-none-manylinux2014_x86_64.whl", hash = "sha256:a84d15d5e1da416dd4774cb42edf5e954a3e60cc945698dc1d5be02321c44dc8"}, - {file = "nvidia_cuda_runtime_cu12-12.6.77-py3-none-win_amd64.whl", hash = "sha256:86c58044c824bf3c173c49a2dbc7a6c8b53cb4e4dca50068be0bf64e9dab3f7f"}, -] - -[[package]] -name = "nvidia-cuda-runtime-cu12" -version = "12.8.90" -description = "CUDA Runtime native Libraries" -optional = false -python-versions = ">=3" -groups = ["main"] -markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and python_version < \"3.13\"" -files = [ - {file = "nvidia_cuda_runtime_cu12-12.8.90-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:52bf7bbee900262ffefe5e9d5a2a69a30d97e2bc5bb6cc866688caa976966e3d"}, - {file = "nvidia_cuda_runtime_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:adade8dcbd0edf427b7204d480d6066d33902cab2a4707dcfc48a2d0fd44ab90"}, - {file = "nvidia_cuda_runtime_cu12-12.8.90-py3-none-win_amd64.whl", hash = "sha256:c0c6027f01505bfed6c3b21ec546f69c687689aad5f1a377554bc6ca4aa993a8"}, -] - -[[package]] -name = "nvidia-cudnn-cu12" -version = "9.5.1.17" -description = "cuDNN runtime libraries" -optional = false -python-versions = ">=3" -groups = ["main"] -markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and python_version >= \"3.13\"" -files = [ - {file = "nvidia_cudnn_cu12-9.5.1.17-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:9fd4584468533c61873e5fda8ca41bac3a38bcb2d12350830c69b0a96a7e4def"}, - {file = "nvidia_cudnn_cu12-9.5.1.17-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:30ac3869f6db17d170e0e556dd6cc5eee02647abc31ca856634d5a40f82c15b2"}, - {file = "nvidia_cudnn_cu12-9.5.1.17-py3-none-win_amd64.whl", hash = "sha256:d7af0f8a4f3b4b9dbb3122f2ef553b45694ed9c384d5a75bab197b8eefb79ab8"}, -] - -[package.dependencies] -nvidia-cublas-cu12 = "*" - -[[package]] -name = "nvidia-cudnn-cu12" -version = "9.10.2.21" -description = "cuDNN runtime libraries" -optional = false -python-versions = ">=3" -groups = ["main"] -markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and python_version < \"3.13\"" -files = [ - {file = "nvidia_cudnn_cu12-9.10.2.21-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:c9132cc3f8958447b4910a1720036d9eff5928cc3179b0a51fb6d167c6cc87d8"}, - {file = "nvidia_cudnn_cu12-9.10.2.21-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:949452be657fa16687d0930933f032835951ef0892b37d2d53824d1a84dc97a8"}, - {file = "nvidia_cudnn_cu12-9.10.2.21-py3-none-win_amd64.whl", hash = "sha256:c6288de7d63e6cf62988f0923f96dc339cea362decb1bf5b3141883392a7d65e"}, -] - -[package.dependencies] -nvidia-cublas-cu12 = "*" - -[[package]] -name = "nvidia-cufft-cu12" -version = "11.3.0.4" -description = "CUFFT native runtime libraries" -optional = false -python-versions = ">=3" -groups = ["main"] -markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and python_version >= \"3.13\"" -files = [ - {file = "nvidia_cufft_cu12-11.3.0.4-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d16079550df460376455cba121db6564089176d9bac9e4f360493ca4741b22a6"}, - {file = "nvidia_cufft_cu12-11.3.0.4-py3-none-manylinux2014_aarch64.whl", hash = "sha256:8510990de9f96c803a051822618d42bf6cb8f069ff3f48d93a8486efdacb48fb"}, - {file = "nvidia_cufft_cu12-11.3.0.4-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ccba62eb9cef5559abd5e0d54ceed2d9934030f51163df018532142a8ec533e5"}, - {file = "nvidia_cufft_cu12-11.3.0.4-py3-none-manylinux2014_x86_64.whl", hash = "sha256:768160ac89f6f7b459bee747e8d175dbf53619cfe74b2a5636264163138013ca"}, - {file = "nvidia_cufft_cu12-11.3.0.4-py3-none-win_amd64.whl", hash = "sha256:6048ebddfb90d09d2707efb1fd78d4e3a77cb3ae4dc60e19aab6be0ece2ae464"}, -] - -[package.dependencies] -nvidia-nvjitlink-cu12 = "*" - -[[package]] -name = "nvidia-cufft-cu12" -version = "11.3.3.83" -description = "CUFFT native runtime libraries" -optional = false -python-versions = ">=3" -groups = ["main"] -markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and python_version < \"3.13\"" -files = [ - {file = "nvidia_cufft_cu12-11.3.3.83-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:848ef7224d6305cdb2a4df928759dca7b1201874787083b6e7550dd6765ce69a"}, - {file = "nvidia_cufft_cu12-11.3.3.83-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d2dd21ec0b88cf61b62e6b43564355e5222e4a3fb394cac0db101f2dd0d4f74"}, - {file = "nvidia_cufft_cu12-11.3.3.83-py3-none-win_amd64.whl", hash = "sha256:7a64a98ef2a7c47f905aaf8931b69a3a43f27c55530c698bb2ed7c75c0b42cb7"}, -] - -[package.dependencies] -nvidia-nvjitlink-cu12 = "*" - -[[package]] -name = "nvidia-cufile-cu12" -version = "1.11.1.6" -description = "cuFile GPUDirect libraries" -optional = false -python-versions = ">=3" -groups = ["main"] -markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and python_version >= \"3.13\"" -files = [ - {file = "nvidia_cufile_cu12-1.11.1.6-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc23469d1c7e52ce6c1d55253273d32c565dd22068647f3aa59b3c6b005bf159"}, - {file = "nvidia_cufile_cu12-1.11.1.6-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:8f57a0051dcf2543f6dc2b98a98cb2719c37d3cee1baba8965d57f3bbc90d4db"}, -] - -[[package]] -name = "nvidia-cufile-cu12" -version = "1.13.1.3" -description = "cuFile GPUDirect libraries" -optional = false -python-versions = ">=3" -groups = ["main"] -markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and python_version < \"3.13\"" -files = [ - {file = "nvidia_cufile_cu12-1.13.1.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1d069003be650e131b21c932ec3d8969c1715379251f8d23a1860554b1cb24fc"}, - {file = "nvidia_cufile_cu12-1.13.1.3-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:4beb6d4cce47c1a0f1013d72e02b0994730359e17801d395bdcbf20cfb3bb00a"}, -] - -[[package]] -name = "nvidia-curand-cu12" -version = "10.3.7.77" -description = "CURAND native runtime libraries" -optional = false -python-versions = ">=3" -groups = ["main"] -markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and python_version >= \"3.13\"" -files = [ - {file = "nvidia_curand_cu12-10.3.7.77-py3-none-manylinux2014_aarch64.whl", hash = "sha256:6e82df077060ea28e37f48a3ec442a8f47690c7499bff392a5938614b56c98d8"}, - {file = "nvidia_curand_cu12-10.3.7.77-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a42cd1344297f70b9e39a1e4f467a4e1c10f1da54ff7a85c12197f6c652c8bdf"}, - {file = "nvidia_curand_cu12-10.3.7.77-py3-none-manylinux2014_x86_64.whl", hash = "sha256:99f1a32f1ac2bd134897fc7a203f779303261268a65762a623bf30cc9fe79117"}, - {file = "nvidia_curand_cu12-10.3.7.77-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:7b2ed8e95595c3591d984ea3603dd66fe6ce6812b886d59049988a712ed06b6e"}, - {file = "nvidia_curand_cu12-10.3.7.77-py3-none-win_amd64.whl", hash = "sha256:6d6d935ffba0f3d439b7cd968192ff068fafd9018dbf1b85b37261b13cfc9905"}, -] - -[[package]] -name = "nvidia-curand-cu12" -version = "10.3.9.90" -description = "CURAND native runtime libraries" -optional = false -python-versions = ">=3" -groups = ["main"] -markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and python_version < \"3.13\"" -files = [ - {file = "nvidia_curand_cu12-10.3.9.90-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:dfab99248034673b779bc6decafdc3404a8a6f502462201f2f31f11354204acd"}, - {file = "nvidia_curand_cu12-10.3.9.90-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:b32331d4f4df5d6eefa0554c565b626c7216f87a06a4f56fab27c3b68a830ec9"}, - {file = "nvidia_curand_cu12-10.3.9.90-py3-none-win_amd64.whl", hash = "sha256:f149a8ca457277da854f89cf282d6ef43176861926c7ac85b2a0fbd237c587ec"}, -] - -[[package]] -name = "nvidia-cusolver-cu12" -version = "11.7.1.2" -description = "CUDA solver native runtime libraries" -optional = false -python-versions = ">=3" -groups = ["main"] -markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and python_version >= \"3.13\"" -files = [ - {file = "nvidia_cusolver_cu12-11.7.1.2-py3-none-manylinux2014_aarch64.whl", hash = "sha256:0ce237ef60acde1efc457335a2ddadfd7610b892d94efee7b776c64bb1cac9e0"}, - {file = "nvidia_cusolver_cu12-11.7.1.2-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e9e49843a7707e42022babb9bcfa33c29857a93b88020c4e4434656a655b698c"}, - {file = "nvidia_cusolver_cu12-11.7.1.2-py3-none-manylinux2014_x86_64.whl", hash = "sha256:6cf28f17f64107a0c4d7802be5ff5537b2130bfc112f25d5a30df227058ca0e6"}, - {file = "nvidia_cusolver_cu12-11.7.1.2-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:dbbe4fc38ec1289c7e5230e16248365e375c3673c9c8bac5796e2e20db07f56e"}, - {file = "nvidia_cusolver_cu12-11.7.1.2-py3-none-win_amd64.whl", hash = "sha256:6813f9d8073f555444a8705f3ab0296d3e1cb37a16d694c5fc8b862a0d8706d7"}, -] - -[package.dependencies] -nvidia-cublas-cu12 = "*" -nvidia-cusparse-cu12 = "*" -nvidia-nvjitlink-cu12 = "*" - -[[package]] -name = "nvidia-cusolver-cu12" -version = "11.7.3.90" -description = "CUDA solver native runtime libraries" -optional = false -python-versions = ">=3" -groups = ["main"] -markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and python_version < \"3.13\"" -files = [ - {file = "nvidia_cusolver_cu12-11.7.3.90-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:db9ed69dbef9715071232caa9b69c52ac7de3a95773c2db65bdba85916e4e5c0"}, - {file = "nvidia_cusolver_cu12-11.7.3.90-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:4376c11ad263152bd50ea295c05370360776f8c3427b30991df774f9fb26c450"}, - {file = "nvidia_cusolver_cu12-11.7.3.90-py3-none-win_amd64.whl", hash = "sha256:4a550db115fcabc4d495eb7d39ac8b58d4ab5d8e63274d3754df1c0ad6a22d34"}, -] - -[package.dependencies] -nvidia-cublas-cu12 = "*" -nvidia-cusparse-cu12 = "*" -nvidia-nvjitlink-cu12 = "*" - -[[package]] -name = "nvidia-cusparse-cu12" -version = "12.5.4.2" -description = "CUSPARSE native runtime libraries" -optional = false -python-versions = ">=3" -groups = ["main"] -markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and python_version >= \"3.13\"" -files = [ - {file = "nvidia_cusparse_cu12-12.5.4.2-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d25b62fb18751758fe3c93a4a08eff08effedfe4edf1c6bb5afd0890fe88f887"}, - {file = "nvidia_cusparse_cu12-12.5.4.2-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7aa32fa5470cf754f72d1116c7cbc300b4e638d3ae5304cfa4a638a5b87161b1"}, - {file = "nvidia_cusparse_cu12-12.5.4.2-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7556d9eca156e18184b94947ade0fba5bb47d69cec46bf8660fd2c71a4b48b73"}, - {file = "nvidia_cusparse_cu12-12.5.4.2-py3-none-manylinux2014_x86_64.whl", hash = "sha256:23749a6571191a215cb74d1cdbff4a86e7b19f1200c071b3fcf844a5bea23a2f"}, - {file = "nvidia_cusparse_cu12-12.5.4.2-py3-none-win_amd64.whl", hash = "sha256:4acb8c08855a26d737398cba8fb6f8f5045d93f82612b4cfd84645a2332ccf20"}, -] - -[package.dependencies] -nvidia-nvjitlink-cu12 = "*" - -[[package]] -name = "nvidia-cusparse-cu12" -version = "12.5.8.93" -description = "CUSPARSE native runtime libraries" -optional = false -python-versions = ">=3" -groups = ["main"] -markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and python_version < \"3.13\"" -files = [ - {file = "nvidia_cusparse_cu12-12.5.8.93-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9b6c161cb130be1a07a27ea6923df8141f3c295852f4b260c65f18f3e0a091dc"}, - {file = "nvidia_cusparse_cu12-12.5.8.93-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ec05d76bbbd8b61b06a80e1eaf8cf4959c3d4ce8e711b65ebd0443bb0ebb13b"}, - {file = "nvidia_cusparse_cu12-12.5.8.93-py3-none-win_amd64.whl", hash = "sha256:9a33604331cb2cac199f2e7f5104dfbb8a5a898c367a53dfda9ff2acb6b6b4dd"}, -] - -[package.dependencies] -nvidia-nvjitlink-cu12 = "*" - -[[package]] -name = "nvidia-cusparselt-cu12" -version = "0.6.3" -description = "NVIDIA cuSPARSELt" -optional = false -python-versions = "*" -groups = ["main"] -markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and python_version >= \"3.13\"" -files = [ - {file = "nvidia_cusparselt_cu12-0.6.3-py3-none-manylinux2014_aarch64.whl", hash = "sha256:8371549623ba601a06322af2133c4a44350575f5a3108fb75f3ef20b822ad5f1"}, - {file = "nvidia_cusparselt_cu12-0.6.3-py3-none-manylinux2014_x86_64.whl", hash = "sha256:e5c8a26c36445dd2e6812f1177978a24e2d37cacce7e090f297a688d1ec44f46"}, - {file = "nvidia_cusparselt_cu12-0.6.3-py3-none-win_amd64.whl", hash = "sha256:3b325bcbd9b754ba43df5a311488fca11a6b5dc3d11df4d190c000cf1a0765c7"}, -] - -[[package]] -name = "nvidia-cusparselt-cu12" -version = "0.7.1" -description = "NVIDIA cuSPARSELt" -optional = false -python-versions = "*" -groups = ["main"] -markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and python_version < \"3.13\"" -files = [ - {file = "nvidia_cusparselt_cu12-0.7.1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:8878dce784d0fac90131b6817b607e803c36e629ba34dc5b433471382196b6a5"}, - {file = "nvidia_cusparselt_cu12-0.7.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:f1bb701d6b930d5a7cea44c19ceb973311500847f81b634d802b7b539dc55623"}, - {file = "nvidia_cusparselt_cu12-0.7.1-py3-none-win_amd64.whl", hash = "sha256:f67fbb5831940ec829c9117b7f33807db9f9678dc2a617fbe781cac17b4e1075"}, -] - -[[package]] -name = "nvidia-nccl-cu12" -version = "2.26.2" -description = "NVIDIA Collective Communication Library (NCCL) Runtime" -optional = false -python-versions = ">=3" -groups = ["main"] -markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and python_version >= \"3.13\"" -files = [ - {file = "nvidia_nccl_cu12-2.26.2-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c196e95e832ad30fbbb50381eb3cbd1fadd5675e587a548563993609af19522"}, - {file = "nvidia_nccl_cu12-2.26.2-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:694cf3879a206553cc9d7dbda76b13efaf610fdb70a50cba303de1b0d1530ac6"}, -] - -[[package]] -name = "nvidia-nccl-cu12" -version = "2.27.5" -description = "NVIDIA Collective Communication Library (NCCL) Runtime" -optional = false -python-versions = ">=3" -groups = ["main"] -markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and python_version < \"3.13\"" -files = [ - {file = "nvidia_nccl_cu12-2.27.5-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:31432ad4d1fb1004eb0c56203dc9bc2178a1ba69d1d9e02d64a6938ab5e40e7a"}, - {file = "nvidia_nccl_cu12-2.27.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ad730cf15cb5d25fe849c6e6ca9eb5b76db16a80f13f425ac68d8e2e55624457"}, -] - -[[package]] -name = "nvidia-nvjitlink-cu12" -version = "12.6.85" -description = "Nvidia JIT LTO Library" -optional = false -python-versions = ">=3" -groups = ["main"] -markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and python_version >= \"3.13\"" -files = [ - {file = "nvidia_nvjitlink_cu12-12.6.85-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:eedc36df9e88b682efe4309aa16b5b4e78c2407eac59e8c10a6a47535164369a"}, - {file = "nvidia_nvjitlink_cu12-12.6.85-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cf4eaa7d4b6b543ffd69d6abfb11efdeb2db48270d94dfd3a452c24150829e41"}, - {file = "nvidia_nvjitlink_cu12-12.6.85-py3-none-win_amd64.whl", hash = "sha256:e61120e52ed675747825cdd16febc6a0730537451d867ee58bee3853b1b13d1c"}, -] - -[[package]] -name = "nvidia-nvjitlink-cu12" -version = "12.8.93" -description = "Nvidia JIT LTO Library" -optional = false -python-versions = ">=3" -groups = ["main"] -markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and python_version < \"3.13\"" -files = [ - {file = "nvidia_nvjitlink_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:81ff63371a7ebd6e6451970684f916be2eab07321b73c9d244dc2b4da7f73b88"}, - {file = "nvidia_nvjitlink_cu12-12.8.93-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:adccd7161ace7261e01bb91e44e88da350895c270d23f744f0820c818b7229e7"}, - {file = "nvidia_nvjitlink_cu12-12.8.93-py3-none-win_amd64.whl", hash = "sha256:bd93fbeeee850917903583587f4fc3a4eafa022e34572251368238ab5e6bd67f"}, -] - -[[package]] -name = "nvidia-nvshmem-cu12" -version = "3.3.20" -description = "NVSHMEM creates a global address space that provides efficient and scalable communication for NVIDIA GPU clusters." -optional = false -python-versions = ">=3" -groups = ["main"] -markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and python_version < \"3.13\"" -files = [ - {file = "nvidia_nvshmem_cu12-3.3.20-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0b0b960da3842212758e4fa4696b94f129090b30e5122fea3c5345916545cff0"}, - {file = "nvidia_nvshmem_cu12-3.3.20-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d00f26d3f9b2e3c3065be895e3059d6479ea5c638a3f38c9fec49b1b9dd7c1e5"}, -] - -[[package]] -name = "nvidia-nvtx-cu12" -version = "12.6.77" -description = "NVIDIA Tools Extension" -optional = false -python-versions = ">=3" -groups = ["main"] -markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and python_version >= \"3.13\"" -files = [ - {file = "nvidia_nvtx_cu12-12.6.77-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f44f8d86bb7d5629988d61c8d3ae61dddb2015dee142740536bc7481b022fe4b"}, - {file = "nvidia_nvtx_cu12-12.6.77-py3-none-manylinux2014_aarch64.whl", hash = "sha256:adcaabb9d436c9761fca2b13959a2d237c5f9fd406c8e4b723c695409ff88059"}, - {file = "nvidia_nvtx_cu12-12.6.77-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b90bed3df379fa79afbd21be8e04a0314336b8ae16768b58f2d34cb1d04cd7d2"}, - {file = "nvidia_nvtx_cu12-12.6.77-py3-none-manylinux2014_x86_64.whl", hash = "sha256:6574241a3ec5fdc9334353ab8c479fe75841dbe8f4532a8fc97ce63503330ba1"}, - {file = "nvidia_nvtx_cu12-12.6.77-py3-none-win_amd64.whl", hash = "sha256:2fb11a4af04a5e6c84073e6404d26588a34afd35379f0855a99797897efa75c0"}, -] - -[[package]] -name = "nvidia-nvtx-cu12" -version = "12.8.90" -description = "NVIDIA Tools Extension" -optional = false -python-versions = ">=3" -groups = ["main"] -markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and python_version < \"3.13\"" -files = [ - {file = "nvidia_nvtx_cu12-12.8.90-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d7ad891da111ebafbf7e015d34879f7112832fc239ff0d7d776b6cb685274615"}, - {file = "nvidia_nvtx_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5b17e2001cc0d751a5bc2c6ec6d26ad95913324a4adb86788c944f8ce9ba441f"}, - {file = "nvidia_nvtx_cu12-12.8.90-py3-none-win_amd64.whl", hash = "sha256:619c8304aedc69f02ea82dd244541a83c3d9d40993381b3b590f1adaed3db41e"}, -] - -[[package]] -name = "openai" -version = "1.109.1" -description = "The official Python library for the openai API" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "openai-1.109.1-py3-none-any.whl", hash = "sha256:6bcaf57086cf59159b8e27447e4e7dd019db5d29a438072fbd49c290c7e65315"}, - {file = "openai-1.109.1.tar.gz", hash = "sha256:d173ed8dbca665892a6db099b4a2dfac624f94d20a93f46eb0b56aae940ed869"}, -] - -[package.dependencies] -anyio = ">=3.5.0,<5" -distro = ">=1.7.0,<2" -httpx = ">=0.23.0,<1" -jiter = ">=0.4.0,<1" -pydantic = ">=1.9.0,<3" -sniffio = "*" -tqdm = ">4" -typing-extensions = ">=4.11,<5" - -[package.extras] -aiohttp = ["aiohttp", "httpx-aiohttp (>=0.1.8)"] -datalib = ["numpy (>=1)", "pandas (>=1.2.3)", "pandas-stubs (>=1.1.0.11)"] -realtime = ["websockets (>=13,<16)"] -voice-helpers = ["numpy (>=2.0.2)", "sounddevice (>=0.5.1)"] - -[[package]] -name = "opentelemetry-api" -version = "1.38.0" -description = "OpenTelemetry Python API" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "opentelemetry_api-1.38.0-py3-none-any.whl", hash = "sha256:2891b0197f47124454ab9f0cf58f3be33faca394457ac3e09daba13ff50aa582"}, - {file = "opentelemetry_api-1.38.0.tar.gz", hash = "sha256:f4c193b5e8acb0912b06ac5b16321908dd0843d75049c091487322284a3eea12"}, -] - -[package.dependencies] -importlib-metadata = ">=6.0,<8.8.0" -typing-extensions = ">=4.5.0" - -[[package]] -name = "opentelemetry-exporter-otlp" -version = "1.38.0" -description = "OpenTelemetry Collector Exporters" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "opentelemetry_exporter_otlp-1.38.0-py3-none-any.whl", hash = "sha256:bc6562cef229fac8887ed7109fc5abc52315f39d9c03fd487bb8b4ef8fbbc231"}, - {file = "opentelemetry_exporter_otlp-1.38.0.tar.gz", hash = "sha256:2f55acdd475e4136117eff20fbf1b9488b1b0b665ab64407516e1ac06f9c3f9d"}, -] - -[package.dependencies] -opentelemetry-exporter-otlp-proto-grpc = "1.38.0" -opentelemetry-exporter-otlp-proto-http = "1.38.0" - -[[package]] -name = "opentelemetry-exporter-otlp-proto-common" -version = "1.38.0" -description = "OpenTelemetry Protobuf encoding" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "opentelemetry_exporter_otlp_proto_common-1.38.0-py3-none-any.whl", hash = "sha256:03cb76ab213300fe4f4c62b7d8f17d97fcfd21b89f0b5ce38ea156327ddda74a"}, - {file = "opentelemetry_exporter_otlp_proto_common-1.38.0.tar.gz", hash = "sha256:e333278afab4695aa8114eeb7bf4e44e65c6607d54968271a249c180b2cb605c"}, -] - -[package.dependencies] -opentelemetry-proto = "1.38.0" - -[[package]] -name = "opentelemetry-exporter-otlp-proto-grpc" -version = "1.38.0" -description = "OpenTelemetry Collector Protobuf over gRPC Exporter" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "opentelemetry_exporter_otlp_proto_grpc-1.38.0-py3-none-any.whl", hash = "sha256:7c49fd9b4bd0dbe9ba13d91f764c2d20b0025649a6e4ac35792fb8d84d764bc7"}, - {file = "opentelemetry_exporter_otlp_proto_grpc-1.38.0.tar.gz", hash = "sha256:2473935e9eac71f401de6101d37d6f3f0f1831db92b953c7dcc912536158ebd6"}, -] - -[package.dependencies] -googleapis-common-protos = ">=1.57,<2.0" -grpcio = [ - {version = ">=1.63.2,<2.0.0", markers = "python_version < \"3.13\""}, - {version = ">=1.66.2,<2.0.0", markers = "python_version >= \"3.13\""}, -] -opentelemetry-api = ">=1.15,<2.0" -opentelemetry-exporter-otlp-proto-common = "1.38.0" -opentelemetry-proto = "1.38.0" -opentelemetry-sdk = ">=1.38.0,<1.39.0" -typing-extensions = ">=4.6.0" - -[[package]] -name = "opentelemetry-exporter-otlp-proto-http" -version = "1.38.0" -description = "OpenTelemetry Collector Protobuf over HTTP Exporter" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "opentelemetry_exporter_otlp_proto_http-1.38.0-py3-none-any.whl", hash = "sha256:84b937305edfc563f08ec69b9cb2298be8188371217e867c1854d77198d0825b"}, - {file = "opentelemetry_exporter_otlp_proto_http-1.38.0.tar.gz", hash = "sha256:f16bd44baf15cbe07633c5112ffc68229d0edbeac7b37610be0b2def4e21e90b"}, -] - -[package.dependencies] -googleapis-common-protos = ">=1.52,<2.0" -opentelemetry-api = ">=1.15,<2.0" -opentelemetry-exporter-otlp-proto-common = "1.38.0" -opentelemetry-proto = "1.38.0" -opentelemetry-sdk = ">=1.38.0,<1.39.0" -requests = ">=2.7,<3.0" -typing-extensions = ">=4.5.0" - -[[package]] -name = "opentelemetry-instrumentation" -version = "0.59b0" -description = "Instrumentation Tools & Auto Instrumentation for OpenTelemetry Python" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "opentelemetry_instrumentation-0.59b0-py3-none-any.whl", hash = "sha256:44082cc8fe56b0186e87ee8f7c17c327c4c2ce93bdbe86496e600985d74368ee"}, - {file = "opentelemetry_instrumentation-0.59b0.tar.gz", hash = "sha256:6010f0faaacdaf7c4dff8aac84e226d23437b331dcda7e70367f6d73a7db1adc"}, -] - -[package.dependencies] -opentelemetry-api = ">=1.4,<2.0" -opentelemetry-semantic-conventions = "0.59b0" -packaging = ">=18.0" -wrapt = ">=1.0.0,<2.0.0" - -[[package]] -name = "opentelemetry-instrumentation-asgi" -version = "0.59b0" -description = "ASGI instrumentation for OpenTelemetry" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "opentelemetry_instrumentation_asgi-0.59b0-py3-none-any.whl", hash = "sha256:ba9703e09d2c33c52fa798171f344c8123488fcd45017887981df088452d3c53"}, - {file = "opentelemetry_instrumentation_asgi-0.59b0.tar.gz", hash = "sha256:2509d6fe9fd829399ce3536e3a00426c7e3aa359fc1ed9ceee1628b56da40e7a"}, -] - -[package.dependencies] -asgiref = ">=3.0,<4.0" -opentelemetry-api = ">=1.12,<2.0" -opentelemetry-instrumentation = "0.59b0" -opentelemetry-semantic-conventions = "0.59b0" -opentelemetry-util-http = "0.59b0" - -[package.extras] -instruments = ["asgiref (>=3.0,<4.0)"] - -[[package]] -name = "opentelemetry-instrumentation-fastapi" -version = "0.59b0" -description = "OpenTelemetry FastAPI Instrumentation" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "opentelemetry_instrumentation_fastapi-0.59b0-py3-none-any.whl", hash = "sha256:0d8d00ff7d25cca40a4b2356d1d40a8f001e0668f60c102f5aa6bb721d660c4f"}, - {file = "opentelemetry_instrumentation_fastapi-0.59b0.tar.gz", hash = "sha256:e8fe620cfcca96a7d634003df1bc36a42369dedcdd6893e13fb5903aeeb89b2b"}, -] - -[package.dependencies] -opentelemetry-api = ">=1.12,<2.0" -opentelemetry-instrumentation = "0.59b0" -opentelemetry-instrumentation-asgi = "0.59b0" -opentelemetry-semantic-conventions = "0.59b0" -opentelemetry-util-http = "0.59b0" - -[package.extras] -instruments = ["fastapi (>=0.92,<1.0)"] - -[[package]] -name = "opentelemetry-proto" -version = "1.38.0" -description = "OpenTelemetry Python Proto" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "opentelemetry_proto-1.38.0-py3-none-any.whl", hash = "sha256:b6ebe54d3217c42e45462e2a1ae28c3e2bf2ec5a5645236a490f55f45f1a0a18"}, - {file = "opentelemetry_proto-1.38.0.tar.gz", hash = "sha256:88b161e89d9d372ce723da289b7da74c3a8354a8e5359992be813942969ed468"}, -] - -[package.dependencies] -protobuf = ">=5.0,<7.0" - -[[package]] -name = "opentelemetry-sdk" -version = "1.38.0" -description = "OpenTelemetry Python SDK" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "opentelemetry_sdk-1.38.0-py3-none-any.whl", hash = "sha256:1c66af6564ecc1553d72d811a01df063ff097cdc82ce188da9951f93b8d10f6b"}, - {file = "opentelemetry_sdk-1.38.0.tar.gz", hash = "sha256:93df5d4d871ed09cb4272305be4d996236eedb232253e3ab864c8620f051cebe"}, -] - -[package.dependencies] -opentelemetry-api = "1.38.0" -opentelemetry-semantic-conventions = "0.59b0" -typing-extensions = ">=4.5.0" - -[[package]] -name = "opentelemetry-semantic-conventions" -version = "0.59b0" -description = "OpenTelemetry Semantic Conventions" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "opentelemetry_semantic_conventions-0.59b0-py3-none-any.whl", hash = "sha256:35d3b8833ef97d614136e253c1da9342b4c3c083bbaf29ce31d572a1c3825eed"}, - {file = "opentelemetry_semantic_conventions-0.59b0.tar.gz", hash = "sha256:7a6db3f30d70202d5bf9fa4b69bc866ca6a30437287de6c510fb594878aed6b0"}, -] - -[package.dependencies] -opentelemetry-api = "1.38.0" -typing-extensions = ">=4.5.0" - -[[package]] -name = "opentelemetry-util-http" -version = "0.59b0" -description = "Web util for OpenTelemetry" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "opentelemetry_util_http-0.59b0-py3-none-any.whl", hash = "sha256:6d036a07563bce87bf521839c0671b507a02a0d39d7ea61b88efa14c6e25355d"}, - {file = "opentelemetry_util_http-0.59b0.tar.gz", hash = "sha256:ae66ee91be31938d832f3b4bc4eb8a911f6eddd38969c4a871b1230db2a0a560"}, -] - -[[package]] -name = "overrides" -version = "7.7.0" -description = "A decorator to automatically detect mismatch when overriding a method." -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "overrides-7.7.0-py3-none-any.whl", hash = "sha256:c7ed9d062f78b8e4c1a7b70bd8796b35ead4d9f510227ef9c5dc7626c60d7e49"}, - {file = "overrides-7.7.0.tar.gz", hash = "sha256:55158fa3d93b98cc75299b1e67078ad9003ca27945c76162c1c0766d6f91820a"}, -] - [[package]] name = "packaging" version = "25.0" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" -groups = ["main", "dev"] +groups = ["dev"] files = [ {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, @@ -1900,201 +403,6 @@ files = [ dev = ["pre-commit", "tox"] testing = ["coverage", "pytest", "pytest-benchmark"] -[[package]] -name = "protobuf" -version = "6.33.0" -description = "" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "protobuf-6.33.0-cp310-abi3-win32.whl", hash = "sha256:d6101ded078042a8f17959eccd9236fb7a9ca20d3b0098bbcb91533a5680d035"}, - {file = "protobuf-6.33.0-cp310-abi3-win_amd64.whl", hash = "sha256:9a031d10f703f03768f2743a1c403af050b6ae1f3480e9c140f39c45f81b13ee"}, - {file = "protobuf-6.33.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:905b07a65f1a4b72412314082c7dbfae91a9e8b68a0cc1577515f8df58ecf455"}, - {file = "protobuf-6.33.0-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:e0697ece353e6239b90ee43a9231318302ad8353c70e6e45499fa52396debf90"}, - {file = "protobuf-6.33.0-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:e0a1715e4f27355afd9570f3ea369735afc853a6c3951a6afe1f80d8569ad298"}, - {file = "protobuf-6.33.0-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:35be49fd3f4fefa4e6e2aacc35e8b837d6703c37a2168a55ac21e9b1bc7559ef"}, - {file = "protobuf-6.33.0-cp39-cp39-win32.whl", hash = "sha256:cd33a8e38ea3e39df66e1bbc462b076d6e5ba3a4ebbde58219d777223a7873d3"}, - {file = "protobuf-6.33.0-cp39-cp39-win_amd64.whl", hash = "sha256:c963e86c3655af3a917962c9619e1a6b9670540351d7af9439d06064e3317cc9"}, - {file = "protobuf-6.33.0-py3-none-any.whl", hash = "sha256:25c9e1963c6734448ea2d308cfa610e692b801304ba0908d7bfa564ac5132995"}, - {file = "protobuf-6.33.0.tar.gz", hash = "sha256:140303d5c8d2037730c548f8c7b93b20bb1dc301be280c378b82b8894589c954"}, -] - -[[package]] -name = "psutil" -version = "7.1.1" -description = "Cross-platform lib for process and system monitoring." -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "psutil-7.1.1-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:8fa59d7b1f01f0337f12cd10dbd76e4312a4d3c730a4fedcbdd4e5447a8b8460"}, - {file = "psutil-7.1.1-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:2a95104eae85d088891716db676f780c1404fc15d47fde48a46a5d61e8f5ad2c"}, - {file = "psutil-7.1.1-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:98629cd8567acefcc45afe2f4ba1e9290f579eacf490a917967decce4b74ee9b"}, - {file = "psutil-7.1.1-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:92ebc58030fb054fa0f26c3206ef01c31c29d67aee1367e3483c16665c25c8d2"}, - {file = "psutil-7.1.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:146a704f224fb2ded2be3da5ac67fc32b9ea90c45b51676f9114a6ac45616967"}, - {file = "psutil-7.1.1-cp37-abi3-win32.whl", hash = "sha256:295c4025b5cd880f7445e4379e6826f7307e3d488947bf9834e865e7847dc5f7"}, - {file = "psutil-7.1.1-cp37-abi3-win_amd64.whl", hash = "sha256:9b4f17c5f65e44f69bd3a3406071a47b79df45cf2236d1f717970afcb526bcd3"}, - {file = "psutil-7.1.1-cp37-abi3-win_arm64.whl", hash = "sha256:5457cf741ca13da54624126cd5d333871b454ab133999a9a103fb097a7d7d21a"}, - {file = "psutil-7.1.1.tar.gz", hash = "sha256:092b6350145007389c1cfe5716050f02030a05219d90057ea867d18fe8d372fc"}, -] - -[package.extras] -dev = ["abi3audit", "black", "check-manifest", "coverage", "packaging", "pylint", "pyperf", "pypinfo", "pyreadline ; os_name == \"nt\"", "pytest", "pytest-cov", "pytest-instafail", "pytest-subtests", "pytest-xdist", "pywin32 ; os_name == \"nt\" and platform_python_implementation != \"PyPy\"", "requests", "rstcheck", "ruff", "setuptools", "sphinx", "sphinx_rtd_theme", "toml-sort", "twine", "validate-pyproject[all]", "virtualenv", "vulture", "wheel", "wheel ; os_name == \"nt\" and platform_python_implementation != \"PyPy\"", "wmi ; os_name == \"nt\" and platform_python_implementation != \"PyPy\""] -test = ["pytest", "pytest-instafail", "pytest-subtests", "pytest-xdist", "pywin32 ; os_name == \"nt\" and platform_python_implementation != \"PyPy\"", "setuptools", "wheel ; os_name == \"nt\" and platform_python_implementation != \"PyPy\"", "wmi ; os_name == \"nt\" and platform_python_implementation != \"PyPy\""] - -[[package]] -name = "pydantic" -version = "2.12.3" -description = "Data validation using Python type hints" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "pydantic-2.12.3-py3-none-any.whl", hash = "sha256:6986454a854bc3bc6e5443e1369e06a3a456af9d339eda45510f517d9ea5c6bf"}, - {file = "pydantic-2.12.3.tar.gz", hash = "sha256:1da1c82b0fc140bb0103bc1441ffe062154c8d38491189751ee00fd8ca65ce74"}, -] - -[package.dependencies] -annotated-types = ">=0.6.0" -pydantic-core = "2.41.4" -typing-extensions = ">=4.14.1" -typing-inspection = ">=0.4.2" - -[package.extras] -email = ["email-validator (>=2.0.0)"] -timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""] - -[[package]] -name = "pydantic-core" -version = "2.41.4" -description = "Core functionality for Pydantic validation and serialization" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "pydantic_core-2.41.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2442d9a4d38f3411f22eb9dd0912b7cbf4b7d5b6c92c4173b75d3e1ccd84e36e"}, - {file = "pydantic_core-2.41.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:30a9876226dda131a741afeab2702e2d127209bde3c65a2b8133f428bc5d006b"}, - {file = "pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d55bbac04711e2980645af68b97d445cdbcce70e5216de444a6c4b6943ebcccd"}, - {file = "pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e1d778fb7849a42d0ee5927ab0f7453bf9f85eef8887a546ec87db5ddb178945"}, - {file = "pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1b65077a4693a98b90ec5ad8f203ad65802a1b9b6d4a7e48066925a7e1606706"}, - {file = "pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:62637c769dee16eddb7686bf421be48dfc2fae93832c25e25bc7242e698361ba"}, - {file = "pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dfe3aa529c8f501babf6e502936b9e8d4698502b2cfab41e17a028d91b1ac7b"}, - {file = "pydantic_core-2.41.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ca2322da745bf2eeb581fc9ea3bbb31147702163ccbcbf12a3bb630e4bf05e1d"}, - {file = "pydantic_core-2.41.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e8cd3577c796be7231dcf80badcf2e0835a46665eaafd8ace124d886bab4d700"}, - {file = "pydantic_core-2.41.4-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:1cae8851e174c83633f0833e90636832857297900133705ee158cf79d40f03e6"}, - {file = "pydantic_core-2.41.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a26d950449aae348afe1ac8be5525a00ae4235309b729ad4d3399623125b43c9"}, - {file = "pydantic_core-2.41.4-cp310-cp310-win32.whl", hash = "sha256:0cf2a1f599efe57fa0051312774280ee0f650e11152325e41dfd3018ef2c1b57"}, - {file = "pydantic_core-2.41.4-cp310-cp310-win_amd64.whl", hash = "sha256:a8c2e340d7e454dc3340d3d2e8f23558ebe78c98aa8f68851b04dcb7bc37abdc"}, - {file = "pydantic_core-2.41.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:28ff11666443a1a8cf2a044d6a545ebffa8382b5f7973f22c36109205e65dc80"}, - {file = "pydantic_core-2.41.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:61760c3925d4633290292bad462e0f737b840508b4f722247d8729684f6539ae"}, - {file = "pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eae547b7315d055b0de2ec3965643b0ab82ad0106a7ffd29615ee9f266a02827"}, - {file = "pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ef9ee5471edd58d1fcce1c80ffc8783a650e3e3a193fe90d52e43bb4d87bff1f"}, - {file = "pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:15dd504af121caaf2c95cb90c0ebf71603c53de98305621b94da0f967e572def"}, - {file = "pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3a926768ea49a8af4d36abd6a8968b8790f7f76dd7cbd5a4c180db2b4ac9a3a2"}, - {file = "pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6916b9b7d134bff5440098a4deb80e4cb623e68974a87883299de9124126c2a8"}, - {file = "pydantic_core-2.41.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5cf90535979089df02e6f17ffd076f07237efa55b7343d98760bde8743c4b265"}, - {file = "pydantic_core-2.41.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:7533c76fa647fade2d7ec75ac5cc079ab3f34879626dae5689b27790a6cf5a5c"}, - {file = "pydantic_core-2.41.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:37e516bca9264cbf29612539801ca3cd5d1be465f940417b002905e6ed79d38a"}, - {file = "pydantic_core-2.41.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0c19cb355224037c83642429b8ce261ae108e1c5fbf5c028bac63c77b0f8646e"}, - {file = "pydantic_core-2.41.4-cp311-cp311-win32.whl", hash = "sha256:09c2a60e55b357284b5f31f5ab275ba9f7f70b7525e18a132ec1f9160b4f1f03"}, - {file = "pydantic_core-2.41.4-cp311-cp311-win_amd64.whl", hash = "sha256:711156b6afb5cb1cb7c14a2cc2c4a8b4c717b69046f13c6b332d8a0a8f41ca3e"}, - {file = "pydantic_core-2.41.4-cp311-cp311-win_arm64.whl", hash = "sha256:6cb9cf7e761f4f8a8589a45e49ed3c0d92d1d696a45a6feaee8c904b26efc2db"}, - {file = "pydantic_core-2.41.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:ab06d77e053d660a6faaf04894446df7b0a7e7aba70c2797465a0a1af00fc887"}, - {file = "pydantic_core-2.41.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c53ff33e603a9c1179a9364b0a24694f183717b2e0da2b5ad43c316c956901b2"}, - {file = "pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:304c54176af2c143bd181d82e77c15c41cbacea8872a2225dd37e6544dce9999"}, - {file = "pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:025ba34a4cf4fb32f917d5d188ab5e702223d3ba603be4d8aca2f82bede432a4"}, - {file = "pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b9f5f30c402ed58f90c70e12eff65547d3ab74685ffe8283c719e6bead8ef53f"}, - {file = "pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd96e5d15385d301733113bcaa324c8bcf111275b7675a9c6e88bfb19fc05e3b"}, - {file = "pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98f348cbb44fae6e9653c1055db7e29de67ea6a9ca03a5fa2c2e11a47cff0e47"}, - {file = "pydantic_core-2.41.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ec22626a2d14620a83ca583c6f5a4080fa3155282718b6055c2ea48d3ef35970"}, - {file = "pydantic_core-2.41.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3a95d4590b1f1a43bf33ca6d647b990a88f4a3824a8c4572c708f0b45a5290ed"}, - {file = "pydantic_core-2.41.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:f9672ab4d398e1b602feadcffcdd3af44d5f5e6ddc15bc7d15d376d47e8e19f8"}, - {file = "pydantic_core-2.41.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:84d8854db5f55fead3b579f04bda9a36461dab0730c5d570e1526483e7bb8431"}, - {file = "pydantic_core-2.41.4-cp312-cp312-win32.whl", hash = "sha256:9be1c01adb2ecc4e464392c36d17f97e9110fbbc906bcbe1c943b5b87a74aabd"}, - {file = "pydantic_core-2.41.4-cp312-cp312-win_amd64.whl", hash = "sha256:d682cf1d22bab22a5be08539dca3d1593488a99998f9f412137bc323179067ff"}, - {file = "pydantic_core-2.41.4-cp312-cp312-win_arm64.whl", hash = "sha256:833eebfd75a26d17470b58768c1834dfc90141b7afc6eb0429c21fc5a21dcfb8"}, - {file = "pydantic_core-2.41.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:85e050ad9e5f6fe1004eec65c914332e52f429bc0ae12d6fa2092407a462c746"}, - {file = "pydantic_core-2.41.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7393f1d64792763a48924ba31d1e44c2cfbc05e3b1c2c9abb4ceeadd912cced"}, - {file = "pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94dab0940b0d1fb28bcab847adf887c66a27a40291eedf0b473be58761c9799a"}, - {file = "pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:de7c42f897e689ee6f9e93c4bec72b99ae3b32a2ade1c7e4798e690ff5246e02"}, - {file = "pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:664b3199193262277b8b3cd1e754fb07f2c6023289c815a1e1e8fb415cb247b1"}, - {file = "pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d95b253b88f7d308b1c0b417c4624f44553ba4762816f94e6986819b9c273fb2"}, - {file = "pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1351f5bbdbbabc689727cb91649a00cb9ee7203e0a6e54e9f5ba9e22e384b84"}, - {file = "pydantic_core-2.41.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1affa4798520b148d7182da0615d648e752de4ab1a9566b7471bc803d88a062d"}, - {file = "pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7b74e18052fea4aa8dea2fb7dbc23d15439695da6cbe6cfc1b694af1115df09d"}, - {file = "pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:285b643d75c0e30abda9dc1077395624f314a37e3c09ca402d4015ef5979f1a2"}, - {file = "pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:f52679ff4218d713b3b33f88c89ccbf3a5c2c12ba665fb80ccc4192b4608dbab"}, - {file = "pydantic_core-2.41.4-cp313-cp313-win32.whl", hash = "sha256:ecde6dedd6fff127c273c76821bb754d793be1024bc33314a120f83a3c69460c"}, - {file = "pydantic_core-2.41.4-cp313-cp313-win_amd64.whl", hash = "sha256:d081a1f3800f05409ed868ebb2d74ac39dd0c1ff6c035b5162356d76030736d4"}, - {file = "pydantic_core-2.41.4-cp313-cp313-win_arm64.whl", hash = "sha256:f8e49c9c364a7edcbe2a310f12733aad95b022495ef2a8d653f645e5d20c1564"}, - {file = "pydantic_core-2.41.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:ed97fd56a561f5eb5706cebe94f1ad7c13b84d98312a05546f2ad036bafe87f4"}, - {file = "pydantic_core-2.41.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a870c307bf1ee91fc58a9a61338ff780d01bfae45922624816878dce784095d2"}, - {file = "pydantic_core-2.41.4-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d25e97bc1f5f8f7985bdc2335ef9e73843bb561eb1fa6831fdfc295c1c2061cf"}, - {file = "pydantic_core-2.41.4-cp313-cp313t-win_amd64.whl", hash = "sha256:d405d14bea042f166512add3091c1af40437c2e7f86988f3915fabd27b1e9cd2"}, - {file = "pydantic_core-2.41.4-cp313-cp313t-win_arm64.whl", hash = "sha256:19f3684868309db5263a11bace3c45d93f6f24afa2ffe75a647583df22a2ff89"}, - {file = "pydantic_core-2.41.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:e9205d97ed08a82ebb9a307e92914bb30e18cdf6f6b12ca4bedadb1588a0bfe1"}, - {file = "pydantic_core-2.41.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:82df1f432b37d832709fbcc0e24394bba04a01b6ecf1ee87578145c19cde12ac"}, - {file = "pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc3b4cc4539e055cfa39a3763c939f9d409eb40e85813257dcd761985a108554"}, - {file = "pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b1eb1754fce47c63d2ff57fdb88c351a6c0150995890088b33767a10218eaa4e"}, - {file = "pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e6ab5ab30ef325b443f379ddb575a34969c333004fca5a1daa0133a6ffaad616"}, - {file = "pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:31a41030b1d9ca497634092b46481b937ff9397a86f9f51bd41c4767b6fc04af"}, - {file = "pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a44ac1738591472c3d020f61c6df1e4015180d6262ebd39bf2aeb52571b60f12"}, - {file = "pydantic_core-2.41.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d72f2b5e6e82ab8f94ea7d0d42f83c487dc159c5240d8f83beae684472864e2d"}, - {file = "pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:c4d1e854aaf044487d31143f541f7aafe7b482ae72a022c664b2de2e466ed0ad"}, - {file = "pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b568af94267729d76e6ee5ececda4e283d07bbb28e8148bb17adad93d025d25a"}, - {file = "pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:6d55fb8b1e8929b341cc313a81a26e0d48aa3b519c1dbaadec3a6a2b4fcad025"}, - {file = "pydantic_core-2.41.4-cp314-cp314-win32.whl", hash = "sha256:5b66584e549e2e32a1398df11da2e0a7eff45d5c2d9db9d5667c5e6ac764d77e"}, - {file = "pydantic_core-2.41.4-cp314-cp314-win_amd64.whl", hash = "sha256:557a0aab88664cc552285316809cab897716a372afaf8efdbef756f8b890e894"}, - {file = "pydantic_core-2.41.4-cp314-cp314-win_arm64.whl", hash = "sha256:3f1ea6f48a045745d0d9f325989d8abd3f1eaf47dd00485912d1a3a63c623a8d"}, - {file = "pydantic_core-2.41.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6c1fe4c5404c448b13188dd8bd2ebc2bdd7e6727fa61ff481bcc2cca894018da"}, - {file = "pydantic_core-2.41.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:523e7da4d43b113bf8e7b49fa4ec0c35bf4fe66b2230bfc5c13cc498f12c6c3e"}, - {file = "pydantic_core-2.41.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5729225de81fb65b70fdb1907fcf08c75d498f4a6f15af005aabb1fdadc19dfa"}, - {file = "pydantic_core-2.41.4-cp314-cp314t-win_amd64.whl", hash = "sha256:de2cfbb09e88f0f795fd90cf955858fc2c691df65b1f21f0aa00b99f3fbc661d"}, - {file = "pydantic_core-2.41.4-cp314-cp314t-win_arm64.whl", hash = "sha256:d34f950ae05a83e0ede899c595f312ca976023ea1db100cd5aa188f7005e3ab0"}, - {file = "pydantic_core-2.41.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:646e76293345954acea6966149683047b7b2ace793011922208c8e9da12b0062"}, - {file = "pydantic_core-2.41.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cc8e85a63085a137d286e2791037f5fdfff0aabb8b899483ca9c496dd5797338"}, - {file = "pydantic_core-2.41.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:692c622c8f859a17c156492783902d8370ac7e121a611bd6fe92cc71acf9ee8d"}, - {file = "pydantic_core-2.41.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d1e2906efb1031a532600679b424ef1d95d9f9fb507f813951f23320903adbd7"}, - {file = "pydantic_core-2.41.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e04e2f7f8916ad3ddd417a7abdd295276a0bf216993d9318a5d61cc058209166"}, - {file = "pydantic_core-2.41.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df649916b81822543d1c8e0e1d079235f68acdc7d270c911e8425045a8cfc57e"}, - {file = "pydantic_core-2.41.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66c529f862fdba70558061bb936fe00ddbaaa0c647fd26e4a4356ef1d6561891"}, - {file = "pydantic_core-2.41.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3b4c5a1fd3a311563ed866c2c9b62da06cb6398bee186484ce95c820db71cb"}, - {file = "pydantic_core-2.41.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6e0fc40d84448f941df9b3334c4b78fe42f36e3bf631ad54c3047a0cdddc2514"}, - {file = "pydantic_core-2.41.4-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:44e7625332683b6c1c8b980461475cde9595eff94447500e80716db89b0da005"}, - {file = "pydantic_core-2.41.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:170ee6835f6c71081d031ef1c3b4dc4a12b9efa6a9540f93f95b82f3c7571ae8"}, - {file = "pydantic_core-2.41.4-cp39-cp39-win32.whl", hash = "sha256:3adf61415efa6ce977041ba9745183c0e1f637ca849773afa93833e04b163feb"}, - {file = "pydantic_core-2.41.4-cp39-cp39-win_amd64.whl", hash = "sha256:a238dd3feee263eeaeb7dc44aea4ba1364682c4f9f9467e6af5596ba322c2332"}, - {file = "pydantic_core-2.41.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:a1b2cfec3879afb742a7b0bcfa53e4f22ba96571c9e54d6a3afe1052d17d843b"}, - {file = "pydantic_core-2.41.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:d175600d975b7c244af6eb9c9041f10059f20b8bbffec9e33fdd5ee3f67cdc42"}, - {file = "pydantic_core-2.41.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f184d657fa4947ae5ec9c47bd7e917730fa1cbb78195037e32dcbab50aca5ee"}, - {file = "pydantic_core-2.41.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ed810568aeffed3edc78910af32af911c835cc39ebbfacd1f0ab5dd53028e5c"}, - {file = "pydantic_core-2.41.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:4f5d640aeebb438517150fdeec097739614421900e4a08db4a3ef38898798537"}, - {file = "pydantic_core-2.41.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:4a9ab037b71927babc6d9e7fc01aea9e66dc2a4a34dff06ef0724a4049629f94"}, - {file = "pydantic_core-2.41.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4dab9484ec605c3016df9ad4fd4f9a390bc5d816a3b10c6550f8424bb80b18c"}, - {file = "pydantic_core-2.41.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8a5028425820731d8c6c098ab642d7b8b999758e24acae03ed38a66eca8335"}, - {file = "pydantic_core-2.41.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:1e5ab4fc177dd41536b3c32b2ea11380dd3d4619a385860621478ac2d25ceb00"}, - {file = "pydantic_core-2.41.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:3d88d0054d3fa11ce936184896bed3c1c5441d6fa483b498fac6a5d0dd6f64a9"}, - {file = "pydantic_core-2.41.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b2a054a8725f05b4b6503357e0ac1c4e8234ad3b0c2ac130d6ffc66f0e170e2"}, - {file = "pydantic_core-2.41.4-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b0d9db5a161c99375a0c68c058e227bee1d89303300802601d76a3d01f74e258"}, - {file = "pydantic_core-2.41.4-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:6273ea2c8ffdac7b7fda2653c49682db815aebf4a89243a6feccf5e36c18c347"}, - {file = "pydantic_core-2.41.4-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:4c973add636efc61de22530b2ef83a65f39b6d6f656df97f678720e20de26caa"}, - {file = "pydantic_core-2.41.4-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b69d1973354758007f46cf2d44a4f3d0933f10b6dc9bf15cf1356e037f6f731a"}, - {file = "pydantic_core-2.41.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:3619320641fd212aaf5997b6ca505e97540b7e16418f4a241f44cdf108ffb50d"}, - {file = "pydantic_core-2.41.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:491535d45cd7ad7e4a2af4a5169b0d07bebf1adfd164b0368da8aa41e19907a5"}, - {file = "pydantic_core-2.41.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:54d86c0cada6aba4ec4c047d0e348cbad7063b87ae0f005d9f8c9ad04d4a92a2"}, - {file = "pydantic_core-2.41.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eca1124aced216b2500dc2609eade086d718e8249cb9696660ab447d50a758bd"}, - {file = "pydantic_core-2.41.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6c9024169becccf0cb470ada03ee578d7348c119a0d42af3dcf9eda96e3a247c"}, - {file = "pydantic_core-2.41.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:26895a4268ae5a2849269f4991cdc97236e4b9c010e51137becf25182daac405"}, - {file = "pydantic_core-2.41.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:ca4df25762cf71308c446e33c9b1fdca2923a3f13de616e2a949f38bf21ff5a8"}, - {file = "pydantic_core-2.41.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:5a28fcedd762349519276c36634e71853b4541079cab4acaaac60c4421827308"}, - {file = "pydantic_core-2.41.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c173ddcd86afd2535e2b695217e82191580663a1d1928239f877f5a1649ef39f"}, - {file = "pydantic_core-2.41.4.tar.gz", hash = "sha256:70e47929a9d4a1905a67e4b687d5946026390568a8e952b92824118063cee4d5"}, -] - -[package.dependencies] -typing-extensions = ">=4.14.1" - [[package]] name = "pygments" version = "2.19.2" @@ -2134,33 +442,6 @@ tomli = {version = ">=1", markers = "python_version < \"3.11\""} [package.extras] dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] -[[package]] -name = "python-dateutil" -version = "2.9.0.post0" -description = "Extensions to the standard Python datetime module" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -groups = ["main"] -files = [ - {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, - {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, -] - -[package.dependencies] -six = ">=1.5" - -[[package]] -name = "pytz" -version = "2025.2" -description = "World timezone definitions, modern and historical" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00"}, - {file = "pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3"}, -] - [[package]] name = "pyyaml" version = "6.0.3" @@ -2261,131 +542,6 @@ attrs = ">=22.2.0" rpds-py = ">=0.7.0" typing-extensions = {version = ">=4.4.0", markers = "python_version < \"3.13\""} -[[package]] -name = "regex" -version = "2025.10.23" -description = "Alternative regular expression module, to replace re." -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "regex-2025.10.23-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:17bbcde374bef1c5fad9b131f0e28a6a24856dd90368d8c0201e2b5a69533daa"}, - {file = "regex-2025.10.23-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b4e10434279cc8567f99ca6e018e9025d14f2fded2a603380b6be2090f476426"}, - {file = "regex-2025.10.23-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9c9bb421cbe7012c744a5a56cf4d6c80829c72edb1a2991677299c988d6339c8"}, - {file = "regex-2025.10.23-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:275cd1c2ed8c4a78ebfa489618d7aee762e8b4732da73573c3e38236ec5f65de"}, - {file = "regex-2025.10.23-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7b426ae7952f3dc1e73a86056d520bd4e5f021397484a6835902fc5648bcacce"}, - {file = "regex-2025.10.23-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c5cdaf5b6d37c7da1967dbe729d819461aab6a98a072feef65bbcff0a6e60649"}, - {file = "regex-2025.10.23-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3bfeff0b08f296ab28b4332a7e03ca31c437ee78b541ebc874bbf540e5932f8d"}, - {file = "regex-2025.10.23-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5f97236a67307b775f30a74ef722b64b38b7ab7ba3bb4a2508518a5de545459c"}, - {file = "regex-2025.10.23-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:be19e7de499940cd72475fb8e46ab2ecb1cf5906bebdd18a89f9329afb1df82f"}, - {file = "regex-2025.10.23-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:883df76ee42d9ecb82b37ff8d01caea5895b3f49630a64d21111078bbf8ef64c"}, - {file = "regex-2025.10.23-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2e9117d1d35fc2addae6281019ecc70dc21c30014b0004f657558b91c6a8f1a7"}, - {file = "regex-2025.10.23-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0ff1307f531a5d8cf5c20ea517254551ff0a8dc722193aab66c656c5a900ea68"}, - {file = "regex-2025.10.23-cp310-cp310-win32.whl", hash = "sha256:7888475787cbfee4a7cd32998eeffe9a28129fa44ae0f691b96cb3939183ef41"}, - {file = "regex-2025.10.23-cp310-cp310-win_amd64.whl", hash = "sha256:ec41a905908496ce4906dab20fb103c814558db1d69afc12c2f384549c17936a"}, - {file = "regex-2025.10.23-cp310-cp310-win_arm64.whl", hash = "sha256:b2b7f19a764d5e966d5a62bf2c28a8b4093cc864c6734510bdb4aeb840aec5e6"}, - {file = "regex-2025.10.23-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6c531155bf9179345e85032052a1e5fe1a696a6abf9cea54b97e8baefff970fd"}, - {file = "regex-2025.10.23-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:912e9df4e89d383681268d38ad8f5780d7cccd94ba0e9aa09ca7ab7ab4f8e7eb"}, - {file = "regex-2025.10.23-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4f375c61bfc3138b13e762fe0ae76e3bdca92497816936534a0177201666f44f"}, - {file = "regex-2025.10.23-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e248cc9446081119128ed002a3801f8031e0c219b5d3c64d3cc627da29ac0a33"}, - {file = "regex-2025.10.23-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b52bf9282fdf401e4f4e721f0f61fc4b159b1307244517789702407dd74e38ca"}, - {file = "regex-2025.10.23-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c084889ab2c59765a0d5ac602fd1c3c244f9b3fcc9a65fdc7ba6b74c5287490"}, - {file = "regex-2025.10.23-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d80e8eb79009bdb0936658c44ca06e2fbbca67792013e3818eea3f5f228971c2"}, - {file = "regex-2025.10.23-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b6f259118ba87b814a8ec475380aee5f5ae97a75852a3507cf31d055b01b5b40"}, - {file = "regex-2025.10.23-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:9b8c72a242683dcc72d37595c4f1278dfd7642b769e46700a8df11eab19dfd82"}, - {file = "regex-2025.10.23-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:a8d7b7a0a3df9952f9965342159e0c1f05384c0f056a47ce8b61034f8cecbe83"}, - {file = "regex-2025.10.23-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:413bfea20a484c524858125e92b9ce6ffdd0a4b97d4ff96b5859aa119b0f1bdd"}, - {file = "regex-2025.10.23-cp311-cp311-win32.whl", hash = "sha256:f76deef1f1019a17dad98f408b8f7afc4bd007cbe835ae77b737e8c7f19ae575"}, - {file = "regex-2025.10.23-cp311-cp311-win_amd64.whl", hash = "sha256:59bba9f7125536f23fdab5deeea08da0c287a64c1d3acc1c7e99515809824de8"}, - {file = "regex-2025.10.23-cp311-cp311-win_arm64.whl", hash = "sha256:b103a752b6f1632ca420225718d6ed83f6a6ced3016dd0a4ab9a6825312de566"}, - {file = "regex-2025.10.23-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:7a44d9c00f7a0a02d3b777429281376370f3d13d2c75ae74eb94e11ebcf4a7fc"}, - {file = "regex-2025.10.23-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b83601f84fde939ae3478bb32a3aef36f61b58c3208d825c7e8ce1a735f143f2"}, - {file = "regex-2025.10.23-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ec13647907bb9d15fd192bbfe89ff06612e098a5709e7d6ecabbdd8f7908fc45"}, - {file = "regex-2025.10.23-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78d76dd2957d62501084e7012ddafc5fcd406dd982b7a9ca1ea76e8eaaf73e7e"}, - {file = "regex-2025.10.23-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8668e5f067e31a47699ebb354f43aeb9c0ef136f915bd864243098524482ac43"}, - {file = "regex-2025.10.23-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a32433fe3deb4b2d8eda88790d2808fed0dc097e84f5e683b4cd4f42edef6cca"}, - {file = "regex-2025.10.23-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d97d73818c642c938db14c0668167f8d39520ca9d983604575ade3fda193afcc"}, - {file = "regex-2025.10.23-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bca7feecc72ee33579e9f6ddf8babbe473045717a0e7dbc347099530f96e8b9a"}, - {file = "regex-2025.10.23-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7e24af51e907d7457cc4a72691ec458320b9ae67dc492f63209f01eecb09de32"}, - {file = "regex-2025.10.23-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:d10bcde58bbdf18146f3a69ec46dd03233b94a4a5632af97aa5378da3a47d288"}, - {file = "regex-2025.10.23-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:44383bc0c933388516c2692c9a7503e1f4a67e982f20b9a29d2fb70c6494f147"}, - {file = "regex-2025.10.23-cp312-cp312-win32.whl", hash = "sha256:6040a86f95438a0114bba16e51dfe27f1bc004fd29fe725f54a586f6d522b079"}, - {file = "regex-2025.10.23-cp312-cp312-win_amd64.whl", hash = "sha256:436b4c4352fe0762e3bfa34a5567079baa2ef22aa9c37cf4d128979ccfcad842"}, - {file = "regex-2025.10.23-cp312-cp312-win_arm64.whl", hash = "sha256:f4b1b1991617055b46aff6f6db24888c1f05f4db9801349d23f09ed0714a9335"}, - {file = "regex-2025.10.23-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:b7690f95404a1293923a296981fd943cca12c31a41af9c21ba3edd06398fc193"}, - {file = "regex-2025.10.23-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1a32d77aeaea58a13230100dd8797ac1a84c457f3af2fdf0d81ea689d5a9105b"}, - {file = "regex-2025.10.23-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b24b29402f264f70a3c81f45974323b41764ff7159655360543b7cabb73e7d2f"}, - {file = "regex-2025.10.23-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:563824a08c7c03d96856d84b46fdb3bbb7cfbdf79da7ef68725cda2ce169c72a"}, - {file = "regex-2025.10.23-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a0ec8bdd88d2e2659c3518087ee34b37e20bd169419ffead4240a7004e8ed03b"}, - {file = "regex-2025.10.23-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b577601bfe1d33913fcd9276d7607bbac827c4798d9e14d04bf37d417a6c41cb"}, - {file = "regex-2025.10.23-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c9f2c68ac6cb3de94eea08a437a75eaa2bd33f9e97c84836ca0b610a5804368"}, - {file = "regex-2025.10.23-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:89f8b9ea3830c79468e26b0e21c3585f69f105157c2154a36f6b7839f8afb351"}, - {file = "regex-2025.10.23-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:98fd84c4e4ea185b3bb5bf065261ab45867d8875032f358a435647285c722673"}, - {file = "regex-2025.10.23-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:1e11d3e5887b8b096f96b4154dfb902f29c723a9556639586cd140e77e28b313"}, - {file = "regex-2025.10.23-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f13450328a6634348d47a88367e06b64c9d84980ef6a748f717b13f8ce64e87"}, - {file = "regex-2025.10.23-cp313-cp313-win32.whl", hash = "sha256:37be9296598a30c6a20236248cb8b2c07ffd54d095b75d3a2a2ee5babdc51df1"}, - {file = "regex-2025.10.23-cp313-cp313-win_amd64.whl", hash = "sha256:ea7a3c283ce0f06fe789365841e9174ba05f8db16e2fd6ae00a02df9572c04c0"}, - {file = "regex-2025.10.23-cp313-cp313-win_arm64.whl", hash = "sha256:d9a4953575f300a7bab71afa4cd4ac061c7697c89590a2902b536783eeb49a4f"}, - {file = "regex-2025.10.23-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:7d6606524fa77b3912c9ef52a42ef63c6cfbfc1077e9dc6296cd5da0da286044"}, - {file = "regex-2025.10.23-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:c037aadf4d64bdc38af7db3dbd34877a057ce6524eefcb2914d6d41c56f968cc"}, - {file = "regex-2025.10.23-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:99018c331fb2529084a0c9b4c713dfa49fafb47c7712422e49467c13a636c656"}, - {file = "regex-2025.10.23-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fd8aba965604d70306eb90a35528f776e59112a7114a5162824d43b76fa27f58"}, - {file = "regex-2025.10.23-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:238e67264b4013e74136c49f883734f68656adf8257bfa13b515626b31b20f8e"}, - {file = "regex-2025.10.23-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b2eb48bd9848d66fd04826382f5e8491ae633de3233a3d64d58ceb4ecfa2113a"}, - {file = "regex-2025.10.23-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d36591ce06d047d0c0fe2fc5f14bfbd5b4525d08a7b6a279379085e13f0e3d0e"}, - {file = "regex-2025.10.23-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b5d4ece8628d6e364302006366cea3ee887db397faebacc5dacf8ef19e064cf8"}, - {file = "regex-2025.10.23-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:39a7e8083959cb1c4ff74e483eecb5a65d3b3e1d821b256e54baf61782c906c6"}, - {file = "regex-2025.10.23-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:842d449a8fefe546f311656cf8c0d6729b08c09a185f1cad94c756210286d6a8"}, - {file = "regex-2025.10.23-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d614986dc68506be8f00474f4f6960e03e4ca9883f7df47744800e7d7c08a494"}, - {file = "regex-2025.10.23-cp313-cp313t-win32.whl", hash = "sha256:a5b7a26b51a9df473ec16a1934d117443a775ceb7b39b78670b2e21893c330c9"}, - {file = "regex-2025.10.23-cp313-cp313t-win_amd64.whl", hash = "sha256:ce81c5544a5453f61cb6f548ed358cfb111e3b23f3cd42d250a4077a6be2a7b6"}, - {file = "regex-2025.10.23-cp313-cp313t-win_arm64.whl", hash = "sha256:e9bf7f6699f490e4e43c44757aa179dab24d1960999c84ab5c3d5377714ed473"}, - {file = "regex-2025.10.23-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:5b5cb5b6344c4c4c24b2dc87b0bfee78202b07ef7633385df70da7fcf6f7cec6"}, - {file = "regex-2025.10.23-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a6ce7973384c37bdf0f371a843f95a6e6f4e1489e10e0cf57330198df72959c5"}, - {file = "regex-2025.10.23-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2ee3663f2c334959016b56e3bd0dd187cbc73f948e3a3af14c3caaa0c3035d10"}, - {file = "regex-2025.10.23-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2003cc82a579107e70d013482acce8ba773293f2db534fb532738395c557ff34"}, - {file = "regex-2025.10.23-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:182c452279365a93a9f45874f7f191ec1c51e1f1eb41bf2b16563f1a40c1da3a"}, - {file = "regex-2025.10.23-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b1249e9ff581c5b658c8f0437f883b01f1edcf424a16388591e7c05e5e9e8b0c"}, - {file = "regex-2025.10.23-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b841698f93db3ccc36caa1900d2a3be281d9539b822dc012f08fc80b46a3224"}, - {file = "regex-2025.10.23-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:956d89e0c92d471e8f7eee73f73fdff5ed345886378c45a43175a77538a1ffe4"}, - {file = "regex-2025.10.23-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5c259cb363299a0d90d63b5c0d7568ee98419861618a95ee9d91a41cb9954462"}, - {file = "regex-2025.10.23-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:185d2b18c062820b3a40d8fefa223a83f10b20a674bf6e8c4a432e8dfd844627"}, - {file = "regex-2025.10.23-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:281d87fa790049c2b7c1b4253121edd80b392b19b5a3d28dc2a77579cb2a58ec"}, - {file = "regex-2025.10.23-cp314-cp314-win32.whl", hash = "sha256:63b81eef3656072e4ca87c58084c7a9c2b81d41a300b157be635a8a675aacfb8"}, - {file = "regex-2025.10.23-cp314-cp314-win_amd64.whl", hash = "sha256:0967c5b86f274800a34a4ed862dfab56928144d03cb18821c5153f8777947796"}, - {file = "regex-2025.10.23-cp314-cp314-win_arm64.whl", hash = "sha256:c70dfe58b0a00b36aa04cdb0f798bf3e0adc31747641f69e191109fd8572c9a9"}, - {file = "regex-2025.10.23-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:1f5799ea1787aa6de6c150377d11afad39a38afd033f0c5247aecb997978c422"}, - {file = "regex-2025.10.23-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:a9639ab7540cfea45ef57d16dcbea2e22de351998d614c3ad2f9778fa3bdd788"}, - {file = "regex-2025.10.23-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:08f52122c352eb44c3421dab78b9b73a8a77a282cc8314ae576fcaa92b780d10"}, - {file = "regex-2025.10.23-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ebf1baebef1c4088ad5a5623decec6b52950f0e4d7a0ae4d48f0a99f8c9cb7d7"}, - {file = "regex-2025.10.23-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:16b0f1c2e2d566c562d5c384c2b492646be0a19798532fdc1fdedacc66e3223f"}, - {file = "regex-2025.10.23-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7ada5d9dceafaab92646aa00c10a9efd9b09942dd9b0d7c5a4b73db92cc7e61"}, - {file = "regex-2025.10.23-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3a36b4005770044bf08edecc798f0e41a75795b9e7c9c12fe29da8d792ef870c"}, - {file = "regex-2025.10.23-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:af7b2661dcc032da1fae82069b5ebf2ac1dfcd5359ef8b35e1367bfc92181432"}, - {file = "regex-2025.10.23-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:1cb976810ac1416a67562c2e5ba0accf6f928932320fef302e08100ed681b38e"}, - {file = "regex-2025.10.23-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:1a56a54be3897d62f54290190fbcd754bff6932934529fbf5b29933da28fcd43"}, - {file = "regex-2025.10.23-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8f3e6d202fb52c2153f532043bbcf618fd177df47b0b306741eb9b60ba96edc3"}, - {file = "regex-2025.10.23-cp314-cp314t-win32.whl", hash = "sha256:1fa1186966b2621b1769fd467c7b22e317e6ba2d2cdcecc42ea3089ef04a8521"}, - {file = "regex-2025.10.23-cp314-cp314t-win_amd64.whl", hash = "sha256:08a15d40ce28362eac3e78e83d75475147869c1ff86bc93285f43b4f4431a741"}, - {file = "regex-2025.10.23-cp314-cp314t-win_arm64.whl", hash = "sha256:a93e97338e1c8ea2649e130dcfbe8cd69bba5e1e163834752ab64dcb4de6d5ed"}, - {file = "regex-2025.10.23-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d8d286760ee5b77fd21cf6b33cc45e0bffd1deeda59ca65b9be996f590a9828a"}, - {file = "regex-2025.10.23-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9e72e3b84b170fec02193d32620a0a7060a22e52c46e45957dcd14742e0d28fb"}, - {file = "regex-2025.10.23-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ec506e8114fa12d21616deb44800f536d6bf2e1a69253dbf611f69af92395c99"}, - {file = "regex-2025.10.23-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d7e481f9710e8e24228ce2c77b41db7662a3f68853395da86a292b49dadca2aa"}, - {file = "regex-2025.10.23-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4663ff2fc367735ae7b90b4f0e05b25554446df4addafc76fdaacaaa0ba852b5"}, - {file = "regex-2025.10.23-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0879dd3251a42d2e9b938e1e03b1e9f60de90b4d153015193f5077a376a18439"}, - {file = "regex-2025.10.23-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:651c58aecbab7e97bdf8ec76298a28d2bf2b6238c099ec6bf32e6d41e2f9a9cb"}, - {file = "regex-2025.10.23-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ceabc62a0e879169cd1bf066063bd6991c3e41e437628936a2ce66e0e2071c32"}, - {file = "regex-2025.10.23-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:bfdf4e9aa3e7b7d02fda97509b4ceeed34542361694ecc0a81db1688373ecfbd"}, - {file = "regex-2025.10.23-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:92f565ff9beb9f51bc7cc8c578a7e92eb5c4576b69043a4c58cd05d73fda83c5"}, - {file = "regex-2025.10.23-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:abbea548b1076eaf8635caf1071c9d86efdf0fa74abe71fca26c05a2d64cda80"}, - {file = "regex-2025.10.23-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:33535dcf34f47821381e341f7b715cbd027deda4223af4d3932adcd371d3192a"}, - {file = "regex-2025.10.23-cp39-cp39-win32.whl", hash = "sha256:345c9df49a15bf6460534b104b336581bc5f35c286cac526416e7a63d389b09b"}, - {file = "regex-2025.10.23-cp39-cp39-win_amd64.whl", hash = "sha256:f668fe1fd3358c5423355a289a4a003e58005ce829d217b828f80bd605a90145"}, - {file = "regex-2025.10.23-cp39-cp39-win_arm64.whl", hash = "sha256:07a3fd25d9074923e4d7258b551ae35ab6bdfe01904b8f0d5341c7d8b20eb18d"}, - {file = "regex-2025.10.23.tar.gz", hash = "sha256:8cbaf8ceb88f96ae2356d01b9adf5e6306fa42fa6f7eab6b97794e37c959ac26"}, -] - [[package]] name = "requests" version = "2.32.5" @@ -2533,160 +689,6 @@ files = [ {file = "rpds_py-0.28.0.tar.gz", hash = "sha256:abd4df20485a0983e2ca334a216249b6186d6e3c1627e106651943dbdb791aea"}, ] -[[package]] -name = "safetensors" -version = "0.6.2" -description = "" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "safetensors-0.6.2-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:9c85ede8ec58f120bad982ec47746981e210492a6db876882aa021446af8ffba"}, - {file = "safetensors-0.6.2-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:d6675cf4b39c98dbd7d940598028f3742e0375a6b4d4277e76beb0c35f4b843b"}, - {file = "safetensors-0.6.2-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d2d2b3ce1e2509c68932ca03ab8f20570920cd9754b05063d4368ee52833ecd"}, - {file = "safetensors-0.6.2-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:93de35a18f46b0f5a6a1f9e26d91b442094f2df02e9fd7acf224cfec4238821a"}, - {file = "safetensors-0.6.2-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:89a89b505f335640f9120fac65ddeb83e40f1fd081cb8ed88b505bdccec8d0a1"}, - {file = "safetensors-0.6.2-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fc4d0d0b937e04bdf2ae6f70cd3ad51328635fe0e6214aa1fc811f3b576b3bda"}, - {file = "safetensors-0.6.2-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8045db2c872db8f4cbe3faa0495932d89c38c899c603f21e9b6486951a5ecb8f"}, - {file = "safetensors-0.6.2-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:81e67e8bab9878bb568cffbc5f5e655adb38d2418351dc0859ccac158f753e19"}, - {file = "safetensors-0.6.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b0e4d029ab0a0e0e4fdf142b194514695b1d7d3735503ba700cf36d0fc7136ce"}, - {file = "safetensors-0.6.2-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:fa48268185c52bfe8771e46325a1e21d317207bcabcb72e65c6e28e9ffeb29c7"}, - {file = "safetensors-0.6.2-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:d83c20c12c2d2f465997c51b7ecb00e407e5f94d7dec3ea0cc11d86f60d3fde5"}, - {file = "safetensors-0.6.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:d944cea65fad0ead848b6ec2c37cc0b197194bec228f8020054742190e9312ac"}, - {file = "safetensors-0.6.2-cp38-abi3-win32.whl", hash = "sha256:cab75ca7c064d3911411461151cb69380c9225798a20e712b102edda2542ddb1"}, - {file = "safetensors-0.6.2-cp38-abi3-win_amd64.whl", hash = "sha256:c7b214870df923cbc1593c3faee16bec59ea462758699bd3fee399d00aac072c"}, - {file = "safetensors-0.6.2.tar.gz", hash = "sha256:43ff2aa0e6fa2dc3ea5524ac7ad93a9839256b8703761e76e2d0b2a3fa4f15d9"}, -] - -[package.extras] -all = ["safetensors[jax]", "safetensors[numpy]", "safetensors[paddlepaddle]", "safetensors[pinned-tf]", "safetensors[quality]", "safetensors[testing]", "safetensors[torch]"] -dev = ["safetensors[all]"] -jax = ["flax (>=0.6.3)", "jax (>=0.3.25)", "jaxlib (>=0.3.25)", "safetensors[numpy]"] -mlx = ["mlx (>=0.0.9)"] -numpy = ["numpy (>=1.21.6)"] -paddlepaddle = ["paddlepaddle (>=2.4.1)", "safetensors[numpy]"] -pinned-tf = ["safetensors[numpy]", "tensorflow (==2.18.0)"] -quality = ["ruff"] -tensorflow = ["safetensors[numpy]", "tensorflow (>=2.11.0)"] -testing = ["h5py (>=3.7.0)", "huggingface-hub (>=0.12.1)", "hypothesis (>=6.70.2)", "pytest (>=7.2.0)", "pytest-benchmark (>=4.0.0)", "safetensors[numpy]", "setuptools-rust (>=1.5.2)"] -testingfree = ["huggingface-hub (>=0.12.1)", "hypothesis (>=6.70.2)", "pytest (>=7.2.0)", "pytest-benchmark (>=4.0.0)", "safetensors[numpy]", "setuptools-rust (>=1.5.2)"] -torch = ["safetensors[numpy]", "torch (>=1.10)"] - -[[package]] -name = "setuptools" -version = "80.9.0" -description = "Easily download, build, install, upgrade, and uninstall Python packages" -optional = false -python-versions = ">=3.9" -groups = ["main"] -markers = "python_version >= \"3.12\"" -files = [ - {file = "setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922"}, - {file = "setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c"}, -] - -[package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.8.0) ; sys_platform != \"cygwin\""] -core = ["importlib_metadata (>=6) ; python_version < \"3.10\"", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1) ; python_version < \"3.11\"", "wheel (>=0.43.0)"] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] -enabler = ["pytest-enabler (>=2.2)"] -test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] -type = ["importlib_metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.14.*)", "pytest-mypy"] - -[[package]] -name = "six" -version = "1.17.0" -description = "Python 2 and 3 compatibility utilities" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -groups = ["main"] -files = [ - {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, - {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, -] - -[[package]] -name = "sniffio" -version = "1.3.1" -description = "Sniff out which async library your code is running under" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, - {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, -] - -[[package]] -name = "starlette" -version = "0.38.6" -description = "The little ASGI library that shines." -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "starlette-0.38.6-py3-none-any.whl", hash = "sha256:4517a1409e2e73ee4951214ba012052b9e16f60e90d73cfb06192c19203bbb05"}, - {file = "starlette-0.38.6.tar.gz", hash = "sha256:863a1588f5574e70a821dadefb41e4881ea451a47a3cd1b4df359d4ffefe5ead"}, -] - -[package.dependencies] -anyio = ">=3.4.0,<5" - -[package.extras] -full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.7)", "pyyaml"] - -[[package]] -name = "sympy" -version = "1.14.0" -description = "Computer algebra system (CAS) in Python" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5"}, - {file = "sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517"}, -] - -[package.dependencies] -mpmath = ">=1.1.0,<1.4" - -[package.extras] -dev = ["hypothesis (>=6.70.0)", "pytest (>=7.1.0)"] - -[[package]] -name = "tokenizers" -version = "0.22.1" -description = "" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "tokenizers-0.22.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:59fdb013df17455e5f950b4b834a7b3ee2e0271e6378ccb33aa74d178b513c73"}, - {file = "tokenizers-0.22.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:8d4e484f7b0827021ac5f9f71d4794aaef62b979ab7608593da22b1d2e3c4edc"}, - {file = "tokenizers-0.22.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19d2962dd28bc67c1f205ab180578a78eef89ac60ca7ef7cbe9635a46a56422a"}, - {file = "tokenizers-0.22.1-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:38201f15cdb1f8a6843e6563e6e79f4abd053394992b9bbdf5213ea3469b4ae7"}, - {file = "tokenizers-0.22.1-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d1cbe5454c9a15df1b3443c726063d930c16f047a3cc724b9e6e1a91140e5a21"}, - {file = "tokenizers-0.22.1-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e7d094ae6312d69cc2a872b54b91b309f4f6fbce871ef28eb27b52a98e4d0214"}, - {file = "tokenizers-0.22.1-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afd7594a56656ace95cdd6df4cca2e4059d294c5cfb1679c57824b605556cb2f"}, - {file = "tokenizers-0.22.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2ef6063d7a84994129732b47e7915e8710f27f99f3a3260b8a38fc7ccd083f4"}, - {file = "tokenizers-0.22.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ba0a64f450b9ef412c98f6bcd2a50c6df6e2443b560024a09fa6a03189726879"}, - {file = "tokenizers-0.22.1-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:331d6d149fa9c7d632cde4490fb8bbb12337fa3a0232e77892be656464f4b446"}, - {file = "tokenizers-0.22.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:607989f2ea68a46cb1dfbaf3e3aabdf3f21d8748312dbeb6263d1b3b66c5010a"}, - {file = "tokenizers-0.22.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a0f307d490295717726598ef6fa4f24af9d484809223bbc253b201c740a06390"}, - {file = "tokenizers-0.22.1-cp39-abi3-win32.whl", hash = "sha256:b5120eed1442765cd90b903bb6cfef781fd8fe64e34ccaecbae4c619b7b12a82"}, - {file = "tokenizers-0.22.1-cp39-abi3-win_amd64.whl", hash = "sha256:65fd6e3fb11ca1e78a6a93602490f134d1fdeb13bcef99389d5102ea318ed138"}, - {file = "tokenizers-0.22.1.tar.gz", hash = "sha256:61de6522785310a309b3407bac22d99c4db5dba349935e99e4d15ea2226af2d9"}, -] - -[package.dependencies] -huggingface-hub = ">=0.16.4,<2.0" - -[package.extras] -dev = ["tokenizers[testing]"] -docs = ["setuptools-rust", "sphinx", "sphinx-rtd-theme"] -testing = ["black (==22.3)", "datasets", "numpy", "pytest", "pytest-asyncio", "requests", "ruff"] - [[package]] name = "tomli" version = "2.3.0" @@ -2740,290 +742,6 @@ files = [ {file = "tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549"}, ] -[[package]] -name = "torch" -version = "2.7.1" -description = "Tensors and Dynamic neural networks in Python with strong GPU acceleration" -optional = false -python-versions = ">=3.9.0" -groups = ["main"] -markers = "python_version >= \"3.13\"" -files = [ - {file = "torch-2.7.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:a103b5d782af5bd119b81dbcc7ffc6fa09904c423ff8db397a1e6ea8fd71508f"}, - {file = "torch-2.7.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:fe955951bdf32d182ee8ead6c3186ad54781492bf03d547d31771a01b3d6fb7d"}, - {file = "torch-2.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:885453d6fba67d9991132143bf7fa06b79b24352f4506fd4d10b309f53454162"}, - {file = "torch-2.7.1-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:d72acfdb86cee2a32c0ce0101606f3758f0d8bb5f8f31e7920dc2809e963aa7c"}, - {file = "torch-2.7.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:236f501f2e383f1cb861337bdf057712182f910f10aeaf509065d54d339e49b2"}, - {file = "torch-2.7.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:06eea61f859436622e78dd0cdd51dbc8f8c6d76917a9cf0555a333f9eac31ec1"}, - {file = "torch-2.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:8273145a2e0a3c6f9fd2ac36762d6ee89c26d430e612b95a99885df083b04e52"}, - {file = "torch-2.7.1-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:aea4fc1bf433d12843eb2c6b2204861f43d8364597697074c8d38ae2507f8730"}, - {file = "torch-2.7.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:27ea1e518df4c9de73af7e8a720770f3628e7f667280bce2be7a16292697e3fa"}, - {file = "torch-2.7.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:c33360cfc2edd976c2633b3b66c769bdcbbf0e0b6550606d188431c81e7dd1fc"}, - {file = "torch-2.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:d8bf6e1856ddd1807e79dc57e54d3335f2b62e6f316ed13ed3ecfe1fc1df3d8b"}, - {file = "torch-2.7.1-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:787687087412c4bd68d315e39bc1223f08aae1d16a9e9771d95eabbb04ae98fb"}, - {file = "torch-2.7.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:03563603d931e70722dce0e11999d53aa80a375a3d78e6b39b9f6805ea0a8d28"}, - {file = "torch-2.7.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:d632f5417b6980f61404a125b999ca6ebd0b8b4bbdbb5fbbba44374ab619a412"}, - {file = "torch-2.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:23660443e13995ee93e3d844786701ea4ca69f337027b05182f5ba053ce43b38"}, - {file = "torch-2.7.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:0da4f4dba9f65d0d203794e619fe7ca3247a55ffdcbd17ae8fb83c8b2dc9b585"}, - {file = "torch-2.7.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:e08d7e6f21a617fe38eeb46dd2213ded43f27c072e9165dc27300c9ef9570934"}, - {file = "torch-2.7.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:30207f672328a42df4f2174b8f426f354b2baa0b7cca3a0adb3d6ab5daf00dc8"}, - {file = "torch-2.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:79042feca1c634aaf6603fe6feea8c6b30dfa140a6bbc0b973e2260c7e79a22e"}, - {file = "torch-2.7.1-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:988b0cbc4333618a1056d2ebad9eb10089637b659eb645434d0809d8d937b946"}, - {file = "torch-2.7.1-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:e0d81e9a12764b6f3879a866607c8ae93113cbcad57ce01ebde63eb48a576369"}, - {file = "torch-2.7.1-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:8394833c44484547ed4a47162318337b88c97acdb3273d85ea06e03ffff44998"}, - {file = "torch-2.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:df41989d9300e6e3c19ec9f56f856187a6ef060c3662fe54f4b6baf1fc90bd19"}, - {file = "torch-2.7.1-cp39-none-macosx_11_0_arm64.whl", hash = "sha256:a737b5edd1c44a5c1ece2e9f3d00df9d1b3fb9541138bee56d83d38293fb6c9d"}, -] - -[package.dependencies] -filelock = "*" -fsspec = "*" -jinja2 = "*" -networkx = "*" -nvidia-cublas-cu12 = {version = "12.6.4.1", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} -nvidia-cuda-cupti-cu12 = {version = "12.6.80", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} -nvidia-cuda-nvrtc-cu12 = {version = "12.6.77", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} -nvidia-cuda-runtime-cu12 = {version = "12.6.77", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} -nvidia-cudnn-cu12 = {version = "9.5.1.17", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} -nvidia-cufft-cu12 = {version = "11.3.0.4", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} -nvidia-cufile-cu12 = {version = "1.11.1.6", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} -nvidia-curand-cu12 = {version = "10.3.7.77", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} -nvidia-cusolver-cu12 = {version = "11.7.1.2", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} -nvidia-cusparse-cu12 = {version = "12.5.4.2", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} -nvidia-cusparselt-cu12 = {version = "0.6.3", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} -nvidia-nccl-cu12 = {version = "2.26.2", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} -nvidia-nvjitlink-cu12 = {version = "12.6.85", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} -nvidia-nvtx-cu12 = {version = "12.6.77", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} -setuptools = {version = "*", markers = "python_version >= \"3.12\""} -sympy = ">=1.13.3" -triton = {version = "3.3.1", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} -typing-extensions = ">=4.10.0" - -[package.extras] -opt-einsum = ["opt-einsum (>=3.3)"] -optree = ["optree (>=0.13.0)"] - -[[package]] -name = "torch" -version = "2.9.0" -description = "Tensors and Dynamic neural networks in Python with strong GPU acceleration" -optional = false -python-versions = ">=3.10" -groups = ["main"] -markers = "python_version < \"3.13\"" -files = [ - {file = "torch-2.9.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:030bbfe367379ae6a4ae4042b6c44da25383343b8b3c68abaa9c7231efbaf2dd"}, - {file = "torch-2.9.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:51cb63902182a78e90886e8068befd8ea102af4b00e420263591a3d70c7d3c6c"}, - {file = "torch-2.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:3f6aad4d2f0ee2248bac25339d74858ff846c3969b27d14ac235821f055af83d"}, - {file = "torch-2.9.0-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:413e1654c9203733138858780e184d9fc59442f0b3b209e16f39354eb893db9b"}, - {file = "torch-2.9.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:c596708b5105d0b199215acf0c9be7c1db5f1680d88eddadf4b75a299259a677"}, - {file = "torch-2.9.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:51de31219c97c51cf4bf2be94d622e3deb5dcc526c6dc00e97c17eaec0fc1d67"}, - {file = "torch-2.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:dd515c70059afd95f48b8192733764c08ca37a1d19803af6401b5ecad7c8676e"}, - {file = "torch-2.9.0-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:614a185e4986326d526a91210c8fc1397e76e8cfafa78baf6296a790e53a9eec"}, - {file = "torch-2.9.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:e5f7af1dc4c0a7c4a260c2534f41ddaf209714f7c89145e644c44712fbd6b642"}, - {file = "torch-2.9.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:01cff95ecd9a212ea2f141db28acccdceb6a4c54f64e6c51091146f5e2a772c6"}, - {file = "torch-2.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:4582b162f541651f0cb184d3e291c05c2f556c7117c64a9873e2ee158d40062b"}, - {file = "torch-2.9.0-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:33f58e9a102a91259af289d50525c30323b5c9ae1d31322b6447c0814da68695"}, - {file = "torch-2.9.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:c30a17fc83eeab346913e237c64b15b5ba6407fff812f6c541e322e19bc9ea0e"}, - {file = "torch-2.9.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:8f25033b8667b57857dfd01458fbf2a9e6a6df1f8def23aef0dc46292f6aa642"}, - {file = "torch-2.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:d037f1b4ffd25013be4a7bf3651a0a910c68554956c7b2c92ebe87c76475dece"}, - {file = "torch-2.9.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e4e5b5cba837a2a8d1a497ba9a58dae46fa392593eaa13b871c42f71847503a5"}, - {file = "torch-2.9.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:64693568f5dc4dbd5f880a478b1cea0201cc6b510d91d1bc54fea86ac5d1a637"}, - {file = "torch-2.9.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:f8ed31ddd7d10bfb3fbe0b9fe01b1243577f13d75e6f4a0839a283915ce3791e"}, - {file = "torch-2.9.0-cp313-cp313t-win_amd64.whl", hash = "sha256:eff527d4e4846e6f70d2afd8058b73825761203d66576a7e04ea2ecfebcb4ab8"}, - {file = "torch-2.9.0-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:f8877779cf56d1ce431a7636703bdb13307f5960bb1af49716d8b179225e0e6a"}, - {file = "torch-2.9.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7e614fae699838038d888729f82b687c03413c5989ce2a9481f9a7e7a396e0bb"}, - {file = "torch-2.9.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:dfb5b8cd310ba3436c7e14e8b7833ef658cf3045e50d2bdaed23c8fc517065eb"}, - {file = "torch-2.9.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:b3d29524993a478e46f5d598b249cd824b7ed98d7fba538bd9c4cde6c803948f"}, - {file = "torch-2.9.0-cp314-cp314-win_amd64.whl", hash = "sha256:71c7578984f5ec0eb645eb4816ac8435fcf3e3e2ae1901bcd2f519a9cafb5125"}, - {file = "torch-2.9.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:71d9309aee457bbe0b164bce2111cd911c4ed4e847e65d5077dbbcd3aba6befc"}, - {file = "torch-2.9.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:c08fb654d783899e204a32cca758a7ce8a45b2d78eeb89517cc937088316f78e"}, - {file = "torch-2.9.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:ec8feb0099b2daa5728fbc7abb0b05730fd97e0f359ff8bda09865aaa7bd7d4b"}, - {file = "torch-2.9.0-cp314-cp314t-win_amd64.whl", hash = "sha256:695ba920f234ad4170c9c50e28d56c848432f8f530e6bc7f88fcb15ddf338e75"}, -] - -[package.dependencies] -filelock = "*" -fsspec = ">=0.8.5" -jinja2 = "*" -networkx = ">=2.5.1" -nvidia-cublas-cu12 = {version = "12.8.4.1", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} -nvidia-cuda-cupti-cu12 = {version = "12.8.90", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} -nvidia-cuda-nvrtc-cu12 = {version = "12.8.93", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} -nvidia-cuda-runtime-cu12 = {version = "12.8.90", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} -nvidia-cudnn-cu12 = {version = "9.10.2.21", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} -nvidia-cufft-cu12 = {version = "11.3.3.83", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} -nvidia-cufile-cu12 = {version = "1.13.1.3", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} -nvidia-curand-cu12 = {version = "10.3.9.90", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} -nvidia-cusolver-cu12 = {version = "11.7.3.90", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} -nvidia-cusparse-cu12 = {version = "12.5.8.93", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} -nvidia-cusparselt-cu12 = {version = "0.7.1", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} -nvidia-nccl-cu12 = {version = "2.27.5", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} -nvidia-nvjitlink-cu12 = {version = "12.8.93", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} -nvidia-nvshmem-cu12 = {version = "3.3.20", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} -nvidia-nvtx-cu12 = {version = "12.8.90", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} -setuptools = {version = "*", markers = "python_version >= \"3.12\""} -sympy = ">=1.13.3" -triton = {version = "3.5.0", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} -typing-extensions = ">=4.10.0" - -[package.extras] -opt-einsum = ["opt-einsum (>=3.3)"] -optree = ["optree (>=0.13.0)"] -pyyaml = ["pyyaml"] - -[[package]] -name = "tqdm" -version = "4.67.1" -description = "Fast, Extensible Progress Meter" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2"}, - {file = "tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "platform_system == \"Windows\""} - -[package.extras] -dev = ["nbval", "pytest (>=6)", "pytest-asyncio (>=0.24)", "pytest-cov", "pytest-timeout"] -discord = ["requests"] -notebook = ["ipywidgets (>=6)"] -slack = ["slack-sdk"] -telegram = ["requests"] - -[[package]] -name = "transformers" -version = "4.57.1" -description = "State-of-the-art Machine Learning for JAX, PyTorch and TensorFlow" -optional = false -python-versions = ">=3.9.0" -groups = ["main"] -files = [ - {file = "transformers-4.57.1-py3-none-any.whl", hash = "sha256:b10d05da8fa67dc41644dbbf9bc45a44cb86ae33da6f9295f5fbf5b7890bd267"}, - {file = "transformers-4.57.1.tar.gz", hash = "sha256:f06c837959196c75039809636cd964b959f6604b75b8eeec6fdfc0440b89cc55"}, -] - -[package.dependencies] -filelock = "*" -huggingface-hub = ">=0.34.0,<1.0" -numpy = ">=1.17" -packaging = ">=20.0" -pyyaml = ">=5.1" -regex = "!=2019.12.17" -requests = "*" -safetensors = ">=0.4.3" -tokenizers = ">=0.22.0,<=0.23.0" -tqdm = ">=4.27" - -[package.extras] -accelerate = ["accelerate (>=0.26.0)"] -all = ["Pillow (>=10.0.1,<=15.0)", "Pillow (>=10.0.1,<=15.0)", "accelerate (>=0.26.0)", "accelerate (>=0.26.0)", "av", "codecarbon (>=2.8.1)", "flax (>=0.4.1,<=0.7.0)", "jax (>=0.4.1,<=0.4.13)", "jaxlib (>=0.4.1,<=0.4.13)", "jinja2 (>=3.1.0)", "kenlm", "keras-nlp (>=0.3.1,<0.14.0)", "kernels (>=0.6.1,<=0.9)", "librosa", "mistral-common[opencv] (>=1.6.3)", "num2words", "onnxconverter-common", "optax (>=0.0.8,<=0.1.4)", "optuna", "phonemizer", "protobuf", "pyctcdecode (>=0.4.0)", "ray[tune] (>=2.7.0)", "scipy (<1.13.0)", "sentencepiece (>=0.1.91,!=0.1.92)", "tensorflow (>2.9,<2.16)", "tensorflow-text (<2.16)", "tf2onnx", "timm (!=1.0.18,<=1.0.19)", "tokenizers (>=0.22.0,<=0.23.0)", "torch (>=2.2)", "torchaudio", "torchvision"] -audio = ["kenlm", "librosa", "phonemizer", "pyctcdecode (>=0.4.0)"] -benchmark = ["optimum-benchmark (>=0.3.0)"] -chat-template = ["jinja2 (>=3.1.0)"] -codecarbon = ["codecarbon (>=2.8.1)"] -deepspeed = ["accelerate (>=0.26.0)", "deepspeed (>=0.9.3)"] -deepspeed-testing = ["GitPython (<3.1.19)", "accelerate (>=0.26.0)", "accelerate (>=0.26.0)", "beautifulsoup4", "cookiecutter (==1.7.3)", "datasets (>=2.15.0)", "datasets (>=2.15.0)", "deepspeed (>=0.9.3)", "dill (<0.3.5)", "evaluate (>=0.2.0)", "faiss-cpu", "fastapi", "libcst", "mistral-common[opencv] (>=1.6.3)", "nltk (<=3.8.1)", "openai (>=1.98.0)", "optuna", "parameterized (>=0.9)", "protobuf", "psutil", "pydantic (>=2)", "pydantic (>=2)", "pytest (>=7.2.0)", "pytest-asyncio", "pytest-order", "pytest-rerunfailures (<16.0)", "pytest-rich", "pytest-timeout", "pytest-xdist", "rjieba", "rouge-score (!=0.0.7,!=0.0.8,!=0.1,!=0.1.1)", "ruff (==0.13.1)", "sacrebleu (>=1.4.12,<2.0.0)", "sacremoses", "sentencepiece (>=0.1.91,!=0.1.92)", "sentencepiece (>=0.1.91,!=0.1.92)", "starlette", "tensorboard", "timeout-decorator", "torch (>=2.2)", "uvicorn"] -dev = ["GitPython (<3.1.19)", "GitPython (<3.1.19)", "Pillow (>=10.0.1,<=15.0)", "Pillow (>=10.0.1,<=15.0)", "accelerate (>=0.26.0)", "accelerate (>=0.26.0)", "accelerate (>=0.26.0)", "av", "beautifulsoup4", "codecarbon (>=2.8.1)", "cookiecutter (==1.7.3)", "cookiecutter (==1.7.3)", "datasets (>=2.15.0)", "datasets (>=2.15.0)", "datasets (>=2.15.0)", "dill (<0.3.5)", "evaluate (>=0.2.0)", "faiss-cpu", "fastapi", "flax (>=0.4.1,<=0.7.0)", "fugashi (>=1.0)", "ipadic (>=1.0.0,<2.0)", "jax (>=0.4.1,<=0.4.13)", "jaxlib (>=0.4.1,<=0.4.13)", "jinja2 (>=3.1.0)", "kenlm", "keras-nlp (>=0.3.1,<0.14.0)", "kernels (>=0.6.1,<=0.9)", "libcst", "libcst", "librosa", "mistral-common[opencv] (>=1.6.3)", "mistral-common[opencv] (>=1.6.3)", "nltk (<=3.8.1)", "num2words", "onnxconverter-common", "openai (>=1.98.0)", "optax (>=0.0.8,<=0.1.4)", "optuna", "pandas (<2.3.0)", "parameterized (>=0.9)", "phonemizer", "protobuf", "psutil", "pyctcdecode (>=0.4.0)", "pydantic (>=2)", "pydantic (>=2)", "pytest (>=7.2.0)", "pytest-asyncio", "pytest-order", "pytest-rerunfailures (<16.0)", "pytest-rich", "pytest-timeout", "pytest-xdist", "ray[tune] (>=2.7.0)", "rhoknp (>=1.1.0,<1.3.1)", "rich", "rjieba", "rouge-score (!=0.0.7,!=0.0.8,!=0.1,!=0.1.1)", "ruff (==0.13.1)", "ruff (==0.13.1)", "sacrebleu (>=1.4.12,<2.0.0)", "sacremoses", "scikit-learn", "scipy (<1.13.0)", "sentencepiece (>=0.1.91,!=0.1.92)", "sentencepiece (>=0.1.91,!=0.1.92)", "starlette", "sudachidict_core (>=20220729)", "sudachipy (>=0.6.6)", "tensorboard", "tensorflow (>2.9,<2.16)", "tensorflow-text (<2.16)", "tf2onnx", "timeout-decorator", "timm (!=1.0.18,<=1.0.19)", "tokenizers (>=0.22.0,<=0.23.0)", "torch (>=2.2)", "torch (>=2.2)", "torchaudio", "torchvision", "unidic (>=1.0.2)", "unidic_lite (>=1.0.7)", "urllib3 (<2.0.0)", "uvicorn"] -dev-tensorflow = ["GitPython (<3.1.19)", "GitPython (<3.1.19)", "Pillow (>=10.0.1,<=15.0)", "accelerate (>=0.26.0)", "beautifulsoup4", "cookiecutter (==1.7.3)", "cookiecutter (==1.7.3)", "datasets (>=2.15.0)", "datasets (>=2.15.0)", "datasets (>=2.15.0)", "dill (<0.3.5)", "evaluate (>=0.2.0)", "faiss-cpu", "fastapi", "kenlm", "keras-nlp (>=0.3.1,<0.14.0)", "libcst", "libcst", "librosa", "mistral-common[opencv] (>=1.6.3)", "nltk (<=3.8.1)", "onnxconverter-common", "onnxconverter-common", "onnxruntime (>=1.4.0)", "onnxruntime-tools (>=1.4.2)", "openai (>=1.98.0)", "pandas (<2.3.0)", "parameterized (>=0.9)", "phonemizer", "protobuf", "psutil", "pyctcdecode (>=0.4.0)", "pydantic (>=2)", "pydantic (>=2)", "pytest (>=7.2.0)", "pytest-asyncio", "pytest-order", "pytest-rerunfailures (<16.0)", "pytest-rich", "pytest-timeout", "pytest-xdist", "rich", "rjieba", "rouge-score (!=0.0.7,!=0.0.8,!=0.1,!=0.1.1)", "ruff (==0.13.1)", "ruff (==0.13.1)", "sacrebleu (>=1.4.12,<2.0.0)", "sacremoses", "scikit-learn", "sentencepiece (>=0.1.91,!=0.1.92)", "sentencepiece (>=0.1.91,!=0.1.92)", "starlette", "tensorboard", "tensorflow (>2.9,<2.16)", "tensorflow-text (<2.16)", "tf2onnx", "tf2onnx", "timeout-decorator", "tokenizers (>=0.22.0,<=0.23.0)", "torch (>=2.2)", "urllib3 (<2.0.0)", "uvicorn"] -dev-torch = ["GitPython (<3.1.19)", "GitPython (<3.1.19)", "Pillow (>=10.0.1,<=15.0)", "Pillow (>=10.0.1,<=15.0)", "accelerate (>=0.26.0)", "accelerate (>=0.26.0)", "beautifulsoup4", "codecarbon (>=2.8.1)", "cookiecutter (==1.7.3)", "cookiecutter (==1.7.3)", "datasets (>=2.15.0)", "datasets (>=2.15.0)", "datasets (>=2.15.0)", "dill (<0.3.5)", "evaluate (>=0.2.0)", "faiss-cpu", "fastapi", "fugashi (>=1.0)", "ipadic (>=1.0.0,<2.0)", "kenlm", "kernels (>=0.6.1,<=0.9)", "libcst", "libcst", "librosa", "mistral-common[opencv] (>=1.6.3)", "nltk (<=3.8.1)", "num2words", "onnxruntime (>=1.4.0)", "onnxruntime-tools (>=1.4.2)", "openai (>=1.98.0)", "optuna", "pandas (<2.3.0)", "parameterized (>=0.9)", "phonemizer", "protobuf", "psutil", "pyctcdecode (>=0.4.0)", "pydantic (>=2)", "pydantic (>=2)", "pytest (>=7.2.0)", "pytest-asyncio", "pytest-order", "pytest-rerunfailures (<16.0)", "pytest-rich", "pytest-timeout", "pytest-xdist", "ray[tune] (>=2.7.0)", "rhoknp (>=1.1.0,<1.3.1)", "rich", "rjieba", "rouge-score (!=0.0.7,!=0.0.8,!=0.1,!=0.1.1)", "ruff (==0.13.1)", "ruff (==0.13.1)", "sacrebleu (>=1.4.12,<2.0.0)", "sacremoses", "scikit-learn", "sentencepiece (>=0.1.91,!=0.1.92)", "sentencepiece (>=0.1.91,!=0.1.92)", "starlette", "sudachidict_core (>=20220729)", "sudachipy (>=0.6.6)", "tensorboard", "timeout-decorator", "timm (!=1.0.18,<=1.0.19)", "tokenizers (>=0.22.0,<=0.23.0)", "torch (>=2.2)", "torch (>=2.2)", "torchaudio", "torchvision", "unidic (>=1.0.2)", "unidic_lite (>=1.0.7)", "urllib3 (<2.0.0)", "uvicorn"] -flax = ["flax (>=0.4.1,<=0.7.0)", "jax (>=0.4.1,<=0.4.13)", "jaxlib (>=0.4.1,<=0.4.13)", "optax (>=0.0.8,<=0.1.4)", "scipy (<1.13.0)"] -flax-speech = ["kenlm", "librosa", "phonemizer", "pyctcdecode (>=0.4.0)"] -ftfy = ["ftfy"] -hf-xet = ["hf_xet"] -hub-kernels = ["kernels (>=0.6.1,<=0.9)"] -integrations = ["kernels (>=0.6.1,<=0.9)", "optuna", "ray[tune] (>=2.7.0)"] -ja = ["fugashi (>=1.0)", "ipadic (>=1.0.0,<2.0)", "rhoknp (>=1.1.0,<1.3.1)", "sudachidict_core (>=20220729)", "sudachipy (>=0.6.6)", "unidic (>=1.0.2)", "unidic_lite (>=1.0.7)"] -mistral-common = ["mistral-common[opencv] (>=1.6.3)"] -modelcreation = ["cookiecutter (==1.7.3)"] -natten = ["natten (>=0.14.6,<0.15.0)"] -num2words = ["num2words"] -onnx = ["onnxconverter-common", "onnxruntime (>=1.4.0)", "onnxruntime-tools (>=1.4.2)", "tf2onnx"] -onnxruntime = ["onnxruntime (>=1.4.0)", "onnxruntime-tools (>=1.4.2)"] -open-telemetry = ["opentelemetry-api", "opentelemetry-exporter-otlp", "opentelemetry-sdk"] -optuna = ["optuna"] -quality = ["GitPython (<3.1.19)", "datasets (>=2.15.0)", "libcst", "pandas (<2.3.0)", "rich", "ruff (==0.13.1)", "urllib3 (<2.0.0)"] -ray = ["ray[tune] (>=2.7.0)"] -retrieval = ["datasets (>=2.15.0)", "faiss-cpu"] -ruff = ["ruff (==0.13.1)"] -sagemaker = ["sagemaker (>=2.31.0)"] -sentencepiece = ["protobuf", "sentencepiece (>=0.1.91,!=0.1.92)"] -serving = ["accelerate (>=0.26.0)", "fastapi", "openai (>=1.98.0)", "pydantic (>=2)", "starlette", "torch (>=2.2)", "uvicorn"] -sigopt = ["sigopt"] -sklearn = ["scikit-learn"] -speech = ["kenlm", "librosa", "phonemizer", "pyctcdecode (>=0.4.0)", "torchaudio"] -testing = ["GitPython (<3.1.19)", "accelerate (>=0.26.0)", "beautifulsoup4", "cookiecutter (==1.7.3)", "datasets (>=2.15.0)", "datasets (>=2.15.0)", "dill (<0.3.5)", "evaluate (>=0.2.0)", "faiss-cpu", "fastapi", "libcst", "mistral-common[opencv] (>=1.6.3)", "nltk (<=3.8.1)", "openai (>=1.98.0)", "parameterized (>=0.9)", "psutil", "pydantic (>=2)", "pydantic (>=2)", "pytest (>=7.2.0)", "pytest-asyncio", "pytest-order", "pytest-rerunfailures (<16.0)", "pytest-rich", "pytest-timeout", "pytest-xdist", "rjieba", "rouge-score (!=0.0.7,!=0.0.8,!=0.1,!=0.1.1)", "ruff (==0.13.1)", "sacrebleu (>=1.4.12,<2.0.0)", "sacremoses", "sentencepiece (>=0.1.91,!=0.1.92)", "starlette", "tensorboard", "timeout-decorator", "torch (>=2.2)", "uvicorn"] -tf = ["keras-nlp (>=0.3.1,<0.14.0)", "onnxconverter-common", "tensorflow (>2.9,<2.16)", "tensorflow-text (<2.16)", "tf2onnx"] -tf-cpu = ["keras (>2.9,<2.16)", "keras-nlp (>=0.3.1,<0.14.0)", "onnxconverter-common", "tensorflow-cpu (>2.9,<2.16)", "tensorflow-probability (<0.24)", "tensorflow-text (<2.16)", "tf2onnx"] -tf-speech = ["kenlm", "librosa", "phonemizer", "pyctcdecode (>=0.4.0)"] -tiktoken = ["blobfile", "tiktoken"] -timm = ["timm (!=1.0.18,<=1.0.19)"] -tokenizers = ["tokenizers (>=0.22.0,<=0.23.0)"] -torch = ["accelerate (>=0.26.0)", "torch (>=2.2)"] -torch-speech = ["kenlm", "librosa", "phonemizer", "pyctcdecode (>=0.4.0)", "torchaudio"] -torch-vision = ["Pillow (>=10.0.1,<=15.0)", "torchvision"] -torchhub = ["filelock", "huggingface-hub (>=0.34.0,<1.0)", "importlib_metadata", "numpy (>=1.17)", "packaging (>=20.0)", "protobuf", "regex (!=2019.12.17)", "requests", "sentencepiece (>=0.1.91,!=0.1.92)", "tokenizers (>=0.22.0,<=0.23.0)", "torch (>=2.2)", "tqdm (>=4.27)"] -video = ["av"] -vision = ["Pillow (>=10.0.1,<=15.0)"] - -[[package]] -name = "triton" -version = "3.3.1" -description = "A language and compiler for custom Deep Learning operations" -optional = false -python-versions = "*" -groups = ["main"] -markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and python_version >= \"3.13\"" -files = [ - {file = "triton-3.3.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b74db445b1c562844d3cfad6e9679c72e93fdfb1a90a24052b03bb5c49d1242e"}, - {file = "triton-3.3.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b31e3aa26f8cb3cc5bf4e187bf737cbacf17311e1112b781d4a059353dfd731b"}, - {file = "triton-3.3.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9999e83aba21e1a78c1f36f21bce621b77bcaa530277a50484a7cb4a822f6e43"}, - {file = "triton-3.3.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b89d846b5a4198317fec27a5d3a609ea96b6d557ff44b56c23176546023c4240"}, - {file = "triton-3.3.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3198adb9d78b77818a5388bff89fa72ff36f9da0bc689db2f0a651a67ce6a42"}, - {file = "triton-3.3.1-cp39-cp39-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f6139aeb04a146b0b8e0fbbd89ad1e65861c57cfed881f21d62d3cb94a36bab7"}, -] - -[package.dependencies] -setuptools = ">=40.8.0" - -[package.extras] -build = ["cmake (>=3.20)", "lit"] -tests = ["autopep8", "isort", "llnl-hatchet", "numpy", "pytest", "pytest-forked", "pytest-xdist", "scipy (>=1.7.1)"] -tutorials = ["matplotlib", "pandas", "tabulate"] - -[[package]] -name = "triton" -version = "3.5.0" -description = "A language and compiler for custom Deep Learning operations" -optional = false -python-versions = "<3.15,>=3.10" -groups = ["main"] -markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and python_version < \"3.13\"" -files = [ - {file = "triton-3.5.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6f90de6a6566bb619b4c0adc9855729e1b1b5e26533fca1bf6206e96b6d277a3"}, - {file = "triton-3.5.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5d3b3d480debf24eaa739623c9a42446b0b77f95593d30eb1f64cd2278cc1f0"}, - {file = "triton-3.5.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8457b22148defefdcb7fa8144b05ce211b9faefad650a1ce85b23df488d5549c"}, - {file = "triton-3.5.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f34bfa21c5b3a203c0f0eab28dcc1e49bd1f67d22724e77fb6665a659200a4ec"}, - {file = "triton-3.5.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7da21fccceafc163e3a5e857abe34351ef76345af06cabf9637a914742671f0b"}, - {file = "triton-3.5.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9e71db82261c4ffa3921cd050cd5faa18322d2d405c30eb56084afaff3b0833"}, - {file = "triton-3.5.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:188da5b81fa2f8322c27fec1627703eac24cb9bb7ab0dfbe9925973bc1b070d3"}, - {file = "triton-3.5.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e6bb9aa5519c084a333acdba443789e50012a4b851cd486c54f0b8dc2a8d3a12"}, - {file = "triton-3.5.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:03127d9b33aaf979c856676b394bc059ec1d68cb6da68ae03f62dd8ad77a04ae"}, - {file = "triton-3.5.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c83f2343e1a220a716c7b3ab9fccfcbe3ad4020d189549200e2d2e8d5868bed9"}, - {file = "triton-3.5.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:468936651d383f4a6d10068d34a627505e13af55be5d002b9f27b987e7a5f0ac"}, - {file = "triton-3.5.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da0fa67ccd76c3dcfb0bffe1b1c57c685136a6bd33d141c24d9655d4185b1289"}, - {file = "triton-3.5.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7ceef21410229ac23173a28eee5cfc0e37c1dfdb8b4bc11ecda2e3ecec7c686"}, - {file = "triton-3.5.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:317fe477ea8fd4524a6a8c499fb0a36984a56d0b75bf9c9cb6133a1c56d5a6e7"}, -] - -[package.extras] -build = ["cmake (>=3.20,<4.0)", "lit"] -tests = ["autopep8", "isort", "llnl-hatchet", "numpy", "pytest", "pytest-forked", "pytest-xdist", "scipy (>=1.7.1)"] -tutorials = ["matplotlib", "pandas", "tabulate"] - [[package]] name = "typing-extensions" version = "4.15.0" @@ -3035,53 +753,7 @@ files = [ {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, ] -markers = {dev = "python_version == \"3.10\""} - -[[package]] -name = "typing-inspection" -version = "0.4.2" -description = "Runtime typing introspection tools" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7"}, - {file = "typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464"}, -] - -[package.dependencies] -typing-extensions = ">=4.12.0" - -[[package]] -name = "tzdata" -version = "2025.2" -description = "Provider of IANA time zone data" -optional = false -python-versions = ">=2" -groups = ["main"] -markers = "platform_system == \"Windows\"" -files = [ - {file = "tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8"}, - {file = "tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9"}, -] - -[[package]] -name = "tzlocal" -version = "5.3.1" -description = "tzinfo object for the local timezone" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "tzlocal-5.3.1-py3-none-any.whl", hash = "sha256:eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d"}, - {file = "tzlocal-5.3.1.tar.gz", hash = "sha256:cceffc7edecefea1f595541dbd6e990cb1ea3d19bf01b2809f362a03dd7921fd"}, -] - -[package.dependencies] -tzdata = {version = "*", markers = "platform_system == \"Windows\""} - -[package.extras] -devenv = ["check-manifest", "pytest (>=4.3)", "pytest-cov", "pytest-mock (>=3.3)", "zest.releaser"] +markers = {main = "python_version < \"3.13\"", dev = "python_version == \"3.10\""} [[package]] name = "urllib3" @@ -3101,138 +773,7 @@ h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] -[[package]] -name = "uvicorn" -version = "0.31.0" -description = "The lightning-fast ASGI server." -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "uvicorn-0.31.0-py3-none-any.whl", hash = "sha256:cac7be4dd4d891c363cd942160a7b02e69150dcbc7a36be04d5f4af4b17c8ced"}, - {file = "uvicorn-0.31.0.tar.gz", hash = "sha256:13bc21373d103859f68fe739608e2eb054a816dea79189bc3ca08ea89a275906"}, -] - -[package.dependencies] -click = ">=7.0" -h11 = ">=0.8" -typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""} - -[package.extras] -standard = ["colorama (>=0.4) ; sys_platform == \"win32\"", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1) ; sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\"", "watchfiles (>=0.13)", "websockets (>=10.4)"] - -[[package]] -name = "wrapt" -version = "1.17.3" -description = "Module for decorators, wrappers and monkey patching." -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "wrapt-1.17.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88bbae4d40d5a46142e70d58bf664a89b6b4befaea7b2ecc14e03cedb8e06c04"}, - {file = "wrapt-1.17.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6b13af258d6a9ad602d57d889f83b9d5543acd471eee12eb51f5b01f8eb1bc2"}, - {file = "wrapt-1.17.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd341868a4b6714a5962c1af0bd44f7c404ef78720c7de4892901e540417111c"}, - {file = "wrapt-1.17.3-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f9b2601381be482f70e5d1051a5965c25fb3625455a2bf520b5a077b22afb775"}, - {file = "wrapt-1.17.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:343e44b2a8e60e06a7e0d29c1671a0d9951f59174f3709962b5143f60a2a98bd"}, - {file = "wrapt-1.17.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:33486899acd2d7d3066156b03465b949da3fd41a5da6e394ec49d271baefcf05"}, - {file = "wrapt-1.17.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e6f40a8aa5a92f150bdb3e1c44b7e98fb7113955b2e5394122fa5532fec4b418"}, - {file = "wrapt-1.17.3-cp310-cp310-win32.whl", hash = "sha256:a36692b8491d30a8c75f1dfee65bef119d6f39ea84ee04d9f9311f83c5ad9390"}, - {file = "wrapt-1.17.3-cp310-cp310-win_amd64.whl", hash = "sha256:afd964fd43b10c12213574db492cb8f73b2f0826c8df07a68288f8f19af2ebe6"}, - {file = "wrapt-1.17.3-cp310-cp310-win_arm64.whl", hash = "sha256:af338aa93554be859173c39c85243970dc6a289fa907402289eeae7543e1ae18"}, - {file = "wrapt-1.17.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:273a736c4645e63ac582c60a56b0acb529ef07f78e08dc6bfadf6a46b19c0da7"}, - {file = "wrapt-1.17.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5531d911795e3f935a9c23eb1c8c03c211661a5060aab167065896bbf62a5f85"}, - {file = "wrapt-1.17.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0610b46293c59a3adbae3dee552b648b984176f8562ee0dba099a56cfbe4df1f"}, - {file = "wrapt-1.17.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b32888aad8b6e68f83a8fdccbf3165f5469702a7544472bdf41f582970ed3311"}, - {file = "wrapt-1.17.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cccf4f81371f257440c88faed6b74f1053eef90807b77e31ca057b2db74edb1"}, - {file = "wrapt-1.17.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8a210b158a34164de8bb68b0e7780041a903d7b00c87e906fb69928bf7890d5"}, - {file = "wrapt-1.17.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:79573c24a46ce11aab457b472efd8d125e5a51da2d1d24387666cd85f54c05b2"}, - {file = "wrapt-1.17.3-cp311-cp311-win32.whl", hash = "sha256:c31eebe420a9a5d2887b13000b043ff6ca27c452a9a22fa71f35f118e8d4bf89"}, - {file = "wrapt-1.17.3-cp311-cp311-win_amd64.whl", hash = "sha256:0b1831115c97f0663cb77aa27d381237e73ad4f721391a9bfb2fe8bc25fa6e77"}, - {file = "wrapt-1.17.3-cp311-cp311-win_arm64.whl", hash = "sha256:5a7b3c1ee8265eb4c8f1b7d29943f195c00673f5ab60c192eba2d4a7eae5f46a"}, - {file = "wrapt-1.17.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ab232e7fdb44cdfbf55fc3afa31bcdb0d8980b9b95c38b6405df2acb672af0e0"}, - {file = "wrapt-1.17.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9baa544e6acc91130e926e8c802a17f3b16fbea0fd441b5a60f5cf2cc5c3deba"}, - {file = "wrapt-1.17.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b538e31eca1a7ea4605e44f81a48aa24c4632a277431a6ed3f328835901f4fd"}, - {file = "wrapt-1.17.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:042ec3bb8f319c147b1301f2393bc19dba6e176b7da446853406d041c36c7828"}, - {file = "wrapt-1.17.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3af60380ba0b7b5aeb329bc4e402acd25bd877e98b3727b0135cb5c2efdaefe9"}, - {file = "wrapt-1.17.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b02e424deef65c9f7326d8c19220a2c9040c51dc165cddb732f16198c168396"}, - {file = "wrapt-1.17.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:74afa28374a3c3a11b3b5e5fca0ae03bef8450d6aa3ab3a1e2c30e3a75d023dc"}, - {file = "wrapt-1.17.3-cp312-cp312-win32.whl", hash = "sha256:4da9f45279fff3543c371d5ababc57a0384f70be244de7759c85a7f989cb4ebe"}, - {file = "wrapt-1.17.3-cp312-cp312-win_amd64.whl", hash = "sha256:e71d5c6ebac14875668a1e90baf2ea0ef5b7ac7918355850c0908ae82bcb297c"}, - {file = "wrapt-1.17.3-cp312-cp312-win_arm64.whl", hash = "sha256:604d076c55e2fdd4c1c03d06dc1a31b95130010517b5019db15365ec4a405fc6"}, - {file = "wrapt-1.17.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0"}, - {file = "wrapt-1.17.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77"}, - {file = "wrapt-1.17.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7"}, - {file = "wrapt-1.17.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6fd1ad24dc235e4ab88cda009e19bf347aabb975e44fd5c2fb22a3f6e4141277"}, - {file = "wrapt-1.17.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ed61b7c2d49cee3c027372df5809a59d60cf1b6c2f81ee980a091f3afed6a2d"}, - {file = "wrapt-1.17.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:423ed5420ad5f5529db9ce89eac09c8a2f97da18eb1c870237e84c5a5c2d60aa"}, - {file = "wrapt-1.17.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e01375f275f010fcbf7f643b4279896d04e571889b8a5b3f848423d91bf07050"}, - {file = "wrapt-1.17.3-cp313-cp313-win32.whl", hash = "sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8"}, - {file = "wrapt-1.17.3-cp313-cp313-win_amd64.whl", hash = "sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb"}, - {file = "wrapt-1.17.3-cp313-cp313-win_arm64.whl", hash = "sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16"}, - {file = "wrapt-1.17.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39"}, - {file = "wrapt-1.17.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235"}, - {file = "wrapt-1.17.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c"}, - {file = "wrapt-1.17.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b"}, - {file = "wrapt-1.17.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa"}, - {file = "wrapt-1.17.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7"}, - {file = "wrapt-1.17.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4"}, - {file = "wrapt-1.17.3-cp314-cp314-win32.whl", hash = "sha256:fbd3c8319de8e1dc79d346929cd71d523622da527cca14e0c1d257e31c2b8b10"}, - {file = "wrapt-1.17.3-cp314-cp314-win_amd64.whl", hash = "sha256:e1a4120ae5705f673727d3253de3ed0e016f7cd78dc463db1b31e2463e1f3cf6"}, - {file = "wrapt-1.17.3-cp314-cp314-win_arm64.whl", hash = "sha256:507553480670cab08a800b9463bdb881b2edeed77dc677b0a5915e6106e91a58"}, - {file = "wrapt-1.17.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a"}, - {file = "wrapt-1.17.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067"}, - {file = "wrapt-1.17.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454"}, - {file = "wrapt-1.17.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e"}, - {file = "wrapt-1.17.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f"}, - {file = "wrapt-1.17.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056"}, - {file = "wrapt-1.17.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804"}, - {file = "wrapt-1.17.3-cp314-cp314t-win32.whl", hash = "sha256:41b1d2bc74c2cac6f9074df52b2efbef2b30bdfe5f40cb78f8ca22963bc62977"}, - {file = "wrapt-1.17.3-cp314-cp314t-win_amd64.whl", hash = "sha256:73d496de46cd2cdbdbcce4ae4bcdb4afb6a11234a1df9c085249d55166b95116"}, - {file = "wrapt-1.17.3-cp314-cp314t-win_arm64.whl", hash = "sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6"}, - {file = "wrapt-1.17.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:70d86fa5197b8947a2fa70260b48e400bf2ccacdcab97bb7de47e3d1e6312225"}, - {file = "wrapt-1.17.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:df7d30371a2accfe4013e90445f6388c570f103d61019b6b7c57e0265250072a"}, - {file = "wrapt-1.17.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:caea3e9c79d5f0d2c6d9ab96111601797ea5da8e6d0723f77eabb0d4068d2b2f"}, - {file = "wrapt-1.17.3-cp38-cp38-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:758895b01d546812d1f42204bd443b8c433c44d090248bf22689df673ccafe00"}, - {file = "wrapt-1.17.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02b551d101f31694fc785e58e0720ef7d9a10c4e62c1c9358ce6f63f23e30a56"}, - {file = "wrapt-1.17.3-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:656873859b3b50eeebe6db8b1455e99d90c26ab058db8e427046dbc35c3140a5"}, - {file = "wrapt-1.17.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:a9a2203361a6e6404f80b99234fe7fb37d1fc73487b5a78dc1aa5b97201e0f22"}, - {file = "wrapt-1.17.3-cp38-cp38-win32.whl", hash = "sha256:55cbbc356c2842f39bcc553cf695932e8b30e30e797f961860afb308e6b1bb7c"}, - {file = "wrapt-1.17.3-cp38-cp38-win_amd64.whl", hash = "sha256:ad85e269fe54d506b240d2d7b9f5f2057c2aa9a2ea5b32c66f8902f768117ed2"}, - {file = "wrapt-1.17.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:30ce38e66630599e1193798285706903110d4f057aab3168a34b7fdc85569afc"}, - {file = "wrapt-1.17.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:65d1d00fbfb3ea5f20add88bbc0f815150dbbde3b026e6c24759466c8b5a9ef9"}, - {file = "wrapt-1.17.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a7c06742645f914f26c7f1fa47b8bc4c91d222f76ee20116c43d5ef0912bba2d"}, - {file = "wrapt-1.17.3-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7e18f01b0c3e4a07fe6dfdb00e29049ba17eadbc5e7609a2a3a4af83ab7d710a"}, - {file = "wrapt-1.17.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f5f51a6466667a5a356e6381d362d259125b57f059103dd9fdc8c0cf1d14139"}, - {file = "wrapt-1.17.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:59923aa12d0157f6b82d686c3fd8e1166fa8cdfb3e17b42ce3b6147ff81528df"}, - {file = "wrapt-1.17.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:46acc57b331e0b3bcb3e1ca3b421d65637915cfcd65eb783cb2f78a511193f9b"}, - {file = "wrapt-1.17.3-cp39-cp39-win32.whl", hash = "sha256:3e62d15d3cfa26e3d0788094de7b64efa75f3a53875cdbccdf78547aed547a81"}, - {file = "wrapt-1.17.3-cp39-cp39-win_amd64.whl", hash = "sha256:1f23fa283f51c890eda8e34e4937079114c74b4c81d2b2f1f1d94948f5cc3d7f"}, - {file = "wrapt-1.17.3-cp39-cp39-win_arm64.whl", hash = "sha256:24c2ed34dc222ed754247a2702b1e1e89fdbaa4016f324b4b8f1a802d4ffe87f"}, - {file = "wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22"}, - {file = "wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0"}, -] - -[[package]] -name = "zipp" -version = "3.23.0" -description = "Backport of pathlib-compatible object wrapper for zip files" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e"}, - {file = "zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166"}, -] - -[package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -enabler = ["pytest-enabler (>=2.2)"] -test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more_itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] -type = ["pytest-mypy"] - [metadata] lock-version = "2.1" python-versions = ">=3.10" -content-hash = "c831ac5cd8333c5519b29a9c1b8d95de79d19a2f651bac1c998c0e5b64ff3240" +content-hash = "597534a122f4396a842eec6430e963f8fc6dd8d929cae1d5d6306aa262fcd1ee" diff --git a/arch/tools/pyproject.toml b/arch/tools/pyproject.toml index a38d794e..7bff20ba 100644 --- a/arch/tools/pyproject.toml +++ b/arch/tools/pyproject.toml @@ -1,34 +1,28 @@ -[project] -name = "archgw" -version = "0.3.18" -description = "Python-based CLI tool to manage Arch Gateway." -authors = [{ name = "Katanemo Labs, Inc." }] -readme = "README.md" -requires-python = ">=3.10" -dependencies = [ - "archgw_modelserver==0.3.18", - "click>=8.1.7,<9.0.0", - "jinja2>=3.1.4,<4.0.0", - "jsonschema>=4.23.0,<5.0.0", - "pyyaml>=6.0.2,<7.0.0", -] - -[project.scripts] -archgw = "cli.main:main" - -[dependency-groups] -dev = [ - "pytest>=8.4.1,<9.0.0", -] - [tool.poetry] +name = "archgw" +version = "0.3.22" +description = "Python-based CLI tool to manage Arch Gateway." +authors = ["Katanemo Labs, Inc."] +readme = "README.md" packages = [{ include = "cli" }] -dependencies = { archgw_modelserver = { path = "../../model_server", develop = true } } + +[tool.poetry.dependencies] +python = ">=3.10" +click = ">=8.1.7,<9.0.0" +jinja2 = ">=3.1.4,<4.0.0" +jsonschema = ">=4.23.0,<5.0.0" +pyyaml = ">=6.0.2,<7.0.0" +requests = ">=2.31.0,<3.0.0" + +[tool.poetry.group.dev.dependencies] +pytest = ">=8.4.1,<9.0.0" + +[tool.poetry.scripts] +archgw = "cli.main:main" [build-system] requires = ["poetry-core>=2.0.0"] build-backend = "poetry.core.masonry.api" - [tool.pytest.ini_options] addopts = ["-v"] diff --git a/arch/tools/test/test_config_generator.py b/arch/tools/test/test_config_generator.py index 7016a34f..5a19c424 100644 --- a/arch/tools/test/test_config_generator.py +++ b/arch/tools/test/test_config_generator.py @@ -94,21 +94,16 @@ def test_validate_and_render_happy_path_agent_config(monkeypatch): version: v0.3.0 agents: - - name: query_rewriter - kind: openai - endpoint: http://localhost:10500 - - name: context_builder - kind: openai - endpoint: http://localhost:10501 - - name: response_generator - kind: openai - endpoint: http://localhost:10502 - - name: research_agent - kind: openai - endpoint: http://localhost:10500 - - name: input_guard_rails - kind: openai - endpoint: http://localhost:10503 + - id: query_rewriter + url: http://localhost:10500 + - id: context_builder + url: http://localhost:10501 + - id: response_generator + url: http://localhost:10502 + - id: research_agent + url: http://localhost:10500 + - id: input_guard_rails + url: http://localhost:10503 listeners: - name: tmobile @@ -156,7 +151,7 @@ listeners: mock.mock_open().return_value, # ARCH_CONFIG_FILE_RENDERED (write) ] with mock.patch("builtins.open", m_open): - with mock.patch("config_generator.Environment"): + with mock.patch("cli.config_generator.Environment"): validate_and_render_schema() diff --git a/archgw.code-workspace b/archgw.code-workspace index 840bd2dd..bd24f82a 100644 --- a/archgw.code-workspace +++ b/archgw.code-workspace @@ -12,10 +12,6 @@ "name": "archgw_cli", "path": "arch/tools" }, - { - "name": "model_server", - "path": "model_server" - }, { "name": "tests_e2e", "path": "tests/e2e" @@ -24,10 +20,6 @@ "name": "tests_archgw", "path": "tests/archgw" }, - { - "name": "tests_modelserver", - "path": "tests/modelserver" - }, { "name": "chatbot_ui", "path": "demos/shared/chatbot_ui" @@ -42,6 +34,7 @@ "editor.defaultFormatter": "ms-python.black-formatter", "editor.formatOnSave": true }, + "rust-analyzer.cargo.features": ["trace-collection"] }, "extensions": { "recommendations": [ diff --git a/build_filter_image.sh b/build_filter_image.sh index ea3c5c31..7f1d7b31 100644 --- a/build_filter_image.sh +++ b/build_filter_image.sh @@ -1 +1 @@ -docker build -f arch/Dockerfile . -t katanemo/archgw -t katanemo/archgw:0.3.2 +docker build -f arch/Dockerfile . -t katanemo/archgw -t katanemo/archgw:0.3.22 diff --git a/crates/Cargo.lock b/crates/Cargo.lock index 0115151e..4dbcae49 100644 --- a/crates/Cargo.lock +++ b/crates/Cargo.lock @@ -78,6 +78,43 @@ dependencies = [ "serde_json", ] +[[package]] +name = "async-openai" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bf39a15c8d613eb61892dc9a287c02277639ebead41ee611ad23aaa613f1a82" +dependencies = [ + "async-openai-macros", + "backoff", + "base64 0.22.1", + "bytes", + "derive_builder", + "eventsource-stream", + "futures", + "rand 0.9.2", + "reqwest", + "reqwest-eventsource", + "secrecy", + "serde", + "serde_json", + "thiserror 2.0.12", + "tokio", + "tokio-stream", + "tokio-util", + "tracing", +] + +[[package]] +name = "async-openai-macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0289cba6d5143bfe8251d57b4a8cac036adf158525a76533a7082ba65ec76398" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + [[package]] name = "async-trait" version = "0.1.88" @@ -130,6 +167,75 @@ dependencies = [ "time", ] +[[package]] +name = "axum" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" +dependencies = [ + "async-trait", + "axum-core", + "bytes", + "futures-util", + "http 1.3.1", + "http-body 1.0.1", + "http-body-util", + "hyper 1.6.0", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower 0.5.2", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http 1.3.1", + "http-body 1.0.1", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "backoff" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b62ddb9cb1ec0a098ad4bbf9344d0713fa193ae1a80af55febcff2627b6a00c1" +dependencies = [ + "futures-core", + "getrandom 0.2.16", + "instant", + "pin-project-lite", + "rand 0.8.5", + "tokio", +] + [[package]] name = "backtrace" version = "0.3.75" @@ -201,10 +307,14 @@ dependencies = [ name = "brightstaff" version = "0.1.0" dependencies = [ + "async-openai", + "async-trait", "bytes", + "chrono", "common", "eventsource-client", "eventsource-stream", + "flate2", "futures", "futures-util", "hermesllm", @@ -219,6 +329,7 @@ dependencies = [ "opentelemetry-stdout", "opentelemetry_sdk", "pretty_assertions", + "rand 0.9.2", "reqwest", "serde", "serde_json", @@ -227,10 +338,12 @@ dependencies = [ "thiserror 2.0.12", "time", "tokio", + "tokio-postgres", "tokio-stream", "tracing", "tracing-opentelemetry", "tracing-subscriber", + "uuid", ] [[package]] @@ -250,6 +363,12 @@ version = "3.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "793db76d6187cd04dff33004d8e6c9cc4e05cd330500379d2394209271b4aeee" +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "bytes" version = "1.10.1" @@ -281,6 +400,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chrono" version = "0.4.41" @@ -289,8 +414,10 @@ checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" dependencies = [ "android-tzdata", "iana-time-zone", + "js-sys", "num-traits", "serde", + "wasm-bindgen", "windows-link", ] @@ -307,6 +434,7 @@ dependencies = [ name = "common" version = "0.1.0" dependencies = [ + "axum", "derivative", "duration-string", "governor", @@ -316,12 +444,16 @@ dependencies = [ "pretty_assertions", "proxy-wasm", "rand 0.8.5", + "reqwest", "serde", "serde_json", "serde_with", "serde_yaml", + "serial_test", "thiserror 1.0.69", "tiktoken-rs", + "tokio", + "tracing", "url", "urlencoding", ] @@ -336,6 +468,16 @@ dependencies = [ "libc", ] +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -426,6 +568,37 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "derive_builder" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "derive_builder_macro" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" +dependencies = [ + "derive_builder_core", + "syn 2.0.101", +] + [[package]] name = "diff" version = "0.1.13" @@ -440,6 +613,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", + "subtle", ] [[package]] @@ -527,6 +701,12 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "fallible-iterator" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" + [[package]] name = "fancy-regex" version = "0.12.0" @@ -543,6 +723,16 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "flate2" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "fnv" version = "1.0.7" @@ -650,6 +840,12 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +[[package]] +name = "futures-timer" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" + [[package]] name = "futures-util" version = "0.3.31" @@ -685,8 +881,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", ] [[package]] @@ -696,9 +894,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi", "wasi 0.14.2+wasi-0.2.4", + "wasm-bindgen", ] [[package]] @@ -798,10 +998,12 @@ version = "0.1.0" dependencies = [ "aws-smithy-eventstream", "bytes", + "log", "serde", "serde_json", "serde_with", "thiserror 2.0.12", + "uuid", ] [[package]] @@ -810,6 +1012,15 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + [[package]] name = "http" version = "0.2.12" @@ -934,7 +1145,7 @@ dependencies = [ "hyper 0.14.32", "log", "rustls 0.21.12", - "rustls-native-certs", + "rustls-native-certs 0.6.3", "tokio", "tokio-rustls 0.24.1", ] @@ -949,6 +1160,7 @@ dependencies = [ "hyper 1.6.0", "hyper-util", "rustls 0.23.27", + "rustls-native-certs 0.8.2", "rustls-pki-types", "tokio", "tokio-rustls 0.26.2", @@ -1181,6 +1393,15 @@ dependencies = [ "serde", ] +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", +] + [[package]] name = "ipnet" version = "2.11.0" @@ -1234,6 +1455,17 @@ version = "0.2.172" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" +[[package]] +name = "libredox" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" +dependencies = [ + "bitflags", + "libc", + "redox_syscall", +] + [[package]] name = "linux-raw-sys" version = "0.9.4" @@ -1285,6 +1517,12 @@ version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + [[package]] name = "matchers" version = "0.1.0" @@ -1294,6 +1532,22 @@ dependencies = [ "regex-automata 0.1.10", ] +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + [[package]] name = "md5" version = "0.7.0" @@ -1312,6 +1566,16 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -1325,6 +1589,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" dependencies = [ "adler2", + "simd-adler32", ] [[package]] @@ -1354,7 +1619,7 @@ dependencies = [ "hyper 1.6.0", "hyper-util", "log", - "rand 0.9.1", + "rand 0.9.2", "regex", "serde_json", "serde_urlencoded", @@ -1374,7 +1639,7 @@ dependencies = [ "openssl-probe", "openssl-sys", "schannel", - "security-framework", + "security-framework 2.11.1", "security-framework-sys", "tempfile", ] @@ -1581,7 +1846,7 @@ dependencies = [ "glob", "opentelemetry", "percent-encoding", - "rand 0.9.1", + "rand 0.9.2", "serde_json", "thiserror 2.0.12", "tracing", @@ -1628,6 +1893,24 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + [[package]] name = "pin-project" version = "1.1.10" @@ -1672,6 +1955,37 @@ version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" +[[package]] +name = "postgres-protocol" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbef655056b916eb868048276cfd5d6a7dea4f81560dfd047f97c8c6fe3fcfd4" +dependencies = [ + "base64 0.22.1", + "byteorder", + "bytes", + "fallible-iterator", + "hmac", + "md-5", + "memchr", + "rand 0.9.2", + "sha2", + "stringprep", +] + +[[package]] +name = "postgres-types" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613283563cd90e1dfc3518d548caee47e0e725455ed619881f5cf21f36de4b48" +dependencies = [ + "bytes", + "fallible-iterator", + "postgres-protocol", + "serde", + "serde_json", +] + [[package]] name = "potential_utf" version = "0.1.2" @@ -1770,6 +2084,61 @@ dependencies = [ "log", ] +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash 2.1.1", + "rustls 0.23.27", + "socket2", + "thiserror 2.0.12", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +dependencies = [ + "bytes", + "getrandom 0.3.3", + "lru-slab", + "rand 0.9.2", + "ring", + "rustc-hash 2.1.1", + "rustls 0.23.27", + "rustls-pki-types", + "slab", + "thiserror 2.0.12", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.59.0", +] + [[package]] name = "quote" version = "1.0.40" @@ -1798,9 +2167,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.9.1" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.3", @@ -1846,9 +2215,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.12" +version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ "bitflags", ] @@ -1941,10 +2310,14 @@ dependencies = [ "js-sys", "log", "mime", + "mime_guess", "native-tls", "once_cell", "percent-encoding", "pin-project-lite", + "quinn", + "rustls 0.23.27", + "rustls-native-certs 0.8.2", "rustls-pki-types", "serde", "serde_json", @@ -1952,6 +2325,7 @@ dependencies = [ "sync_wrapper", "tokio", "tokio-native-tls", + "tokio-rustls 0.26.2", "tokio-util", "tower 0.5.2", "tower-http", @@ -1963,6 +2337,22 @@ dependencies = [ "web-sys", ] +[[package]] +name = "reqwest-eventsource" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "632c55746dbb44275691640e7b40c907c16a2dc1a5842aa98aaec90da6ec6bde" +dependencies = [ + "eventsource-stream", + "futures-core", + "futures-timer", + "mime", + "nom", + "pin-project-lite", + "reqwest", + "thiserror 1.0.69", +] + [[package]] name = "ring" version = "0.17.14" @@ -1989,6 +2379,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + [[package]] name = "rustix" version = "1.0.7" @@ -2021,6 +2417,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "730944ca083c1c233a75c09f199e973ca499344a2b7ba9e755c457e86fb4a321" dependencies = [ "once_cell", + "ring", "rustls-pki-types", "rustls-webpki 0.103.3", "subtle", @@ -2036,7 +2433,19 @@ dependencies = [ "openssl-probe", "rustls-pemfile", "schannel", - "security-framework", + "security-framework 2.11.1", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9980d917ebb0c0536119ba501e90834767bffc3d60641457fd84a1f3fd337923" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework 3.5.1", ] [[package]] @@ -2054,6 +2463,7 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" dependencies = [ + "web-time", "zeroize", ] @@ -2142,6 +2552,16 @@ version = "3.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "584e070911c7017da6cb2eb0788d09f43d789029b5877d3e5ecc8acf86ceee21" +[[package]] +name = "secrecy" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e891af845473308773346dc847b2c23ee78fe442e0472ac50e22a18a93d3ae5a" +dependencies = [ + "serde", + "zeroize", +] + [[package]] name = "security-framework" version = "2.11.1" @@ -2149,7 +2569,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ "bitflags", - "core-foundation", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework" +version = "3.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" +dependencies = [ + "bitflags", + "core-foundation 0.10.1", "core-foundation-sys", "libc", "security-framework-sys", @@ -2191,12 +2624,23 @@ version = "1.0.140" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" dependencies = [ + "indexmap 2.9.0", "itoa", "memchr", "ryu", "serde", ] +[[package]] +name = "serde_path_to_error" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59fab13f937fa393d08645bf3a84bdfe86e296747b506ada67bb15f10f218b2a" +dependencies = [ + "itoa", + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -2313,12 +2757,24 @@ dependencies = [ "libc", ] +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + [[package]] name = "similar" version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + [[package]] name = "slab" version = "0.4.9" @@ -2359,6 +2815,17 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + [[package]] name = "strsim" version = "0.11.1" @@ -2420,7 +2887,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ "bitflags", - "core-foundation", + "core-foundation 0.9.4", "system-configuration-sys", ] @@ -2509,7 +2976,7 @@ dependencies = [ "fancy-regex", "lazy_static", "parking_lot", - "rustc-hash", + "rustc-hash 1.1.0", ] [[package]] @@ -2553,6 +3020,21 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tokio" version = "1.45.1" @@ -2602,6 +3084,32 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-postgres" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c95d533c83082bb6490e0189acaa0bbeef9084e60471b696ca6988cd0541fb0" +dependencies = [ + "async-trait", + "byteorder", + "bytes", + "fallible-iterator", + "futures-channel", + "futures-util", + "log", + "parking_lot", + "percent-encoding", + "phf", + "pin-project-lite", + "postgres-protocol", + "postgres-types", + "rand 0.9.2", + "socket2", + "tokio", + "tokio-util", + "whoami", +] + [[package]] name = "tokio-rustls" version = "0.24.1" @@ -2705,6 +3213,7 @@ dependencies = [ "tokio", "tower-layer", "tower-service", + "tracing", ] [[package]] @@ -2743,6 +3252,7 @@ version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ + "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -2829,12 +3339,39 @@ version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" +[[package]] +name = "unicase" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + [[package]] name = "unicode-ident" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + [[package]] name = "unsafe-libyaml" version = "0.2.11" @@ -2870,6 +3407,18 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "uuid" +version = "1.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" +dependencies = [ + "getrandom 0.3.3", + "js-sys", + "serde", + "wasm-bindgen", +] + [[package]] name = "valuable" version = "0.1.1" @@ -2918,6 +3467,12 @@ dependencies = [ "wit-bindgen-rt", ] +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + [[package]] name = "wasm-bindgen" version = "0.2.100" @@ -3022,6 +3577,17 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "whoami" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" +dependencies = [ + "libredox", + "wasite", + "web-sys", +] + [[package]] name = "winapi" version = "0.3.9" diff --git a/crates/Cargo.toml b/crates/Cargo.toml index 5cd6b29c..c22e252e 100644 --- a/crates/Cargo.toml +++ b/crates/Cargo.toml @@ -1,3 +1,7 @@ [workspace] resolver = "2" members = ["llm_gateway", "prompt_gateway", "common", "brightstaff", "hermesllm"] + +[workspace.metadata.rust-analyzer] +# Enable features for better IDE support +cargo.features = ["trace-collection"] diff --git a/crates/brightstaff/Cargo.toml b/crates/brightstaff/Cargo.toml index d424b0e6..233a4da3 100644 --- a/crates/brightstaff/Cargo.toml +++ b/crates/brightstaff/Cargo.toml @@ -4,10 +4,14 @@ version = "0.1.0" edition = "2021" [dependencies] +async-openai = "0.30.1" +async-trait = "0.1" bytes = "1.10.1" -common = { version = "0.1.0", path = "../common" } +chrono = "0.4" +common = { version = "0.1.0", path = "../common", features = ["trace-collection"] } eventsource-client = "0.15.0" eventsource-stream = "0.2.3" +flate2 = "1.0" futures = "0.3.31" futures-util = "0.3.31" hermesllm = { version = "0.1.0", path = "../hermesllm" } @@ -21,6 +25,7 @@ opentelemetry-otlp = {version="0.29.0", features=["trace", "tonic", "grpc-tonic" opentelemetry-stdout = "0.29.0" opentelemetry_sdk = "0.29.0" pretty_assertions = "1.4.1" +rand = "0.9.2" reqwest = { version = "0.12.15", features = ["stream"] } serde = { version = "1.0.219", features = ["derive"] } serde_json = "1.0.140" @@ -28,10 +33,12 @@ serde_with = "3.13.0" serde_yaml = "0.9.34" thiserror = "2.0.12" tokio = { version = "1.44.2", features = ["full"] } +tokio-postgres = { version = "0.7", features = ["with-serde_json-1"] } tokio-stream = "0.1" time = { version = "0.3", features = ["formatting", "macros"] } tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } +uuid = { version = "1.0", features = ["v4", "serde"] } [dev-dependencies] mockito = "1.0" diff --git a/crates/brightstaff/src/handlers/agent_chat_completions.rs b/crates/brightstaff/src/handlers/agent_chat_completions.rs index a1a00f88..c6675b06 100644 --- a/crates/brightstaff/src/handlers/agent_chat_completions.rs +++ b/crates/brightstaff/src/handlers/agent_chat_completions.rs @@ -1,16 +1,24 @@ use std::sync::Arc; +use std::time::{Instant, SystemTime}; use bytes::Bytes; -use hermesllm::apis::openai::ChatCompletionsRequest; +use common::consts::TRACE_PARENT_HEADER; +use common::traces::{SpanBuilder, SpanKind, parse_traceparent, generate_random_span_id}; +use hermesllm::apis::OpenAIMessage; +use hermesllm::clients::SupportedAPIsFromClient; +use hermesllm::providers::request::ProviderRequest; +use hermesllm::ProviderRequestType; use http_body_util::combinators::BoxBody; use http_body_util::BodyExt; use hyper::{Request, Response}; +use serde::ser::Error as SerError; use tracing::{debug, info, warn}; use super::agent_selector::{AgentSelectionError, AgentSelector}; use super::pipeline_processor::{PipelineError, PipelineProcessor}; use super::response_handler::ResponseHandler; use crate::router::llm_router::RouterService; +use crate::tracing::{OperationNameBuilder, operation_component, http}; /// Main errors for agent chat completions #[derive(Debug, thiserror::Error)] @@ -33,11 +41,51 @@ pub async fn agent_chat( _: String, agents_list: Arc>>>, listeners: Arc>>, + trace_collector: Arc, ) -> Result>, hyper::Error> { - match handle_agent_chat(request, router_service, agents_list, listeners).await { + match handle_agent_chat( + request, + router_service, + agents_list, + listeners, + trace_collector, + ) + .await + { Ok(response) => Ok(response), Err(err) => { - // Print detailed error information with full error chain + // Check if this is a client error from the pipeline that should be cascaded + if let AgentFilterChainError::Pipeline(PipelineError::ClientError { + agent, + status, + body, + }) = &err + { + warn!( + "Client error from agent '{}' (HTTP {}): {}", + agent, status, body + ); + + // Create error response with the original status code and body + let error_json = serde_json::json!({ + "error": "ClientError", + "agent": agent, + "status": status, + "agent_response": body + }); + + let json_string = error_json.to_string(); + let mut response = Response::new(ResponseHandler::create_full_body(json_string)); + *response.status_mut() = hyper::StatusCode::from_u16(*status) + .unwrap_or(hyper::StatusCode::INTERNAL_SERVER_ERROR); + response.headers_mut().insert( + hyper::header::CONTENT_TYPE, + "application/json".parse().unwrap(), + ); + return Ok(response); + } + + // Print detailed error information with full error chain for other errors let mut error_chain = Vec::new(); let mut current_error: &dyn std::error::Error = &err; @@ -78,10 +126,11 @@ async fn handle_agent_chat( router_service: Arc, agents_list: Arc>>>, listeners: Arc>>, + trace_collector: Arc, ) -> Result>, AgentFilterChainError> { // Initialize services let agent_selector = AgentSelector::new(router_service); - let pipeline_processor = PipelineProcessor::default(); + let mut pipeline_processor = PipelineProcessor::default(); let response_handler = ResponseHandler::new(); // Extract listener name from headers @@ -101,6 +150,13 @@ async fn handle_agent_chat( info!("Handling request for listener: {}", listener.name); // Parse request body + let request_path = request + .uri() + .path() + .to_string() + .strip_prefix("/agents") + .unwrap() + .to_string(); let request_headers = request.headers().clone(); let chat_request_bytes = request.collect().await?.to_bytes(); @@ -109,61 +165,141 @@ async fn handle_agent_chat( String::from_utf8_lossy(&chat_request_bytes) ); - let chat_completions_request: ChatCompletionsRequest = - serde_json::from_slice(&chat_request_bytes).map_err(|err| { - warn!( - "Failed to parse request body as ChatCompletionsRequest: {}", - err - ); - AgentFilterChainError::RequestParsing(err) + // Determine the API type from the endpoint + let api_type = + SupportedAPIsFromClient::from_endpoint(request_path.as_str()).ok_or_else(|| { + let err_msg = format!("Unsupported endpoint: {}", request_path); + warn!("{}", err_msg); + AgentFilterChainError::RequestParsing(serde_json::Error::custom(err_msg)) })?; + let client_request = match ProviderRequestType::try_from((&chat_request_bytes[..], &api_type)) { + Ok(request) => request, + Err(err) => { + warn!("Failed to parse request as ProviderRequestType: {}", err); + let err_msg = format!("Failed to parse request: {}", err); + return Err(AgentFilterChainError::RequestParsing( + serde_json::Error::custom(err_msg), + )); + } + }; + + let message: Vec = client_request.get_messages(); + + // let chat_completions_request: ChatCompletionsRequest = + // serde_json::from_slice(&chat_request_bytes).map_err(|err| { + // warn!( + // "Failed to parse request body as ChatCompletionsRequest: {}", + // err + // ); + // AgentFilterChainError::RequestParsing(err) + // })?; + // Extract trace parent for routing let trace_parent = request_headers .iter() - .find(|(key, _)| key.as_str() == "traceparent") + .find(|(key, _)| key.as_str() == TRACE_PARENT_HEADER) .map(|(_, value)| value.to_str().unwrap_or_default().to_string()); - // Select appropriate agent using arch router llm model - let selected_agent = agent_selector - .select_agent(&chat_completions_request.messages, &listener, trace_parent) - .await?; - - debug!("Processing agent pipeline: {}", selected_agent.id); - - // Create agent map for pipeline processing + // Create agent map for pipeline processing and agent selection let agent_map = { let agents = agents_list.read().await; let agents = agents.as_ref().unwrap(); agent_selector.create_agent_map(agents) }; + // Parse trace parent to get trace_id and parent_span_id + let (trace_id, parent_span_id) = if let Some(ref tp) = trace_parent { + parse_traceparent(tp) + } else { + (String::new(), None) + }; + + // Select appropriate agent using arch router llm model + let selected_agent = agent_selector + .select_agent(&message, &listener, trace_parent.clone()) + .await?; + + debug!("Processing agent pipeline: {}", selected_agent.id); + + // Record the start time for agent span + let agent_start_time = SystemTime::now(); + let agent_start_instant = Instant::now(); + // let (span_id, trace_id) = trace_collector.start_span( + // trace_parent.clone(), + // operation_component::AGENT, + // &format!("/agents{}", request_path), + // &selected_agent.id, + // ); + + let span_id = generate_random_span_id(); + // Process the filter chain - let processed_messages = pipeline_processor + let chat_history = pipeline_processor .process_filter_chain( - &chat_completions_request, + &message, &selected_agent, &agent_map, &request_headers, + Some(&trace_collector), + trace_id.clone(), + span_id.clone(), ) .await?; // Get terminal agent and send final response - let terminal_agent_name = selected_agent.id; + let terminal_agent_name = selected_agent.id.clone(); let terminal_agent = agent_map.get(&terminal_agent_name).unwrap(); debug!("Processing terminal agent: {}", terminal_agent_name); debug!("Terminal agent details: {:?}", terminal_agent); let llm_response = pipeline_processor - .invoke_upstream_agent( - &processed_messages, - &chat_completions_request, + .invoke_agent( + &chat_history, + client_request, terminal_agent, &request_headers, + trace_id.clone(), + span_id.clone(), ) .await?; + // Record agent span after processing is complete + let agent_end_time = SystemTime::now(); + let agent_elapsed = agent_start_instant.elapsed(); + + // Build full path with /agents prefix + let full_path = format!("/agents{}", request_path); + + // Build operation name: POST {full_path} {agent_name} + let operation_name = OperationNameBuilder::new() + .with_method("POST") + .with_path(&full_path) + .with_target(&terminal_agent_name) + .build(); + + let mut span_builder = SpanBuilder::new(&operation_name) + .with_span_id(span_id) + .with_kind(SpanKind::Internal) + .with_start_time(agent_start_time) + .with_end_time(agent_end_time) + .with_attribute(http::METHOD, "POST") + .with_attribute(http::TARGET, full_path) + .with_attribute("agent.name", terminal_agent_name.clone()) + .with_attribute("duration_ms", format!("{:.2}", agent_elapsed.as_secs_f64() * 1000.0)); + + if !trace_id.is_empty() { + span_builder = span_builder.with_trace_id(trace_id); + } + if let Some(parent_id) = parent_span_id { + span_builder = span_builder.with_parent_span_id(parent_id); + } + + let span = span_builder.build(); + // Use plano(agent) as service name for the agent processing span + trace_collector.record_span(operation_component::AGENT, span); + // Create streaming response response_handler .create_streaming_response(llm_response) diff --git a/crates/brightstaff/src/handlers/agent_selector.rs b/crates/brightstaff/src/handlers/agent_selector.rs index 0fff1198..02e52df2 100644 --- a/crates/brightstaff/src/handlers/agent_selector.rs +++ b/crates/brightstaff/src/handlers/agent_selector.rs @@ -20,6 +20,8 @@ pub enum AgentSelectionError { RoutingError(String), #[error("Default agent not found for listener: {0}")] DefaultAgentNotFound(String), + #[error("MCP client error: {0}")] + McpError(String), } /// Service for selecting agents based on routing preferences and listener configuration @@ -29,7 +31,9 @@ pub struct AgentSelector { impl AgentSelector { pub fn new(router_service: Arc) -> Self { - Self { router_service } + Self { + router_service, + } } /// Find listener by name from the request headers @@ -77,7 +81,9 @@ impl AgentSelector { return Ok(agents[0].clone()); } - let usage_preferences = self.convert_agent_description_to_routing_preferences(agents); + let usage_preferences = self + .convert_agent_description_to_routing_preferences(agents) + .await; debug!( "Agents usage preferences for agent routing str: {}", serde_json::to_string(&usage_preferences).unwrap_or_default() @@ -131,20 +137,23 @@ impl AgentSelector { } /// Convert agent descriptions to routing preferences - fn convert_agent_description_to_routing_preferences( + async fn convert_agent_description_to_routing_preferences( &self, agents: &[AgentFilterChain], ) -> Vec { - agents - .iter() - .map(|agent| ModelUsagePreference { - model: agent.id.clone(), + let mut preferences = Vec::new(); + + for agent_chain in agents { + preferences.push(ModelUsagePreference { + model: agent_chain.id.clone(), routing_preferences: vec![RoutingPreference { - name: agent.id.clone(), - description: agent.description.as_ref().unwrap_or(&String::new()).clone(), + name: agent_chain.id.clone(), + description: agent_chain.description.clone().unwrap_or_default(), }], - }) - .collect() + }); + } + + preferences } } @@ -183,8 +192,10 @@ mod tests { fn create_test_agent_struct(name: &str) -> Agent { Agent { id: name.to_string(), - kind: Some("test".to_string()), + agent_type: Some("test".to_string()), url: "http://localhost:8080".to_string(), + tool: None, + transport: None, } } @@ -240,8 +251,8 @@ mod tests { assert!(agent_map.contains_key("agent2")); } - #[test] - fn test_convert_agent_description_to_routing_preferences() { + #[tokio::test] + async fn test_convert_agent_description_to_routing_preferences() { let router_service = create_test_router_service(); let selector = AgentSelector::new(router_service); @@ -250,7 +261,9 @@ mod tests { create_test_agent("agent2", "Second agent description", false), ]; - let preferences = selector.convert_agent_description_to_routing_preferences(&agents); + let preferences = selector + .convert_agent_description_to_routing_preferences(&agents) + .await; assert_eq!(preferences.len(), 2); assert_eq!(preferences[0].model, "agent1"); diff --git a/crates/brightstaff/src/handlers/chat_completions.rs b/crates/brightstaff/src/handlers/chat_completions.rs deleted file mode 100644 index 1b15e389..00000000 --- a/crates/brightstaff/src/handlers/chat_completions.rs +++ /dev/null @@ -1,276 +0,0 @@ -use bytes::Bytes; -use common::configuration::{ModelAlias, ModelUsagePreference}; -use common::consts::{ARCH_IS_STREAMING_HEADER, ARCH_PROVIDER_HINT_HEADER}; -use hermesllm::apis::openai::ChatCompletionsRequest; -use hermesllm::clients::endpoints::SupportedUpstreamAPIs; -use hermesllm::clients::SupportedAPIs; -use hermesllm::{ProviderRequest, ProviderRequestType}; -use http_body_util::combinators::BoxBody; -use http_body_util::{BodyExt, Full, StreamBody}; -use hyper::body::Frame; -use hyper::header::{self}; -use hyper::{Request, Response, StatusCode}; -use std::collections::HashMap; -use std::sync::Arc; -use tokio::sync::mpsc; -use tokio_stream::wrappers::ReceiverStream; -use tokio_stream::StreamExt; -use tracing::{debug, info, warn}; - -use crate::router::llm_router::RouterService; - -fn full>(chunk: T) -> BoxBody { - Full::new(chunk.into()) - .map_err(|never| match never {}) - .boxed() -} - -pub async fn chat( - request: Request, - router_service: Arc, - full_qualified_llm_provider_url: String, - model_aliases: Arc>>, -) -> Result>, hyper::Error> { - let request_path = request.uri().path().to_string(); - let mut request_headers = request.headers().clone(); - let chat_request_bytes = request.collect().await?.to_bytes(); - - debug!( - "Received request body (raw utf8): {}", - String::from_utf8_lossy(&chat_request_bytes) - ); - - let mut client_request = match ProviderRequestType::try_from(( - &chat_request_bytes[..], - &SupportedAPIs::from_endpoint(request_path.as_str()).unwrap(), - )) { - Ok(request) => request, - Err(err) => { - warn!("Failed to parse request as ProviderRequestType: {}", err); - let err_msg = format!("Failed to parse request: {}", err); - let mut bad_request = Response::new(full(err_msg)); - *bad_request.status_mut() = StatusCode::BAD_REQUEST; - return Ok(bad_request); - } - }; - - // Model alias resolution: update model field in client_request immediately - // This ensures all downstream objects use the resolved model - let model_from_request = client_request.model().to_string(); - let is_streaming_request = client_request.is_streaming(); - let resolved_model = if let Some(model_aliases) = model_aliases.as_ref() { - if let Some(model_alias) = model_aliases.get(&model_from_request) { - debug!( - "Model Alias: 'From {}' -> 'To{}'", - model_from_request, model_alias.target - ); - model_alias.target.clone() - } else { - model_from_request.clone() - } - } else { - model_from_request.clone() - }; - client_request.set_model(resolved_model.clone()); - - // Clone metadata for routing and remove archgw_preference_config from original - let routing_metadata = client_request.metadata().clone(); - - if client_request.remove_metadata_key("archgw_preference_config") { - debug!("Removed archgw_preference_config from metadata"); - } - - let client_request_bytes_for_upstream = ProviderRequestType::to_bytes(&client_request).unwrap(); - - // Convert to ChatCompletionsRequest regardless of input type (clone to avoid moving original) - let chat_completions_request_for_arch_router: ChatCompletionsRequest = - match ProviderRequestType::try_from(( - client_request, - &SupportedUpstreamAPIs::OpenAIChatCompletions( - hermesllm::apis::OpenAIApi::ChatCompletions, - ), - )) { - Ok(ProviderRequestType::ChatCompletionsRequest(req)) => req, - Ok( - ProviderRequestType::MessagesRequest(_) - | ProviderRequestType::BedrockConverse(_) - | ProviderRequestType::BedrockConverseStream(_), - ) => { - // This should not happen after conversion to OpenAI format - warn!("Unexpected: got MessagesRequest after converting to OpenAI format"); - let err_msg = "Request conversion failed".to_string(); - let mut bad_request = Response::new(full(err_msg)); - *bad_request.status_mut() = StatusCode::BAD_REQUEST; - return Ok(bad_request); - } - Err(err) => { - warn!( - "Failed to convert request to ChatCompletionsRequest: {}", - err - ); - let err_msg = format!("Failed to convert request: {}", err); - let mut bad_request = Response::new(full(err_msg)); - *bad_request.status_mut() = StatusCode::BAD_REQUEST; - return Ok(bad_request); - } - }; - - debug!( - "[ARCH_ROUTER REQ]: {}", - &serde_json::to_string(&chat_completions_request_for_arch_router).unwrap() - ); - - let trace_parent = request_headers - .iter() - .find(|(ty, _)| ty.as_str() == "traceparent") - .map(|(_, value)| value.to_str().unwrap_or_default().to_string()); - - let usage_preferences_str: Option = routing_metadata.as_ref().and_then(|metadata| { - metadata - .get("archgw_preference_config") - .map(|value| value.to_string()) - }); - - let usage_preferences: Option> = usage_preferences_str - .as_ref() - .and_then(|s| serde_yaml::from_str(s).ok()); - - let latest_message_for_log = chat_completions_request_for_arch_router - .messages - .last() - .map_or("None".to_string(), |msg| { - msg.content.to_string().replace('\n', "\\n") - }); - - const MAX_MESSAGE_LENGTH: usize = 50; - let latest_message_for_log = if latest_message_for_log.chars().count() > MAX_MESSAGE_LENGTH { - let truncated: String = latest_message_for_log - .chars() - .take(MAX_MESSAGE_LENGTH) - .collect(); - format!("{}...", truncated) - } else { - latest_message_for_log - }; - - info!( - "request received, request type: chat_completion, usage preferences from request: {}, request path: {}, latest message: {}", - usage_preferences.is_some(), - request_path, - latest_message_for_log - ); - - debug!("usage preferences from request: {:?}", usage_preferences); - - let model_name = match router_service - .determine_route( - &chat_completions_request_for_arch_router.messages, - trace_parent.clone(), - usage_preferences, - ) - .await - { - Ok(route) => match route { - Some((_, model_name)) => model_name, - None => { - info!( - "No route determined, using default model from request: {}", - chat_completions_request_for_arch_router.model - ); - chat_completions_request_for_arch_router.model.clone() - } - }, - Err(err) => { - let err_msg = format!("Failed to determine route: {}", err); - let mut internal_error = Response::new(full(err_msg)); - *internal_error.status_mut() = StatusCode::INTERNAL_SERVER_ERROR; - return Ok(internal_error); - } - }; - - debug!( - "[ARCH_ROUTER] URL: {}, Resolved Model: {}", - full_qualified_llm_provider_url, model_name - ); - - request_headers.insert( - ARCH_PROVIDER_HINT_HEADER, - header::HeaderValue::from_str(&model_name).unwrap(), - ); - - request_headers.insert( - header::HeaderName::from_static(ARCH_IS_STREAMING_HEADER), - header::HeaderValue::from_str(&is_streaming_request.to_string()).unwrap(), - ); - - if let Some(trace_parent) = trace_parent { - request_headers.insert( - header::HeaderName::from_static("traceparent"), - header::HeaderValue::from_str(&trace_parent).unwrap(), - ); - } - // remove content-length header if it exists - request_headers.remove(header::CONTENT_LENGTH); - - let llm_response = match reqwest::Client::new() - .post(full_qualified_llm_provider_url) - .headers(request_headers) - .body(client_request_bytes_for_upstream) - .send() - .await - { - Ok(res) => res, - Err(err) => { - let err_msg = format!("Failed to send request: {}", err); - let mut internal_error = Response::new(full(err_msg)); - *internal_error.status_mut() = StatusCode::INTERNAL_SERVER_ERROR; - return Ok(internal_error); - } - }; - - // copy over the headers and status code from the original response - let response_headers = llm_response.headers().clone(); - let upstream_status = llm_response.status(); - let mut response = Response::builder().status(upstream_status); - let headers = response.headers_mut().unwrap(); - for (header_name, header_value) in response_headers.iter() { - headers.insert(header_name, header_value.clone()); - } - - // channel to create async stream - let (tx, rx) = mpsc::channel::(16); - - // Spawn a task to send data as it becomes available - tokio::spawn(async move { - let mut byte_stream = llm_response.bytes_stream(); - - while let Some(item) = byte_stream.next().await { - let item = match item { - Ok(item) => item, - Err(err) => { - warn!("Error receiving chunk: {:?}", err); - break; - } - }; - - if tx.send(item).await.is_err() { - warn!("Receiver dropped"); - break; - } - } - }); - - let stream = ReceiverStream::new(rx).map(|chunk| Ok::<_, hyper::Error>(Frame::data(chunk))); - - let stream_body = BoxBody::new(StreamBody::new(stream)); - - match response.body(stream_body) { - Ok(response) => Ok(response), - Err(err) => { - let err_msg = format!("Failed to create response: {}", err); - let mut internal_error = Response::new(full(err_msg)); - *internal_error.status_mut() = StatusCode::INTERNAL_SERVER_ERROR; - Ok(internal_error) - } - } -} diff --git a/crates/brightstaff/src/handlers/function_calling.rs b/crates/brightstaff/src/handlers/function_calling.rs new file mode 100644 index 00000000..295228b3 --- /dev/null +++ b/crates/brightstaff/src/handlers/function_calling.rs @@ -0,0 +1,1934 @@ +use hermesllm::apis::openai::{ + ChatCompletionsRequest, ChatCompletionsResponse, Choice, FinishReason, FunctionCall, Message, + MessageContent, ResponseMessage, Role, Tool, ToolCall, Usage, +}; +use serde::{Deserialize, Serialize}; +use serde_json::{json, Value}; +use std::collections::HashMap; +use thiserror::Error; +use tracing::{info, error}; +use futures::StreamExt; +use bytes::Bytes; +use http_body_util::{combinators::BoxBody, BodyExt, Full}; +use hyper::body::Incoming; +use hyper::{Request, Response, StatusCode}; +use eventsource_stream::Eventsource; + + + +// ============================================================================ +// CONSTANTS FOR HALLUCINATION DETECTION +// ============================================================================ + +const FUNC_NAME_START_PATTERN: &[&str] = &[r#"{"name":""#, r#"{'name':'"#]; +const FUNC_NAME_END_TOKEN: &[&str] = &["\",", "',"]; +const END_TOOL_CALL_TOKEN: &str = "}}"; + +const FIRST_PARAM_NAME_START_PATTERN: &[&str] = &[r#""arguments":{"#, r#"'arguments':{'"#]; +const PARAMETER_NAME_END_TOKENS: &[&str] = &["\":", ":\"", "':", ":'", "\":\"", "':'"]; +const PARAMETER_NAME_START_PATTERN: &[&str] = &["\",\"", "','"]; +const PARAMETER_VALUE_START_PATTERN: &[&str] = &["\":", "':"]; +const PARAMETER_VALUE_END_TOKEN: &[&str] = &["\",", "\"}"]; +const ARCH_FUNCTION_MODEL_NAME: &str = "Arch-Function"; + +/// Default hallucination detection thresholds +#[derive(Debug, Clone)] +pub struct HallucinationThresholds { + pub entropy: f64, + pub varentropy: f64, + pub probability: f64, +} + +impl Default for HallucinationThresholds { + fn default() -> Self { + Self { + entropy: 0.0001, + varentropy: 0.0001, + probability: 0.8, + } + } +} + +// ============================================================================ +// ERROR TYPES +// ============================================================================ + +#[derive(Debug, Error)] +pub enum FunctionCallingError { + #[error("Failed to parse JSON: {0}")] + JsonParseError(#[from] serde_json::Error), + + #[error("Failed to fix malformed JSON: {0}")] + JsonFixError(String), + + #[error("Invalid model response: {0}")] + InvalidModelResponse(String), + + #[error("Tool call verification failed: {0}")] + ToolCallVerificationError(String), + + #[error("Data type conversion error: {0}")] + DataTypeConversionError(String), + + #[error("Unsupported data type: {0}")] + UnsupportedDataType(String), + + #[error("HTTP request error: {0}")] + HttpError(#[from] reqwest::Error), + + #[error("Invalid tool call: {0}")] + InvalidToolCall(String), +} + +pub type Result = std::result::Result; + +// ============================================================================ +// CONFIGURATION STRUCTURES +// ============================================================================ + +/// Configuration for Arch Function Calling +#[derive(Debug, Clone)] +pub struct ArchFunctionConfig { + pub task_prompt: String, + pub format_prompt: String, + pub generation_params: GenerationParams, + pub support_data_types: Vec, +} + +impl Default for ArchFunctionConfig { + fn default() -> Self { + Self { + // Raw string so that \n sequences remain literal in the final prompt + task_prompt: r#"You are a helpful assistant designed to assist with the user query by making one or more function calls if needed.\n\nYou are provided with function signatures within XML tags:\n\n{tools}\n\n\nYour task is to decide which functions are needed and collect missing parameters if necessary."#.to_string(), + // Use raw string to preserve literal \n sequences instead of real newlines + format_prompt: r#"\n\nBased on your analysis, provide your response in one of the following JSON formats:\n1. If no functions are needed:\n```json\n{\"response\": \"Your response text here\"}\n```\n2. If functions are needed but some required parameters are missing:\n```json\n{\"required_functions\": [\"func_name1\", \"func_name2\", ...], \"clarification\": \"Text asking for missing parameters\"}\n```\n3. If functions are needed and all required parameters are available:\n```json\n{\"tool_calls\": [{\"name\": \"func_name1\", \"arguments\": {\"argument1\": \"value1\", \"argument2\": \"value2\"}},... (more tool calls as required)]}\n```"#.to_string(), + generation_params: GenerationParams::default(), + support_data_types: vec![ + "int".to_string(), + "float".to_string(), + "bool".to_string(), + "str".to_string(), + "list".to_string(), + "tuple".to_string(), + "set".to_string(), + "dict".to_string(), + // JSON Schema names (standard) + "integer".to_string(), + "number".to_string(), + "boolean".to_string(), + "string".to_string(), + "array".to_string(), + "object".to_string(), + ], + } + } +} + +/// Configuration for Arch Agent (extends ArchFunctionConfig with different generation params) +#[derive(Debug, Clone)] +pub struct ArchAgentConfig { + pub task_prompt: String, + pub format_prompt: String, + pub generation_params: GenerationParams, + pub support_data_types: Vec, +} + +impl Default for ArchAgentConfig { + fn default() -> Self { + let base = ArchFunctionConfig::default(); + Self { + task_prompt: base.task_prompt, + format_prompt: base.format_prompt, + generation_params: GenerationParams { + temperature: 0.01, + top_p: 1.0, + top_k: 10, + max_tokens: 1024, + stop_token_ids: vec![151645], + logprobs: Some(true), + top_logprobs: Some(10), + }, + support_data_types: base.support_data_types, + } + } +} + +/// Generation parameters for LLM +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GenerationParams { + pub temperature: f32, + pub top_p: f32, + pub top_k: u32, + pub max_tokens: u32, + pub stop_token_ids: Vec, + pub logprobs: Option, + pub top_logprobs: Option, +} + +impl Default for GenerationParams { + fn default() -> Self { + Self { + temperature: 0.1, + top_p: 1.0, + top_k: 10, + max_tokens: 1024, + stop_token_ids: vec![151645], + logprobs: Some(true), + top_logprobs: Some(10), + } + } +} + +// ============================================================================ +// PARSED MODEL RESPONSE +// ============================================================================ + +/// Parsed response from the model +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct ParsedModelResponse { + pub raw_response: String, + pub response: Option, + pub required_functions: Vec, + pub clarification: String, + pub tool_calls: Vec, + pub is_valid: bool, + pub error_message: String, +} + +// ============================================================================ +// TOOL CALL VERIFICATION RESULT +// ============================================================================ + +/// Result of tool call verification +#[derive(Debug, Clone)] +pub struct ToolCallVerification { + pub is_valid: bool, + pub invalid_tool_call: Option, + pub error_message: String, +} + +impl Default for ToolCallVerification { + fn default() -> Self { + Self { + is_valid: true, + invalid_tool_call: None, + error_message: String::new(), + } + } +} + +/// Main handler for Arch Function Calling +pub struct ArchFunctionHandler { + pub model_name: String, + pub config: ArchFunctionConfig, + pub default_prefix: String, + pub clarify_prefix: String, + pub endpoint_url: String, + pub http_client: reqwest::Client, +} + +impl ArchFunctionHandler { + /// Creates a new ArchFunctionHandler + pub fn new(model_name: String, config: ArchFunctionConfig, endpoint_url: String) -> Self { + use common::consts::ARCH_PROVIDER_HINT_HEADER; + use reqwest::header; + + // Create custom HTTP client with Arch provider hint header + let mut headers = header::HeaderMap::new(); + headers.insert( + header::HeaderName::from_static(ARCH_PROVIDER_HINT_HEADER), + header::HeaderValue::from_str(&model_name).unwrap(), + ); + + let http_client = reqwest::ClientBuilder::new() + .default_headers(headers) + .build() + .expect("Failed to create HTTP client"); + + Self { + model_name, + config, + default_prefix: r#"```json\n{\""#.to_string(), + clarify_prefix: r#"```json\n{\"required_functions\":"#.to_string(), + endpoint_url, + http_client, + } + } + + /// Converts a list of tools into JSON format string + pub fn convert_tools(&self, tools: &[Tool]) -> Result { + let converted: std::result::Result, serde_json::Error> = tools + .iter() + .map(|tool| serde_json::to_string(&tool.function)) + .collect(); + + converted + .map(|v| v.join("\\n")) + .map_err(FunctionCallingError::from) + } + + /// Fixes malformed JSON strings by ensuring proper bracket matching + pub fn fix_json_string(&self, json_str: &str) -> Result { + let json_str = json_str.trim(); + let mut stack: Vec = Vec::new(); + let mut fixed_str = String::new(); + + let matching_bracket: HashMap = + [(')', '('), ('}', '{'), (']', '[')] + .iter() + .cloned() + .collect(); + + let opening_bracket: HashMap = matching_bracket + .iter() + .map(|(k, v)| (*v, *k)) + .collect(); + + for ch in json_str.chars() { + if ch == '{' || ch == '[' || ch == '(' { + stack.push(ch); + fixed_str.push(ch); + } else if ch == '}' || ch == ']' || ch == ')' { + if let Some(&last) = stack.last() { + if matching_bracket.get(&ch) == Some(&last) { + stack.pop(); + fixed_str.push(ch); + } + // Ignore unmatched closing brackets + } + } else { + fixed_str.push(ch); + } + } + + // Add corresponding closing brackets for unmatched opening brackets + while let Some(unmatched_opening) = stack.pop() { + if let Some(&closing) = opening_bracket.get(&unmatched_opening) { + fixed_str.push(closing); + } + } + + // Try to parse the fixed JSON + match serde_json::from_str::(&fixed_str) { + Ok(val) => serde_json::to_string(&val).map_err(FunctionCallingError::from), + Err(_) => { + // Try replacing single quotes with double quotes + let fixed_str = fixed_str.replace('\'', "\""); + match serde_json::from_str::(&fixed_str) { + Ok(val) => serde_json::to_string(&val).map_err(FunctionCallingError::from), + Err(e) => Err(FunctionCallingError::JsonFixError(format!( + "Failed to fix JSON: {}", + e + ))), + } + } + } + } + + /// Parses the model response and extracts tool call information + pub fn parse_model_response(&self, content: &str) -> ParsedModelResponse { + let mut response_dict = ParsedModelResponse::default(); + + // Remove markdown code blocks + let mut content = content.trim().to_string(); + if content.starts_with("```") && content.ends_with("```") { + content = content.trim_start_matches("```").trim_end_matches("```").to_string(); + if content.starts_with("json") { + content = content.trim_start_matches("json").to_string(); + } + // Trim again after removing code blocks to eliminate internal whitespace + content = content.trim_start_matches(r"\n").trim_end_matches(r"\n").to_string(); + content = content.trim().to_string(); + // Unescape the quotes: \" -> " + // The model sometimes returns escaped JSON inside markdown blocks + content = content.replace(r#"\""#, "\""); + } + + // Try to fix JSON if needed + let fixed_content = match self.fix_json_string(&content) { + Ok(fixed) => { + response_dict.raw_response = format!("```json\n{}\n```", fixed); + fixed + } + Err(e) => { + response_dict.is_valid = false; + response_dict.error_message = format!("Failed to fix JSON: {}", e); + return response_dict; + } + }; + // Parse the JSON + match serde_json::from_str::(&fixed_content) { + Ok(model_response) => { + // Successfully parsed - mark as valid + response_dict.is_valid = true; + + // Extract response field + if let Some(resp) = model_response.get("response") { + if let Some(resp_str) = resp.as_str() { + response_dict.response = Some(resp_str.to_string()); + } + } + + // Extract required_functions + if let Some(funcs) = model_response.get("required_functions") { + if let Some(funcs_arr) = funcs.as_array() { + response_dict.required_functions = funcs_arr + .iter() + .filter_map(|v| v.as_str().map(String::from)) + .collect(); + } + } + + // Extract clarification + if let Some(clarif) = model_response.get("clarification") { + if let Some(clarif_str) = clarif.as_str() { + response_dict.clarification = clarif_str.to_string(); + } + } + + // Extract tool_calls + if let Some(tool_calls) = model_response.get("tool_calls") { + if let Some(tool_calls_arr) = tool_calls.as_array() { + for tool_call_val in tool_calls_arr { + let id = format!("call_{}", rand::random::() % 10000 + 1000); + + let name = tool_call_val + .get("name") + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(); + + let arguments = tool_call_val + .get("arguments") + .map(|v| serde_json::to_string(v).unwrap_or_default()) + .unwrap_or_default(); + + response_dict.tool_calls.push(ToolCall { + id, + call_type: "function".to_string(), + function: FunctionCall { name, arguments }, + }); + } + } + } + } + Err(e) => { + response_dict.is_valid = false; + response_dict.error_message = format!("Failed to parse model response: {}", e); + } + } + + response_dict + } + + /// Converts data type from one type to another + pub fn convert_data_type(&self, value: &Value, target_type: &str) -> Result { + match target_type { + // Handle float/number conversions + "float" | "number" => { + if let Some(int_val) = value.as_i64() { + return Ok(json!(int_val as f64)); + } + } + // Handle list/array conversions + "list" | "array" => { + if let Some(str_val) = value.as_str() { + // Try to parse as JSON array + if let Ok(arr) = serde_json::from_str::>(str_val) { + return Ok(json!(arr)); + } + } + } + // Handle str/string conversions + "str" | "string" => { + if !value.is_string() { + return Ok(json!(value.to_string())); + } + } + _ => {} + } + Ok(value.clone()) + } + + /// Helper method to check if a value matches the expected type + fn check_value_type(&self, value: &Value, target_type: &str) -> bool { + match target_type { + "int" | "integer" => value.is_i64() || value.is_u64(), + "float" | "number" => value.is_f64() || value.is_i64() || value.is_u64(), + "bool" | "boolean" => value.is_boolean(), + "str" | "string" => value.is_string(), + "list" | "array" => value.is_array(), + "dict" | "object" => value.is_object(), + _ => true, + } + } + + /// Helper method to validate and potentially convert a parameter value to match the target type + /// Returns Ok(true) if the value is valid (either originally or after conversion) + /// Returns Ok(false) if the value cannot be converted to the target type + fn validate_or_convert_parameter( + &self, + param_value: &Value, + target_type: &str, + ) -> Result { + // First check: Is it already the correct type? + if self.check_value_type(param_value, target_type) { + return Ok(true); + } + + // Try to convert + let converted = self.convert_data_type(param_value, target_type)?; + + // Second check: Is it the correct type after conversion? + Ok(self.check_value_type(&converted, target_type)) + } + + /// Verifies the validity of extracted tool calls against the provided tools + pub fn verify_tool_calls( + &self, + tools: &[Tool], + tool_calls: &[ToolCall], + ) -> ToolCallVerification { + let mut verification = ToolCallVerification::default(); + + // Build a map of function name to parameters + let mut functions: HashMap = HashMap::new(); + for tool in tools { + functions.insert(tool.function.name.clone(), &tool.function.parameters); + } + + for tool_call in tool_calls { + if !verification.is_valid { + break; + } + + let func_name = &tool_call.function.name; + + // Parse arguments as JSON + let func_args: HashMap = match serde_json::from_str(&tool_call.function.arguments) { + Ok(args) => args, + Err(e) => { + verification.is_valid = false; + verification.invalid_tool_call = Some(tool_call.clone()); + verification.error_message = format!("Failed to parse arguments for function '{}': {}", func_name, e); + break; + } + }; + + // Check if function is available + if let Some(function_params) = functions.get(func_name) { + // Check if all required parameters are present + if let Some(required) = function_params.get("required") { + if let Some(required_arr) = required.as_array() { + for required_param in required_arr { + if let Some(param_name) = required_param.as_str() { + if !func_args.contains_key(param_name) { + verification.is_valid = false; + verification.invalid_tool_call = Some(tool_call.clone()); + verification.error_message = format!( + "`{}` is required by the function `{}` but not found in the tool call!", + param_name, func_name + ); + break; + } + } + } + } + } + + // Verify the data type of each parameter + if let Some(properties) = function_params.get("properties") { + if let Some(properties_obj) = properties.as_object() { + for (param_name, param_value) in &func_args { + if let Some(param_schema) = properties_obj.get(param_name) { + if let Some(target_type) = param_schema.get("type").and_then(|v| v.as_str()) { + if self.config.support_data_types.contains(&target_type.to_string()) { + // Validate data type using helper method + match self.validate_or_convert_parameter(param_value, target_type) { + Ok(is_valid) => { + if !is_valid { + verification.is_valid = false; + verification.invalid_tool_call = Some(tool_call.clone()); + verification.error_message = format!( + "Parameter `{}` is expected to have the data type `{}`, got incompatible type.", + param_name, target_type + ); + break; + } + } + Err(_) => { + verification.is_valid = false; + verification.invalid_tool_call = Some(tool_call.clone()); + verification.error_message = format!( + "Parameter `{}` is expected to have the data type `{}`, got incompatible type.", + param_name, target_type + ); + break; + } + } + } else { + verification.is_valid = false; + verification.invalid_tool_call = Some(tool_call.clone()); + verification.error_message = format!("Data type `{}` is not supported.", target_type); + break; + } + } + } else { + verification.is_valid = false; + verification.invalid_tool_call = Some(tool_call.clone()); + verification.error_message = format!( + "Parameter `{}` is not defined in the function `{}`.", + param_name, func_name + ); + break; + } + } + } + } + } else { + verification.is_valid = false; + verification.invalid_tool_call = Some(tool_call.clone()); + verification.error_message = format!("{} is not available!", func_name); + } + } + + verification + } + + /// Formats the system prompt with tools + pub fn format_system_prompt(&self, tools: &[Tool]) -> Result { + let tools_str = self.convert_tools(tools)?; + let system_prompt = self + .config + .task_prompt + .replace("{tools}", &tools_str) + + &self.config.format_prompt; + + Ok(system_prompt) + } + + /// Processes messages and formats them appropriately for the model + pub fn process_messages( + &self, + messages: &[Message], + tools: Option<&[Tool]>, + extra_instruction: Option<&str>, + max_tokens: usize, + metadata: Option<&HashMap>, + ) -> Result> { + let mut processed_messages = Vec::new(); + + // Add system message with tools if provided + if let Some(tools) = tools { + let system_prompt = self.format_system_prompt(tools)?; + processed_messages.push(Message { + role: Role::System, + content: MessageContent::Text(system_prompt), + name: None, + tool_calls: None, + tool_call_id: None, + }); + } + + // Process each message + for (idx, message) in messages.iter().enumerate() { + let mut role = message.role.clone(); + let mut content = match &message.content { + MessageContent::Text(text) => text.clone(), + MessageContent::Parts(_) => String::new(), + }; + + // Handle tool calls + if let Some(tool_calls) = &message.tool_calls { + if !tool_calls.is_empty() { + role = Role::Assistant; + let tool_call_json = serde_json::to_string(&tool_calls[0].function)?; + content = format!("\n{}\n", tool_call_json); + } + } else if role == Role::Tool { + role = Role::User; + + // Check if we should optimize context window + let optimize_context = metadata + .and_then(|m| m.get("optimize_context_window")) + .and_then(|v| v.as_str()) + .map(|s| s.to_lowercase() == "true") + .unwrap_or(false); + + if optimize_context { + content = "\n\n".to_string(); + } else { + // Get the tool call from previous message + if idx > 0 { + if let MessageContent::Text(prev_content) = &messages[idx - 1].content { + let mut tool_call_msg = prev_content.clone(); + + // Strip markdown code blocks + if tool_call_msg.starts_with("```") && tool_call_msg.ends_with("```") { + tool_call_msg = tool_call_msg.trim_start_matches("```").trim_end_matches("```").trim().to_string(); + if tool_call_msg.starts_with("json") { + tool_call_msg = tool_call_msg.trim_start_matches("json").trim().to_string(); + } + } + + // Extract function name + if let Ok(parsed) = serde_json::from_str::(&tool_call_msg) { + if let Some(tool_calls_arr) = parsed.get("tool_calls").and_then(|v| v.as_array()) { + if let Some(first_tool_call) = tool_calls_arr.first() { + let func_name = first_tool_call + .get("name") + .and_then(|v| v.as_str()) + .unwrap_or("no_name"); + + let tool_response = json!({ + "name": func_name, + "result": content, + }); + + content = format!("\n{}\n", + serde_json::to_string(&tool_response)?); + } + } + } + } + } + } + } + + processed_messages.push(Message { + role, + content: MessageContent::Text(content), + name: message.name.clone(), + tool_calls: None, + tool_call_id: None, + }); + } + + // Ensure last message is from user + if let Some(last) = processed_messages.last() { + if last.role != Role::User { + return Err(FunctionCallingError::InvalidModelResponse( + "Last message must be from user".to_string(), + )); + } + } + + // Add extra instruction if provided + if let Some(instruction) = extra_instruction { + if let Some(last) = processed_messages.last_mut() { + if let MessageContent::Text(content) = &mut last.content { + content.push_str("\n"); + content.push_str(instruction); + } + } + } + + // Truncate messages if they exceed max_tokens + let processed_messages = self.truncate_messages(processed_messages, max_tokens); + + Ok(processed_messages) + } + + /// Truncates messages to fit within max_tokens limit + fn truncate_messages(&self, messages: Vec, max_tokens: usize) -> Vec { + let mut num_tokens = 0; + let mut conversation_idx = 0; + + // Keep system message if present + if let Some(first) = messages.first() { + if first.role == Role::System { + if let MessageContent::Text(content) = &first.content { + num_tokens += content.len() / 4; // Approximate 4 chars per token + } + conversation_idx = 1; + } + } + + // Calculate from the end backwards + // Start with message_idx pointing past the end (will be used if no truncation needed) + let mut message_idx = messages.len(); + for i in (conversation_idx..messages.len()).rev() { + if let MessageContent::Text(content) = &messages[i].content { + num_tokens += content.len() / 4; + if num_tokens >= max_tokens { + if messages[i].role == Role::User { + // Set message_idx to current position and break + // This matches Python's behavior where message_idx is set before break + message_idx = i; + break; + } + } + } + // Only update message_idx if we haven't hit the token limit yet + // This ensures message_idx points to where truncation should start + if num_tokens < max_tokens { + message_idx = i; + } + } + + // Return system message + truncated conversation + let mut result = Vec::new(); + if conversation_idx > 0 { + result.push(messages[0].clone()); + } + result.extend_from_slice(&messages[message_idx..]); + + result + } + + /// Prefills a message by adding an assistant message with the prefix + pub fn prefill_message(&self, mut messages: Vec, prefill: &str) -> Vec { + messages.push(Message { + role: Role::Assistant, + content: MessageContent::Text(prefill.to_string()), + name: None, + tool_calls: None, + tool_call_id: None, + }); + messages + } + + /// Helper to create a request with VLLM-specific parameters + fn create_request_with_extra_body(&self, messages: Vec, stream: bool) -> ChatCompletionsRequest { + ChatCompletionsRequest { + model: self.model_name.clone(), + messages, + temperature: Some(self.config.generation_params.temperature), + top_p: Some(self.config.generation_params.top_p), + max_tokens: Some(self.config.generation_params.max_tokens), + stream: Some(stream), + logprobs: self.config.generation_params.logprobs, + top_logprobs: self.config.generation_params.top_logprobs, + // VLLM-specific parameters + continue_final_message: Some(true), + add_generation_prompt: Some(false), + top_k: Some(self.config.generation_params.top_k), + stop_token_ids: if !self.config.generation_params.stop_token_ids.is_empty() { + Some(self.config.generation_params.stop_token_ids.clone()) + } else { + None + }, + ..Default::default() + } + } + + /// Makes a streaming request and returns the SSE event stream + async fn make_streaming_request(&self, request: ChatCompletionsRequest) -> Result> + Send>>> { + let request_body = serde_json::to_string(&request) + .map_err(|e| FunctionCallingError::InvalidModelResponse(format!("Failed to serialize request: {}", e)))?; + + let response = self.http_client + .post(&self.endpoint_url) + .header("Content-Type", "application/json") + .body(request_body) + .send() + .await + .map_err(|e| FunctionCallingError::HttpError(e))?; + + if !response.status().is_success() { + let status = response.status(); + let error_text = response.text().await.unwrap_or_else(|_| "Unknown error".to_string()); + return Err(FunctionCallingError::InvalidModelResponse( + format!("HTTP error {}: {}", status, error_text) + )); + } + + // Parse SSE stream + let stream = response.bytes_stream().eventsource(); + let parsed_stream = stream.filter_map(|event_result| async move { + match event_result { + Ok(event) => { + // Skip [DONE] sentinel + if event.data == "[DONE]" { + return None; + } + // Parse JSON + match serde_json::from_str::(&event.data) { + Ok(json) => Some(Ok(json)), + Err(e) => Some(Err(format!("JSON parse error: {}", e))), + } + } + Err(e) => Some(Err(format!("SSE stream error: {}", e))), + } + }); + + Ok(Box::pin(parsed_stream)) + } + + /// Makes a non-streaming request and returns the response + async fn make_non_streaming_request(&self, request: ChatCompletionsRequest) -> Result { + let request_body = serde_json::to_string(&request) + .map_err(|e| FunctionCallingError::InvalidModelResponse(format!("Failed to serialize request: {}", e)))?; + + let response = self.http_client + .post(&self.endpoint_url) + .header("Content-Type", "application/json") + .body(request_body) + .send() + .await + .map_err(|e| FunctionCallingError::HttpError(e))?; + + if !response.status().is_success() { + let status = response.status(); + let error_text = response.text().await.unwrap_or_else(|_| "Unknown error".to_string()); + return Err(FunctionCallingError::InvalidModelResponse( + format!("HTTP error {}: {}", status, error_text) + )); + } + + let response_text = response.text().await + .map_err(|e| FunctionCallingError::HttpError(e))?; + + serde_json::from_str(&response_text) + .map_err(|e| FunctionCallingError::JsonParseError(e)) + } + + pub async fn function_calling_chat( + &self, + request: ChatCompletionsRequest, + ) -> Result { + use tracing::{info, error}; + + info!("[Arch-Function] - ChatCompletion"); + + let messages = self.process_messages( + &request.messages, + request.tools.as_deref(), + None, + self.config.generation_params.max_tokens as usize, + request.metadata.as_ref(), + )?; + + info!("[request to arch-fc]: model: {}, messages count: {}", + self.model_name, messages.len()); + + let use_agent_orchestrator = request.metadata + .as_ref() + .and_then(|m| m.get("use_agent_orchestrator")) + .and_then(|v| v.as_bool()) + .unwrap_or(false); + + let prefilled_messages = self.prefill_message(messages.clone(), &self.default_prefix); + + // Create request with extra_body parameters + let stream_request = self.create_request_with_extra_body(prefilled_messages.clone(), true); + let mut stream = self.make_streaming_request(stream_request).await?; + + let mut model_response = String::new(); + + if use_agent_orchestrator { + while let Some(chunk_result) = stream.next().await { + let chunk = chunk_result.map_err(|e| FunctionCallingError::InvalidModelResponse(e))?; + // Extract content from JSON response + if let Some(choices) = chunk.get("choices").and_then(|v| v.as_array()) { + if let Some(choice) = choices.first() { + if let Some(content) = choice.get("delta") + .and_then(|d| d.get("content")) + .and_then(|c| c.as_str()) { + model_response.push_str(content); + } + } + } + } + info!("[Agent Orchestrator]: response received"); + } else { + if let Some(tools) = request.tools.as_ref() { + let mut hallucination_state = HallucinationState::new(tools); + let mut has_tool_calls = None; + let mut has_hallucination = false; + + while let Some(chunk_result) = stream.next().await { + let chunk = chunk_result.map_err(|e| FunctionCallingError::InvalidModelResponse(e))?; + + // Extract content and logprobs from JSON response + if let Some(choices) = chunk.get("choices").and_then(|v| v.as_array()) { + if let Some(choice) = choices.first() { + if let Some(content) = choice.get("delta") + .and_then(|d| d.get("content")) + .and_then(|c| c.as_str()) { + + // Extract logprobs + let logprobs: Vec = choice.get("logprobs") + .and_then(|lp| lp.get("content")) + .and_then(|c| c.as_array()) + .and_then(|arr| arr.first()) + .and_then(|token| token.get("top_logprobs")) + .and_then(|tlp| tlp.as_array()) + .map(|arr| { + arr.iter() + .filter_map(|v| v.get("logprob").and_then(|lp| lp.as_f64())) + .collect() + }) + .unwrap_or_default(); + + if hallucination_state.append_and_check_token_hallucination(content.to_string(), logprobs) { + has_hallucination = true; + break; + } + + if hallucination_state.tokens.len() > 5 && has_tool_calls.is_none() { + let collected_content = hallucination_state.tokens.join(""); + has_tool_calls = Some(collected_content.contains("tool_calls")); + } + } + } + } + } + + if has_tool_calls == Some(true) && has_hallucination { + info!("[Hallucination]: {}", hallucination_state.error_message); + + let clarify_messages = self.prefill_message(messages.clone(), &self.clarify_prefix); + let clarify_request = self.create_request_with_extra_body(clarify_messages, false); + + let retry_response = self.make_non_streaming_request(clarify_request).await?; + + if let Some(choice) = retry_response.choices.first() { + if let Some(content) = &choice.message.content { + model_response = content.clone(); + } + } + } else { + model_response = hallucination_state.tokens.join(""); + } + } else { + while let Some(chunk_result) = stream.next().await { + let chunk = chunk_result.map_err(|e| FunctionCallingError::InvalidModelResponse(e))?; + if let Some(choices) = chunk.get("choices").and_then(|v| v.as_array()) { + if let Some(choice) = choices.first() { + if let Some(content) = choice.get("delta") + .and_then(|d| d.get("content")) + .and_then(|c| c.as_str()) { + model_response.push_str(content); + } + } + } + } + } + } + + let response_dict = self.parse_model_response(&model_response); + + info!("[arch-fc]: raw model response: {}", response_dict.raw_response); + + // General model response (no intent matched - should route to default target) + let model_message = if response_dict.response.as_ref().map_or(false, |s| !s.is_empty()) { + // When arch-fc returns a "response" field, it means no intent was matched + // Return empty content and empty tool_calls so prompt_gateway routes to default target + ResponseMessage { + role: Role::Assistant, + content: Some(String::new()), + refusal: None, + annotations: None, + audio: None, + function_call: None, + tool_calls: None, + } + } else if !response_dict.required_functions.is_empty() { + if !use_agent_orchestrator { + ResponseMessage { + role: Role::Assistant, + content: Some(response_dict.clarification.clone()), + refusal: None, + annotations: None, + audio: None, + function_call: None, + tool_calls: None, + } + } else { + ResponseMessage { + role: Role::Assistant, + content: Some(String::new()), + refusal: None, + annotations: None, + audio: None, + function_call: None, + tool_calls: None, + } + } + } else if !response_dict.tool_calls.is_empty() { + if response_dict.is_valid { + if !use_agent_orchestrator { + if let Some(tools) = request.tools.as_ref() { + let verification = self.verify_tool_calls(tools, &response_dict.tool_calls); + + if verification.is_valid { + info!("[Tool calls]: {:?}", + response_dict.tool_calls.iter() + .map(|tc| &tc.function) + .collect::>() + ); + ResponseMessage { + role: Role::Assistant, + content: Some(String::new()), + refusal: None, + annotations: None, + audio: None, + function_call: None, + tool_calls: Some(response_dict.tool_calls.clone()), + } + } else { + error!("Invalid tool call - {}", verification.error_message); + ResponseMessage { + role: Role::Assistant, + content: Some(String::new()), + refusal: None, + annotations: None, + audio: None, + function_call: None, + tool_calls: None, + } + } + } else { + error!("Tool calls present but no tools provided in request"); + ResponseMessage { + role: Role::Assistant, + content: Some(String::new()), + refusal: None, + annotations: None, + audio: None, + function_call: None, + tool_calls: None, + } + } + } else { + info!("[Tool calls]: {:?}", + response_dict.tool_calls.iter() + .map(|tc| &tc.function) + .collect::>() + ); + ResponseMessage { + role: Role::Assistant, + content: Some(String::new()), + refusal: None, + annotations: None, + audio: None, + function_call: None, + tool_calls: Some(response_dict.tool_calls.clone()), + } + } + } else { + error!("Invalid tool calls in response: {}", response_dict.error_message); + ResponseMessage { + role: Role::Assistant, + content: Some(String::new()), + refusal: None, + annotations: None, + audio: None, + function_call: None, + tool_calls: None, + } + } + } else { + error!("Invalid model response - {}", model_response); + ResponseMessage { + role: Role::Assistant, + content: Some(String::new()), + refusal: None, + annotations: None, + audio: None, + function_call: None, + tool_calls: None, + } + }; + + // Create metadata with the raw model response + let mut metadata = HashMap::new(); + metadata.insert( + "x-arch-fc-model-response".to_string(), + serde_json::to_value(&response_dict.raw_response) + .unwrap_or_else(|_| Value::String(response_dict.raw_response.clone())), + ); + + let chat_completion_response = ChatCompletionsResponse { + id: format!("chatcmpl-{}", uuid::Uuid::new_v4()), + object: Some("chat.completion".to_string()), + created: chrono::Utc::now().timestamp() as u64, + model: request.model.clone(), + choices: vec![Choice { + index: 0, + message: model_message, + finish_reason: Some(FinishReason::Stop), + logprobs: None, + }], + usage: Usage { + prompt_tokens: 0, + completion_tokens: 0, + total_tokens: 0, + prompt_tokens_details: None, + completion_tokens_details: None, + }, + system_fingerprint: None, + service_tier: None, + metadata: Some(metadata), + }; + + info!("[response arch-fc]: {:?}", chat_completion_response); + + Ok(chat_completion_response) + } +} + +// ============================================================================ +// ARCH AGENT HANDLER +// ============================================================================ + +/// Handler for Arch Agent (extends ArchFunctionHandler with specialized behavior) +pub struct ArchAgentHandler { + pub function_handler: ArchFunctionHandler, +} + +impl ArchAgentHandler { + /// Creates a new ArchAgentHandler + pub fn new(model_name: String, endpoint_url: String) -> Self { + let config = ArchAgentConfig::default(); + Self { + function_handler: ArchFunctionHandler::new( + model_name, + ArchFunctionConfig { + task_prompt: config.task_prompt, + format_prompt: config.format_prompt, + generation_params: GenerationParams { + temperature: config.generation_params.temperature, + top_p: config.generation_params.top_p, + top_k: config.generation_params.top_k, + max_tokens: config.generation_params.max_tokens, + stop_token_ids: config.generation_params.stop_token_ids, + logprobs: config.generation_params.logprobs, + top_logprobs: config.generation_params.top_logprobs, + }, + support_data_types: config.support_data_types, + }, + endpoint_url, + ), + } + } + + /// Converts tools with special handling for empty parameters + /// This is the key difference from ArchFunctionHandler + pub fn convert_tools(&self, tools: &[Tool]) -> Result { + let mut converted = Vec::new(); + + for tool in tools { + let mut tool_copy = tool.clone(); + + // Delete parameters key if its empty + if let Some(props) = tool_copy.function.parameters.get("properties") { + if props.is_object() && props.as_object().unwrap().is_empty() { + // Create new parameters without properties + if let Some(params_obj) = tool_copy.function.parameters.as_object_mut() { + params_obj.remove("properties"); + } + } + } + + converted.push(serde_json::to_string(&tool_copy.function)?); + } + + Ok(converted.join("\n")) + } +} + +// ============================================================================ +// HTTP HANDLER FOR FUNCTION CALLING ENDPOINT +// ============================================================================ + +fn full>(chunk: T) -> BoxBody { + Full::new(chunk.into()) + .map_err(|never| match never {}) + .boxed() +} + +pub async fn function_calling_chat_handler( + req: Request, + llm_provider_url: String, +) -> std::result::Result>, hyper::Error> { + + use hermesllm::apis::openai::ChatCompletionsRequest; + let whole_body = req.collect().await?.to_bytes(); + + // Parse as JSON Value first to modify it + let mut body_json: Value = match serde_json::from_slice(&whole_body) { + Ok(json) => json, + Err(e) => { + error!("Failed to parse request body as JSON: {}", e); + let mut response = Response::new(full( + serde_json::json!({ + "error": format!("Invalid request body: {}", e) + }).to_string() + )); + *response.status_mut() = StatusCode::BAD_REQUEST; + response.headers_mut().insert("Content-Type", "application/json".parse().unwrap()); + return Ok(response); + } + }; + + // Add "model": "Arch-Function" to the request + if let Some(obj) = body_json.as_object_mut() { + obj.insert("model".to_string(), ARCH_FUNCTION_MODEL_NAME.into()); + } + + // Parse as ChatCompletionsRequest + let chat_request: ChatCompletionsRequest = match serde_json::from_value(body_json) { + Ok(req) => { + info!("[request body]: {}", serde_json::to_string(&req).unwrap_or_default()); + req + }, + Err(e) => { + error!("Failed to parse request body: {}", e); + let mut response = Response::new(full( + serde_json::json!({ + "error": format!("Invalid request body: {}", e) + }).to_string() + )); + *response.status_mut() = StatusCode::BAD_REQUEST; + response.headers_mut().insert("Content-Type", "application/json".parse().unwrap()); + return Ok(response); + } + }; + + // Determine which handler to use based on metadata + let use_agent_orchestrator = chat_request.metadata + .as_ref() + .and_then(|m| m.get("use_agent_orchestrator")) + .and_then(|v| v.as_bool()) + .unwrap_or(false); + + info!("Use agent orchestrator: {}", use_agent_orchestrator); + + // Create the appropriate handler + let handler_name = if use_agent_orchestrator { + "Arch-Agent" + } else { + "Arch-Function" + }; + + // Call the handler + let final_response = if use_agent_orchestrator { + let handler = ArchAgentHandler::new( + ARCH_FUNCTION_MODEL_NAME.to_string(), + llm_provider_url.clone(), + ); + handler.function_handler.function_calling_chat(chat_request).await + } else { + let handler = ArchFunctionHandler::new( + ARCH_FUNCTION_MODEL_NAME.to_string(), + ArchFunctionConfig::default(), + llm_provider_url.clone(), + ); + handler.function_calling_chat(chat_request).await + }; + + match final_response { + Ok(response_data) => { + let response_json = serde_json::to_string(&response_data).unwrap_or_else(|e| { + error!("Failed to serialize response: {}", e); + serde_json::json!({"error": "Failed to serialize response"}).to_string() + }); + + let mut response = Response::new(full(response_json)); + *response.status_mut() = StatusCode::OK; + response.headers_mut().insert("Content-Type", "application/json".parse().unwrap()); + + Ok(response) + } + Err(e) => { + error!("[{}] - Error in function calling: {}", handler_name, e); + + let error_response = serde_json::json!({ + "error": format!("[{}] - Error in function calling: {}", handler_name, e) + }); + + let mut response = Response::new(full(error_response.to_string())); + *response.status_mut() = StatusCode::INTERNAL_SERVER_ERROR; + response.headers_mut().insert("Content-Type", "application/json".parse().unwrap()); + Ok(response) + } + } +} + + +// ============================================================================ +// TESTS +// ============================================================================ + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_arch_function_config_default() { + let config = ArchFunctionConfig::default(); + assert!(config.task_prompt.contains("helpful assistant")); + assert!(config.format_prompt.contains("JSON formats")); + assert_eq!(config.generation_params.temperature, 0.1); + assert_eq!(config.support_data_types.len(), 14); // 8 Python-style + 6 JSON Schema names + + // Verify prompt formatting for literal escaped newlines ("\\n") instead of actual newline chars + // The user requirement changed prompts to display "\\n" sequences literally. + assert!(config.task_prompt.contains("\\n\\nYou are provided")); + assert!(config.task_prompt.contains("\\n\\n")); + + // Format prompt should contain literal escaped newlines and proper JSON examples + assert!(config.format_prompt.contains("\\n\\nBased on your analysis")); + assert!(config.format_prompt.contains(r#"{\"response\": \"Your response text here\"}"#)); + assert!(config.format_prompt.contains(r#"{\"tool_calls\": [{"#)); + + } + + #[test] + fn test_arch_agent_config_default() { + let config = ArchAgentConfig::default(); + assert_eq!(config.generation_params.temperature, 0.01); // Different from ArchFunctionConfig + } + + #[test] + fn test_fix_json_string_valid() { + let handler = ArchFunctionHandler::new("test-model".to_string(), ArchFunctionConfig::default(), "http://localhost:8000".to_string()); + let json_str = r#"{"name": "test", "value": 123}"#; + let result = handler.fix_json_string(json_str); + assert!(result.is_ok()); + } + + #[test] + fn test_fix_json_string_missing_bracket() { + let handler = ArchFunctionHandler::new("test-model".to_string(), ArchFunctionConfig::default(), "http://localhost:8000".to_string()); + let json_str = r#"{"name": "test", "value": 123"#; + let result = handler.fix_json_string(json_str); + assert!(result.is_ok()); + let fixed = result.unwrap(); + assert!(fixed.contains("}")); + } + + #[test] + fn test_parse_model_response_with_tool_calls() { + let handler = ArchFunctionHandler::new("test-model".to_string(), ArchFunctionConfig::default(), "http://localhost:8000".to_string()); + let content = r#"{"tool_calls": [{"name": "get_weather", "arguments": {"location": "NYC"}}]}"#; + let result = handler.parse_model_response(content); + + assert!(result.is_valid); + assert_eq!(result.tool_calls.len(), 1); + assert_eq!(result.tool_calls[0].function.name, "get_weather"); + } + + #[test] + fn test_parse_model_response_with_clarification() { + let handler = ArchFunctionHandler::new("test-model".to_string(), ArchFunctionConfig::default(), "http://localhost:8000".to_string()); + let content = r#"{"required_functions": ["get_weather"], "clarification": "What location?"}"#; + let result = handler.parse_model_response(content); + + assert!(result.is_valid); + assert_eq!(result.required_functions.len(), 1); + assert_eq!(result.clarification, "What location?"); + } + + #[test] + fn test_convert_data_type_int_to_float() { + let handler = ArchFunctionHandler::new("test-model".to_string(), ArchFunctionConfig::default(), "http://localhost:8000".to_string()); + let value = json!(42); + let result = handler.convert_data_type(&value, "float"); + assert!(result.is_ok()); + assert!(result.unwrap().is_f64()); + } +} + +// ============================================================================ +// HALLUCINATION DETECTION MODULE +// ============================================================================ + +/// Mask token types for tracking parsing state +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum MaskToken { + FunctionName, + ParameterValue, + ParameterName, + NotUsed, + ToolCall, +} + +/// Uncertainty metrics calculated from log probabilities +#[derive(Debug, Clone)] +pub struct UncertaintyMetrics { + pub entropy: f64, + pub varentropy: f64, + pub probability: f64, +} + +/// Calculates uncertainty metrics from log probabilities +/// +/// This is a simplified Rust implementation that avoids torch/tensor dependencies. +/// Uses basic statistical calculations instead of tensor operations. +pub fn calculate_uncertainty(log_probs: &[f64]) -> UncertaintyMetrics { + if log_probs.is_empty() { + return UncertaintyMetrics { + entropy: 0.0, + varentropy: 0.0, + probability: 0.0, + }; + } + + // Convert log probabilities to probabilities + let token_probs: Vec = log_probs.iter().map(|&lp| lp.exp()).collect(); + + // Calculate entropy: -sum(p * log(p)) / log(2) + let mut entropy = 0.0; + for i in 0..log_probs.len() { + entropy -= log_probs[i] * token_probs[i]; + } + entropy /= 2_f64.ln(); // Convert to bits + + // Calculate variance of entropy + let mut varentropy = 0.0; + for i in 0..log_probs.len() { + let diff = log_probs[i] / 2_f64.ln() + entropy; + varentropy += token_probs[i] * diff * diff; + } + + // Get the top probability + let probability = token_probs.first().copied().unwrap_or(0.0); + + UncertaintyMetrics { + entropy, + varentropy, + probability, + } +} + +/// Checks if uncertainty metrics exceed thresholds +pub fn check_threshold( + entropy: f64, + varentropy: f64, + thresholds: &HallucinationThresholds, +) -> bool { + entropy > thresholds.entropy && varentropy > thresholds.varentropy +} + +/// Checks if a parameter is required in the function description +pub fn is_parameter_required( + function_description: &Value, + parameter_name: &str, +) -> bool { + if let Some(required) = function_description.get("required") { + if let Some(required_arr) = required.as_array() { + return required_arr.iter().any(|v| v.as_str() == Some(parameter_name)); + } + } + false +} + +/// Checks if a parameter has a specific property +pub fn is_parameter_property( + function_description: &Value, + parameter_name: &str, + property_name: &str, +) -> bool { + if let Some(properties) = function_description.get("properties") { + if let Some(param_info) = properties.get(parameter_name) { + return param_info.get(property_name).is_some(); + } + } + false +} + +/// State for hallucination detection during streaming +/// +/// This is a simplified version of the Python HallucinationState that doesn't +/// require torch/tensor dependencies. It provides the core functionality needed +/// for detecting hallucinations during function calling. +#[derive(Debug)] +pub struct HallucinationState { + pub tokens: Vec, + pub logprobs: Vec>, + pub state: Option, + pub mask: Vec, + pub parameter_name_done: bool, + pub hallucination: bool, + pub error_message: String, + pub parameter_name: Vec, + pub token_probs_map: Vec<(String, f64, f64, f64)>, + pub function_properties: HashMap, + pub open_bracket: bool, + pub bracket: Option, + pub function_name: String, + pub check_parameter_name: HashMap, + pub thresholds: HallucinationThresholds, +} + +impl HallucinationState { + /// Creates a new HallucinationState with function definitions + pub fn new(functions: &[Tool]) -> Self { + let function_properties: HashMap = functions + .iter() + .map(|tool| { + ( + tool.function.name.clone(), + tool.function.parameters.clone(), + ) + }) + .collect(); + + Self { + tokens: Vec::new(), + logprobs: Vec::new(), + state: None, + mask: Vec::new(), + parameter_name_done: false, + hallucination: false, + error_message: String::new(), + parameter_name: Vec::new(), + token_probs_map: Vec::new(), + function_properties, + open_bracket: false, + bracket: None, + function_name: String::new(), + check_parameter_name: HashMap::new(), + thresholds: HallucinationThresholds::default(), + } + } + + /// Appends a token and checks for hallucination + pub fn append_and_check_token_hallucination( + &mut self, + token: String, + logprob: Vec, + ) -> bool { + self.tokens.push(token); + self.logprobs.push(logprob); + self.process_token(); + self.hallucination + } + + /// Resets internal parameters + fn reset_parameters(&mut self) { + self.state = None; + self.parameter_name_done = false; + self.hallucination = false; + self.error_message.clear(); + self.open_bracket = false; + self.bracket = None; + self.check_parameter_name.clear(); + } + + /// Processes the current token and updates state + fn process_token(&mut self) { + let content: String = self.tokens.join("").replace(' ', ""); + + // Handle end of tool call + if content.ends_with(END_TOOL_CALL_TOKEN) { + self.reset_parameters(); + } + + // Function name extraction logic + if self.state.as_deref() == Some("function_name") { + if !FUNC_NAME_END_TOKEN.iter().any(|&t| self.tokens.last().map_or(false, |tok| tok == t)) { + self.mask.push(MaskToken::FunctionName); + } else { + self.state = None; + self.get_function_name(); + } + } + + // Check for function name start + if FUNC_NAME_START_PATTERN.iter().any(|&p| content.ends_with(p)) { + self.state = Some("function_name".to_string()); + } + + // Parameter name extraction logic + if self.state.as_deref() == Some("parameter_name") + && !PARAMETER_NAME_END_TOKENS.iter().any(|&t| content.ends_with(t)) { + self.mask.push(MaskToken::ParameterName); + } else if self.state.as_deref() == Some("parameter_name") + && PARAMETER_NAME_END_TOKENS.iter().any(|&t| content.ends_with(t)) { + self.state = None; + self.parameter_name_done = true; + self.get_parameter_name(); + } else if self.parameter_name_done + && !self.open_bracket + && PARAMETER_NAME_START_PATTERN.iter().any(|&p| content.ends_with(p)) { + self.state = Some("parameter_name".to_string()); + } + + // First parameter value start + if FIRST_PARAM_NAME_START_PATTERN.iter().any(|&p| content.ends_with(p)) { + self.state = Some("parameter_name".to_string()); + } + + // Parameter value extraction logic + if self.state.as_deref() == Some("parameter_value") + && !PARAMETER_VALUE_END_TOKEN.iter().any(|&t| content.ends_with(t)) { + + // Check for brackets + if let Some(last_token) = self.tokens.last() { + let open_brackets: Vec = last_token + .trim() + .chars() + .filter(|&c| c == '(' || c == '{' || c == '[') + .collect(); + + if !open_brackets.is_empty() { + self.open_bracket = true; + self.bracket = Some(open_brackets[0]); + } + + if self.open_bracket { + let closing = match self.bracket { + Some('(') => ')', + Some('{') => '}', + Some('[') => ']', + _ => '\0', + }; + if last_token.trim().contains(closing) { + self.open_bracket = false; + self.bracket = None; + } + } + + // Check if token has actual value content + let has_non_punct = last_token.trim().chars().any(|c| !c.is_ascii_punctuation()); + if has_non_punct && !last_token.trim().is_empty() { + self.mask.push(MaskToken::ParameterValue); + + // Check hallucination for required parameters without enum + if self.function_properties.contains_key(&self.function_name) { + if self.mask.len() > 1 + && self.mask[self.mask.len() - 2] != MaskToken::ParameterValue + && !self.parameter_name.is_empty() + { + let last_param = self.parameter_name[self.parameter_name.len() - 1].clone(); + if let Some(func_props) = self.function_properties.get(&self.function_name) { + if is_parameter_required(func_props, &last_param) + && !is_parameter_property(func_props, &last_param, "enum") + && !self.check_parameter_name.contains_key(&last_param) + { + self.check_logprob(); + self.check_parameter_name.insert(last_param, true); + } + } + } + } else if !self.function_name.is_empty() { + self.check_logprob(); + self.error_message = format!( + "Function name {} not found in function properties", + self.function_name + ); + } + } else { + self.mask.push(MaskToken::NotUsed); + } + } + } else if self.state.as_deref() == Some("parameter_value") + && !self.open_bracket + && PARAMETER_VALUE_END_TOKEN.iter().any(|&t| content.ends_with(t)) { + self.state = None; + } else if self.parameter_name_done + && PARAMETER_VALUE_START_PATTERN.iter().any(|&p| content.ends_with(p)) { + self.state = Some("parameter_value".to_string()); + } + + // Maintain consistency between tokens and mask + if self.mask.len() != self.tokens.len() { + self.mask.push(MaskToken::NotUsed); + } + } + + /// Checks log probability and detects hallucination + fn check_logprob(&mut self) { + if let Some(probs) = self.logprobs.last() { + let metrics = calculate_uncertainty(probs); + + if let Some(token) = self.tokens.last() { + self.token_probs_map.push(( + token.clone(), + metrics.entropy, + metrics.varentropy, + metrics.probability, + )); + + if check_threshold(metrics.entropy, metrics.varentropy, &self.thresholds) { + self.hallucination = true; + self.error_message = format!( + "token '{}' is uncertain. Generated response:\n{}", + token, + self.tokens.join("") + ); + } + } + } + } + + /// Counts consecutive tokens of a specific type in the mask + fn count_consecutive_token(&self, token_type: MaskToken) -> usize { + if self.mask.is_empty() || self.mask.last() != Some(&token_type) { + return 0; + } + + self.mask + .iter() + .rev() + .take_while(|&&t| t == token_type) + .count() + } + + /// Extracts the parameter name from recent tokens + fn get_parameter_name(&mut self) { + let p_len = self.count_consecutive_token(MaskToken::ParameterName); + if p_len > 0 && self.tokens.len() > 1 { + let start_idx = self.tokens.len().saturating_sub(p_len + 1); + let end_idx = self.tokens.len().saturating_sub(1); + let parameter_name: String = self.tokens[start_idx..end_idx].join(""); + self.parameter_name.push(parameter_name); + } + } + + /// Extracts the function name from recent tokens + fn get_function_name(&mut self) { + let f_len = self.count_consecutive_token(MaskToken::FunctionName); + if f_len > 0 && self.tokens.len() > 1 { + let start_idx = self.tokens.len().saturating_sub(f_len + 1); + let end_idx = self.tokens.len().saturating_sub(1); + self.function_name = self.tokens[start_idx..end_idx].join(""); + } + } +} + +#[cfg(test)] +mod hallucination_tests { + use super::*; + + #[test] + fn test_calculate_uncertainty() { + let log_probs = vec![-0.1, -2.0, -3.0]; + let metrics = calculate_uncertainty(&log_probs); + assert!(metrics.entropy >= 0.0); + assert!(metrics.varentropy >= 0.0); + assert!(metrics.probability > 0.0 && metrics.probability <= 1.0); + } + + #[test] + fn test_calculate_uncertainty_empty() { + let log_probs: Vec = vec![]; + let metrics = calculate_uncertainty(&log_probs); + assert_eq!(metrics.entropy, 0.0); + assert_eq!(metrics.varentropy, 0.0); + assert_eq!(metrics.probability, 0.0); + } + + #[test] + fn test_check_threshold() { + let thresholds = HallucinationThresholds::default(); + assert!(check_threshold(0.001, 0.001, &thresholds)); + assert!(!check_threshold(0.00001, 0.00001, &thresholds)); + } + + #[test] + fn test_is_parameter_required() { + let func_desc = json!({ + "required": ["param1", "param2"] + }); + assert!(is_parameter_required(&func_desc, "param1")); + assert!(!is_parameter_required(&func_desc, "param3")); + } + + #[test] + fn test_is_parameter_property() { + let func_desc = json!({ + "properties": { + "param1": { + "type": "string", + "enum": ["a", "b"] + } + } + }); + assert!(is_parameter_property(&func_desc, "param1", "enum")); + assert!(!is_parameter_property(&func_desc, "param1", "default")); + } + + #[test] + fn test_check_value_type() { + let handler = ArchFunctionHandler::new( + "test-model".to_string(), + ArchFunctionConfig::default(), + "http://localhost:8000".to_string() + ); + + // Test integer types + assert!(handler.check_value_type(&json!(42), "integer")); + assert!(handler.check_value_type(&json!(42), "int")); + assert!(!handler.check_value_type(&json!(3.14), "integer")); + + // Test number types (accepts both int and float) + assert!(handler.check_value_type(&json!(3.14), "number")); + assert!(handler.check_value_type(&json!(42), "number")); + assert!(handler.check_value_type(&json!(3.14), "float")); + + // Test boolean + assert!(handler.check_value_type(&json!(true), "boolean")); + assert!(handler.check_value_type(&json!(false), "bool")); + assert!(!handler.check_value_type(&json!("true"), "boolean")); + + // Test string + assert!(handler.check_value_type(&json!("hello"), "string")); + assert!(handler.check_value_type(&json!("hello"), "str")); + assert!(!handler.check_value_type(&json!(123), "string")); + + // Test array + assert!(handler.check_value_type(&json!([1, 2, 3]), "array")); + assert!(handler.check_value_type(&json!([1, 2, 3]), "list")); + assert!(!handler.check_value_type(&json!({}), "array")); + + // Test object + assert!(handler.check_value_type(&json!({"key": "value"}), "object")); + assert!(handler.check_value_type(&json!({"key": "value"}), "dict")); + assert!(!handler.check_value_type(&json!([]), "object")); + + // Test unknown type (should return true) + assert!(handler.check_value_type(&json!(42), "unknown_type")); + } + + #[test] + fn test_validate_or_convert_parameter() { + let handler = ArchFunctionHandler::new( + "test-model".to_string(), + ArchFunctionConfig::default(), + "http://localhost:8000".to_string() + ); + + // Test valid type - no conversion needed + assert!(handler.validate_or_convert_parameter(&json!(42), "integer").unwrap()); + assert!(handler.validate_or_convert_parameter(&json!("hello"), "string").unwrap()); + + // Test integer to float conversion (convert_data_type supports this) + let result = handler.validate_or_convert_parameter(&json!(42), "float"); + assert!(result.is_ok()); + assert!(result.unwrap()); // Should be valid after conversion + + // Test invalid type that cannot be converted + // A string cannot be converted to integer (convert_data_type doesn't support this) + let result = handler.validate_or_convert_parameter(&json!("abc"), "integer"); + // Since convert_data_type returns Ok(value.clone()) for unsupported conversions, + // the validation will fail because "abc" string is not an integer + assert!(!result.unwrap()); + + // Test number accepting both int and float + assert!(handler.validate_or_convert_parameter(&json!(42), "number").unwrap()); + assert!(handler.validate_or_convert_parameter(&json!(3.14), "number").unwrap()); + } + + #[test] + fn test_hallucination_state_new() { + let tools = vec![Tool { + tool_type: "function".to_string(), + function: hermesllm::apis::openai::Function { + name: "test_func".to_string(), + description: Some("Test function".to_string()), + parameters: json!({"type": "object"}), + strict: None, + }, + }]; + + let state = HallucinationState::new(&tools); + assert_eq!(state.tokens.len(), 0); + assert!(!state.hallucination); + assert!(state.function_properties.contains_key("test_func")); + } +} diff --git a/crates/brightstaff/src/handlers/integration_tests.rs b/crates/brightstaff/src/handlers/integration_tests.rs index e09ed4f2..42686796 100644 --- a/crates/brightstaff/src/handlers/integration_tests.rs +++ b/crates/brightstaff/src/handlers/integration_tests.rs @@ -42,19 +42,23 @@ mod integration_tests { // Setup services let router_service = create_test_router_service(); let agent_selector = AgentSelector::new(router_service); - let pipeline_processor = PipelineProcessor::default(); + let mut pipeline_processor = PipelineProcessor::default(); // Create test data let agents = vec![ Agent { id: "filter-agent".to_string(), - kind: Some("filter".to_string()), + agent_type: Some("filter".to_string()), url: "http://localhost:8081".to_string(), + tool: None, + transport: None, }, Agent { id: "terminal-agent".to_string(), - kind: Some("terminal".to_string()), + agent_type: Some("terminal".to_string()), url: "http://localhost:8082".to_string(), + tool: None, + transport: None, }, ]; @@ -107,7 +111,15 @@ mod integration_tests { let headers = HeaderMap::new(); let result = pipeline_processor - .process_filter_chain(&request, &test_pipeline, &agent_map, &headers) + .process_filter_chain( + &request.messages, + &test_pipeline, + &agent_map, + &headers, + None, + String::new(), + String::new(), + ) .await; println!("Pipeline processing result: {:?}", result); diff --git a/crates/brightstaff/src/handlers/jsonrpc.rs b/crates/brightstaff/src/handlers/jsonrpc.rs new file mode 100644 index 00000000..0f8b9373 --- /dev/null +++ b/crates/brightstaff/src/handlers/jsonrpc.rs @@ -0,0 +1,49 @@ +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +pub const JSON_RPC_VERSION: &str = "2.0"; +pub const TOOL_CALL_METHOD : &str = "tools/call"; +pub const MCP_INITIALIZE: &str = "initialize"; +pub const MCP_INITIALIZE_NOTIFICATION: &str = "initialize/notification"; + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum JsonRpcId { + String(String), + Number(u64), +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct JsonRpcRequest { + pub jsonrpc: String, + pub id: JsonRpcId, + pub method: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub params: Option>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct JsonRpcNotification { + pub jsonrpc: String, + pub method: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub params: Option>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct JsonRpcError { + pub code: i32, + pub message: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub data: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct JsonRpcResponse { + pub jsonrpc: String, + pub id: JsonRpcId, + #[serde(skip_serializing_if = "Option::is_none")] + pub result: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, +} diff --git a/crates/brightstaff/src/handlers/llm.rs b/crates/brightstaff/src/handlers/llm.rs new file mode 100644 index 00000000..5c5bcf01 --- /dev/null +++ b/crates/brightstaff/src/handlers/llm.rs @@ -0,0 +1,462 @@ +use bytes::Bytes; +use common::configuration::{LlmProvider, ModelAlias}; +use common::consts::{ARCH_IS_STREAMING_HEADER, ARCH_PROVIDER_HINT_HEADER, REQUEST_ID_HEADER, TRACE_PARENT_HEADER}; +use common::traces::TraceCollector; +use hermesllm::apis::openai_responses::InputParam; +use hermesllm::clients::{SupportedAPIsFromClient, SupportedUpstreamAPIs}; +use hermesllm::{ProviderRequest, ProviderRequestType}; +use http_body_util::combinators::BoxBody; +use http_body_util::{BodyExt, Full}; +use hyper::header::{self}; +use hyper::{Request, Response, StatusCode}; +use std::collections::HashMap; +use std::sync::Arc; +use tokio::sync::RwLock; +use tracing::{debug, info, warn}; + +use crate::router::llm_router::RouterService; +use crate::handlers::utils::{create_streaming_response, ObservableStreamProcessor, truncate_message}; +use crate::handlers::router_chat::router_chat_get_upstream_model; +use crate::state::response_state_processor::ResponsesStateProcessor; +use crate::state::{ + StateStorage, StateStorageError, + extract_input_items, retrieve_and_combine_input +}; +use crate::tracing::operation_component; + +fn full>(chunk: T) -> BoxBody { + Full::new(chunk.into()) + .map_err(|never| match never {}) + .boxed() +} + +pub async fn llm_chat( + request: Request, + router_service: Arc, + full_qualified_llm_provider_url: String, + model_aliases: Arc>>, + llm_providers: Arc>>, + trace_collector: Arc, + state_storage: Option>, +) -> Result>, hyper::Error> { + + let request_path = request.uri().path().to_string(); + let request_headers = request.headers().clone(); + let request_id = request_headers + .get(REQUEST_ID_HEADER) + .and_then(|h| h.to_str().ok()) + .map(|s| s.to_string()) + .unwrap_or_else(|| "unknown".to_string()); + + // Extract or generate traceparent - this establishes the trace context for all spans + let traceparent: String = request_headers + .get(TRACE_PARENT_HEADER) + .and_then(|h| h.to_str().ok()) + .map(|s| s.to_string()) + .unwrap_or_else(|| { + use uuid::Uuid; + let trace_id = Uuid::new_v4().to_string().replace("-", ""); + format!("00-{}-0000000000000000-01", trace_id) + }); + + let mut request_headers = request_headers; + let chat_request_bytes = request.collect().await?.to_bytes(); + + debug!( + "[PLANO_REQ_ID:{}] | REQUEST_BODY (UTF8): {}", + request_id, + String::from_utf8_lossy(&chat_request_bytes) + ); + + let mut client_request = match ProviderRequestType::try_from(( + &chat_request_bytes[..], + &SupportedAPIsFromClient::from_endpoint(request_path.as_str()).unwrap(), + )) { + Ok(request) => request, + Err(err) => { + warn!("[PLANO_REQ_ID:{}] | FAILURE | Failed to parse request as ProviderRequestType: {}", request_id, err); + let err_msg = format!("[PLANO_REQ_ID:{}] | FAILURE | Failed to parse request: {}", request_id, err); + let mut bad_request = Response::new(full(err_msg)); + *bad_request.status_mut() = StatusCode::BAD_REQUEST; + return Ok(bad_request); + } + }; + + // === v1/responses state management: Extract input items early === + let mut original_input_items = Vec::new(); + let client_api = SupportedAPIsFromClient::from_endpoint(request_path.as_str()); + let is_responses_api_client = matches!(client_api, Some(SupportedAPIsFromClient::OpenAIResponsesAPI(_))); + + // Model alias resolution: update model field in client_request immediately + // This ensures all downstream objects use the resolved model + let model_from_request = client_request.model().to_string(); + let temperature = client_request.get_temperature(); + let is_streaming_request = client_request.is_streaming(); + let resolved_model = resolve_model_alias(&model_from_request, &model_aliases); + + // Extract tool names and user message preview for span attributes + let tool_names = client_request.get_tool_names(); + let user_message_preview = client_request.get_recent_user_message() + .map(|msg| truncate_message(&msg, 50)); + + client_request.set_model(resolved_model.clone()); + if client_request.remove_metadata_key("archgw_preference_config") { + debug!("[PLANO_REQ_ID:{}] Removed archgw_preference_config from metadata", request_id); + } + + // === v1/responses state management: Determine upstream API and combine input if needed === + // Do this BEFORE routing since routing consumes the request + // Only process state if state_storage is configured + let mut should_manage_state = false; + if is_responses_api_client && state_storage.is_some() { + if let ProviderRequestType::ResponsesAPIRequest(ref mut responses_req) = client_request { + // Extract original input once + original_input_items = extract_input_items(&responses_req.input); + + // Get the upstream path and check if it's ResponsesAPI + let upstream_path = get_upstream_path( + &llm_providers, + &resolved_model, + &request_path, + &resolved_model, + is_streaming_request, + ).await; + + let upstream_api = SupportedUpstreamAPIs::from_endpoint(&upstream_path); + + // Only manage state if upstream is NOT OpenAIResponsesAPI (needs translation) + should_manage_state = !matches!(upstream_api, Some(SupportedUpstreamAPIs::OpenAIResponsesAPI(_))); + + if should_manage_state { + // Retrieve and combine conversation history if previous_response_id exists + if let Some(ref prev_resp_id) = responses_req.previous_response_id { + match retrieve_and_combine_input( + state_storage.as_ref().unwrap().clone(), + prev_resp_id, + original_input_items, // Pass ownership instead of cloning + ) + .await + { + Ok(combined_input) => { + // Update both the request and original_input_items + responses_req.input = InputParam::Items(combined_input.clone()); + original_input_items = combined_input; + info!("[PLANO_REQ_ID:{}] | STATE_PROCESSOR | Updated request with conversation history ({} items)", request_id, original_input_items.len()); + } + Err(StateStorageError::NotFound(_)) => { + // Return 409 Conflict when previous_response_id not found + warn!("[PLANO_REQ_ID:{}] | STATE_PROCESSOR | Previous response_id not found: {}", request_id, prev_resp_id); + let err_msg = format!( + "[PLANO_REQ_ID:{}] | STATE_PROCESSOR | Conversation state not found for previous_response_id: {}", + request_id, prev_resp_id + ); + let mut conflict_response = Response::new(full(err_msg)); + *conflict_response.status_mut() = StatusCode::CONFLICT; + return Ok(conflict_response); + } + Err(e) => { + // Log warning but continue on other storage errors + warn!( + "[PLANO_REQ_ID:{}] | STATE_PROCESSOR | Failed to retrieve conversation state for {}: {}", + request_id, prev_resp_id, e + ); + // Restore original_input_items since we passed ownership + original_input_items = extract_input_items(&responses_req.input); + } + } + } + } else { + debug!("[PLANO_REQ_ID:{}] | BRIGHT_STAFF | Upstream supports ResponsesAPI natively.", request_id); + } + } + } + + // Serialize request for upstream BEFORE router consumes it + let client_request_bytes_for_upstream = ProviderRequestType::to_bytes(&client_request).unwrap(); + + // Determine routing using the dedicated router_chat module + let routing_result = match router_chat_get_upstream_model( + router_service, + client_request, // Pass the original request - router_chat will convert it + &request_headers, + trace_collector.clone(), + &traceparent, + &request_path, + ) + .await + { + Ok(result) => result, + Err(err) => { + let mut internal_error = Response::new(full(err.message)); + *internal_error.status_mut() = err.status_code; + return Ok(internal_error); + } + }; + + let model_name = routing_result.model_name; + + debug!( + "[PLANO_REQ_ID:{}] | ARCH_ROUTER URL | {}, Resolved Model: {}", + request_id, full_qualified_llm_provider_url, model_name + ); + + request_headers.insert( + ARCH_PROVIDER_HINT_HEADER, + header::HeaderValue::from_str(&model_name).unwrap(), + ); + + request_headers.insert( + header::HeaderName::from_static(ARCH_IS_STREAMING_HEADER), + header::HeaderValue::from_str(&is_streaming_request.to_string()).unwrap(), + ); + // remove content-length header if it exists + request_headers.remove(header::CONTENT_LENGTH); + + // Capture start time right before sending request to upstream + let request_start_time = std::time::Instant::now(); + let request_start_system_time = std::time::SystemTime::now(); + + let llm_response = match reqwest::Client::new() + .post(full_qualified_llm_provider_url) + .headers(request_headers) + .body(client_request_bytes_for_upstream) + .send() + .await + { + Ok(res) => res, + Err(err) => { + let err_msg = format!("Failed to send request: {}", err); + let mut internal_error = Response::new(full(err_msg)); + *internal_error.status_mut() = StatusCode::INTERNAL_SERVER_ERROR; + return Ok(internal_error); + } + }; + + // copy over the headers and status code from the original response + let response_headers = llm_response.headers().clone(); + let upstream_status = llm_response.status(); + let mut response = Response::builder().status(upstream_status); + let headers = response.headers_mut().unwrap(); + for (header_name, header_value) in response_headers.iter() { + headers.insert(header_name, header_value.clone()); + } + + // Build LLM span with actual status code using constants + let byte_stream = llm_response.bytes_stream(); + + // Build the LLM span (will be finalized after streaming completes) + let llm_span = build_llm_span( + &traceparent, + &request_path, + &resolved_model, + &model_name, + upstream_status.as_u16(), + is_streaming_request, + request_start_system_time, + tool_names, + user_message_preview, + temperature, + &llm_providers, + ).await; + + // Create base processor for metrics and tracing + let base_processor = ObservableStreamProcessor::new( + trace_collector, + operation_component::LLM, + llm_span, + request_start_time, + ); + + // === v1/responses state management: Wrap with ResponsesStateProcessor === + // Only wrap if we need to manage state (client is ResponsesAPI AND upstream is NOT ResponsesAPI AND state_storage is configured) + let streaming_response = if should_manage_state && !original_input_items.is_empty() && state_storage.is_some() { + // Extract Content-Encoding header to handle decompression for state parsing + let content_encoding = response_headers + .get("content-encoding") + .and_then(|v| v.to_str().ok()) + .map(|s| s.to_string()); + + // Wrap with state management processor to store state after response completes + let state_processor = ResponsesStateProcessor::new( + base_processor, + state_storage.unwrap(), + original_input_items, + resolved_model.clone(), + model_name.clone(), + is_streaming_request, + false, // Not OpenAI upstream since should_manage_state is true + content_encoding, + request_id.clone(), + ); + create_streaming_response(byte_stream, state_processor, 16) + } else { + // Use base processor without state management + create_streaming_response(byte_stream, base_processor, 16) + }; + + match response.body(streaming_response.body) { + Ok(response) => Ok(response), + Err(err) => { + let err_msg = format!("Failed to create response: {}", err); + let mut internal_error = Response::new(full(err_msg)); + *internal_error.status_mut() = StatusCode::INTERNAL_SERVER_ERROR; + Ok(internal_error) + } + } +} + +/// Resolves model aliases by looking up the requested model in the model_aliases map. +/// Returns the target model if an alias is found, otherwise returns the original model. +fn resolve_model_alias( + model_from_request: &str, + model_aliases: &Arc>>, +) -> String { + if let Some(aliases) = model_aliases.as_ref() { + if let Some(model_alias) = aliases.get(model_from_request) { + debug!( + "Model Alias: 'From {}' -> 'To {}'", + model_from_request, model_alias.target + ); + return model_alias.target.clone(); + } + } + model_from_request.to_string() +} + +/// Builds the LLM span with all required and optional attributes. +async fn build_llm_span( + traceparent: &str, + request_path: &str, + resolved_model: &str, + model_name: &str, + status_code: u16, + is_streaming: bool, + start_time: std::time::SystemTime, + tool_names: Option>, + user_message_preview: Option, + temperature: Option, + llm_providers: &Arc>>, +) -> common::traces::Span { + use common::traces::{SpanBuilder, SpanKind, parse_traceparent}; + use crate::tracing::{http, llm, OperationNameBuilder}; + + // Calculate the upstream path based on provider configuration + let upstream_path = get_upstream_path( + llm_providers, + model_name, + request_path, + resolved_model, + is_streaming, + ).await; + + // Build operation name showing path transformation if different + let operation_name = if request_path != upstream_path { + OperationNameBuilder::new() + .with_method("POST") + .with_path(&format!("{} >> {}", request_path, upstream_path)) + .with_target(resolved_model) + .build() + } else { + OperationNameBuilder::new() + .with_method("POST") + .with_path(request_path) + .with_target(resolved_model) + .build() + }; + + let (trace_id, parent_span_id) = parse_traceparent(traceparent); + + let mut span_builder = SpanBuilder::new(&operation_name) + .with_trace_id(&trace_id) + .with_kind(SpanKind::Client) + .with_start_time(start_time) + .with_attribute(http::METHOD, "POST") + .with_attribute(http::STATUS_CODE, status_code.to_string()) + .with_attribute(http::TARGET, request_path.to_string()) + .with_attribute(http::UPSTREAM_TARGET, upstream_path) + .with_attribute(llm::MODEL_NAME, resolved_model.to_string()) + .with_attribute(llm::IS_STREAMING, is_streaming.to_string()); + + // Only set parent span ID if it exists (not a root span) + if let Some(parent) = parent_span_id { + span_builder = span_builder.with_parent_span_id(&parent); + } + + // Add optional attributes + if let Some(temp) = temperature { + span_builder = span_builder.with_attribute(llm::TEMPERATURE, temp.to_string()); + } + + if let Some(tools) = tool_names { + let formatted_tools = tools.iter() + .map(|name| format!("{}(...)", name)) + .collect::>() + .join("\n"); + span_builder = span_builder.with_attribute(llm::TOOLS, formatted_tools); + } + + if let Some(preview) = user_message_preview { + span_builder = span_builder.with_attribute(llm::USER_MESSAGE_PREVIEW, preview); + } + + span_builder.build() +} + +/// Calculates the upstream path for the provider based on the model name. +/// Looks up provider configuration, gets the ProviderId and base_url_path_prefix, +/// then uses target_endpoint_for_provider to calculate the correct upstream path. +async fn get_upstream_path( + llm_providers: &Arc>>, + model_name: &str, + request_path: &str, + resolved_model: &str, + is_streaming: bool, +) -> String { + let (provider_id, base_url_path_prefix) = get_provider_info(llm_providers, model_name).await; + + // Calculate the upstream path using the proper API + let client_api = SupportedAPIsFromClient::from_endpoint(request_path) + .expect("Should have valid API endpoint"); + + client_api.target_endpoint_for_provider( + &provider_id, + request_path, + resolved_model, + is_streaming, + base_url_path_prefix.as_deref(), + ) +} + +/// Helper function to get provider info (ProviderId and base_url_path_prefix) +async fn get_provider_info( + llm_providers: &Arc>>, + model_name: &str, +) -> (hermesllm::ProviderId, Option) { + let providers_lock = llm_providers.read().await; + + // First, try to find by model name or provider name + let provider = providers_lock.iter().find(|p| { + p.model.as_ref().map(|m| m == model_name).unwrap_or(false) + || p.name == model_name + }); + + if let Some(provider) = provider { + let provider_id = provider.provider_interface.to_provider_id(); + let prefix = provider.base_url_path_prefix.clone(); + return (provider_id, prefix); + } + + let default_provider = providers_lock.iter().find(|p| { + p.default.unwrap_or(false) + }); + + if let Some(provider) = default_provider { + let provider_id = provider.provider_interface.to_provider_id(); + let prefix = provider.base_url_path_prefix.clone(); + (provider_id, prefix) + } else { + // Last resort: use OpenAI as hardcoded fallback + warn!("No default provider found, falling back to OpenAI"); + (hermesllm::ProviderId::OpenAI, None) + } +} diff --git a/crates/brightstaff/src/handlers/mod.rs b/crates/brightstaff/src/handlers/mod.rs index 66c5449b..e63958d5 100644 --- a/crates/brightstaff/src/handlers/mod.rs +++ b/crates/brightstaff/src/handlers/mod.rs @@ -1,9 +1,13 @@ pub mod agent_chat_completions; pub mod agent_selector; -pub mod chat_completions; +pub mod llm; +pub mod router_chat; pub mod models; +pub mod function_calling; pub mod pipeline_processor; pub mod response_handler; +pub mod utils; +pub mod jsonrpc; #[cfg(test)] mod integration_tests; diff --git a/crates/brightstaff/src/handlers/pipeline_processor.rs b/crates/brightstaff/src/handlers/pipeline_processor.rs index b62ce175..88e8238e 100644 --- a/crates/brightstaff/src/handlers/pipeline_processor.rs +++ b/crates/brightstaff/src/handlers/pipeline_processor.rs @@ -1,10 +1,24 @@ use std::collections::HashMap; use common::configuration::{Agent, AgentFilterChain}; -use common::consts::{ARCH_UPSTREAM_HOST_HEADER, ENVOY_RETRY_HEADER}; -use hermesllm::apis::openai::{ChatCompletionsRequest, Message}; +use common::consts::{ + ARCH_UPSTREAM_HOST_HEADER, BRIGHT_STAFF_SERVICE_NAME, ENVOY_RETRY_HEADER, TRACE_PARENT_HEADER, +}; +use common::traces::{SpanBuilder, SpanKind, generate_random_span_id}; +use hermesllm::apis::openai::Message; +use hermesllm::{ProviderRequest, ProviderRequestType}; use hyper::header::HeaderMap; -use tracing::{debug, warn}; +use std::time::{Instant, SystemTime}; +use tracing::{debug, info, warn}; + +use crate::tracing::operation_component::{self}; +use crate::tracing::{http, OperationNameBuilder}; + +use crate::handlers::jsonrpc::{ + JsonRpcId, JsonRpcNotification, JsonRpcRequest, JsonRpcResponse, JSON_RPC_VERSION, + MCP_INITIALIZE, MCP_INITIALIZE_NOTIFICATION, TOOL_CALL_METHOD, +}; +use uuid::Uuid; /// Errors that can occur during pipeline processing #[derive(Debug, thiserror::Error)] @@ -19,19 +33,41 @@ pub enum PipelineError { NoChoicesInResponse(String), #[error("No content in response from agent '{0}'")] NoContentInResponse(String), + #[error("No result in response from agent '{0}'")] + NoResultInResponse(String), + #[error("No structured content in response from agent '{0}'")] + NoStructuredContentInResponse(String), + #[error("No messages in response from agent '{0}'")] + NoMessagesInResponse(String), + #[error("Client error from agent '{agent}' (HTTP {status}): {body}")] + ClientError { + agent: String, + status: u16, + body: String, + }, + #[error("Server error from agent '{agent}' (HTTP {status}): {body}")] + ServerError { + agent: String, + status: u16, + body: String, + }, } /// Service for processing agent pipelines pub struct PipelineProcessor { client: reqwest::Client, url: String, + agent_id_session_map: HashMap, } +const ENVOY_API_ROUTER_ADDRESS: &str = "http://localhost:11000"; + impl Default for PipelineProcessor { fn default() -> Self { Self { client: reqwest::Client::new(), - url: "http://localhost:11000/v1/chat/completions".to_string(), + url: ENVOY_API_ROUTER_ADDRESS.to_string(), + agent_id_session_map: HashMap::new(), } } } @@ -41,18 +77,128 @@ impl PipelineProcessor { Self { client: reqwest::Client::new(), url, + agent_id_session_map: HashMap::new(), } } + /// Record a span for filter execution + fn record_filter_span( + &self, + collector: &std::sync::Arc, + agent_name: &str, + tool_name: &str, + start_time: SystemTime, + end_time: SystemTime, + elapsed: std::time::Duration, + trace_id: String, + parent_span_id: String, + span_id: String, + ) -> String { + // let (trace_id, parent_span_id) = self.extract_trace_context(); + + // Build operation name: POST /agents/* {filter_name} + // Using generic path since we don't have access to specific endpoint here + let operation_name = OperationNameBuilder::new() + .with_method("POST") + .with_path("/agents/*") + .with_target(agent_name) + .build(); + + let mut span_builder = SpanBuilder::new(&operation_name) + .with_span_id(span_id.clone()) + .with_kind(SpanKind::Client) + .with_start_time(start_time) + .with_end_time(end_time) + .with_attribute(http::METHOD, "POST") + .with_attribute(http::TARGET, "/agents/*") + .with_attribute("filter.name", agent_name.to_string()) + .with_attribute("filter.tool_name", tool_name.to_string()) + .with_attribute( + "duration_ms", + format!("{:.2}", elapsed.as_secs_f64() * 1000.0), + ); + + if !trace_id.is_empty() { + span_builder = span_builder.with_trace_id(trace_id); + } + if !parent_span_id.is_empty() { + span_builder = span_builder.with_parent_span_id(parent_span_id); + } + + let span = span_builder.build(); + // Use plano(filter) as service name for filter execution spans + collector.record_span(operation_component::AGENT_FILTER, span); + span_id.clone() + } + + /// Record a span for MCP protocol interactions + fn record_mcp_span( + &self, + collector: &std::sync::Arc, + operation: &str, + agent_id: &str, + start_time: SystemTime, + end_time: SystemTime, + elapsed: std::time::Duration, + additional_attrs: Option>, + trace_id: String, + parent_span_id: String, + span_id: Option, + ) { + // let (trace_id, parent_span_id) = self.extract_trace_context(); + + // Build operation name: POST /mcp {agent_id} + let operation_name = OperationNameBuilder::new() + .with_method("POST") + .with_path("/mcp") + .with_operation(operation) + .with_target(agent_id) + .build(); + + let mut span_builder = SpanBuilder::new(&operation_name) + .with_span_id(span_id.unwrap_or_else(|| generate_random_span_id())) + .with_kind(SpanKind::Client) + .with_start_time(start_time) + .with_end_time(end_time) + .with_attribute(http::METHOD, "POST") + .with_attribute(http::TARGET, &format!("/mcp ({})", operation.to_string())) + .with_attribute("mcp.operation", operation.to_string()) + .with_attribute("mcp.agent_id", agent_id.to_string()) + .with_attribute( + "duration_ms", + format!("{:.2}", elapsed.as_secs_f64() * 1000.0), + ); + + if let Some(attrs) = additional_attrs { + for (key, value) in attrs { + span_builder = span_builder.with_attribute(key, value); + } + } + + if !trace_id.is_empty() { + span_builder = span_builder.with_trace_id(trace_id); + } + if !parent_span_id.is_empty() { + span_builder = span_builder.with_parent_span_id(parent_span_id); + } + + let span = span_builder.build(); + // MCP spans also use plano(filter) service name as they are part of filter operations + collector.record_span(operation_component::AGENT_FILTER, span); + } + /// Process the filter chain of agents (all except the terminal agent) pub async fn process_filter_chain( - &self, - initial_request: &ChatCompletionsRequest, + &mut self, + chat_history: &[Message], agent_filter_chain: &AgentFilterChain, agent_map: &HashMap, request_headers: &HeaderMap, + trace_collector: Option<&std::sync::Arc>, + trace_id: String, + parent_span_id: String, ) -> Result, PipelineError> { - let mut chat_completions_history = initial_request.messages.clone(); + let mut chat_history_updated = chat_history.to_vec(); for agent_name in &agent_filter_chain.filter_chain { debug!("Processing filter agent: {}", agent_name); @@ -61,101 +207,490 @@ impl PipelineProcessor { .get(agent_name) .ok_or_else(|| PipelineError::AgentNotFound(agent_name.clone()))?; - debug!("Agent details: {:?}", agent); + let tool_name = agent.tool.as_deref().unwrap_or(&agent.id); - let response_content = self - .send_agent_filter_chain_request( - &chat_completions_history, - initial_request, + info!( + "executing filter: {}/{}, url: {}, conversation length: {}", + agent_name, + tool_name, + agent.url, + chat_history.len() + ); + + let start_time = SystemTime::now(); + let start_instant = Instant::now(); + + // Generate filter span ID before execution so MCP spans can use it as parent + let filter_span_id = generate_random_span_id(); + + chat_history_updated = self + .execute_filter( + &chat_history_updated, agent, request_headers, + trace_collector, + trace_id.clone(), + filter_span_id.clone(), ) .await?; - debug!("Received response from filter agent {}", agent_name); + let end_time = SystemTime::now(); + let elapsed = start_instant.elapsed(); - // Parse the response content as new message history - chat_completions_history = - serde_json::from_str(&response_content).inspect_err(|err| { - warn!( - "Failed to parse response from agent {}, err: {}, response: {}", - agent_name, err, response_content - ) - })?; + info!( + "Filter '{}' completed in {:.2}ms, updated conversation length: {}", + agent_name, + elapsed.as_secs_f64() * 1000.0, + chat_history_updated.len() + ); + + // Record span for this filter execution + if let Some(collector) = trace_collector { + self.record_filter_span( + collector, + agent_name, + tool_name, + start_time, + end_time, + elapsed, + trace_id.clone(), + parent_span_id.clone(), + filter_span_id, + ); + } } - Ok(chat_completions_history) + Ok(chat_history_updated) } - /// Send request to a specific agent and return the response content - async fn send_agent_filter_chain_request( + /// Build common MCP headers for requests + fn build_mcp_headers( &self, - messages: &[Message], - original_request: &ChatCompletionsRequest, - agent: &Agent, request_headers: &HeaderMap, - ) -> Result { - let mut request = original_request.clone(); - request.messages = messages.to_vec(); + agent_id: &str, + session_id: Option<&str>, + trace_id: String, + parent_span_id: String, + ) -> Result { + let trace_parent = format!("00-{}-{}-01", trace_id, parent_span_id); + let mut headers = request_headers.clone(); + headers.remove(hyper::header::CONTENT_LENGTH); - let request_body = serde_json::to_string(&request)?; - debug!("Sending request to agent {}", agent.id); - - let mut agent_headers = request_headers.clone(); - agent_headers.remove(hyper::header::CONTENT_LENGTH); - agent_headers.insert( - ARCH_UPSTREAM_HOST_HEADER, - hyper::header::HeaderValue::from_str(&agent.id) - .map_err(|_| PipelineError::AgentNotFound(agent.id.clone()))?, + headers.remove(TRACE_PARENT_HEADER); + headers.insert( + TRACE_PARENT_HEADER, + hyper::header::HeaderValue::from_str(&trace_parent).unwrap(), ); - agent_headers.insert( + headers.insert( + ARCH_UPSTREAM_HOST_HEADER, + hyper::header::HeaderValue::from_str(agent_id) + .map_err(|_| PipelineError::AgentNotFound(agent_id.to_string()))?, + ); + + headers.insert( ENVOY_RETRY_HEADER, hyper::header::HeaderValue::from_str("3").unwrap(), ); + headers.insert( + "Accept", + hyper::header::HeaderValue::from_static("application/json, text/event-stream"), + ); + + headers.insert( + "Content-Type", + hyper::header::HeaderValue::from_static("application/json"), + ); + + if let Some(sid) = session_id { + headers.insert( + "mcp-session-id", + hyper::header::HeaderValue::from_str(sid).unwrap(), + ); + } + + Ok(headers) + } + + /// Parse SSE formatted response and extract JSON-RPC data + fn parse_sse_response( + &self, + response_bytes: &[u8], + agent_id: &str, + ) -> Result { + let response_str = String::from_utf8_lossy(response_bytes); + let lines: Vec<&str> = response_str.lines().collect(); + + // Validate SSE format: first line should be "event: message" + if lines.is_empty() || lines[0] != "event: message" { + warn!( + "Invalid SSE response format from agent {}: expected 'event: message' as first line, got: {:?}", + agent_id, + lines.first() + ); + return Err(PipelineError::NoContentInResponse(format!( + "Invalid SSE response format from agent {}: expected 'event: message' as first line", + agent_id + ))); + } + + // Find the data line + let data_lines: Vec<&str> = lines + .iter() + .filter(|line| line.starts_with("data: ")) + .copied() + .collect(); + + if data_lines.len() != 1 { + warn!( + "Expected exactly one 'data:' line from agent {}, found {}", + agent_id, + data_lines.len() + ); + return Err(PipelineError::NoContentInResponse(format!( + "Expected exactly one 'data:' line from agent {}, found {}", + agent_id, + data_lines.len() + ))); + } + + // Skip "data: " prefix + Ok(data_lines[0][6..].to_string()) + } + + /// Send an MCP request and return the response + async fn send_mcp_request( + &self, + json_rpc_request: &JsonRpcRequest, + headers: HeaderMap, + agent_id: &str, + ) -> Result { + let request_body = serde_json::to_string(json_rpc_request)?; + + debug!( + "Sending MCP request to agent {}: {}", + agent_id, request_body + ); + let response = self .client - .post(&self.url) - .headers(agent_headers) + .post(format!("{}/mcp", self.url)) + .headers(headers) .body(request_body) .send() .await?; + Ok(response) + } + + /// Build a tools/call JSON-RPC request + fn build_tool_call_request( + &self, + tool_name: &str, + messages: &[Message], + ) -> Result { + let mut arguments = HashMap::new(); + arguments.insert("messages".to_string(), serde_json::to_value(messages)?); + + let mut params = HashMap::new(); + params.insert("name".to_string(), serde_json::to_value(tool_name)?); + params.insert("arguments".to_string(), serde_json::to_value(arguments)?); + + Ok(JsonRpcRequest { + jsonrpc: JSON_RPC_VERSION.to_string(), + id: JsonRpcId::String(Uuid::new_v4().to_string()), + method: TOOL_CALL_METHOD.to_string(), + params: Some(params), + }) + } + + /// Send request to a specific agent and return the response content + async fn execute_filter( + &mut self, + messages: &[Message], + agent: &Agent, + request_headers: &HeaderMap, + trace_collector: Option<&std::sync::Arc>, + trace_id: String, + filter_span_id: String, + ) -> Result, PipelineError> { + // Get or create MCP session + let mcp_session_id = if let Some(session_id) = self.agent_id_session_map.get(&agent.id) { + session_id.clone() + } else { + let session_id = self + .get_new_session_id( + &agent.id, + trace_id.clone(), + filter_span_id.clone(), + ) + .await; + self.agent_id_session_map + .insert(agent.id.clone(), session_id.clone()); + session_id + }; + + info!( + "Using MCP session ID {} for agent {}", + mcp_session_id, agent.id + ); + + // Build JSON-RPC request + let tool_name = agent.tool.as_deref().unwrap_or(&agent.id); + let json_rpc_request = self.build_tool_call_request(tool_name, messages)?; + + // Generate span ID for this MCP tool call (child of filter span) + let mcp_span_id = generate_random_span_id(); + + // Build headers + let agent_headers = + self.build_mcp_headers(request_headers, &agent.id, Some(&mcp_session_id), trace_id.clone(), mcp_span_id.clone())?; + + // Send request with tracing + let start_time = SystemTime::now(); + let start_instant = Instant::now(); + + let response = self + .send_mcp_request( + &json_rpc_request, + agent_headers, + &agent.id, + ) + .await?; + let http_status = response.status(); let response_bytes = response.bytes().await?; - // Parse the response as JSON to extract the content - let response_json: serde_json::Value = serde_json::from_slice(&response_bytes)?; + let end_time = SystemTime::now(); + let elapsed = start_instant.elapsed(); - let content = response_json - .get("choices") - .and_then(|choices| choices.as_array()) - .and_then(|choices| choices.first()) - .and_then(|choice| choice.get("message")) - .and_then(|message| message.get("content")) - .and_then(|content| content.as_str()) - .ok_or_else(|| PipelineError::NoContentInResponse(agent.id.clone()))? + // Record MCP tool call span + if let Some(collector) = trace_collector { + let mut attrs = HashMap::new(); + attrs.insert("mcp.method", "tools/call".to_string()); + attrs.insert("mcp.tool_name", tool_name.to_string()); + attrs.insert("mcp.session_id", mcp_session_id.clone()); + attrs.insert("http.status_code", http_status.as_u16().to_string()); + + self.record_mcp_span( + collector, + "tool_call", + &agent.id, + start_time, + end_time, + elapsed, + Some(attrs), + trace_id.clone(), + filter_span_id.clone(), + Some(mcp_span_id), + ); + } + + // Handle HTTP errors + if !http_status.is_success() { + let error_body = String::from_utf8_lossy(&response_bytes).to_string(); + return Err(if http_status.is_client_error() { + PipelineError::ClientError { + agent: agent.id.clone(), + status: http_status.as_u16(), + body: error_body, + } + } else { + PipelineError::ServerError { + agent: agent.id.clone(), + status: http_status.as_u16(), + body: error_body, + } + }); + } + + info!( + "Response from agent {}: {}", + agent.id, + String::from_utf8_lossy(&response_bytes) + ); + + // Parse SSE response + let data_chunk = self.parse_sse_response(&response_bytes, &agent.id)?; + let response: JsonRpcResponse = serde_json::from_str(&data_chunk)?; + let response_result = response + .result + .ok_or_else(|| PipelineError::NoResultInResponse(agent.id.clone()))?; + + // Check if error field is set in response result + if response_result + .get("isError") + .and_then(|v| v.as_bool()) + .unwrap_or(false) + { + let error_message = response_result + .get("content") + .and_then(|v| v.as_array()) + .and_then(|arr| arr.first()) + .and_then(|v| v.get("text")) + .and_then(|v| v.as_str()) + .unwrap_or("unknown_error") + .to_string(); + + return Err(PipelineError::ClientError { + agent: agent.id.clone(), + status: http_status.as_u16(), + body: error_message, + }); + } + + // Extract structured content and parse messages + let response_json = response_result + .get("structuredContent") + .ok_or_else(|| PipelineError::NoStructuredContentInResponse(agent.id.clone()))?; + + let messages: Vec = response_json + .get("result") + .and_then(|v| v.as_array()) + .ok_or_else(|| PipelineError::NoMessagesInResponse(agent.id.clone()))? + .iter() + .map(|msg_value| serde_json::from_value(msg_value.clone())) + .collect::, _>>() + .map_err(PipelineError::ParseError)?; + + Ok(messages) + } + + /// Build an initialize JSON-RPC request + fn build_initialize_request(&self) -> JsonRpcRequest { + JsonRpcRequest { + jsonrpc: JSON_RPC_VERSION.to_string(), + id: JsonRpcId::String(Uuid::new_v4().to_string()), + method: MCP_INITIALIZE.to_string(), + params: Some({ + let mut params = HashMap::new(); + params.insert( + "protocolVersion".to_string(), + serde_json::Value::String("2024-11-05".to_string()), + ); + params.insert("capabilities".to_string(), serde_json::json!({})); + params.insert( + "clientInfo".to_string(), + serde_json::json!({ + "name": BRIGHT_STAFF_SERVICE_NAME, + "version": "1.0.0" + }), + ); + params + }), + } + } + + /// Send initialized notification after session creation + async fn send_initialized_notification( + &self, + agent_id: &str, + session_id: &str, + trace_id: String, + parent_span_id: String, + ) -> Result<(), PipelineError> { + let initialized_notification = JsonRpcNotification { + jsonrpc: JSON_RPC_VERSION.to_string(), + method: MCP_INITIALIZE_NOTIFICATION.to_string(), + params: None, + }; + + let notification_body = serde_json::to_string(&initialized_notification)?; + debug!("Sending initialized notification for agent {}", agent_id); + + let headers = self.build_mcp_headers(&HeaderMap::new(), agent_id, Some(session_id), trace_id.clone(), parent_span_id.clone())?; + + let response = self + .client + .post(format!("{}/mcp", self.url)) + .headers(headers) + .body(notification_body) + .send() + .await?; + + info!( + "Initialized notification response status: {}", + response.status() + ); + + Ok(()) + } + + async fn get_new_session_id( + &self, + agent_id: &str, + trace_id: String, + parent_span_id: String, + ) -> String { + info!("Initializing MCP session for agent {}", agent_id); + + let initialize_request = self.build_initialize_request(); + let headers = self + .build_mcp_headers(&HeaderMap::new(), agent_id, None, trace_id.clone(), parent_span_id.clone()) + .expect("Failed to build headers for initialization"); + + let response = self + .send_mcp_request(&initialize_request, headers, agent_id) + .await + .expect("Failed to initialize MCP session"); + + info!("Initialize response status: {}", response.status()); + + let session_id = response + .headers() + .get("mcp-session-id") + .and_then(|v| v.to_str().ok()) + .expect("No mcp-session-id in response") .to_string(); - Ok(content) + info!( + "Created new MCP session for agent {}: {}", + agent_id, session_id + ); + + // Send initialized notification + self.send_initialized_notification( + agent_id, + &session_id, + trace_id.clone(), + parent_span_id.clone(), + ) + .await + .expect("Failed to send initialized notification"); + + session_id } /// Send request to terminal agent and return the raw response for streaming - pub async fn invoke_upstream_agent( + pub async fn invoke_agent( &self, messages: &[Message], - original_request: &ChatCompletionsRequest, + mut original_request: ProviderRequestType, terminal_agent: &Agent, request_headers: &HeaderMap, + trace_id: String, + agent_span_id: String, ) -> Result { - let mut request = original_request.clone(); - request.messages = messages.to_vec(); + // let mut request = original_request.clone(); + original_request.set_messages(messages); - let request_body = serde_json::to_string(&request)?; + let request_body = ProviderRequestType::to_bytes(&original_request).unwrap(); + // let request_body = serde_json::to_string(&request)?; debug!("Sending request to terminal agent {}", terminal_agent.id); let mut agent_headers = request_headers.clone(); agent_headers.remove(hyper::header::CONTENT_LENGTH); + + // Set traceparent header to make the egress span a child of the agent span + if !trace_id.is_empty() && !agent_span_id.is_empty() { + let trace_parent = format!("00-{}-{}-01", trace_id, agent_span_id); + agent_headers.remove(TRACE_PARENT_HEADER); + agent_headers.insert( + TRACE_PARENT_HEADER, + hyper::header::HeaderValue::from_str(&trace_parent).unwrap(), + ); + } + agent_headers.insert( ARCH_UPSTREAM_HOST_HEADER, hyper::header::HeaderValue::from_str(&terminal_agent.id) @@ -169,7 +704,7 @@ impl PipelineProcessor { let response = self .client - .post(&self.url) + .post(format!("{}/v1/chat/completions", self.url)) .headers(agent_headers) .body(request_body) .send() @@ -183,6 +718,7 @@ impl PipelineProcessor { mod tests { use super::*; use hermesllm::apis::openai::{Message, MessageContent, Role}; + use mockito::Server; use std::collections::HashMap; fn create_test_message(role: Role, content: &str) -> Message { @@ -206,23 +742,149 @@ mod tests { #[tokio::test] async fn test_agent_not_found_error() { - let processor = PipelineProcessor::default(); + let mut processor = PipelineProcessor::default(); let agent_map = HashMap::new(); let request_headers = HeaderMap::new(); - let initial_request = ChatCompletionsRequest { - messages: vec![create_test_message(Role::User, "Hello")], - model: "test-model".to_string(), - ..Default::default() - }; + let messages = vec![create_test_message(Role::User, "Hello")]; let pipeline = create_test_pipeline(vec!["nonexistent-agent", "terminal-agent"]); let result = processor - .process_filter_chain(&initial_request, &pipeline, &agent_map, &request_headers) + .process_filter_chain(&messages, &pipeline, &agent_map, &request_headers, None, String::new(), String::new()) .await; assert!(result.is_err()); matches!(result.unwrap_err(), PipelineError::AgentNotFound(_)); } + + #[tokio::test] + async fn test_execute_filter_http_status_error() { + let mut server = Server::new_async().await; + let _m = server + .mock("POST", "/mcp") + .with_status(500) + .with_body("boom") + .create(); + + let server_url = server.url(); + let mut processor = PipelineProcessor::new(server_url.clone()); + processor + .agent_id_session_map + .insert("agent-1".to_string(), "session-1".to_string()); + + let agent = Agent { + id: "agent-1".to_string(), + transport: None, + tool: None, + url: server_url, + agent_type: None, + }; + + let messages = vec![create_test_message(Role::User, "Hello")]; + let request_headers = HeaderMap::new(); + + let result = processor + .execute_filter(&messages, &agent, &request_headers, None, "trace-123".to_string(), "span-123".to_string()) + .await; + + match result { + Err(PipelineError::ServerError { status, body, .. }) => { + assert_eq!(status, 500); + assert_eq!(body, "boom"); + } + _ => panic!("Expected server error for 500 status"), + } + } + + #[tokio::test] + async fn test_execute_filter_http_client_error() { + let mut server = Server::new_async().await; + let _m = server + .mock("POST", "/mcp") + .with_status(400) + .with_body("bad request") + .create(); + + let server_url = server.url(); + let mut processor = PipelineProcessor::new(server_url.clone()); + processor + .agent_id_session_map + .insert("agent-3".to_string(), "session-3".to_string()); + + let agent = Agent { + id: "agent-3".to_string(), + transport: None, + tool: None, + url: server_url, + agent_type: None, + }; + + let messages = vec![create_test_message(Role::User, "Ping")]; + let request_headers = HeaderMap::new(); + + let result = processor + .execute_filter(&messages, &agent, &request_headers, None, "trace-456".to_string(), "span-456".to_string()) + .await; + + match result { + Err(PipelineError::ClientError { status, body, .. }) => { + assert_eq!(status, 400); + assert_eq!(body, "bad request"); + } + _ => panic!("Expected client error for 400 status"), + } + } + + #[tokio::test] + async fn test_execute_filter_mcp_error_flag() { + let rpc_body = serde_json::json!({ + "jsonrpc": JSON_RPC_VERSION, + "id": "1", + "result": { + "isError": true, + "content": [ + { "text": "bad tool call" } + ] + } + }); + + let sse_body = format!("event: message\ndata: {}\n\n", rpc_body.to_string()); + + let mut server = Server::new_async().await; + let _m = server + .mock("POST", "/mcp") + .with_status(200) + .with_body(sse_body) + .create(); + + let server_url = server.url(); + let mut processor = PipelineProcessor::new(server_url.clone()); + processor + .agent_id_session_map + .insert("agent-2".to_string(), "session-2".to_string()); + + let agent = Agent { + id: "agent-2".to_string(), + transport: None, + tool: None, + url: server_url, + agent_type: None, + }; + + let messages = vec![create_test_message(Role::User, "Hi")]; + let request_headers = HeaderMap::new(); + + let result = processor + .execute_filter(&messages, &agent, &request_headers, None, "trace-789".to_string(), "span-789".to_string()) + .await; + + match result { + Err(PipelineError::ClientError { status, body, .. }) => { + assert_eq!(status, 200); + assert_eq!(body, "bad tool call"); + } + _ => panic!("Expected client error when isError flag is set"), + } + } } diff --git a/crates/brightstaff/src/handlers/router_chat.rs b/crates/brightstaff/src/handlers/router_chat.rs new file mode 100644 index 00000000..a927a0eb --- /dev/null +++ b/crates/brightstaff/src/handlers/router_chat.rs @@ -0,0 +1,249 @@ +use common::configuration::ModelUsagePreference; +use common::consts::{REQUEST_ID_HEADER}; +use common::traces::{TraceCollector, SpanKind, SpanBuilder, parse_traceparent}; +use hermesllm::clients::endpoints::SupportedUpstreamAPIs; +use hermesllm::{ProviderRequest, ProviderRequestType}; +use hyper::StatusCode; +use std::collections::HashMap; +use std::sync::Arc; +use tracing::{debug, info, warn}; + +use crate::router::llm_router::RouterService; +use crate::tracing::{OperationNameBuilder, operation_component, http, routing}; + +pub struct RoutingResult { + pub model_name: String +} + +pub struct RoutingError { + pub message: String, + pub status_code: StatusCode, +} + +impl RoutingError { + pub fn internal_error(message: String) -> Self { + Self { + message, + status_code: StatusCode::INTERNAL_SERVER_ERROR + } + } +} + +/// Determines the routing decision if +/// +/// # Returns +/// * `Ok(RoutingResult)` - Contains the selected model name and span ID +/// * `Err(RoutingError)` - Contains error details and optional span ID +pub async fn router_chat_get_upstream_model( + router_service: Arc, + client_request: ProviderRequestType, + request_headers: &hyper::HeaderMap, + trace_collector: Arc, + traceparent: &str, + request_path: &str, +) -> Result { + // Clone metadata for routing before converting (which consumes client_request) + let routing_metadata = client_request.metadata().clone(); + let request_id = request_headers + .get(REQUEST_ID_HEADER) + .and_then(|value| value.to_str().ok()) + .unwrap_or("unknown"); + + // Convert to ChatCompletionsRequest for routing (regardless of input type) + let chat_request = match ProviderRequestType::try_from(( + client_request, + &SupportedUpstreamAPIs::OpenAIChatCompletions( + hermesllm::apis::OpenAIApi::ChatCompletions, + ), + )) { + Ok(ProviderRequestType::ChatCompletionsRequest(req)) => req, + Ok( + ProviderRequestType::MessagesRequest(_) + | ProviderRequestType::BedrockConverse(_) + | ProviderRequestType::BedrockConverseStream(_) + | ProviderRequestType::ResponsesAPIRequest(_), + ) => { + warn!("Unexpected: got non-ChatCompletions request after converting to OpenAI format"); + return Err(RoutingError::internal_error( + "Request conversion failed".to_string(), + )); + } + Err(err) => { + warn!("Failed to convert request to ChatCompletionsRequest: {}", err); + return Err(RoutingError::internal_error(format!( + "Failed to convert request: {}", + err + ))); + } + }; + + debug!( + "[PLANO_REQ_ID: {}]: ROUTER_REQ: {}", + request_id, + &serde_json::to_string(&chat_request).unwrap() + ); + + // Extract trace_parent from headers + let trace_parent = request_headers + .iter() + .find(|(ty, _)| ty.as_str() == "traceparent") + .map(|(_, value)| value.to_str().unwrap_or_default().to_string()); + + // Extract usage preferences from metadata + let usage_preferences_str: Option = routing_metadata.as_ref().and_then(|metadata| { + metadata + .get("archgw_preference_config") + .map(|value| value.to_string()) + }); + + let usage_preferences: Option> = usage_preferences_str + .as_ref() + .and_then(|s| serde_yaml::from_str(s).ok()); + + // Prepare log message with latest message from chat request + let latest_message_for_log = chat_request + .messages + .last() + .map_or("None".to_string(), |msg| { + msg.content.to_string().replace('\n', "\\n") + }); + + const MAX_MESSAGE_LENGTH: usize = 50; + let latest_message_for_log = if latest_message_for_log.chars().count() > MAX_MESSAGE_LENGTH { + let truncated: String = latest_message_for_log + .chars() + .take(MAX_MESSAGE_LENGTH) + .collect(); + format!("{}...", truncated) + } else { + latest_message_for_log + }; + + info!( + "[PLANO_REQ_ID: {}] | ROUTER_REQ | Usage preferences from request: {}, request_path: {}, latest message: {}", + request_id, + usage_preferences.is_some(), + request_path, + latest_message_for_log + ); + + // Capture start time for routing span + let routing_start_time = std::time::Instant::now(); + let routing_start_system_time = std::time::SystemTime::now(); + + // Attempt to determine route using the router service + let routing_result = router_service + .determine_route(&chat_request.messages, trace_parent, usage_preferences) + .await; + + match routing_result { + Ok(route) => match route { + Some((_, model_name)) => { + // Record successful routing span + let mut attrs: HashMap = HashMap::new(); + attrs.insert("route.selected_model".to_string(), model_name.clone()); + record_routing_span( + trace_collector, + traceparent, + routing_start_time, + routing_start_system_time, + attrs, + ) + .await; + + Ok(RoutingResult { + model_name + }) + } + None => { + // No route determined, use default model from request + info!( + "[PLANO_REQ_ID: {}] | ROUTER_REQ | No route determined, using default model from request: {}", + request_id, + chat_request.model + ); + + let default_model = chat_request.model.clone(); + let mut attrs = HashMap::new(); + attrs.insert("route.selected_model".to_string(), default_model.clone()); + record_routing_span( + trace_collector, + traceparent, + routing_start_time, + routing_start_system_time, + attrs, + ) + .await; + + Ok(RoutingResult { + model_name: default_model + }) + } + }, + Err(err) => { + // Record failed routing span + let mut attrs = HashMap::new(); + attrs.insert("route.selected_model".to_string(), "unknown".to_string()); + attrs.insert("error.message".to_string(), err.to_string()); + record_routing_span( + trace_collector, + traceparent, + routing_start_time, + routing_start_system_time, + attrs, + ) + .await; + + Err(RoutingError::internal_error( + format!("Failed to determine route: {}", err) + )) + } + } +} + +/// Helper function to record a routing span with the given attributes. +/// Reduces code duplication across different routing outcomes. +async fn record_routing_span( + trace_collector: Arc, + traceparent: &str, + start_time: std::time::Instant, + start_system_time: std::time::SystemTime, + attrs: HashMap, +) { + // The routing always uses OpenAI Chat Completions format internally, + // so we log that as the actual API being used for routing + let routing_api_path = "/v1/chat/completions"; + + let routing_operation_name = OperationNameBuilder::new() + .with_method("POST") + .with_path(routing_api_path) + .with_target("Arch-Router-1.5B") + .build(); + + let (trace_id, parent_span_id) = parse_traceparent(traceparent); + + // Build the routing span directly using constants + let mut span_builder = SpanBuilder::new(&routing_operation_name) + .with_trace_id(&trace_id) + .with_kind(SpanKind::Client) + .with_start_time(start_system_time) + .with_end_time(std::time::SystemTime::now()) + .with_attribute(http::METHOD, "POST") + .with_attribute(http::TARGET, routing_api_path.to_string()) + .with_attribute(routing::ROUTE_DETERMINATION_MS, start_time.elapsed().as_millis().to_string()); + + // Only set parent span ID if it exists (not a root span) + if let Some(parent) = parent_span_id { + span_builder = span_builder.with_parent_span_id(&parent); + } + + // Add all custom attributes + for (key, value) in attrs { + span_builder = span_builder.with_attribute(key, value); + } + + let span = span_builder.build(); + + // Record the span directly to the collector + trace_collector.record_span(operation_component::ROUTING, span); +} diff --git a/crates/brightstaff/src/handlers/utils.rs b/crates/brightstaff/src/handlers/utils.rs new file mode 100644 index 00000000..6f84c1f3 --- /dev/null +++ b/crates/brightstaff/src/handlers/utils.rs @@ -0,0 +1,259 @@ +use bytes::Bytes; +use common::traces::{Span, Attribute, AttributeValue, TraceCollector, Event}; +use http_body_util::combinators::BoxBody; +use http_body_util::StreamBody; +use hyper::body::Frame; +use std::sync::Arc; +use std::time::{Instant, SystemTime}; +use tokio::sync::mpsc; +use tokio_stream::wrappers::ReceiverStream; +use tokio_stream::StreamExt; +use tracing::warn; + +// Import tracing constants +use crate::tracing::{llm, error}; + +/// Trait for processing streaming chunks +/// Implementors can inject custom logic during streaming (e.g., hallucination detection, logging) +pub trait StreamProcessor: Send + 'static { + /// Process an incoming chunk of bytes + fn process_chunk(&mut self, chunk: Bytes) -> Result, String>; + + /// Called when the first bytes are received (for time-to-first-token tracking) + fn on_first_bytes(&mut self) {} + + /// Called when streaming completes successfully + fn on_complete(&mut self) {} + + /// Called when streaming encounters an error + fn on_error(&mut self, _error: &str) {} +} + +/// A processor that tracks streaming metrics and finalizes the span +pub struct ObservableStreamProcessor { + collector: Arc, + service_name: String, + span: Span, + total_bytes: usize, + chunk_count: usize, + start_time: Instant, + time_to_first_token: Option, +} + +impl ObservableStreamProcessor { + /// Create a new passthrough processor + /// + /// # Arguments + /// * `collector` - The trace collector to record the span to + /// * `service_name` - The service name for this span (e.g., "archgw(llm)") + /// * `span` - The span to finalize after streaming completes + /// * `start_time` - When the request started (for duration calculation) + pub fn new( + collector: Arc, + service_name: impl Into, + span: Span, + start_time: Instant, + ) -> Self { + Self { + collector, + service_name: service_name.into(), + span, + total_bytes: 0, + chunk_count: 0, + start_time, + time_to_first_token: None, + } + } +} + +impl StreamProcessor for ObservableStreamProcessor { + fn process_chunk(&mut self, chunk: Bytes) -> Result, String> { + self.total_bytes += chunk.len(); + self.chunk_count += 1; + Ok(Some(chunk)) + } + + fn on_first_bytes(&mut self) { + // Record time to first token (only for streaming) + if self.time_to_first_token.is_none() { + self.time_to_first_token = Some(self.start_time.elapsed().as_millis()); + } + } + + fn on_complete(&mut self) { + // Update span with streaming metrics and end time + let end_time_nanos = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap_or_default() + .as_nanos(); + + self.span.end_time_unix_nano = format!("{}", end_time_nanos); + + // Add streaming metrics as attributes using constants + self.span.attributes.push(Attribute { + key: llm::RESPONSE_BYTES.to_string(), + value: AttributeValue { + string_value: Some(self.total_bytes.to_string()), + }, + }); + + + self.span.attributes.push(Attribute { + key: llm::DURATION_MS.to_string(), + value: AttributeValue { + string_value: Some(self.start_time.elapsed().as_millis().to_string()), + }, + }); + + // Add time to first token if available (streaming only) + if let Some(ttft) = self.time_to_first_token { + self.span.attributes.push(Attribute { + key: llm::TIME_TO_FIRST_TOKEN_MS.to_string(), + value: AttributeValue { + string_value: Some(ttft.to_string()), + }, + }); + + // Add time to first token as a span event + // Calculate the timestamp by adding ttft duration to span start time + if let Ok(start_time_nanos) = self.span.start_time_unix_nano.parse::() { + // Convert ttft from milliseconds to nanoseconds and add to start time + let event_timestamp = start_time_nanos + (ttft * 1_000_000); + let mut event = Event::new(llm::TIME_TO_FIRST_TOKEN_MS.to_string(), event_timestamp); + event.add_attribute( + llm::TIME_TO_FIRST_TOKEN_MS.to_string(), + ttft.to_string(), + ); + + // Initialize events vector if needed + if self.span.events.is_none() { + self.span.events = Some(Vec::new()); + } + + if let Some(ref mut events) = self.span.events { + events.push(event); + } + } + } + + // Record the finalized span + self.collector.record_span(&self.service_name, self.span.clone()); + } + + fn on_error(&mut self, error_msg: &str) { + warn!("Stream error in PassthroughProcessor: {}", error_msg); + + // Update span with error info and end time + let end_time_nanos = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap_or_default() + .as_nanos(); + + self.span.end_time_unix_nano = format!("{}", end_time_nanos); + + self.span.attributes.push(Attribute { + key: error::ERROR.to_string(), + value: AttributeValue { + string_value: Some("true".to_string()), + }, + }); + + self.span.attributes.push(Attribute { + key: error::MESSAGE.to_string(), + value: AttributeValue { + string_value: Some(error_msg.to_string()), + }, + }); + + self.span.attributes.push(Attribute { + key: llm::DURATION_MS.to_string(), + value: AttributeValue { + string_value: Some(self.start_time.elapsed().as_millis().to_string()), + }, + }); + + // Record the error span + self.collector.record_span(&self.service_name, self.span.clone()); + } +} + +/// Result of creating a streaming response +pub struct StreamingResponse { + pub body: BoxBody, + pub processor_handle: tokio::task::JoinHandle<()>, +} + +pub fn create_streaming_response( + mut byte_stream: S, + mut processor: P, + buffer_size: usize, +) -> StreamingResponse +where + S: StreamExt> + Send + Unpin + 'static, + P: StreamProcessor, +{ + let (tx, rx) = mpsc::channel::(buffer_size); + + // Spawn a task to process and forward chunks + let processor_handle = tokio::spawn(async move { + let mut is_first_chunk = true; + + while let Some(item) = byte_stream.next().await { + let chunk = match item { + Ok(chunk) => chunk, + Err(err) => { + let err_msg = format!("Error receiving chunk: {:?}", err); + warn!("{}", err_msg); + processor.on_error(&err_msg); + break; + } + }; + + // Call on_first_bytes for the first chunk + if is_first_chunk { + processor.on_first_bytes(); + is_first_chunk = false; + } + + // Process the chunk + match processor.process_chunk(chunk) { + Ok(Some(processed_chunk)) => { + if tx.send(processed_chunk).await.is_err() { + warn!("Receiver dropped"); + break; + } + } + Ok(None) => { + // Skip this chunk + continue; + } + Err(err) => { + warn!("Processor error: {}", err); + processor.on_error(&err); + break; + } + } + } + + processor.on_complete(); + }); + + // Convert channel receiver to HTTP stream + let stream = ReceiverStream::new(rx).map(|chunk| Ok::<_, hyper::Error>(Frame::data(chunk))); + let stream_body = BoxBody::new(StreamBody::new(stream)); + + StreamingResponse { + body: stream_body, + processor_handle, + } +} + +/// Truncates a message to the specified maximum length, adding "..." if truncated. +pub fn truncate_message(message: &str, max_length: usize) -> String { + if message.chars().count() > max_length { + let truncated: String = message.chars().take(max_length).collect(); + format!("{}...", truncated) + } else { + message.to_string() + } +} diff --git a/crates/brightstaff/src/lib.rs b/crates/brightstaff/src/lib.rs index a4b5b3ae..36fc902f 100644 --- a/crates/brightstaff/src/lib.rs +++ b/crates/brightstaff/src/lib.rs @@ -1,3 +1,5 @@ pub mod handlers; pub mod router; +pub mod state; +pub mod tracing; pub mod utils; diff --git a/crates/brightstaff/src/main.rs b/crates/brightstaff/src/main.rs index 57dd9fe9..9a2b6706 100644 --- a/crates/brightstaff/src/main.rs +++ b/crates/brightstaff/src/main.rs @@ -1,11 +1,16 @@ use brightstaff::handlers::agent_chat_completions::agent_chat; -use brightstaff::handlers::chat_completions::chat; +use brightstaff::handlers::function_calling::function_calling_chat_handler; +use brightstaff::handlers::llm::llm_chat; use brightstaff::handlers::models::list_models; use brightstaff::router::llm_router::RouterService; +use brightstaff::state::StateStorage; +use brightstaff::state::postgresql::PostgreSQLConversationStorage; +use brightstaff::state::memory::MemoryConversationalStorage; use brightstaff::utils::tracing::init_tracer; use bytes::Bytes; -use common::configuration::Configuration; -use common::consts::{CHAT_COMPLETIONS_PATH, MESSAGES_PATH}; +use common::configuration::{Agent, Configuration}; +use common::consts::{CHAT_COMPLETIONS_PATH, MESSAGES_PATH, OPENAI_RESPONSES_API_PATH}; +use common::traces::TraceCollector; use http_body_util::{combinators::BoxBody, BodyExt, Empty}; use hyper::body::Incoming; use hyper::server::conn::http1; @@ -45,10 +50,6 @@ async fn main() -> Result<(), Box> { let _tracer_provider = init_tracer(); let bind_address = env::var("BIND_ADDRESS").unwrap_or_else(|_| BIND_ADDRESS.to_string()); - info!( - "current working directory: {}", - env::current_dir().unwrap().display() - ); // loading arch_config.yaml file let arch_config_path = env::var("ARCH_CONFIG_PATH_RENDERED") .unwrap_or_else(|_| "./arch_config_rendered.yaml".to_string()); @@ -62,22 +63,23 @@ async fn main() -> Result<(), Box> { let arch_config = Arc::new(config); + // combine agents and filters into a single list of agents + let all_agents: Vec = arch_config + .agents + .as_deref() + .unwrap_or_default() + .iter() + .chain(arch_config.filters.as_deref().unwrap_or_default()) + .cloned() + .collect(); + let llm_providers = Arc::new(RwLock::new(arch_config.model_providers.clone())); - let agents_list = Arc::new(RwLock::new(arch_config.agents.clone())); + let combined_agents_filters_list = Arc::new(RwLock::new(Some(all_agents))); let listeners = Arc::new(RwLock::new(arch_config.listeners.clone())); - - debug!( - "arch_config: {:?}", - &serde_json::to_string(arch_config.as_ref()).unwrap() - ); - let llm_provider_url = env::var("LLM_PROVIDER_ENDPOINT").unwrap_or_else(|_| "http://localhost:12001".to_string()); - info!("llm provider url: {}", llm_provider_url); - info!("listening on http://{}", bind_address); let listener = TcpListener::bind(bind_address).await?; - let routing_model_name: String = arch_config .routing .as_ref() @@ -99,18 +101,69 @@ async fn main() -> Result<(), Box> { let model_aliases = Arc::new(arch_config.model_aliases.clone()); + // Initialize trace collector and start background flusher + // Tracing is enabled if the tracing config is present in arch_config.yaml + // Pass Some(true/false) to override, or None to use env var OTEL_TRACING_ENABLED + let tracing_enabled = if arch_config.tracing.is_some() { + info!("Tracing configuration found in arch_config.yaml"); + Some(true) + } else { + info!( + "No tracing configuration in arch_config.yaml, will check OTEL_TRACING_ENABLED env var" + ); + None + }; + let trace_collector = Arc::new(TraceCollector::new(tracing_enabled)); + let _flusher_handle = trace_collector.clone().start_background_flusher(); + + // Initialize conversation state storage for v1/responses + // Configurable via arch_config.yaml state_storage section + // If not configured, state management is disabled + // Environment variables are substituted by envsubst before config is read + let state_storage: Option> = if let Some(storage_config) = &arch_config.state_storage { + let storage: Arc = match storage_config.storage_type { + common::configuration::StateStorageType::Memory => { + info!("Initialized conversation state storage: Memory"); + Arc::new(MemoryConversationalStorage::new()) + } + common::configuration::StateStorageType::Postgres => { + let connection_string = storage_config + .connection_string + .as_ref() + .expect("connection_string is required for postgres state_storage"); + + debug!("Postgres connection string (full): {}", connection_string); + info!("Initializing conversation state storage: Postgres"); + Arc::new( + PostgreSQLConversationStorage::new(connection_string.clone()) + .await + .expect("Failed to initialize Postgres state storage"), + ) + } + }; + Some(storage) + } else { + info!("No state_storage configured - conversation state management disabled"); + None + }; + + loop { let (stream, _) = listener.accept().await?; let peer_addr = stream.peer_addr()?; let io = TokioIo::new(stream); let router_service: Arc = Arc::clone(&router_service); - let model_aliases = Arc::clone(&model_aliases); + let model_aliases: Arc< + Option>, + > = Arc::clone(&model_aliases); let llm_provider_url = llm_provider_url.clone(); let llm_providers = llm_providers.clone(); - let agents_list = agents_list.clone(); + let agents_list = combined_agents_filters_list.clone(); let listeners = listeners.clone(); + let trace_collector = trace_collector.clone(); + let state_storage = state_storage.clone(); let service = service_fn(move |req| { let router_service = Arc::clone(&router_service); let parent_cx = extract_context_from_request(&req); @@ -119,28 +172,46 @@ async fn main() -> Result<(), Box> { let model_aliases = Arc::clone(&model_aliases); let agents_list = agents_list.clone(); let listeners = listeners.clone(); + let trace_collector = trace_collector.clone(); + let state_storage = state_storage.clone(); async move { - match (req.method(), req.uri().path()) { - (&Method::POST, CHAT_COMPLETIONS_PATH | MESSAGES_PATH) => { - let fully_qualified_url = - format!("{}{}", llm_provider_url, req.uri().path()); - chat(req, router_service, fully_qualified_url, model_aliases) - .with_context(parent_cx) - .await - } - (&Method::POST, "/agents/v1/chat/completions") => { - let fully_qualified_url = - format!("{}{}", llm_provider_url, req.uri().path()); - agent_chat( + let path = req.uri().path(); + // Check if path starts with /agents + if path.starts_with("/agents") { + // Check if it matches one of the agent API paths + let stripped_path = path.strip_prefix("/agents").unwrap(); + if matches!( + stripped_path, + CHAT_COMPLETIONS_PATH | MESSAGES_PATH | OPENAI_RESPONSES_API_PATH + ) { + let fully_qualified_url = format!("{}{}", llm_provider_url, stripped_path); + return agent_chat( req, router_service, fully_qualified_url, agents_list, listeners, + trace_collector, ) .with_context(parent_cx) - .await + .await; + } + } + match (req.method(), path) { + (&Method::POST, CHAT_COMPLETIONS_PATH | MESSAGES_PATH | OPENAI_RESPONSES_API_PATH) => { + let fully_qualified_url = + format!("{}{}", llm_provider_url, path); + llm_chat(req, router_service, fully_qualified_url, model_aliases, llm_providers, trace_collector, state_storage) + .with_context(parent_cx) + .await + } + (&Method::POST, "/function_calling") => { + let fully_qualified_url = + format!("{}{}", llm_provider_url, "/v1/chat/completions"); + function_calling_chat_handler(req, fully_qualified_url) + .with_context(parent_cx) + .await } (&Method::GET, "/v1/models" | "/agents/v1/models") => { Ok(list_models(llm_providers).await) diff --git a/crates/brightstaff/src/router/mod.rs b/crates/brightstaff/src/router/mod.rs index e35ea731..b1299477 100644 --- a/crates/brightstaff/src/router/mod.rs +++ b/crates/brightstaff/src/router/mod.rs @@ -1,3 +1,5 @@ pub mod llm_router; +pub mod orchestrator_model; +pub mod orchestrator_model_v1; pub mod router_model; pub mod router_model_v1; diff --git a/crates/brightstaff/src/router/orchestrator_model.rs b/crates/brightstaff/src/router/orchestrator_model.rs new file mode 100644 index 00000000..19c78ca3 --- /dev/null +++ b/crates/brightstaff/src/router/orchestrator_model.rs @@ -0,0 +1,30 @@ +use common::configuration::AgentUsagePreference; +use hermesllm::apis::openai::{ChatCompletionsRequest, Message}; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum OrchestratorModelError { + #[error("Failed to parse JSON: {0}")] + JsonError(#[from] serde_json::Error), +} + +pub type Result = std::result::Result; + +/// OrchestratorModel trait for handling orchestration requests. +/// Unlike RouterModel which returns a single route, OrchestratorModel +/// can return multiple routes as the model output format is: +/// {"route": ["route_name_1", "route_name_2", ...]} +pub trait OrchestratorModel: Send + Sync { + fn generate_request( + &self, + messages: &[Message], + usage_preferences: &Option>, + ) -> ChatCompletionsRequest; + /// Returns a vector of (route_name, model_name) tuples for all matched routes. + fn parse_response( + &self, + content: &str, + usage_preferences: &Option>, + ) -> Result>>; + fn get_model_name(&self) -> String; +} diff --git a/crates/brightstaff/src/router/orchestrator_model_v1.rs b/crates/brightstaff/src/router/orchestrator_model_v1.rs new file mode 100644 index 00000000..8f02c078 --- /dev/null +++ b/crates/brightstaff/src/router/orchestrator_model_v1.rs @@ -0,0 +1,1097 @@ +use std::collections::HashMap; + +use common::configuration::{AgentUsagePreference, OrchestrationPreference}; +use hermesllm::apis::openai::{ChatCompletionsRequest, Message, MessageContent, Role}; +use serde::{Deserialize, Serialize, ser::Serialize as SerializeTrait}; +use tracing::{debug, warn}; + +use super::orchestrator_model::{OrchestratorModel, OrchestratorModelError}; + +/// Custom JSON formatter that produces spaced JSON (space after colons and commas), same as JSON in python +struct SpacedJsonFormatter; + +impl serde_json::ser::Formatter for SpacedJsonFormatter { + fn begin_array(&mut self, writer: &mut W) -> std::io::Result<()> + where + W: ?Sized + std::io::Write, + { + writer.write_all(b"[") + } + + fn end_array(&mut self, writer: &mut W) -> std::io::Result<()> + where + W: ?Sized + std::io::Write, + { + writer.write_all(b"]") + } + + fn begin_array_value(&mut self, writer: &mut W, first: bool) -> std::io::Result<()> + where + W: ?Sized + std::io::Write, + { + if first { + Ok(()) + } else { + writer.write_all(b", ") + } + } + + fn end_array_value(&mut self, _writer: &mut W) -> std::io::Result<()> + where + W: ?Sized + std::io::Write, + { + Ok(()) + } + + fn begin_object(&mut self, writer: &mut W) -> std::io::Result<()> + where + W: ?Sized + std::io::Write, + { + writer.write_all(b"{") + } + + fn end_object(&mut self, writer: &mut W) -> std::io::Result<()> + where + W: ?Sized + std::io::Write, + { + writer.write_all(b"}") + } + + fn begin_object_key(&mut self, writer: &mut W, first: bool) -> std::io::Result<()> + where + W: ?Sized + std::io::Write, + { + if first { + Ok(()) + } else { + writer.write_all(b", ") + } + } + + fn end_object_key(&mut self, _writer: &mut W) -> std::io::Result<()> + where + W: ?Sized + std::io::Write, + { + Ok(()) + } + + fn begin_object_value(&mut self, writer: &mut W) -> std::io::Result<()> + where + W: ?Sized + std::io::Write, + { + writer.write_all(b": ") + } + + fn end_object_value(&mut self, _writer: &mut W) -> std::io::Result<()> + where + W: ?Sized + std::io::Write, + { + Ok(()) + } +} + +/// Serialize a value to JSON with standard spacing (space after colons and commas) +/// e.g. {"name": "foo", "key": "value"} instead of {"name":"foo","key":"value"} +fn to_spaced_json(value: &T) -> String { + let mut buf = Vec::new(); + let mut serializer = serde_json::Serializer::with_formatter(&mut buf, SpacedJsonFormatter); + value.serialize(&mut serializer).unwrap(); + String::from_utf8(buf).unwrap_or_default() +} + +pub const ARCH_ORCHESTRATOR_V1_SYSTEM_PROMPT: &str = r#" +You are a helpful assistant that selects the most suitable routes based on user intent. +You are provided with a list of available routes enclosed within XML tags: + +{routes} + + +You are also given the conversation context enclosed within XML tags: + +{conversation} + + +## Instructions +1. Analyze the latest user intent from the conversation. +2. Compare it against the available routes to find which routes can help fulfill the request. +3. Respond only with the exact route names from . +4. If no routes can help or the intent is already fulfilled, return an empty list. + +## Response Format +Return your answer strictly in JSON as follows: +{{"route": ["route_name_1", "route_name_2", "..."]}} +If no routes are needed, return an empty list for `route`. +"#; + +pub type Result = std::result::Result; +pub struct OrchestratorModelV1 { + agent_orchestration_json_str: String, + agent_orchestration_to_model_map: HashMap, + orchestration_model: String, + max_token_length: usize, +} + +impl OrchestratorModelV1 { + pub fn new( + agent_orchestrations: HashMap>, + orchestration_model: String, + max_token_length: usize, + ) -> Self { + let agent_orchestration_values: Vec = + agent_orchestrations.values().flatten().cloned().collect(); + // Format routes: each route as JSON on its own line with standard spacing + let agent_orchestration_json_str = agent_orchestration_values + .iter() + .map(|pref| to_spaced_json(pref)) + .collect::>() + .join("\n"); + let agent_orchestration_to_model_map: HashMap = agent_orchestrations + .iter() + .flat_map(|(model, prefs)| prefs.iter().map(|pref| (pref.name.clone(), model.clone()))) + .collect(); + + OrchestratorModelV1 { + orchestration_model, + max_token_length, + agent_orchestration_json_str, + agent_orchestration_to_model_map, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct AgentOrchestratorResponse { + /// The route field now expects an array of route names: ["route_name_1", "route_name_2", ...] + pub route: Option>, +} + +const TOKEN_LENGTH_DIVISOR: usize = 4; // Approximate token length divisor for UTF-8 characters + +impl OrchestratorModel for OrchestratorModelV1 { + fn generate_request( + &self, + messages: &[Message], + usage_preferences_from_request: &Option>, + ) -> ChatCompletionsRequest { + // remove system prompt, tool calls, tool call response and messages without content + // if content is empty its likely a tool call + // when role == tool its tool call response + let messages_vec = messages + .iter() + .filter(|m| { + m.role != Role::System && m.role != Role::Tool && !m.content.to_string().is_empty() + }) + .collect::>(); + + // Following code is to ensure that the conversation does not exceed max token length + // Note: we use a simple heuristic to estimate token count based on character length to optimize for performance + let mut token_count = ARCH_ORCHESTRATOR_V1_SYSTEM_PROMPT.len() / TOKEN_LENGTH_DIVISOR; + let mut selected_messages_list_reversed: Vec<&Message> = vec![]; + for (selected_messsage_count, message) in messages_vec.iter().rev().enumerate() { + let message_token_count = message.content.to_string().len() / TOKEN_LENGTH_DIVISOR; + token_count += message_token_count; + if token_count > self.max_token_length { + debug!( + "OrchestratorModelV1: token count {} exceeds max token length {}, truncating conversation, selected message count {}, total message count: {}", + token_count, + self.max_token_length + , selected_messsage_count, + messages_vec.len() + ); + if message.role == Role::User { + // If message that exceeds max token length is from user, we need to keep it + selected_messages_list_reversed.push(message); + } + break; + } + // If we are here, it means that the message is within the max token length + selected_messages_list_reversed.push(message); + } + + if selected_messages_list_reversed.is_empty() { + debug!( + "OrchestratorModelV1: no messages selected, using the last message in the conversation" + ); + if let Some(last_message) = messages_vec.last() { + selected_messages_list_reversed.push(last_message); + } + } + + // ensure that first and last selected message is from user + // Note: selected_messages_list_reversed is in reverse order, so: + // - first() is the last message in the original conversation + // - last() is the first message in the original conversation + if let Some(first_message) = selected_messages_list_reversed.first() { + if first_message.role != Role::User { + warn!("OrchestratorModelV1: last message in the conversation is not from user, this may lead to incorrect orchestration"); + } + } + if let Some(last_message) = selected_messages_list_reversed.last() { + if last_message.role != Role::User { + warn!("OrchestratorModelV1: first message in the selected conversation is not from user, this may lead to incorrect orchestration"); + } + } + + // Reverse the selected messages to maintain the conversation order + let selected_conversation_list = selected_messages_list_reversed + .iter() + .rev() + .map(|message| { + Message { + role: message.role.clone(), + content: MessageContent::Text(message.content.to_string()), + name: None, + tool_calls: None, + tool_call_id: None, + } + }) + .collect::>(); + + // Generate the orchestrator request message based on the usage preferences. + // If preferences are passed in request then we use them; + // Otherwise, we use the default orchestration modelpreferences. + let orchestrator_message = match convert_to_orchestrator_preferences(usage_preferences_from_request) { + Some(prefs) => generate_orchestrator_message(&prefs, &selected_conversation_list), + None => generate_orchestrator_message(&self.agent_orchestration_json_str, &selected_conversation_list), + }; + + ChatCompletionsRequest { + model: self.orchestration_model.clone(), + messages: vec![Message { + content: MessageContent::Text(orchestrator_message), + role: Role::User, + name: None, + tool_calls: None, + tool_call_id: None, + }], + temperature: Some(0.01), + ..Default::default() + } + } + + fn parse_response( + &self, + content: &str, + usage_preferences: &Option>, + ) -> Result>> { + if content.is_empty() { + return Ok(None); + } + let orchestrator_resp_fixed = fix_json_response(content); + let orchestrator_response: AgentOrchestratorResponse = serde_json::from_str(orchestrator_resp_fixed.as_str())?; + + let selected_routes = orchestrator_response.route.unwrap_or_default(); + + // Filter out empty routes + let valid_routes: Vec = selected_routes + .into_iter() + .filter(|route| !route.is_empty()) + .collect(); + + if valid_routes.is_empty() { + return Ok(None); + } + + let mut result: Vec<(String, String)> = Vec::new(); + + if let Some(usage_preferences) = usage_preferences { + // If usage preferences are defined, we need to find the model that matches each selected route + for selected_route in valid_routes { + let model_name: Option = usage_preferences + .iter() + .find(|pref| { + pref.orchestration_preferences + .iter() + .any(|orchestration_pref| orchestration_pref.name == selected_route) + }) + .map(|pref| pref.model.clone()); + + if let Some(model_name) = model_name { + result.push((selected_route, model_name)); + } else { + warn!( + "No matching model found for route: {}, usage preferences: {:?}", + selected_route, usage_preferences + ); + } + } + } else { + // If no usage preferences are passed in request then use the default orchestration model preferences + for selected_route in valid_routes { + if let Some(model) = self.agent_orchestration_to_model_map.get(&selected_route).cloned() { + result.push((selected_route, model)); + } else { + warn!( + "No model found for route: {}, orchestrator model preferences: {:?}", + selected_route, self.agent_orchestration_to_model_map + ); + } + } + } + + if result.is_empty() { + return Ok(None); + } + + Ok(Some(result)) + } + + fn get_model_name(&self) -> String { + self.orchestration_model.clone() + } +} + +fn generate_orchestrator_message(prefs: &str, selected_conversation_list: &Vec) -> String { + // Format conversation with 4-space indentation (equivalent to Python's json.dumps(obj, indent=4)) + let formatter = serde_json::ser::PrettyFormatter::with_indent(b" "); + let mut conversation_buf = Vec::new(); + let mut serializer = serde_json::Serializer::with_formatter(&mut conversation_buf, formatter); + SerializeTrait::serialize(&selected_conversation_list, &mut serializer).unwrap(); + let conversation_json = String::from_utf8(conversation_buf).unwrap_or_default(); + + ARCH_ORCHESTRATOR_V1_SYSTEM_PROMPT + .replace("{routes}", prefs) + .replace("{conversation}", &conversation_json) +} + +fn convert_to_orchestrator_preferences( + prefs_from_request: &Option>, +) -> Option { + if let Some(usage_preferences) = prefs_from_request { + let orchestration_preferences: Vec = usage_preferences + .iter() + .flat_map(|pref| { + pref.orchestration_preferences + .iter() + .map(|orchestration_pref| OrchestrationPreference { + name: orchestration_pref.name.clone(), + description: orchestration_pref.description.clone(), + }) + }) + .collect(); + + // Format routes: each route as JSON on its own line with standard spacing + let routes_str = orchestration_preferences + .iter() + .map(|pref| to_spaced_json(pref)) + .collect::>() + .join("\n"); + + return Some(routes_str); + } + + None +} + +fn fix_json_response(body: &str) -> String { + body.replace("'", "\"").replace("\\n", "") +} + +impl std::fmt::Debug for dyn OrchestratorModel { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "OrchestratorModel") + } +} + +#[cfg(test)] +mod tests { + use super::*; + use pretty_assertions::assert_eq; + + #[test] + fn test_spaced_json_formatter() { + // Test basic object + let obj = serde_json::json!({"name": "foo", "value": 123}); + let result = to_spaced_json(&obj); + assert_eq!(result, r#"{"name": "foo", "value": 123}"#); + + // Test nested object + let nested = serde_json::json!({"outer": {"inner": "value"}}); + let result = to_spaced_json(&nested); + assert_eq!(result, r#"{"outer": {"inner": "value"}}"#); + + // Test array + let arr = serde_json::json!(["a", "b", "c"]); + let result = to_spaced_json(&arr); + assert_eq!(result, r#"["a", "b", "c"]"#); + + // Test object with array + let obj_arr = serde_json::json!({"items": [1, 2, 3]}); + let result = to_spaced_json(&obj_arr); + assert_eq!(result, r#"{"items": [1, 2, 3]}"#); + + // CRITICAL: Test that colons inside string values are NOT modified + let with_colon = serde_json::json!({"name": "foo:bar", "url": "http://example.com"}); + let result = to_spaced_json(&with_colon); + assert_eq!(result, r#"{"name": "foo:bar", "url": "http://example.com"}"#); + + // Test empty object and array + let empty_obj = serde_json::json!({}); + let result = to_spaced_json(&empty_obj); + assert_eq!(result, r#"{}"#); + + let empty_arr = serde_json::json!([]); + let result = to_spaced_json(&empty_arr); + assert_eq!(result, r#"[]"#); + + // Test complex nested structure with special characters in values + // Note: serde_json doesn't guarantee field order, so we verify the formatting is correct + // by checking key properties of the output + let complex = serde_json::json!({ + "type": "object", + "properties": {}, + "urls": ["https://api.example.com:8080/path", "file:///local/path"] + }); + let result = to_spaced_json(&complex); + // Verify URLs with colons are preserved correctly + assert!(result.contains(r#""urls": ["https://api.example.com:8080/path", "file:///local/path"]"#)); + // Verify spacing format + assert!(result.contains(r#""type": "object""#)); + assert!(result.contains(r#""properties": {}"#)); + } + + #[test] + fn test_system_prompt_format() { + let expected_prompt = r#" +You are a helpful assistant that selects the most suitable routes based on user intent. +You are provided with a list of available routes enclosed within XML tags: + +{"name": "Image generation", "description": "generating image", "parameters": {"type": "object", "properties": {}, "required": []}} + + +You are also given the conversation context enclosed within XML tags: + +[ + { + "role": "user", + "content": "hi" + }, + { + "role": "assistant", + "content": "Hello! How can I assist you today?" + }, + { + "role": "user", + "content": "given the image In style of Andy Warhol, portrait of Bart and Lisa Simpson" + } +] + + +## Instructions +1. Analyze the latest user intent from the conversation. +2. Compare it against the available routes to find which routes can help fulfill the request. +3. Respond only with the exact route names from . +4. If no routes can help or the intent is already fulfilled, return an empty list. + +## Response Format +Return your answer strictly in JSON as follows: +{{"route": ["route_name_1", "route_name_2", "..."]}} +If no routes are needed, return an empty list for `route`. +"#; + let orchestrations_str = r#" + { + "gpt-4o": [ + {"name": "Image generation", "description": "generating image"} + ] + } + "#; + let agent_orchestrations = + serde_json::from_str::>>(orchestrations_str).unwrap(); + let orchestration_model = "test-model".to_string(); + let orchestrator = OrchestratorModelV1::new(agent_orchestrations, orchestration_model.clone(), usize::MAX); + + let conversation_str = r#" + [ + { + "role": "user", + "content": "hi" + }, + { + "role": "assistant", + "content": "Hello! How can I assist you today?" + }, + { + "role": "user", + "content": "given the image In style of Andy Warhol, portrait of Bart and Lisa Simpson" + } + ] + "#; + let conversation: Vec = serde_json::from_str(conversation_str).unwrap(); + + let req = orchestrator.generate_request(&conversation, &None); + + let prompt = req.messages[0].content.to_string(); + + assert_eq!(expected_prompt, prompt); + } + + #[test] + fn test_system_prompt_format_usage_preferences() { + let expected_prompt = r#" +You are a helpful assistant that selects the most suitable routes based on user intent. +You are provided with a list of available routes enclosed within XML tags: + +{"name": "code-generation", "description": "generating new code snippets, functions, or boilerplate based on user prompts or requirements", "parameters": {"type": "object", "properties": {}, "required": []}} + + +You are also given the conversation context enclosed within XML tags: + +[ + { + "role": "user", + "content": "hi" + }, + { + "role": "assistant", + "content": "Hello! How can I assist you today?" + }, + { + "role": "user", + "content": "given the image In style of Andy Warhol, portrait of Bart and Lisa Simpson" + } +] + + +## Instructions +1. Analyze the latest user intent from the conversation. +2. Compare it against the available routes to find which routes can help fulfill the request. +3. Respond only with the exact route names from . +4. If no routes can help or the intent is already fulfilled, return an empty list. + +## Response Format +Return your answer strictly in JSON as follows: +{{"route": ["route_name_1", "route_name_2", "..."]}} +If no routes are needed, return an empty list for `route`. +"#; + // Empty orchestrations map - not used when usage_preferences are provided + let agent_orchestrations: HashMap> = HashMap::new(); + let orchestration_model = "test-model".to_string(); + let orchestrator = OrchestratorModelV1::new(agent_orchestrations, orchestration_model.clone(), usize::MAX); + + let conversation_str = r#" + [ + { + "role": "user", + "content": "hi" + }, + { + "role": "assistant", + "content": "Hello! How can I assist you today?" + }, + { + "role": "user", + "content": "given the image In style of Andy Warhol, portrait of Bart and Lisa Simpson" + } + ] + "#; + let conversation: Vec = serde_json::from_str(conversation_str).unwrap(); + + let usage_preferences = Some(vec![AgentUsagePreference { + model: "claude/claude-3-7-sonnet".to_string(), + orchestration_preferences: vec![OrchestrationPreference { + name: "code-generation".to_string(), + description: "generating new code snippets, functions, or boilerplate based on user prompts or requirements".to_string(), + }], + }]); + let req = orchestrator.generate_request(&conversation, &usage_preferences); + + let prompt = req.messages[0].content.to_string(); + + assert_eq!(expected_prompt, prompt); + } + + #[test] + fn test_conversation_exceed_token_count() { + let expected_prompt = r#" +You are a helpful assistant that selects the most suitable routes based on user intent. +You are provided with a list of available routes enclosed within XML tags: + +{"name": "Image generation", "description": "generating image", "parameters": {"type": "object", "properties": {}, "required": []}} + + +You are also given the conversation context enclosed within XML tags: + +[ + { + "role": "user", + "content": "given the image In style of Andy Warhol, portrait of Bart and Lisa Simpson" + } +] + + +## Instructions +1. Analyze the latest user intent from the conversation. +2. Compare it against the available routes to find which routes can help fulfill the request. +3. Respond only with the exact route names from . +4. If no routes can help or the intent is already fulfilled, return an empty list. + +## Response Format +Return your answer strictly in JSON as follows: +{{"route": ["route_name_1", "route_name_2", "..."]}} +If no routes are needed, return an empty list for `route`. +"#; + + let orchestrations_str = r#" + { + "gpt-4o": [ + {"name": "Image generation", "description": "generating image"} + ] + } + "#; + let agent_orchestrations = + serde_json::from_str::>>(orchestrations_str).unwrap(); + let orchestration_model = "test-model".to_string(); + let orchestrator = OrchestratorModelV1::new(agent_orchestrations, orchestration_model.clone(), 235); + + let conversation_str = r#" + [ + { + "role": "user", + "content": "hi" + }, + { + "role": "assistant", + "content": "Hello! How can I assist you today?" + }, + { + "role": "user", + "content": "given the image In style of Andy Warhol, portrait of Bart and Lisa Simpson" + } + ] + "#; + + let conversation: Vec = serde_json::from_str(conversation_str).unwrap(); + + let req = orchestrator.generate_request(&conversation, &None); + + let prompt = req.messages[0].content.to_string(); + + assert_eq!(expected_prompt, prompt); + } + + #[test] + fn test_conversation_exceed_token_count_large_single_message() { + let expected_prompt = r#" +You are a helpful assistant that selects the most suitable routes based on user intent. +You are provided with a list of available routes enclosed within XML tags: + +{"name": "Image generation", "description": "generating image", "parameters": {"type": "object", "properties": {}, "required": []}} + + +You are also given the conversation context enclosed within XML tags: + +[ + { + "role": "user", + "content": "given the image In style of Andy Warhol, portrait of Bart and Lisa Simpson and this is a very long message that exceeds the max token length of the routing model, so it should be truncated and only the last user message should be included in the conversation for routing." + } +] + + +## Instructions +1. Analyze the latest user intent from the conversation. +2. Compare it against the available routes to find which routes can help fulfill the request. +3. Respond only with the exact route names from . +4. If no routes can help or the intent is already fulfilled, return an empty list. + +## Response Format +Return your answer strictly in JSON as follows: +{{"route": ["route_name_1", "route_name_2", "..."]}} +If no routes are needed, return an empty list for `route`. +"#; + + let orchestrations_str = r#" + { + "gpt-4o": [ + {"name": "Image generation", "description": "generating image"} + ] + } + "#; + let agent_orchestrations = + serde_json::from_str::>>(orchestrations_str).unwrap(); + + let orchestration_model = "test-model".to_string(); + let orchestrator = OrchestratorModelV1::new(agent_orchestrations, orchestration_model.clone(), 200); + + let conversation_str = r#" + [ + { + "role": "user", + "content": "hi" + }, + { + "role": "assistant", + "content": "Hello! How can I assist you today?" + }, + { + "role": "user", + "content": "given the image In style of Andy Warhol, portrait of Bart and Lisa Simpson and this is a very long message that exceeds the max token length of the routing model, so it should be truncated and only the last user message should be included in the conversation for routing." + } + ] + "#; + + let conversation: Vec = serde_json::from_str(conversation_str).unwrap(); + + let req = orchestrator.generate_request(&conversation, &None); + + let prompt = req.messages[0].content.to_string(); + + assert_eq!(expected_prompt, prompt); + } + + #[test] + fn test_conversation_trim_upto_user_message() { + let expected_prompt = r#" +You are a helpful assistant that selects the most suitable routes based on user intent. +You are provided with a list of available routes enclosed within XML tags: + +{"name": "Image generation", "description": "generating image", "parameters": {"type": "object", "properties": {}, "required": []}} + + +You are also given the conversation context enclosed within XML tags: + +[ + { + "role": "user", + "content": "given the image In style of Andy Warhol" + }, + { + "role": "assistant", + "content": "ok here is the image" + }, + { + "role": "user", + "content": "pls give me another image about Bart and Lisa" + } +] + + +## Instructions +1. Analyze the latest user intent from the conversation. +2. Compare it against the available routes to find which routes can help fulfill the request. +3. Respond only with the exact route names from . +4. If no routes can help or the intent is already fulfilled, return an empty list. + +## Response Format +Return your answer strictly in JSON as follows: +{{"route": ["route_name_1", "route_name_2", "..."]}} +If no routes are needed, return an empty list for `route`. +"#; + + let orchestrations_str = r#" + { + "gpt-4o": [ + {"name": "Image generation", "description": "generating image"} + ] + } + "#; + let agent_orchestrations = + serde_json::from_str::>>(orchestrations_str).unwrap(); + let orchestration_model = "test-model".to_string(); + let orchestrator = OrchestratorModelV1::new(agent_orchestrations, orchestration_model.clone(), 230); + + let conversation_str = r#" + [ + { + "role": "user", + "content": "hi" + }, + { + "role": "assistant", + "content": "Hello! How can I assist you today?" + }, + { + "role": "user", + "content": "given the image In style of Andy Warhol" + }, + { + "role": "assistant", + "content": "ok here is the image" + }, + { + "role": "user", + "content": "pls give me another image about Bart and Lisa" + } + ] + "#; + + let conversation: Vec = serde_json::from_str(conversation_str).unwrap(); + + let req = orchestrator.generate_request(&conversation, &None); + + let prompt = req.messages[0].content.to_string(); + + assert_eq!(expected_prompt, prompt); + } + + #[test] + fn test_non_text_input() { + let expected_prompt = r#" +You are a helpful assistant that selects the most suitable routes based on user intent. +You are provided with a list of available routes enclosed within XML tags: + +{"name": "Image generation", "description": "generating image", "parameters": {"type": "object", "properties": {}, "required": []}} + + +You are also given the conversation context enclosed within XML tags: + +[ + { + "role": "user", + "content": "hi" + }, + { + "role": "assistant", + "content": "Hello! How can I assist you today?" + }, + { + "role": "user", + "content": "given the image In style of Andy Warhol, portrait of Bart and Lisa Simpson" + } +] + + +## Instructions +1. Analyze the latest user intent from the conversation. +2. Compare it against the available routes to find which routes can help fulfill the request. +3. Respond only with the exact route names from . +4. If no routes can help or the intent is already fulfilled, return an empty list. + +## Response Format +Return your answer strictly in JSON as follows: +{{"route": ["route_name_1", "route_name_2", "..."]}} +If no routes are needed, return an empty list for `route`. +"#; + let orchestrations_str = r#" + { + "gpt-4o": [ + {"name": "Image generation", "description": "generating image"} + ] + } + "#; + let agent_orchestrations = + serde_json::from_str::>>(orchestrations_str).unwrap(); + let orchestration_model = "test-model".to_string(); + let orchestrator = OrchestratorModelV1::new(agent_orchestrations, orchestration_model.clone(), usize::MAX); + + let conversation_str = r#" + [ + { + "role": "user", + "content": [ + { + "type": "text", + "text": "hi" + }, + { + "type": "image_url", + "image_url": { + "url": "https://example.com/image.png" + } + } + ] + }, + { + "role": "assistant", + "content": "Hello! How can I assist you today?" + }, + { + "role": "user", + "content": "given the image In style of Andy Warhol, portrait of Bart and Lisa Simpson" + } + ] + "#; + let conversation: Vec = serde_json::from_str(conversation_str).unwrap(); + + let req = orchestrator.generate_request(&conversation, &None); + + let prompt = req.messages[0].content.to_string(); + + assert_eq!(expected_prompt, prompt); + } + + #[test] + fn test_skip_tool_call() { + let expected_prompt = r#" +You are a helpful assistant that selects the most suitable routes based on user intent. +You are provided with a list of available routes enclosed within XML tags: + +{"name": "Image generation", "description": "generating image", "parameters": {"type": "object", "properties": {}, "required": []}} + + +You are also given the conversation context enclosed within XML tags: + +[ + { + "role": "user", + "content": "What's the weather like in Tokyo?" + }, + { + "role": "assistant", + "content": "The current weather in Tokyo is 22°C and sunny." + }, + { + "role": "user", + "content": "What about in New York?" + } +] + + +## Instructions +1. Analyze the latest user intent from the conversation. +2. Compare it against the available routes to find which routes can help fulfill the request. +3. Respond only with the exact route names from . +4. If no routes can help or the intent is already fulfilled, return an empty list. + +## Response Format +Return your answer strictly in JSON as follows: +{{"route": ["route_name_1", "route_name_2", "..."]}} +If no routes are needed, return an empty list for `route`. +"#; + let orchestrations_str = r#" + { + "gpt-4o": [ + {"name": "Image generation", "description": "generating image"} + ] + } + "#; + let agent_orchestrations = + serde_json::from_str::>>(orchestrations_str).unwrap(); + let orchestration_model = "test-model".to_string(); + let orchestrator = OrchestratorModelV1::new(agent_orchestrations, orchestration_model.clone(), usize::MAX); + + let conversation_str = r#" + [ + { + "role": "user", + "content": "What's the weather like in Tokyo?" + }, + { + "role": "assistant", + "content": "", + "tool_calls": [ + { + "id": "toolcall-abc123", + "type": "function", + "function": { + "name": "get_weather", + "arguments": "{ \"location\": \"Tokyo\" }" + } + } + ] + }, + { + "role": "tool", + "tool_call_id": "toolcall-abc123", + "content": "{ \"temperature\": \"22°C\", \"condition\": \"Sunny\" }" + }, + { + "role": "assistant", + "content": "The current weather in Tokyo is 22°C and sunny." + }, + { + "role": "user", + "content": "What about in New York?" + } + ] + "#; + + // expects conversation to look like this + + // [ + // { + // "role": "user", + // "content": "What's the weather like in Tokyo?" + // }, + // { + // "role": "assistant", + // "content": "The current weather in Tokyo is 22°C and sunny." + // }, + // { + // "role": "user", + // "content": "What about in New York?" + // } + // ] + + let conversation: Vec = serde_json::from_str(conversation_str).unwrap(); + + let req: ChatCompletionsRequest = orchestrator.generate_request(&conversation, &None); + + let prompt = req.messages[0].content.to_string(); + + assert_eq!(expected_prompt, prompt); + } + + #[test] + fn test_parse_response() { + let orchestrations_str = r#" + { + "gpt-4o": [ + {"name": "Image generation", "description": "generating image"}, + {"name": "Code generation", "description": "generating code"} + ] + } + "#; + let agent_orchestrations = + serde_json::from_str::>>(orchestrations_str).unwrap(); + + let orchestrator = OrchestratorModelV1::new(agent_orchestrations, "test-model".to_string(), 2000); + + // Case 1: Valid JSON with single route in array + let input = r#"{"route": ["Image generation"]}"#; + let result = orchestrator.parse_response(input, &None).unwrap(); + assert_eq!( + result, + Some(vec![("Image generation".to_string(), "gpt-4o".to_string())]) + ); + + // Case 2: Valid JSON with multiple routes in array + let input = r#"{"route": ["Image generation", "Code generation"]}"#; + let result = orchestrator.parse_response(input, &None).unwrap(); + assert_eq!( + result, + Some(vec![ + ("Image generation".to_string(), "gpt-4o".to_string()), + ("Code generation".to_string(), "gpt-4o".to_string()) + ]) + ); + + // Case 3: Valid JSON with empty array + let input = r#"{"route": []}"#; + let result = orchestrator.parse_response(input, &None).unwrap(); + assert_eq!(result, None); + + // Case 4: Valid JSON with null route + let input = r#"{"route": null}"#; + let result = orchestrator.parse_response(input, &None).unwrap(); + assert_eq!(result, None); + + // Case 5: JSON missing route field + let input = r#"{}"#; + let result = orchestrator.parse_response(input, &None).unwrap(); + assert_eq!(result, None); + + // Case 5.1: empty string + let input = r#""#; + let result = orchestrator.parse_response(input, &None).unwrap(); + assert_eq!(result, None); + + // Case 6: Malformed JSON + let input = r#"{"route": ["route1""#; // missing closing ] + let result = orchestrator.parse_response(input, &None); + assert!(result.is_err()); + + // Case 7: Single quotes and \n in JSON + let input = "{'route': ['Image generation']}\\n"; + let result = orchestrator.parse_response(input, &None).unwrap(); + assert_eq!( + result, + Some(vec![("Image generation".to_string(), "gpt-4o".to_string())]) + ); + + // Case 8: Array with unknown route (not in orchestrations map) + let input = r#"{"route": ["Unknown route"]}"#; + let result = orchestrator.parse_response(input, &None).unwrap(); + assert_eq!(result, None); + } +} diff --git a/crates/brightstaff/src/state/memory.rs b/crates/brightstaff/src/state/memory.rs new file mode 100644 index 00000000..d805d655 --- /dev/null +++ b/crates/brightstaff/src/state/memory.rs @@ -0,0 +1,611 @@ +use super::{OpenAIConversationState, StateStorage, StateStorageError}; +use async_trait::async_trait; +use std::collections::HashMap; +use std::sync::Arc; +use tokio::sync::RwLock; +use tracing::{debug, warn}; + +/// In-memory storage backend for conversation state +/// Uses a HashMap wrapped in Arc> for thread-safe access +#[derive(Clone)] +pub struct MemoryConversationalStorage { + storage: Arc>>, +} + +impl MemoryConversationalStorage { + pub fn new() -> Self { + Self { + storage: Arc::new(RwLock::new(HashMap::new())), + } + } +} + +impl Default for MemoryConversationalStorage { + fn default() -> Self { + Self::new() + } +} + +#[async_trait] +impl StateStorage for MemoryConversationalStorage { + async fn put(&self, state: OpenAIConversationState) -> Result<(), StateStorageError> { + let response_id = state.response_id.clone(); + let mut storage = self.storage.write().await; + + debug!( + "[PLANO | BRIGHTSTAFF | MEMORY_STORAGE] RESP_ID:{} | Storing conversation state: model={}, provider={}, input_items={}", + response_id, state.model, state.provider, state.input_items.len() + ); + + storage.insert(response_id, state); + Ok(()) + } + + async fn get(&self, response_id: &str) -> Result { + let storage = self.storage.read().await; + + match storage.get(response_id) { + Some(state) => { + debug!( + "[PLANO | MEMORY_STORAGE | RESP_ID:{} | Retrieved conversation state: input_items={}", + response_id, state.input_items.len() + ); + Ok(state.clone()) + } + None => { + warn!( + "[PLANO_RESP_ID:{} | MEMORY_STORAGE | Conversation state not found", + response_id + ); + Err(StateStorageError::NotFound(response_id.to_string())) + } + } + } + + async fn exists(&self, response_id: &str) -> Result { + let storage = self.storage.read().await; + Ok(storage.contains_key(response_id)) + } + + async fn delete(&self, response_id: &str) -> Result<(), StateStorageError> { + let mut storage = self.storage.write().await; + + if storage.remove(response_id).is_some() { + debug!( + "[PLANO | BRIGHTSTAFF | MEMORY_STORAGE] RESP_ID:{} | Deleted conversation state", + response_id + ); + Ok(()) + } else { + Err(StateStorageError::NotFound(response_id.to_string())) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use hermesllm::apis::openai_responses::{InputItem, InputMessage, MessageRole, InputContent, MessageContent}; + + fn create_test_state(response_id: &str, num_messages: usize) -> OpenAIConversationState { + let mut input_items = Vec::new(); + for i in 0..num_messages { + input_items.push(InputItem::Message(InputMessage { + role: if i % 2 == 0 { MessageRole::User } else { MessageRole::Assistant }, + content: MessageContent::Items(vec![InputContent::InputText { + text: format!("Message {}", i), + }]), + })); + } + + OpenAIConversationState { + response_id: response_id.to_string(), + input_items, + created_at: 1234567890, + model: "claude-3".to_string(), + provider: "anthropic".to_string(), + } + } + + #[tokio::test] + async fn test_put_and_get_success() { + let storage = MemoryConversationalStorage::new(); + let state: OpenAIConversationState = create_test_state("resp_001", 3); + + // Store + storage.put(state.clone()).await.unwrap(); + + // Retrieve + let retrieved = storage.get("resp_001").await.unwrap(); + assert_eq!(retrieved.response_id, state.response_id); + assert_eq!(retrieved.model, state.model); + assert_eq!(retrieved.provider, state.provider); + assert_eq!(retrieved.input_items.len(), 3); + assert_eq!(retrieved.created_at, state.created_at); + } + + #[tokio::test] + async fn test_put_overwrites_existing() { + let storage = MemoryConversationalStorage::new(); + + // First state + let state1 = create_test_state("resp_002", 2); + storage.put(state1).await.unwrap(); + + // Overwrite with new state + let state2 = OpenAIConversationState { + response_id: "resp_002".to_string(), + input_items: vec![], + created_at: 9999999999, + model: "gpt-4".to_string(), + provider: "openai".to_string(), + }; + storage.put(state2.clone()).await.unwrap(); + + // Should retrieve the new state + let retrieved = storage.get("resp_002").await.unwrap(); + assert_eq!(retrieved.model, "gpt-4"); + assert_eq!(retrieved.provider, "openai"); + assert_eq!(retrieved.input_items.len(), 0); + assert_eq!(retrieved.created_at, 9999999999); + } + + #[tokio::test] + async fn test_get_not_found() { + let storage = MemoryConversationalStorage::new(); + + let result = storage.get("nonexistent").await; + assert!(result.is_err()); + + match result.unwrap_err() { + StateStorageError::NotFound(id) => { + assert_eq!(id, "nonexistent"); + } + _ => panic!("Expected NotFound error"), + } + } + + #[tokio::test] + async fn test_exists_returns_false_for_nonexistent() { + let storage = MemoryConversationalStorage::new(); + assert!(!storage.exists("resp_003").await.unwrap()); + } + + #[tokio::test] + async fn test_exists_returns_true_after_put() { + let storage = MemoryConversationalStorage::new(); + let state = create_test_state("resp_004", 1); + + assert!(!storage.exists("resp_004").await.unwrap()); + storage.put(state).await.unwrap(); + assert!(storage.exists("resp_004").await.unwrap()); + } + + #[tokio::test] + async fn test_delete_success() { + let storage = MemoryConversationalStorage::new(); + let state = create_test_state("resp_005", 2); + + storage.put(state).await.unwrap(); + assert!(storage.exists("resp_005").await.unwrap()); + + // Delete + storage.delete("resp_005").await.unwrap(); + + // Should no longer exist + assert!(!storage.exists("resp_005").await.unwrap()); + assert!(storage.get("resp_005").await.is_err()); + } + + #[tokio::test] + async fn test_delete_not_found() { + let storage = MemoryConversationalStorage::new(); + + let result = storage.delete("nonexistent").await; + assert!(result.is_err()); + + match result.unwrap_err() { + StateStorageError::NotFound(id) => { + assert_eq!(id, "nonexistent"); + } + _ => panic!("Expected NotFound error"), + } + } + + #[tokio::test] + async fn test_merge_combines_inputs() { + let storage = MemoryConversationalStorage::new(); + + // Create a previous state with 2 messages + let prev_state = create_test_state("resp_006", 2); + + // Create current input with 1 message + let current_input = vec![InputItem::Message(InputMessage { + role: MessageRole::User, + content: MessageContent::Items(vec![InputContent::InputText { + text: "New message".to_string(), + }]), + })]; + + // Merge + let merged = storage.merge(&prev_state, current_input); + + // Should have 3 messages total (2 from prev + 1 current) + assert_eq!(merged.len(), 3); + } + + #[tokio::test] + async fn test_merge_preserves_order() { + let storage = MemoryConversationalStorage::new(); + + // Previous state has messages 0 and 1 + let prev_state = create_test_state("resp_007", 2); + + // Current input has message 2 + let current_input = vec![InputItem::Message(InputMessage { + role: MessageRole::User, + content: MessageContent::Items(vec![InputContent::InputText { + text: "Message 2".to_string(), + }]), + })]; + + let merged = storage.merge(&prev_state, current_input); + + // Verify order: prev messages first, then current + let InputItem::Message(msg) = &merged[0] else { panic!("Expected Message") }; + match &msg.content { + MessageContent::Items(items) => match &items[0] { + InputContent::InputText { text } => assert_eq!(text, "Message 0"), + _ => panic!("Expected InputText"), + }, + _ => panic!("Expected MessageContent::Items"), + } + + let InputItem::Message(msg) = &merged[2] else { panic!("Expected Message") }; + match &msg.content { + MessageContent::Items(items) => match &items[0] { + InputContent::InputText { text } => assert_eq!(text, "Message 2"), + _ => panic!("Expected InputText"), + }, + _ => panic!("Expected MessageContent::Items"), + } + } + + #[tokio::test] + async fn test_merge_with_empty_current_input() { + let storage = MemoryConversationalStorage::new(); + let prev_state = create_test_state("resp_008", 3); + + let merged = storage.merge(&prev_state, vec![]); + + // Should just have the previous state's items + assert_eq!(merged.len(), 3); + } + + #[tokio::test] + async fn test_merge_with_empty_previous_state() { + let storage = MemoryConversationalStorage::new(); + + let prev_state = OpenAIConversationState { + response_id: "resp_009".to_string(), + input_items: vec![], + created_at: 1234567890, + model: "gpt-4".to_string(), + provider: "openai".to_string(), + }; + + let current_input = vec![InputItem::Message(InputMessage { + role: MessageRole::User, + content: MessageContent::Items(vec![InputContent::InputText { + text: "Only message".to_string(), + }]), + })]; + + let merged = storage.merge(&prev_state, current_input); + + // Should just have the current input + assert_eq!(merged.len(), 1); + } + + #[tokio::test] + async fn test_concurrent_access() { + let storage = MemoryConversationalStorage::new(); + + // Spawn multiple tasks that write concurrently + let mut handles = vec![]; + + for i in 0..10 { + let storage_clone = storage.clone(); + let handle = tokio::spawn(async move { + let state = create_test_state(&format!("resp_{}", i), i % 3); + storage_clone.put(state).await.unwrap(); + }); + handles.push(handle); + } + + // Wait for all tasks + for handle in handles { + handle.await.unwrap(); + } + + // Verify all states were stored + for i in 0..10 { + assert!(storage.exists(&format!("resp_{}", i)).await.unwrap()); + } + } + + #[tokio::test] + async fn test_multiple_operations_on_same_id() { + let storage = MemoryConversationalStorage::new(); + let state = create_test_state("resp_010", 1); + + // Put + storage.put(state.clone()).await.unwrap(); + + // Get + let retrieved = storage.get("resp_010").await.unwrap(); + assert_eq!(retrieved.response_id, "resp_010"); + + // Exists + assert!(storage.exists("resp_010").await.unwrap()); + + // Put again (overwrite) + let new_state = create_test_state("resp_010", 5); + storage.put(new_state).await.unwrap(); + + // Get updated + let updated = storage.get("resp_010").await.unwrap(); + assert_eq!(updated.input_items.len(), 5); + + // Delete + storage.delete("resp_010").await.unwrap(); + + // Should not exist + assert!(!storage.exists("resp_010").await.unwrap()); + } + + #[tokio::test] + async fn test_merge_with_tool_call_flow() { + // This test simulates a realistic tool call conversation flow: + // 1. User sends message: "What's the weather?" + // 2. Model responds with function call (converted to assistant message) + // 3. User sends function call output in next request with previous_response_id + // The merge should combine: user message + assistant function call + function output + + let storage = MemoryConversationalStorage::new(); + + // Step 1: Previous state contains the initial exchange + // - User message: "What's the weather in SF?" + // - Assistant message (converted from FunctionCall): "Called function: get_weather..." + let prev_state = OpenAIConversationState { + response_id: "resp_tool_001".to_string(), + input_items: vec![ + // Original user message + InputItem::Message(InputMessage { + role: MessageRole::User, + content: MessageContent::Items(vec![InputContent::InputText { + text: "What's the weather in San Francisco?".to_string(), + }]), + }), + // Assistant's function call (converted from OutputItem::FunctionCall) + InputItem::Message(InputMessage { + role: MessageRole::Assistant, + content: MessageContent::Items(vec![InputContent::InputText { + text: "Called function: get_weather with arguments: {\"location\":\"San Francisco, CA\"}".to_string(), + }]), + }), + ], + created_at: 1234567890, + model: "claude-3".to_string(), + provider: "anthropic".to_string(), + }; + + // Step 2: Current request includes function call output + let current_input = vec![InputItem::Message(InputMessage { + role: MessageRole::User, + content: MessageContent::Items(vec![InputContent::InputText { + text: "Function result: {\"temperature\": 72, \"condition\": \"sunny\"}".to_string(), + }]), + })]; + + // Step 3: Merge should combine all conversation history + let merged = storage.merge(&prev_state, current_input); + + // Should have 3 items: user question + assistant function call + function output + assert_eq!(merged.len(), 3); + + // Verify the order and content + let InputItem::Message(msg1) = &merged[0] else { panic!("Expected Message") }; + assert!(matches!(msg1.role, MessageRole::User)); + match &msg1.content { + MessageContent::Items(items) => match &items[0] { + InputContent::InputText { text } => { + assert!(text.contains("weather in San Francisco")); + } + _ => panic!("Expected InputText"), + }, + _ => panic!("Expected MessageContent::Items"), + } + + let InputItem::Message(msg2) = &merged[1] else { panic!("Expected Message") }; + assert!(matches!(msg2.role, MessageRole::Assistant)); + match &msg2.content { + MessageContent::Items(items) => match &items[0] { + InputContent::InputText { text } => { + assert!(text.contains("get_weather")); + } + _ => panic!("Expected InputText"), + }, + _ => panic!("Expected MessageContent::Items"), + } + + let InputItem::Message(msg3) = &merged[2] else { panic!("Expected Message") }; + assert!(matches!(msg3.role, MessageRole::User)); + match &msg3.content { + MessageContent::Items(items) => match &items[0] { + InputContent::InputText { text } => { + assert!(text.contains("Function result")); + assert!(text.contains("temperature")); + } + _ => panic!("Expected InputText"), + }, + _ => panic!("Expected MessageContent::Items"), + } + } + + #[tokio::test] + async fn test_merge_with_multiple_tool_calls() { + // Test a more complex scenario with multiple tool calls + let storage = MemoryConversationalStorage::new(); + + // Previous state has: user message + 2 function calls from assistant + let prev_state = OpenAIConversationState { + response_id: "resp_tool_002".to_string(), + input_items: vec![ + InputItem::Message(InputMessage { + role: MessageRole::User, + content: MessageContent::Items(vec![InputContent::InputText { + text: "What's the weather and time in SF?".to_string(), + }]), + }), + InputItem::Message(InputMessage { + role: MessageRole::Assistant, + content: MessageContent::Items(vec![InputContent::InputText { + text: "Called function: get_weather with arguments: {\"location\":\"SF\"}".to_string(), + }]), + }), + InputItem::Message(InputMessage { + role: MessageRole::Assistant, + content: MessageContent::Items(vec![InputContent::InputText { + text: "Called function: get_time with arguments: {\"timezone\":\"America/Los_Angeles\"}".to_string(), + }]), + }), + ], + created_at: 1234567890, + model: "gpt-4".to_string(), + provider: "openai".to_string(), + }; + + // Current input: function outputs for both calls + let current_input = vec![ + InputItem::Message(InputMessage { + role: MessageRole::User, + content: MessageContent::Items(vec![InputContent::InputText { + text: "Weather result: {\"temp\": 68}".to_string(), + }]), + }), + InputItem::Message(InputMessage { + role: MessageRole::User, + content: MessageContent::Items(vec![InputContent::InputText { + text: "Time result: {\"time\": \"14:30\"}".to_string(), + }]), + }), + ]; + + let merged = storage.merge(&prev_state, current_input); + + // Should have 5 items total: 1 user + 2 assistant calls + 2 function outputs + assert_eq!(merged.len(), 5); + + // Verify first item is original user message + let InputItem::Message(first) = &merged[0] else { panic!("Expected Message") }; + assert!(matches!(first.role, MessageRole::User)); + + // Verify last two are function outputs + let InputItem::Message(second_last) = &merged[3] else { panic!("Expected Message") }; + assert!(matches!(second_last.role, MessageRole::User)); + match &second_last.content { + MessageContent::Items(items) => match &items[0] { + InputContent::InputText { text } => assert!(text.contains("Weather result")), + _ => panic!("Expected InputText"), + }, + _ => panic!("Expected MessageContent::Items"), + } + + let InputItem::Message(last) = &merged[4] else { panic!("Expected Message") }; + assert!(matches!(last.role, MessageRole::User)); + match &last.content { + MessageContent::Items(items) => match &items[0] { + InputContent::InputText { text } => assert!(text.contains("Time result")), + _ => panic!("Expected InputText"), + }, + _ => panic!("Expected MessageContent::Items"), + } + } + + #[tokio::test] + async fn test_merge_preserves_conversation_context_for_multi_turn() { + // Simulate a multi-turn conversation with tool calls + let storage = MemoryConversationalStorage::new(); + + // Previous state: full conversation history up to this point + let prev_state = OpenAIConversationState { + response_id: "resp_tool_003".to_string(), + input_items: vec![ + // Turn 1: User asks about weather + InputItem::Message(InputMessage { + role: MessageRole::User, + content: MessageContent::Items(vec![InputContent::InputText { + text: "What's the weather?".to_string(), + }]), + }), + // Turn 1: Assistant calls get_weather + InputItem::Message(InputMessage { + role: MessageRole::Assistant, + content: MessageContent::Items(vec![InputContent::InputText { + text: "Called function: get_weather".to_string(), + }]), + }), + // Turn 2: User provides function output + InputItem::Message(InputMessage { + role: MessageRole::User, + content: MessageContent::Items(vec![InputContent::InputText { + text: "Weather: sunny, 72°F".to_string(), + }]), + }), + // Turn 2: Assistant responds with text + InputItem::Message(InputMessage { + role: MessageRole::Assistant, + content: MessageContent::Items(vec![InputContent::InputText { + text: "It's sunny and 72°F in San Francisco today!".to_string(), + }]), + }), + ], + created_at: 1234567890, + model: "claude-3".to_string(), + provider: "anthropic".to_string(), + }; + + // Turn 3: User asks follow-up question + let current_input = vec![InputItem::Message(InputMessage { + role: MessageRole::User, + content: MessageContent::Items(vec![InputContent::InputText { + text: "Should I bring an umbrella?".to_string(), + }]), + })]; + + let merged = storage.merge(&prev_state, current_input); + + // Should have all 5 messages in order + assert_eq!(merged.len(), 5); + + // Verify the entire conversation flow is preserved + let InputItem::Message(first) = &merged[0] else { panic!("Expected Message") }; + match &first.content { + MessageContent::Items(items) => match &items[0] { + InputContent::InputText { text } => assert!(text.contains("What's the weather")), + _ => panic!("Expected InputText"), + }, + _ => panic!("Expected MessageContent::Items"), + } + + let InputItem::Message(last) = &merged[4] else { panic!("Expected Message") }; + match &last.content { + MessageContent::Items(items) => match &items[0] { + InputContent::InputText { text } => assert!(text.contains("umbrella")), + _ => panic!("Expected InputText"), + }, + _ => panic!("Expected MessageContent::Items"), + } + } +} diff --git a/crates/brightstaff/src/state/mod.rs b/crates/brightstaff/src/state/mod.rs new file mode 100644 index 00000000..f2b96da0 --- /dev/null +++ b/crates/brightstaff/src/state/mod.rs @@ -0,0 +1,147 @@ +use async_trait::async_trait; +use hermesllm::apis::openai_responses::{InputItem, InputMessage, InputContent, MessageContent, MessageRole, InputParam}; +use serde::{Deserialize, Serialize}; +use std::error::Error; +use std::fmt; +use std::sync::Arc; +use tracing::{debug}; + +pub mod memory; +pub mod response_state_processor; +pub mod postgresql; + +/// Represents the conversational state for a v1/responses request +/// Contains the complete input/output history that can be restored +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct OpenAIConversationState { + /// The response ID this state is associated with + pub response_id: String, + + /// The complete input history (original input + accumulated outputs) + /// This is what gets prepended to new requests via previous_response_id + pub input_items: Vec, + + /// Timestamp when this state was created + pub created_at: i64, + + /// Model used for this response + pub model: String, + + /// Provider that generated this response (e.g., "anthropic", "openai") + pub provider: String, +} + +/// Error types for state storage operations +#[derive(Debug)] +pub enum StateStorageError { + /// State not found for given response_id + NotFound(String), + + /// Storage backend error (network, database, etc.) + StorageError(String), + + /// Serialization/deserialization error + SerializationError(String), +} + +impl fmt::Display for StateStorageError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + StateStorageError::NotFound(id) => write!(f, "Conversation state not found for response_id: {}", id), + StateStorageError::StorageError(msg) => write!(f, "Storage error: {}", msg), + StateStorageError::SerializationError(msg) => write!(f, "Serialization error: {}", msg), + } + } +} + +impl Error for StateStorageError {} + +/// Trait for conversation state storage backends +#[async_trait] +pub trait StateStorage: Send + Sync { + /// Store conversation state for a response + async fn put(&self, state: OpenAIConversationState) -> Result<(), StateStorageError>; + + /// Retrieve conversation state by response_id + async fn get(&self, response_id: &str) -> Result; + + /// Check if state exists for a response_id + async fn exists(&self, response_id: &str) -> Result; + + /// Delete state for a response_id (optional, for cleanup) + async fn delete(&self, response_id: &str) -> Result<(), StateStorageError>; + + fn merge( + &self, + prev_state: &OpenAIConversationState, + current_input: Vec, + ) -> Vec { + // Default implementation: prepend previous input, append current + let prev_count = prev_state.input_items.len(); + let current_count = current_input.len(); + + let mut combined_input = prev_state.input_items.clone(); + combined_input.extend(current_input); + + debug!( + "PLANO | BRIGHTSTAFF | STATE_STORAGE | RESP_ID:{} | Merged state: prev_items={}, current_items={}, total_items={}, combined_json={}", + prev_state.response_id, + prev_count, + current_count, + combined_input.len(), + serde_json::to_string(&combined_input).unwrap_or_else(|_| "serialization_error".to_string()) + ); + + combined_input + } +} + + + +/// Storage backend type enum +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum StorageBackend { + Memory, + Supabase, +} + +impl StorageBackend { + pub fn from_str(s: &str) -> Option { + match s.to_lowercase().as_str() { + "memory" => Some(StorageBackend::Memory), + "supabase" => Some(StorageBackend::Supabase), + _ => None, + } + } +} + +// === Utility functions for state management === + +/// Extract input items from InputParam, converting text to structured format +pub fn extract_input_items(input: &InputParam) -> Vec { + match input { + InputParam::Text(text) => { + vec![InputItem::Message(InputMessage { + role: MessageRole::User, + content: MessageContent::Items(vec![InputContent::InputText { + text: text.clone(), + }]), + })] + } + InputParam::Items(items) => items.clone(), + } +} + +/// Retrieve previous conversation state and combine with current input +/// Returns combined input if previous state found, or original input if not found/error +pub async fn retrieve_and_combine_input( + storage: Arc, + previous_response_id: &str, + current_input: Vec, +) -> Result, StateStorageError> { + + // First get the previous state + let prev_state = storage.get(previous_response_id).await?; + let combined_input = storage.merge(&prev_state, current_input); + Ok(combined_input) +} diff --git a/crates/brightstaff/src/state/postgresql.rs b/crates/brightstaff/src/state/postgresql.rs new file mode 100644 index 00000000..529f27e9 --- /dev/null +++ b/crates/brightstaff/src/state/postgresql.rs @@ -0,0 +1,432 @@ +use super::{OpenAIConversationState, StateStorage, StateStorageError}; +use async_trait::async_trait; +use serde_json; +use std::sync::Arc; +use tokio::sync::OnceCell; +use tokio_postgres::{Client, NoTls}; +use tracing::{debug, info, warn}; + +/// Supabase/PostgreSQL storage backend for conversation state +#[derive(Clone)] +pub struct PostgreSQLConversationStorage { + client: Arc, + table_verified: Arc>, +} + +impl PostgreSQLConversationStorage { + /// Creates a new Supabase storage instance with the given connection string + pub async fn new(connection_string: String) -> Result { + let (client, connection) = tokio_postgres::connect(&connection_string, NoTls) + .await + .map_err(|e| { + StateStorageError::StorageError(format!("Failed to connect to database: {}", e)) + })?; + + // Spawn the connection to run in the background + tokio::spawn(async move { + if let Err(e) = connection.await { + warn!("Database connection error: {}", e); + } + }); + + Ok(Self { + client: Arc::new(client), + table_verified: Arc::new(OnceCell::new()), + }) + } + + /// Ensures the conversation_states table exists (checks once, caches result) + async fn ensure_ready(&self) -> Result<(), StateStorageError> { + self.table_verified + .get_or_try_init(|| async { + let row = self + .client + .query_one( + "SELECT EXISTS ( + SELECT FROM pg_tables + WHERE tablename = 'conversation_states' + )", + &[], + ) + .await + .map_err(|e| { + StateStorageError::StorageError(format!( + "Failed to verify table existence: {}", + e + )) + })?; + + let exists: bool = row.get(0); + + if !exists { + return Err(StateStorageError::StorageError( + "Table 'conversation_states' does not exist. \ + Please run the setup SQL from docs/db_setup/conversation_states.sql" + .to_string(), + )); + } + + info!("Conversation state storage table verified"); + Ok(()) + }) + .await?; + + Ok(()) + } +} + +#[async_trait] +impl StateStorage for PostgreSQLConversationStorage { + async fn put(&self, state: OpenAIConversationState) -> Result<(), StateStorageError> { + self.ensure_ready().await?; + + // Serialize input_items to JSONB + let input_items_json = serde_json::to_value(&state.input_items).map_err(|e| { + StateStorageError::StorageError(format!("Failed to serialize input_items: {}", e)) + })?; + + // Upsert the conversation state + self.client + .execute( + r#" + INSERT INTO conversation_states + (response_id, input_items, created_at, model, provider, updated_at) + VALUES ($1, $2, $3, $4, $5, NOW()) + ON CONFLICT (response_id) + DO UPDATE SET + input_items = EXCLUDED.input_items, + model = EXCLUDED.model, + provider = EXCLUDED.provider, + updated_at = NOW() + "#, + &[ + &state.response_id, + &input_items_json, + &state.created_at, + &state.model, + &state.provider, + ], + ) + .await + .map_err(|e| { + StateStorageError::StorageError(format!( + "Failed to store conversation state for {}: {}", + state.response_id, e + )) + })?; + + debug!("Stored conversation state for {}", state.response_id); + Ok(()) + } + + async fn get(&self, response_id: &str) -> Result { + self.ensure_ready().await?; + + let row = self + .client + .query_opt( + r#" + SELECT response_id, input_items, created_at, model, provider + FROM conversation_states + WHERE response_id = $1 + "#, + &[&response_id], + ) + .await + .map_err(|e| { + StateStorageError::StorageError(format!( + "Failed to fetch conversation state for {}: {}", + response_id, e + )) + })?; + + match row { + Some(row) => { + let response_id: String = row.get("response_id"); + let input_items_json: serde_json::Value = row.get("input_items"); + let created_at: i64 = row.get("created_at"); + let model: String = row.get("model"); + let provider: String = row.get("provider"); + + // Deserialize input_items from JSONB + let input_items = + serde_json::from_value(input_items_json).map_err(|e| { + StateStorageError::StorageError(format!( + "Failed to deserialize input_items: {}", + e + )) + })?; + + Ok(OpenAIConversationState { + response_id, + input_items, + created_at, + model, + provider, + }) + } + None => Err(StateStorageError::NotFound(format!( + "Conversation state not found for response_id: {}", + response_id + ))), + } + } + + async fn exists(&self, response_id: &str) -> Result { + self.ensure_ready().await?; + + let row = self + .client + .query_one( + "SELECT EXISTS(SELECT 1 FROM conversation_states WHERE response_id = $1)", + &[&response_id], + ) + .await + .map_err(|e| { + StateStorageError::StorageError(format!( + "Failed to check existence for {}: {}", + response_id, e + )) + })?; + + let exists: bool = row.get(0); + Ok(exists) + } + + async fn delete(&self, response_id: &str) -> Result<(), StateStorageError> { + self.ensure_ready().await?; + + let rows_affected = self + .client + .execute( + "DELETE FROM conversation_states WHERE response_id = $1", + &[&response_id], + ) + .await + .map_err(|e| { + StateStorageError::StorageError(format!( + "Failed to delete conversation state for {}: {}", + response_id, e + )) + })?; + + if rows_affected == 0 { + return Err(StateStorageError::NotFound(format!( + "Conversation state not found for response_id: {}", + response_id + ))); + } + + debug!("Deleted conversation state for {}", response_id); + Ok(()) + } +} + +/* +PostgreSQL schema is maintained in docs/db_setup/conversation_states.sql +Run that SQL file against your database before using this storage backend. +*/ + +#[cfg(test)] +mod tests { + use super::*; + use hermesllm::apis::openai_responses::{InputContent, InputItem, InputMessage, MessageContent, MessageRole}; + + fn create_test_state(response_id: &str) -> OpenAIConversationState { + OpenAIConversationState { + response_id: response_id.to_string(), + input_items: vec![InputItem::Message(InputMessage { + role: MessageRole::User, + content: MessageContent::Items(vec![InputContent::InputText { + text: "Test message".to_string(), + }]), + })], + created_at: 1234567890, + model: "gpt-4".to_string(), + provider: "openai".to_string(), + } + } + + // Note: These tests require a running PostgreSQL database + // Set TEST_DATABASE_URL environment variable to run integration tests + // Example: TEST_DATABASE_URL=postgresql://user:pass@localhost/test_db + + async fn get_test_storage() -> Option { + if let Ok(db_url) = std::env::var("TEST_DATABASE_URL") { + match PostgreSQLConversationStorage::new(db_url).await { + Ok(storage) => Some(storage), + Err(e) => { + eprintln!("Failed to create test storage: {}", e); + None + } + } + } else { + eprintln!("TEST_DATABASE_URL not set, skipping Supabase integration tests"); + None + } + } + + #[tokio::test] + async fn test_supabase_put_and_get_success() { + let Some(storage) = get_test_storage().await else { + return; + }; + + let state = create_test_state("test_resp_001"); + storage.put(state.clone()).await.unwrap(); + + let retrieved = storage.get("test_resp_001").await.unwrap(); + assert_eq!(retrieved.response_id, "test_resp_001"); + assert_eq!(retrieved.input_items.len(), 1); + assert_eq!(retrieved.model, "gpt-4"); + assert_eq!(retrieved.provider, "openai"); + + // Cleanup + let _ = storage.delete("test_resp_001").await; + } + + #[tokio::test] + async fn test_supabase_put_overwrites_existing() { + let Some(storage) = get_test_storage().await else { + return; + }; + + let state1 = create_test_state("test_resp_002"); + storage.put(state1).await.unwrap(); + + let mut state2 = create_test_state("test_resp_002"); + state2.model = "gpt-4-turbo".to_string(); + state2.input_items.push(InputItem::Message(InputMessage { + role: MessageRole::Assistant, + content: MessageContent::Items(vec![InputContent::InputText { + text: "Response".to_string(), + }]), + })); + storage.put(state2).await.unwrap(); + + let retrieved = storage.get("test_resp_002").await.unwrap(); + assert_eq!(retrieved.model, "gpt-4-turbo"); + assert_eq!(retrieved.input_items.len(), 2); + + // Cleanup + let _ = storage.delete("test_resp_002").await; + } + + #[tokio::test] + async fn test_supabase_get_not_found() { + let Some(storage) = get_test_storage().await else { + return; + }; + + let result = storage.get("nonexistent_id").await; + assert!(result.is_err()); + assert!(matches!(result.unwrap_err(), StateStorageError::NotFound(_))); + } + + #[tokio::test] + async fn test_supabase_exists_returns_false() { + let Some(storage) = get_test_storage().await else { + return; + }; + + let exists = storage.exists("nonexistent_id").await.unwrap(); + assert!(!exists); + } + + #[tokio::test] + async fn test_supabase_exists_returns_true_after_put() { + let Some(storage) = get_test_storage().await else { + return; + }; + + let state = create_test_state("test_resp_003"); + storage.put(state).await.unwrap(); + + let exists = storage.exists("test_resp_003").await.unwrap(); + assert!(exists); + + // Cleanup + let _ = storage.delete("test_resp_003").await; + } + + #[tokio::test] + async fn test_supabase_delete_success() { + let Some(storage) = get_test_storage().await else { + return; + }; + + let state = create_test_state("test_resp_004"); + storage.put(state).await.unwrap(); + + storage.delete("test_resp_004").await.unwrap(); + + let exists = storage.exists("test_resp_004").await.unwrap(); + assert!(!exists); + } + + #[tokio::test] + async fn test_supabase_delete_not_found() { + let Some(storage) = get_test_storage().await else { + return; + }; + + let result = storage.delete("nonexistent_id").await; + assert!(result.is_err()); + assert!(matches!(result.unwrap_err(), StateStorageError::NotFound(_))); + } + + #[tokio::test] + async fn test_supabase_merge_works() { + let Some(storage) = get_test_storage().await else { + return; + }; + + let prev_state = create_test_state("test_resp_005"); + let current_input = vec![InputItem::Message(InputMessage { + role: MessageRole::User, + content: MessageContent::Items(vec![InputContent::InputText { + text: "New message".to_string(), + }]), + })]; + + let merged = storage.merge(&prev_state, current_input); + + // Should have 2 messages (1 from prev + 1 current) + assert_eq!(merged.len(), 2); + } + + #[tokio::test] + async fn test_supabase_table_verification() { + let Some(storage) = get_test_storage().await else { + return; + }; + + // This should trigger table verification + let result = storage.ensure_ready().await; + assert!(result.is_ok(), "Table verification should succeed"); + + // Second call should use cached result + let result2 = storage.ensure_ready().await; + assert!(result2.is_ok(), "Cached verification should succeed"); + } + + #[tokio::test] + #[ignore] // Run manually with: cargo test test_verify_data_in_supabase -- --ignored + async fn test_verify_data_in_supabase() { + let Some(storage) = get_test_storage().await else { + return; + }; + + // Create a test record that persists + let state = create_test_state("manual_test_verification"); + storage.put(state).await.unwrap(); + + println!("✅ Data written to Supabase!"); + println!("Check your Supabase dashboard:"); + println!(" SELECT * FROM conversation_states WHERE response_id = 'manual_test_verification';"); + println!("\nTo cleanup, run:"); + println!(" DELETE FROM conversation_states WHERE response_id = 'manual_test_verification';"); + + // DON'T cleanup - leave it for manual verification + } +} diff --git a/crates/brightstaff/src/state/response_state_processor.rs b/crates/brightstaff/src/state/response_state_processor.rs new file mode 100644 index 00000000..b3ce6787 --- /dev/null +++ b/crates/brightstaff/src/state/response_state_processor.rs @@ -0,0 +1,302 @@ +use bytes::Bytes; +use flate2::read::GzDecoder; +use hermesllm::apis::openai_responses::{ + InputItem, OutputItem, ResponsesAPIStreamEvent, +}; +use hermesllm::apis::streaming_shapes::sse::SseStreamIter; +use hermesllm::transforms::response::output_to_input::outputs_to_inputs; +use std::io::Read; +use std::sync::Arc; +use tracing::{info, debug, warn}; + +use crate::handlers::utils::StreamProcessor; +use crate::state::{OpenAIConversationState, StateStorage}; + +/// Processor that wraps another processor and handles v1/responses state management +/// Captures response_id and output from streaming responses, stores state after completion +pub struct ResponsesStateProcessor { + /// The underlying processor (e.g., ObservableStreamProcessor for metrics) + inner: P, + + /// State storage backend + storage: Arc, + + /// Original input items from the request + original_input: Vec, + + /// Model name + model: String, + + /// Provider name + provider: String, + + /// Whether this is a streaming request + is_streaming: bool, + + /// Whether upstream is OpenAI (skip storage if true) + is_openai_upstream: bool, + + /// Content-Encoding header value (e.g., "gzip", "br", None) + content_encoding: Option, + + /// Request ID for logging + request_id: String, + + /// Buffer for accumulating chunks (needed for non-streaming compressed responses) + chunk_buffer: Vec, + + /// Captured response_id from response.completed event + response_id: Option, + + /// Captured output items from response.completed event + output_items: Option>, +} + +impl ResponsesStateProcessor

{ + pub fn new( + inner: P, + storage: Arc, + original_input: Vec, + model: String, + provider: String, + is_streaming: bool, + is_openai_upstream: bool, + content_encoding: Option, + request_id: String, + ) -> Self { + Self { + inner, + storage, + original_input, + model, + provider, + is_streaming, + is_openai_upstream, + content_encoding, + request_id, + chunk_buffer: Vec::new(), + response_id: None, + output_items: None, + } + } + + /// Decompress accumulated buffer based on Content-Encoding header + fn decompress_buffer(&self) -> Vec { + if self.chunk_buffer.is_empty() { + return Vec::new(); + } + + match self.content_encoding.as_deref() { + Some("gzip") => { + let mut decoder = GzDecoder::new(self.chunk_buffer.as_slice()); + let mut decompressed = Vec::new(); + match decoder.read_to_end(&mut decompressed) { + Ok(_) => { + debug!( + "[PLANO_REQ_ID:{}] | STATE_PROCESSOR | Successfully decompressed {} bytes to {} bytes", + self.request_id, + self.chunk_buffer.len(), + decompressed.len() + ); + decompressed + } + Err(e) => { + warn!( + "[PLANO_REQ_ID:{}] | STATE_PROCESSOR | Failed to decompress gzip buffer: {}", + self.request_id, + e + ); + self.chunk_buffer.clone() + } + } + } + Some(encoding) => { + warn!( + "[PLANO_REQ_ID:{}] | STATE_PROCESSOR | Unsupported Content-Encoding: {}. Only gzip is currently supported.", + self.request_id, + encoding + ); + self.chunk_buffer.clone() + } + None => self.chunk_buffer.clone(), + } + } + + /// Parse response to extract response_id and output + /// For streaming: parse SSE events looking for response.completed (per chunk) + /// For non-streaming: buffer all chunks, then decompress and parse on completion + fn try_parse_response_chunk(&mut self, chunk: &[u8]) { + if self.is_streaming { + // Streaming: Try to parse SSE events from this chunk + // Note: For compressed streaming, we'd need to buffer and decompress first + // but most streaming responses aren't compressed since SSE needs to be readable + let sse_iter = match SseStreamIter::try_from(chunk) { + Ok(iter) => iter, + Err(_) => return, // Not valid SSE format, skip + }; + + // Process each SSE event in the chunk, looking for data lines with response.completed + for event in sse_iter { + // Only process data lines (skip event-only lines) + if let Some(data_str) = &event.data { + // Try to parse as ResponsesAPIStreamEvent + if let Ok(stream_event) = serde_json::from_str::(data_str) { + // Check if this is a ResponseCompleted event + if let ResponsesAPIStreamEvent::ResponseCompleted { response, .. } = stream_event { + info!( + "[PLANO_REQ_ID:{}] | STATE_PROCESSOR | Captured streaming response.completed: response_id={}, output_items={}", + self.request_id, + response.id, + response.output.len() + ); + self.response_id = Some(response.id.clone()); + self.output_items = Some(response.output.clone()); + return; // Found what we need, exit early + } + } + } + } + } else { + // Non-streaming: Buffer chunks, will decompress and parse on completion + self.chunk_buffer.extend_from_slice(chunk); + } + } + + /// Parse buffered non-streaming response (called on completion) + fn try_parse_buffered_response(&mut self) { + if self.is_streaming || self.chunk_buffer.is_empty() { + return; + } + + // Decompress if needed + let decompressed = self.decompress_buffer(); + + // Parse complete JSON response + match serde_json::from_slice::(&decompressed) { + Ok(response) => { + info!( + "[PLANO_REQ_ID:{}] | STATE_PROCESSOR | Captured non-streaming response: response_id={}, output_items={}", + self.request_id, + response.id, + response.output.len() + ); + self.response_id = Some(response.id.clone()); + self.output_items = Some(response.output.clone()); + } + Err(e) => { + // Log parse error with chunk preview for debugging + let chunk_preview = String::from_utf8_lossy(&decompressed); + let preview_len = chunk_preview.len().min(200); + warn!( + "[PLANO_REQ_ID:{}] | STATE_PROCESSOR | Failed to parse non-streaming ResponsesAPIResponse: {}. Decompressed preview (first {} bytes): {}", + self.request_id, + e, + preview_len, + &chunk_preview[..preview_len] + ); + } + } + } +} + +impl StreamProcessor for ResponsesStateProcessor

{ + fn process_chunk(&mut self, chunk: Bytes) -> Result, String> { + // Buffer/parse chunk for response extraction + self.try_parse_response_chunk(&chunk); + + // Forward to inner processor + self.inner.process_chunk(chunk) + } + + fn on_first_bytes(&mut self) { + self.inner.on_first_bytes(); + } + + fn on_complete(&mut self) { + // For non-streaming, decompress and parse buffered response + self.try_parse_buffered_response(); + + // First, let the inner processor complete + self.inner.on_complete(); + + // Skip storage for OpenAI upstream + if self.is_openai_upstream { + debug!( + "[PLANO_REQ_ID:{}] | STATE_PROCESSOR | Skipping state storage for OpenAI upstream provider", + self.request_id + ); + return; + } + + // Store state if we captured response_id and output + if let (Some(response_id), Some(output_items)) = (&self.response_id, &self.output_items) { + // Convert output items to input items for next request + let output_as_inputs = outputs_to_inputs(output_items); + + debug!( + "[PLANO_REQ_ID:{}] | STATE_PROCESSOR | Converting outputs to inputs: output_items_count={}, converted_input_items_count={}", + self.request_id, output_items.len(), output_as_inputs.len() + ); + + // Combine original input + output as new input history + let mut combined_input = self.original_input.clone(); + combined_input.extend(output_as_inputs); + + debug!( + "[PLANO_REQ_ID:{}] | STATE_PROCESSOR | Storing state: original_input_count={}, combined_input_count={}, combined_json={}", + self.request_id, + self.original_input.len(), + combined_input.len(), + serde_json::to_string(&combined_input).unwrap_or_else(|_| "serialization_error".to_string()) + ); + + let state = OpenAIConversationState { + response_id: response_id.clone(), + input_items: combined_input, + created_at: std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_secs() as i64, + model: self.model.clone(), + provider: self.provider.clone(), + }; + + // Store asynchronously (fire and forget with logging) + let storage = self.storage.clone(); + let response_id_clone = response_id.clone(); + let request_id = self.request_id.clone(); + let items_count = state.input_items.len(); + tokio::spawn(async move { + match storage.put(state).await { + Ok(()) => { + info!( + "[PLANO_REQ_ID:{}] | STATE_PROCESSOR | Successfully stored conversation state for response_id: {}, items_count={}", + request_id, + response_id_clone, + items_count + ); + } + Err(e) => { + warn!( + "[PLANO_REQ_ID:{}] | STATE_PROCESSOR | Failed to store conversation state for response_id {}: {}", + request_id, + response_id_clone, + e + ); + } + } + }); + } else { + warn!( + "[PLANO_REQ_ID:{}] | STATE_PROCESSOR | No response_id captured from upstream response - cannot store conversation state. response_id present: {}, output present: {}", + self.request_id, + self.response_id.is_some(), + self.output_items.is_some() + ); + } + } + + fn on_error(&mut self, error: &str) { + self.inner.on_error(error); + } +} diff --git a/crates/brightstaff/src/tracing/constants.rs b/crates/brightstaff/src/tracing/constants.rs new file mode 100644 index 00000000..d3eafb2e --- /dev/null +++ b/crates/brightstaff/src/tracing/constants.rs @@ -0,0 +1,335 @@ +/// OpenTelemetry Semantic Conventions +/// +/// This module defines standard attribute keys following OTEL semantic conventions. +/// See: https://opentelemetry.io/docs/specs/semconv/ + +// ============================================================================= +// Span Attributes - HTTP +// ============================================================================= + +/// Semantic conventions for HTTP-related span attributes +pub mod http { + /// HTTP request method + /// Example: "GET", "POST", "PUT" + pub const METHOD: &str = "http.method"; + + /// HTTP response status code + /// Example: "200", "404", "500" + pub const STATUS_CODE: &str = "http.status_code"; + + /// Full HTTP request URL + pub const URL: &str = "http.url"; + + /// HTTP request target (path + query) + /// Example: "/v1/chat/completions?stream=true" + pub const TARGET: &str = "http.target"; + + /// Upstream target path after routing transformation + /// Example: "/api/paas/v4/chat/completions" (for Zhipu provider) + pub const UPSTREAM_TARGET: &str = "http.upstream_target"; + + /// HTTP request scheme + /// Example: "http", "https" + pub const SCHEME: &str = "http.scheme"; + + /// Value of the HTTP User-Agent header + pub const USER_AGENT: &str = "http.user_agent"; + + /// Size of the request payload body in bytes + pub const REQUEST_CONTENT_LENGTH: &str = "http.request_content_length"; + + /// Size of the response payload body in bytes + pub const RESPONSE_CONTENT_LENGTH: &str = "http.response_content_length"; +} + +// ============================================================================= +// Span Attributes - LLM Specific +// ============================================================================= + +/// Custom attributes for LLM operations +/// These follow the emerging OTEL GenAI semantic conventions +pub mod llm { + /// Name of the LLM model being called + /// Example: "gpt-4", "claude-3-sonnet", "llama-2-70b" + pub const MODEL_NAME: &str = "llm.model"; + + /// Provider of the LLM + /// Example: "openai", "anthropic", "azure-openai" + pub const PROVIDER: &str = "llm.provider"; + + /// Type of LLM operation + /// Example: "chat", "completion", "embedding" + pub const OPERATION_TYPE: &str = "llm.operation_type"; + + /// Whether the request is streaming + pub const IS_STREAMING: &str = "llm.is_streaming"; + + /// Total bytes received in the response + pub const RESPONSE_BYTES: &str = "llm.response_bytes"; + + /// Duration of the LLM call in milliseconds + pub const DURATION_MS: &str = "llm.duration_ms"; + + /// Time to first token in milliseconds (streaming only) + pub const TIME_TO_FIRST_TOKEN_MS: &str = "llm.time_to_first_token"; + + /// Number of prompt tokens used + pub const PROMPT_TOKENS: &str = "llm.usage.prompt_tokens"; + + /// Number of completion tokens generated + pub const COMPLETION_TOKENS: &str = "llm.usage.completion_tokens"; + + /// Total tokens used (prompt + completion) + pub const TOTAL_TOKENS: &str = "llm.usage.total_tokens"; + + /// Temperature parameter used + pub const TEMPERATURE: &str = "llm.temperature"; + + /// Max tokens parameter used + pub const MAX_TOKENS: &str = "llm.max_tokens"; + + /// Top-p parameter used + pub const TOP_P: &str = "llm.top_p"; + + /// List of tool names provided in the request + pub const TOOLS: &str = "llm.tools"; + + /// Preview of the user message (truncated) + pub const USER_MESSAGE_PREVIEW: &str = "llm.user_message_preview"; +} + +// ============================================================================= +// Span Attributes - Routing & Gateway +// ============================================================================= + +/// Attributes specific to LLM routing and gateway operations +pub mod routing { + /// Strategy used to select the LLM endpoint + /// Example: "round-robin", "least-latency", "cost-optimized" + pub const STRATEGY: &str = "routing.strategy"; + + /// Selected upstream endpoint + pub const UPSTREAM_ENDPOINT: &str = "routing.upstream_endpoint"; + + /// Time taken to determine the route in milliseconds + pub const ROUTE_DETERMINATION_MS: &str = "routing.determination_ms"; + + /// Whether a fallback endpoint was used + pub const IS_FALLBACK: &str = "routing.is_fallback"; + + /// Reason for route selection + pub const SELECTION_REASON: &str = "routing.selection_reason"; +} + +// ============================================================================= +// Span Attributes - Error Handling +// ============================================================================= + +/// Attributes for error and exception tracking +pub mod error { + /// Whether an error occurred + pub const ERROR: &str = "error"; + + /// Type/class of the error + /// Example: "TimeoutError", "AuthenticationError" + pub const TYPE: &str = "error.type"; + + /// Error message + pub const MESSAGE: &str = "error.message"; + + /// Stack trace of the error + pub const STACK_TRACE: &str = "error.stack_trace"; +} + +// ============================================================================= +// Operation Names +// ============================================================================= + +/// Canonical operation name components for Arch Gateway +pub mod operation_component { + /// Inbound request handling + pub const INBOUND: &str = "plano(inbound)"; + + /// Routing decision phase + pub const ROUTING: &str = "plano(routing)"; + + /// Handoff to upstream service + pub const HANDOFF: &str = "plano(handoff)"; + + /// Agent filter execution + pub const AGENT_FILTER: &str = "plano(filter)"; + + /// Agent execution + pub const AGENT: &str = "plano(agent)"; + + /// LLM call + pub const LLM: &str = "plano(llm)"; +} + +/// Builder for constructing standardized operation names +/// +/// Format: `{method} {path} {target}` +/// +/// The operation component (e.g., "archgw(llm)") is now part of the service name, +/// so the operation name focuses on the HTTP request details and target. +/// +/// # Examples +/// ``` +/// use brightstaff::tracing::OperationNameBuilder; +/// +/// // LLM call operation: "POST /v1/chat/completions gpt-4" +/// // (service name will be "archgw(llm)") +/// let op = OperationNameBuilder::new() +/// .with_method("POST") +/// .with_path("/v1/chat/completions") +/// .with_target("gpt-4") +/// .build(); +/// +/// // Agent filter operation: "POST /agents/v1/chat/completions hallucination-detector" +/// // (service name will be "archgw(agent filter)") +/// let op = OperationNameBuilder::new() +/// .with_method("POST") +/// .with_path("/agents/v1/chat/completions") +/// .with_target("hallucination-detector") +/// .build(); +/// +/// // Routing operation: "POST /v1/chat/completions" +/// // (service name will be "archgw(routing)") +/// let op = OperationNameBuilder::new() +/// .with_method("POST") +/// .with_path("/v1/chat/completions") +/// .build(); +/// ``` +pub struct OperationNameBuilder { + method: Option, + path: Option, + operation: Option, + target: Option, +} + +impl OperationNameBuilder { + /// Create a new operation name builder + pub fn new() -> Self { + Self { + method: None, + path: None, + operation: None, + target: None, + } + } + + /// Set the HTTP method + /// + /// # Arguments + /// * `method` - HTTP method (e.g., "GET", "POST", "PUT") + pub fn with_method(mut self, method: impl Into) -> Self { + self.method = Some(method.into()); + self + } + + /// Set the request path + /// + /// # Arguments + /// * `path` - Request path (e.g., "/v1/chat/completions", "/agents/v1/chat/completions") + pub fn with_path(mut self, path: impl Into) -> Self { + self.path = Some(path.into()); + self + } + + /// Set the operation type (optional, for MCP operations) + /// + /// # Arguments + /// * `operation` - Operation type (e.g., "tool_call", "session_init", "notification") + pub fn with_operation(mut self, operation: impl Into) -> Self { + self.operation = Some(operation.into()); + self + } + + /// Set the target (model name, agent name, or filter name) + /// + /// # Arguments + /// * `target` - Target identifier (e.g., "gpt-4", "my-agent", "hallucination-detector") + pub fn with_target(mut self, target: impl Into) -> Self { + self.target = Some(target.into()); + self + } + + /// Build the operation name string + /// + /// # Format + /// - With all components: `{method} {path} ({operation}) {target}` + /// - Without operation: `{method} {path} {target}` + /// - Without target: `{method} {path}` + /// - Without path: `{method}` + /// - Empty: returns empty string + pub fn build(self) -> String { + let mut parts = Vec::new(); + + if let Some(method) = self.method { + parts.push(method); + } + + if let Some(path) = self.path { + if let Some(operation) = self.operation { + parts.push(format!("{} ({})", path, operation)); + } else { + parts.push(path); + } + } + + if let Some(target) = self.target { + parts.push(target); + } + + parts.join(" ") + } +} + +impl Default for OperationNameBuilder { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_operation_name_full() { + let op = OperationNameBuilder::new() + .with_method("POST") + .with_path("/v1/chat/completions") + .with_target("gpt-4") + .build(); + + assert_eq!(op, "POST /v1/chat/completions gpt-4"); + } + + #[test] + fn test_operation_name_no_target() { + let op = OperationNameBuilder::new() + .with_method("POST") + .with_path("/v1/chat/completions") + .build(); + + assert_eq!(op, "POST /v1/chat/completions"); + } + + #[test] + fn test_operation_name_agent_filter() { + let op = OperationNameBuilder::new() + .with_method("POST") + .with_path("/agents/v1/chat/completions") + .with_target("content-filter") + .build(); + + assert_eq!(op, "POST /agents/v1/chat/completions content-filter"); + } + + #[test] + fn test_operation_name_minimal() { + let op = OperationNameBuilder::new().build(); + assert_eq!(op, ""); + } +} diff --git a/crates/brightstaff/src/tracing/mod.rs b/crates/brightstaff/src/tracing/mod.rs new file mode 100644 index 00000000..bacc9571 --- /dev/null +++ b/crates/brightstaff/src/tracing/mod.rs @@ -0,0 +1,3 @@ +mod constants; + +pub use constants::{OperationNameBuilder, operation_component, http, llm, error, routing}; diff --git a/crates/build.sh b/crates/build.sh new file mode 100644 index 00000000..9c3e59b2 --- /dev/null +++ b/crates/build.sh @@ -0,0 +1 @@ +cargo build --release --target wasm32-wasip1 -p prompt_gateway -p llm_gateway && cargo build --release -p brightstaff diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index aa95e2e4..bcff9b32 100644 --- a/crates/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -14,13 +14,25 @@ derivative = "2.2.0" thiserror = "1.0.64" tiktoken-rs = "0.5.9" rand = "0.8.5" -serde_json = "1.0" +serde_json = { version = "1.0", features = ["preserve_order"] } hex = "0.4.3" urlencoding = "2.1.3" url = "2.5.4" hermesllm = { version = "0.1.0", path = "../hermesllm" } serde_with = "3.13.0" +# Optional dependencies for trace collection (not available in WASM) +tokio = { version = "1.44", features = ["sync", "time"], optional = true } +reqwest = { version = "0.12", features = ["json"], optional = true } +tracing = { version = "0.1", optional = true } + +[features] +default = [] +trace-collection = ["tokio", "reqwest", "tracing"] + [dev-dependencies] pretty_assertions = "1.4.1" serde_json = "1.0.64" +serial_test = "3.2" +axum = "0.7" +tokio = { version = "1.44", features = ["sync", "time", "macros", "rt"] } diff --git a/crates/common/src/api/open_ai.rs b/crates/common/src/api/open_ai.rs index 080923c1..951bfaf5 100644 --- a/crates/common/src/api/open_ai.rs +++ b/crates/common/src/api/open_ai.rs @@ -4,7 +4,6 @@ use crate::{ }; use core::{panic, str}; use serde::{ser::SerializeMap, Deserialize, Serialize}; -use serde_yaml::Value; use std::{ collections::{HashMap, VecDeque}, fmt::Display, @@ -265,7 +264,7 @@ pub struct ToolCall { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct FunctionCallDetail { pub name: String, - pub arguments: Option>, + pub arguments: String, } #[derive(Debug, Deserialize, Serialize)] diff --git a/crates/common/src/configuration.rs b/crates/common/src/configuration.rs index 27f8ebd9..0f8bc78f 100644 --- a/crates/common/src/configuration.rs +++ b/crates/common/src/configuration.rs @@ -21,8 +21,11 @@ pub struct ModelAlias { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Agent { pub id: String, - pub kind: Option, + pub transport: Option, + pub tool: Option, pub url: String, + #[serde(rename = "type")] + pub agent_type: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -41,6 +44,20 @@ pub struct Listener { pub port: u16, } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct StateStorageConfig { + #[serde(rename = "type")] + pub storage_type: StateStorageType, + pub connection_string: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "lowercase")] +pub enum StateStorageType { + Memory, + Postgres, +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Configuration { pub version: String, @@ -57,7 +74,9 @@ pub struct Configuration { pub mode: Option, pub routing: Option, pub agents: Option>, + pub filters: Option>, pub listeners: Vec, + pub state_storage: Option, } #[derive(Debug, Clone, Serialize, Deserialize, Default)] @@ -252,6 +271,39 @@ pub struct RoutingPreference { pub description: String, } +#[derive(Serialize, Deserialize, Debug)] +pub struct AgentUsagePreference { + pub model: String, + pub orchestration_preferences: Vec, +} + +/// OrchestrationPreference with custom serialization to always include default parameters. +/// The parameters field is always serialized as: +/// {"type": "object", "properties": {}, "required": []} +#[derive(Debug, Clone, Deserialize)] +pub struct OrchestrationPreference { + pub name: String, + pub description: String, +} + +impl serde::Serialize for OrchestrationPreference { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut state = serializer.serialize_struct("OrchestrationPreference", 3)?; + state.serialize_field("name", &self.name)?; + state.serialize_field("description", &self.description)?; + state.serialize_field("parameters", &serde_json::json!({ + "type": "object", + "properties": {}, + "required": [] + }))?; + state.end() + } +} + #[derive(Debug, Clone, Serialize, Deserialize)] //TODO: use enum for model, but if there is a new model, we need to update the code pub struct LlmProvider { diff --git a/crates/common/src/consts.rs b/crates/common/src/consts.rs index 13624d8d..ce34fad5 100644 --- a/crates/common/src/consts.rs +++ b/crates/common/src/consts.rs @@ -7,12 +7,13 @@ pub const ARCH_FC_REQUEST_TIMEOUT_MS: u64 = 30000; // 30 seconds pub const DEFAULT_TARGET_REQUEST_TIMEOUT_MS: u64 = 30000; // 30 seconds pub const API_REQUEST_TIMEOUT_MS: u64 = 30000; // 30 seconds pub const MODEL_SERVER_REQUEST_TIMEOUT_MS: u64 = 30000; // 30 seconds -pub const MODEL_SERVER_NAME: &str = "model_server"; +pub const MODEL_SERVER_NAME: &str = "bright_staff"; pub const ARCH_ROUTING_HEADER: &str = "x-arch-llm-provider"; pub const MESSAGES_KEY: &str = "messages"; pub const ARCH_PROVIDER_HINT_HEADER: &str = "x-arch-llm-provider-hint"; pub const ARCH_IS_STREAMING_HEADER: &str = "x-arch-streaming-request"; pub const CHAT_COMPLETIONS_PATH: &str = "/v1/chat/completions"; +pub const OPENAI_RESPONSES_API_PATH: &str = "/v1/responses"; pub const MESSAGES_PATH: &str = "/v1/messages"; pub const HEALTHZ_PATH: &str = "/healthz"; pub const X_ARCH_STATE_HEADER: &str = "x-arch-state"; @@ -31,3 +32,4 @@ pub const OTEL_COLLECTOR_HTTP: &str = "opentelemetry_collector_http"; pub const OTEL_POST_PATH: &str = "/v1/traces"; pub const LLM_ROUTE_HEADER: &str = "x-arch-llm-route"; pub const ENVOY_RETRY_HEADER: &str = "x-envoy-max-retries"; +pub const BRIGHT_STAFF_SERVICE_NAME : &str = "brightstaff"; diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs index 76c368f1..9c8f5787 100644 --- a/crates/common/src/lib.rs +++ b/crates/common/src/lib.rs @@ -11,4 +11,5 @@ pub mod routing; pub mod stats; pub mod tokenizer; pub mod tracing; +pub mod traces; pub mod utils; diff --git a/crates/common/src/routing.rs b/crates/common/src/routing.rs index f4baf896..8813c92d 100644 --- a/crates/common/src/routing.rs +++ b/crates/common/src/routing.rs @@ -40,8 +40,14 @@ pub fn get_llm_provider( let mut rng = thread_rng(); llm_providers .iter() + .filter(|(_, provider)| { + provider.model + .as_ref() + .map(|m| !m.starts_with("Arch")) + .unwrap_or(true) + }) .choose(&mut rng) - .expect("There should always be at least one llm provider") + .expect("There should always be at least one non-Arch llm provider") .1 .clone() } diff --git a/crates/common/src/traces/collector.rs b/crates/common/src/traces/collector.rs new file mode 100644 index 00000000..0fc407b2 --- /dev/null +++ b/crates/common/src/traces/collector.rs @@ -0,0 +1,285 @@ +use super::shapes::Span; +use super::resource_span_builder::ResourceSpanBuilder; +use std::collections::{HashMap, VecDeque}; +use std::sync::Arc; +use tokio::sync::Mutex; +use tokio::time::{interval, Duration}; +use tracing::{debug, error, warn}; + +/// Parse W3C traceparent header into trace_id and parent_span_id +/// Format: "00-{trace_id}-{parent_span_id}-01" +/// +/// Returns (trace_id, Option) +/// - parent_span_id is None if it's all zeros (0000000000000000), indicating a root span +pub fn parse_traceparent(traceparent: &str) -> (String, Option) { + let parts: Vec<&str> = traceparent.split('-').collect(); + if parts.len() == 4 { + let trace_id = parts[1].to_string(); + let parent_span_id = parts[2].to_string(); + + // If parent_span_id is all zeros, this is a root span with no parent + let parent = if parent_span_id == "0000000000000000" { + None + } else { + Some(parent_span_id) + }; + + (trace_id, parent) + } else { + warn!("Invalid traceparent format: {}", traceparent); + // Return empty trace ID and None for parent if parsing fails + (String::new(), None) + } +} + +/// Collects and batches spans, flushing them to an OTEL collector +/// +/// Supports multiple services, with each service (e.g., "archgw(routing)", "archgw(llm)") +/// maintaining its own span queue. Flushes all services together periodically. +/// +/// Tracing can be enabled/disabled in two ways: +/// 1. Via arch_config.yaml: presence of `tracing` configuration section +/// 2. Via environment variable: `OTEL_TRACING_ENABLED=true/false` +/// +/// When disabled, span recording and flushing are no-ops. +pub struct TraceCollector { + /// Spans grouped by service name + /// Key: service name (e.g., "archgw(routing)", "archgw(llm)") + /// Value: queue of spans for that service + spans_by_service: Arc>>>, + flush_interval: Duration, + otel_url: String, + /// Whether tracing is enabled + enabled: bool, +} + +impl TraceCollector { + /// Create a new trace collector + /// + /// # Arguments + /// * `enabled` - Whether tracing is enabled + /// - `Some(true)` - Force enable tracing + /// - `Some(false)` - Force disable tracing + /// - `None` - Check `OTEL_TRACING_ENABLED` env var (defaults to true if not set) + /// + /// Other parameters are read from environment variables: + /// - `TRACE_FLUSH_INTERVAL_MS` - Flush interval in milliseconds (default: 1000) + /// - `OTEL_COLLECTOR_URL` - OTEL collector endpoint (default: http://localhost:9903/v1/traces) + pub fn new(enabled: Option) -> Self { + let flush_interval_ms = std::env::var("TRACE_FLUSH_INTERVAL_MS") + .ok() + .and_then(|s| s.parse().ok()) + .unwrap_or(1000); + + let otel_url = std::env::var("OTEL_COLLECTOR_URL") + .unwrap_or_else(|_| "http://localhost:9903/v1/traces".to_string()); + + // Determine if tracing is enabled: + // 1. Use explicit parameter if provided + // 2. Otherwise check OTEL_TRACING_ENABLED env var + // 3. Default to false if neither is set (tracing opt-in, not opt-out) + let enabled = enabled.unwrap_or_else(|| { + std::env::var("OTEL_TRACING_ENABLED") + .ok() + .and_then(|s| s.parse().ok()) + .unwrap_or(false) + }); + + debug!( + "TraceCollector initialized: flush_interval={}ms, url={}, enabled={}", + flush_interval_ms, otel_url, enabled + ); + + Self { + spans_by_service: Arc::new(Mutex::new(HashMap::new())), + flush_interval: Duration::from_millis(flush_interval_ms), + otel_url, + enabled, + } + } + + /// Record a span for a specific service + /// + /// # Arguments + /// * `service_name` - Name of the service (e.g., "archgw(routing)", "archgw(llm)") + /// * `span` - The span to record + pub fn record_span(&self, service_name: impl Into, span: Span) { + // Skip recording if tracing is disabled + if !self.enabled { + return; + } + + let service_name = service_name.into(); + + // Use try_lock to avoid blocking in async contexts + // If the lock is held, we skip recording (telemetry shouldn't block the app) + if let Ok(mut spans_by_service) = self.spans_by_service.try_lock() { + // Get or create the queue for this service + let spans = spans_by_service + .entry(service_name) + .or_insert_with(VecDeque::new); + + spans.push_back(span); + } else { + // Lock contention - skip recording this span + debug!("Skipped span recording due to lock contention"); + } + // Flushing is handled by the periodic background flusher (see `start_background_flusher`). + } + + /// Flush all buffered spans to the OTEL collector + /// Builds ResourceSpans for each service with spans + pub async fn flush(&self) -> Result<(), Box> { + // Skip flushing if tracing is disabled + if !self.enabled { + return Ok(()); + } + + let mut spans_by_service = self.spans_by_service.lock().await; + + if spans_by_service.is_empty() { + return Ok(()); + } + + // Snapshot and drain all services' spans + let service_batches: Vec<(String, Vec)> = spans_by_service + .iter_mut() + .filter_map(|(service_name, spans)| { + if spans.is_empty() { + None + } else { + Some((service_name.clone(), spans.drain(..).collect())) + } + }) + .collect(); + + drop(spans_by_service); // Release lock before HTTP call + + if service_batches.is_empty() { + return Ok(()); + } + + let total_spans: usize = service_batches.iter().map(|(_, spans)| spans.len()).sum(); + debug!("Flushing {} spans across {} services to OTEL collector", total_spans, service_batches.len()); + + // Build canonical OTEL payload structure - one ResourceSpan per service + let resource_spans = self.build_resource_spans(service_batches); + + match self.send_to_otel(resource_spans).await { + Ok(_) => { + debug!("Successfully flushed {} spans", total_spans); + Ok(()) + } + Err(e) => { + warn!("Failed to send spans to OTEL collector: {:?}", e); + Err(e) + } + } + } + + /// Build OTEL-compliant resource spans from collected spans, one ResourceSpan per service + fn build_resource_spans(&self, service_batches: Vec<(String, Vec)>) -> Vec { + service_batches + .into_iter() + .map(|(service_name, spans)| { + ResourceSpanBuilder::new(&service_name) + .add_spans(spans) + .build() + }) + .collect() + } + + /// Send resource spans to OTEL collector + /// Serializes as {"resourceSpans": [...]} per OTEL spec + async fn send_to_otel( + &self, + resource_spans: Vec, + ) -> Result<(), Box> { + let client = reqwest::Client::new(); + + // Create OTEL payload with proper structure + let payload = serde_json::json!({ + "resourceSpans": resource_spans + }); + + let response = client + .post(&self.otel_url) + .header("Content-Type", "application/json") + .json(&payload) + .timeout(Duration::from_secs(5)) + .send() + .await?; + + if !response.status().is_success() { + warn!( + "OTEL collector returned non-success status: {}", + response.status() + ); + return Err(format!("OTEL collector error: {}", response.status()).into()); + } + + Ok(()) + } + + /// Start a background task that periodically flushes traces + /// Returns a join handle that can be used to stop the flusher + pub fn start_background_flusher(self: Arc) -> tokio::task::JoinHandle<()> { + let flush_interval = self.flush_interval; + + tokio::spawn(async move { + let mut ticker = interval(flush_interval); + + loop { + ticker.tick().await; + + if let Err(e) = self.flush().await { + error!("Background trace flush failed: {:?}", e); + } + } + }) + } + + /// Get current number of buffered spans across all services (for testing/monitoring) + pub async fn buffered_count(&self) -> usize { + self.spans_by_service + .lock() + .await + .values() + .map(|spans| spans.len()) + .sum() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::traces::SpanBuilder; + + #[tokio::test] + async fn test_collector_basic() { + let collector = TraceCollector::new(Some(true)); + + let span = SpanBuilder::new("test_operation") + .with_trace_id("abc123") + .build(); + + collector.record_span("test-service", span); + + assert_eq!(collector.buffered_count().await, 1); + } + + #[tokio::test] + async fn test_collector_auto_flush() { + // Since batch-triggered flush behavior was removed, record two spans and verify both are buffered + let collector = Arc::new(TraceCollector::new(Some(true))); + + let span1 = SpanBuilder::new("test1").build(); + let span2 = SpanBuilder::new("test2").build(); + + collector.record_span("test-service", span1); + collector.record_span("test-service", span2); + + // With no batch-triggered flush, both spans should remain buffered + assert_eq!(collector.buffered_count().await, 2); + } +} diff --git a/crates/common/src/traces/constants.rs b/crates/common/src/traces/constants.rs new file mode 100644 index 00000000..09bdecd5 --- /dev/null +++ b/crates/common/src/traces/constants.rs @@ -0,0 +1,27 @@ +/// OpenTelemetry semantic convention constants for tracing +/// +/// These constants ensure consistency across the codebase and prevent typos + +/// Resource attribute keys following OTEL semantic conventions +pub mod resource { + /// Logical name of the service + pub const SERVICE_NAME: &str = "service.name"; + + /// Version of the service + pub const SERVICE_VERSION: &str = "service.version"; + + /// Service namespace/environment + pub const SERVICE_NAMESPACE: &str = "service.namespace"; + + /// Service instance ID + pub const SERVICE_INSTANCE_ID: &str = "service.instance.id"; +} + +/// Instrumentation scope defaults +pub mod scope { + /// Default scope name for tracing instrumentation + pub const DEFAULT_NAME: &str = "brightstaff.tracing"; + + /// Default scope version + pub const DEFAULT_VERSION: &str = "1.0.0"; +} diff --git a/crates/common/src/traces/mod.rs b/crates/common/src/traces/mod.rs new file mode 100644 index 00000000..c4197995 --- /dev/null +++ b/crates/common/src/traces/mod.rs @@ -0,0 +1,26 @@ +// Original tracing types (OTEL structures) +mod shapes; +// New tracing utilities +mod span_builder; +mod resource_span_builder; +mod constants; + +#[cfg(feature = "trace-collection")] +mod collector; + +#[cfg(all(test, feature = "trace-collection"))] +mod tests; + +// Re-export original types +pub use shapes::{ + Span, Event, Traceparent, TraceparentNewError, + ResourceSpan, Resource, ScopeSpan, Scope, Attribute, AttributeValue, +}; + +// Re-export new utilities +pub use span_builder::{SpanBuilder, SpanKind, generate_random_span_id}; +pub use resource_span_builder::ResourceSpanBuilder; +pub use constants::*; + +#[cfg(feature = "trace-collection")] +pub use collector::{TraceCollector, parse_traceparent}; diff --git a/crates/common/src/traces/resource_span_builder.rs b/crates/common/src/traces/resource_span_builder.rs new file mode 100644 index 00000000..3e0dd88f --- /dev/null +++ b/crates/common/src/traces/resource_span_builder.rs @@ -0,0 +1,121 @@ +use super::shapes::{ResourceSpan, Resource, ScopeSpan, Scope, Span, Attribute, AttributeValue}; +use super::constants::{resource, scope}; +use std::collections::HashMap; + +/// Builder for creating OTEL ResourceSpan structures +/// +/// Provides a fluent API for building the resource/scope/span hierarchy +pub struct ResourceSpanBuilder { + service_name: String, + resource_attributes: HashMap, + scope_name: String, + scope_version: String, + spans: Vec, +} + +impl ResourceSpanBuilder { + /// Create a new ResourceSpan builder with service name + pub fn new(service_name: impl Into) -> Self { + Self { + service_name: service_name.into(), + resource_attributes: HashMap::new(), + scope_name: scope::DEFAULT_NAME.to_string(), + scope_version: scope::DEFAULT_VERSION.to_string(), + spans: Vec::new(), + } + } + + /// Add a resource attribute (e.g., deployment.environment, host.name) + pub fn with_resource_attribute(mut self, key: impl Into, value: impl Into) -> Self { + self.resource_attributes.insert(key.into(), value.into()); + self + } + + /// Set the instrumentation scope name + pub fn with_scope_name(mut self, name: impl Into) -> Self { + self.scope_name = name.into(); + self + } + + /// Set the instrumentation scope version + pub fn with_scope_version(mut self, version: impl Into) -> Self { + self.scope_version = version.into(); + self + } + + /// Add a single span + pub fn add_span(mut self, span: Span) -> Self { + self.spans.push(span); + self + } + + /// Add multiple spans + pub fn add_spans(mut self, spans: Vec) -> Self { + self.spans.extend(spans); + self + } + + /// Build the ResourceSpan + pub fn build(self) -> ResourceSpan { + // Build resource attributes + let mut attributes = vec![ + Attribute { + key: resource::SERVICE_NAME.to_string(), + value: AttributeValue { + string_value: Some(self.service_name), + }, + } + ]; + + // Add custom resource attributes + for (key, value) in self.resource_attributes { + attributes.push(Attribute { + key, + value: AttributeValue { + string_value: Some(value), + }, + }); + } + + let resource = Resource { attributes }; + + let scope = Scope { + name: self.scope_name, + version: self.scope_version, + attributes: Vec::new(), + }; + + let scope_span = ScopeSpan { + scope, + spans: self.spans, + }; + + ResourceSpan { + resource, + scope_spans: vec![scope_span], + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::traces::SpanBuilder; + + #[test] + fn test_resource_span_builder() { + let span1 = SpanBuilder::new("operation1").build(); + let span2 = SpanBuilder::new("operation2").build(); + + let resource_span = ResourceSpanBuilder::new("test-service") + .with_resource_attribute("deployment.environment", "production") + .with_scope_name("test-scope") + .add_span(span1) + .add_span(span2) + .build(); + + assert_eq!(resource_span.resource.attributes.len(), 2); // service.name + custom + assert_eq!(resource_span.scope_spans.len(), 1); + assert_eq!(resource_span.scope_spans[0].spans.len(), 2); + } +} diff --git a/crates/common/src/traces/shapes.rs b/crates/common/src/traces/shapes.rs new file mode 100644 index 00000000..5f521767 --- /dev/null +++ b/crates/common/src/traces/shapes.rs @@ -0,0 +1,123 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug)] +pub struct ResourceSpan { + pub resource: Resource, + #[serde(rename = "scopeSpans")] + pub scope_spans: Vec, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Resource { + pub attributes: Vec, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct ScopeSpan { + pub scope: Scope, + pub spans: Vec, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Scope { + pub name: String, + pub version: String, + pub attributes: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct Span { + #[serde(rename = "traceId")] + pub trace_id: String, + #[serde(rename = "spanId")] + pub span_id: String, + #[serde(rename = "parentSpanId")] + pub parent_span_id: Option, // Optional in case there's no parent span + pub name: String, + #[serde(rename = "startTimeUnixNano")] + pub start_time_unix_nano: String, + #[serde(rename = "endTimeUnixNano")] + pub end_time_unix_nano: String, + pub kind: u32, + pub attributes: Vec, + pub events: Option>, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct Event { + #[serde(rename = "timeUnixNano")] + pub time_unix_nano: String, + pub name: String, + pub attributes: Vec, +} + +impl Event { + pub fn new(name: String, time_unix_nano: u128) -> Self { + Event { + time_unix_nano: format!("{}", time_unix_nano), + name, + attributes: Vec::new(), + } + } + + pub fn add_attribute(&mut self, key: String, value: String) { + self.attributes.push(Attribute { + key, + value: AttributeValue { + string_value: Some(value), + }, + }); + } +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct Attribute { + pub key: String, + pub value: AttributeValue, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct AttributeValue { + #[serde(rename = "stringValue")] + pub string_value: Option, // Use Option to handle different value types +} + +pub struct Traceparent { + pub version: String, + pub trace_id: String, + pub parent_id: String, + pub flags: String, +} + +impl std::fmt::Display for Traceparent { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}-{}-{}-{}", + self.version, self.trace_id, self.parent_id, self.flags + ) + } +} + +#[derive(thiserror::Error, Debug)] +pub enum TraceparentNewError { + #[error("Invalid traceparent: \'{0}\'")] + InvalidTraceparent(String), +} + +impl TryFrom for Traceparent { + type Error = TraceparentNewError; + + fn try_from(traceparent: String) -> Result { + let traceparent_tokens: Vec<&str> = traceparent.split("-").collect::>(); + if traceparent_tokens.len() != 4 { + return Err(TraceparentNewError::InvalidTraceparent(traceparent)); + } + Ok(Traceparent { + version: traceparent_tokens[0].to_string(), + trace_id: traceparent_tokens[1].to_string(), + parent_id: traceparent_tokens[2].to_string(), + flags: traceparent_tokens[3].to_string(), + }) + } +} diff --git a/crates/common/src/traces/span_builder.rs b/crates/common/src/traces/span_builder.rs new file mode 100644 index 00000000..e07cfab9 --- /dev/null +++ b/crates/common/src/traces/span_builder.rs @@ -0,0 +1,200 @@ +use super::shapes::{Span, Attribute, AttributeValue}; +use std::collections::HashMap; +use std::time::SystemTime; + +/// OpenTelemetry span kinds +/// https://opentelemetry.io/docs/specs/otel/trace/api/#spankind +#[derive(Debug, Clone, Copy)] +pub enum SpanKind { + /// Default value. Indicates that the span represents an internal operation within an application + Internal = 0, + /// Indicates that the span describes a request to some remote service + Client = 3, +} + +/// Builder for creating OTEL-compliant spans with a fluent API +/// +/// This is the recommended way to create spans with proper trace context. +/// +/// # Example +/// ```no_run +/// use common::traces::{SpanBuilder, SpanKind}; +/// use std::time::SystemTime; +/// +/// let span = SpanBuilder::new("router_chat") +/// .with_trace_id("abc123") +/// .with_parent_span_id("parent456") +/// .with_kind(SpanKind::Internal) +/// .with_attribute("http.method", "POST") +/// .with_attribute("http.path", "/v1/chat/completions") +/// .build(); +/// ``` +pub struct SpanBuilder { + name: String, + trace_id: Option, + parent_span_id: Option, + start_time: SystemTime, + end_time: Option, + kind: SpanKind, + attributes: HashMap, + span_id: Option, +} + +impl SpanBuilder { + /// Create a new span builder + /// + /// # Arguments + /// * `name` - The operation name for this span (e.g., "router_chat", "determine_route") + pub fn new(name: impl Into) -> Self { + Self { + name: name.into(), + trace_id: None, + parent_span_id: None, + start_time: SystemTime::now(), + end_time: None, + kind: SpanKind::Internal, + attributes: HashMap::new(), + span_id: None, + } + } + + /// Set the trace ID (extracted from traceparent or OpenTelemetry context) + pub fn with_trace_id(mut self, trace_id: impl Into) -> Self { + self.trace_id = Some(trace_id.into()); + self + } + + pub fn with_span_id(mut self, span_id: impl Into) -> Self { + self.span_id = Some(span_id.into()); + self + } + + /// Set the parent span ID to link this span to its parent + pub fn with_parent_span_id(mut self, parent_span_id: impl Into) -> Self { + self.parent_span_id = Some(parent_span_id.into()); + self + } + + /// Set the span kind (defaults to Internal) + pub fn with_kind(mut self, kind: SpanKind) -> Self { + self.kind = kind; + self + } + + /// Set explicit start time (defaults to now) + pub fn with_start_time(mut self, start_time: SystemTime) -> Self { + self.start_time = start_time; + self + } + + /// Set explicit end time (defaults to build time) + pub fn with_end_time(mut self, end_time: SystemTime) -> Self { + self.end_time = Some(end_time); + self + } + + /// Add a single attribute to the span + pub fn with_attribute(mut self, key: impl Into, value: impl Into) -> Self { + self.attributes.insert(key.into(), value.into()); + self + } + + /// Add multiple attributes at once + pub fn with_attributes(mut self, attrs: HashMap) -> Self { + self.attributes.extend(attrs); + self + } + + /// Build the span, consuming the builder + /// + /// Creates a complete OTEL-compliant span with all provided attributes, + /// generating span_id and using provided or random trace_id. + pub fn build(self) -> Span { + let end_time = self.end_time.unwrap_or_else(SystemTime::now); + + let start_nanos = system_time_to_nanos(self.start_time); + let end_nanos = system_time_to_nanos(end_time); + + // Generate trace_id if not provided + let trace_id = self.trace_id.unwrap_or_else(|| generate_random_trace_id()); + + // Create attributes in OTEL format + let attributes: Vec = self.attributes + .into_iter() + .map(|(key, value)| Attribute { + key, + value: AttributeValue { + string_value: Some(value), + }, + }) + .collect(); + + // Build span directly without going through Span::new() + Span { + trace_id, + span_id: self.span_id.unwrap_or_else(|| generate_random_span_id()), + parent_span_id: self.parent_span_id, + name: self.name, + start_time_unix_nano: format!("{}", start_nanos), + end_time_unix_nano: format!("{}", end_nanos), + kind: self.kind as u32, + attributes, + events: None, + } + } +} + +/// Convert SystemTime to nanoseconds since UNIX epoch for OTEL +fn system_time_to_nanos(time: SystemTime) -> u128 { + time.duration_since(SystemTime::UNIX_EPOCH) + .unwrap_or_default() + .as_nanos() +} + +/// Generate a random span ID (16 hex characters = 8 bytes) +pub fn generate_random_span_id() -> String { + use rand::RngCore; + let mut rng = rand::thread_rng(); + let mut random_bytes = [0u8; 8]; + rng.fill_bytes(&mut random_bytes); + hex::encode(random_bytes) +} + +/// Generate a random trace ID (32 hex characters = 16 bytes) +fn generate_random_trace_id() -> String { + use rand::RngCore; + let mut rng = rand::thread_rng(); + let mut random_bytes = [0u8; 16]; + rng.fill_bytes(&mut random_bytes); + hex::encode(random_bytes) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_span_builder_basic() { + let span = SpanBuilder::new("test_operation") + .with_trace_id("abc123") + .with_parent_span_id("parent123") + .with_attribute("key", "value") + .build(); + + assert_eq!(span.name, "test_operation"); + assert_eq!(span.trace_id, "abc123"); + assert_eq!(span.parent_span_id, Some("parent123".to_string())); + assert_eq!(span.attributes.len(), 1); + } + + #[test] + fn test_span_builder_no_parent() { + let span = SpanBuilder::new("root_span") + .with_trace_id("xyz789") + .build(); + + assert_eq!(span.name, "root_span"); + assert_eq!(span.trace_id, "xyz789"); + assert_eq!(span.parent_span_id, None); + } +} diff --git a/crates/common/src/traces/tests/mock_otel_collector.rs b/crates/common/src/traces/tests/mock_otel_collector.rs new file mode 100644 index 00000000..8a154145 --- /dev/null +++ b/crates/common/src/traces/tests/mock_otel_collector.rs @@ -0,0 +1,101 @@ +//! Mock OTEL Collector for testing trace output +//! +//! This module provides a simple HTTP server that mimics an OTEL collector. +//! It exposes three endpoints: +//! - POST /v1/traces: Capture incoming OTLP JSON payloads +//! - GET /v1/traces: Return all captured payloads as JSON array +//! - DELETE /v1/traces: Clear all captured payloads +//! +//! Each test creates its own MockOtelCollector instance. + +use axum::{ + extract::State, + http::StatusCode, + routing::{delete, get, post}, + Json, Router, +}; +use serde_json::Value; +use std::sync::Arc; +use tokio::sync::RwLock; + +type SharedTraces = Arc>>; + +/// POST /v1/traces - capture incoming OTLP payload +async fn post_traces( + State(traces): State, + Json(payload): Json, +) -> StatusCode { + traces.write().await.push(payload); + StatusCode::OK +} + +/// GET /v1/traces - return all captured payloads +async fn get_traces(State(traces): State) -> Json> { + Json(traces.read().await.clone()) +} + +/// DELETE /v1/traces - clear all captured payloads +async fn delete_traces(State(traces): State) -> StatusCode { + traces.write().await.clear(); + StatusCode::NO_CONTENT +} + +/// Mock OTEL collector server +pub struct MockOtelCollector { + address: String, + client: reqwest::Client, + #[allow(dead_code)] + server_handle: tokio::task::JoinHandle<()>, +} + +impl MockOtelCollector { + /// Create and start a new mock collector on a random port + pub async fn start() -> Self { + let traces = Arc::new(RwLock::new(Vec::new())); + + let app = Router::new() + .route("/v1/traces", post(post_traces)) + .route("/v1/traces", get(get_traces)) + .route("/v1/traces", delete(delete_traces)) + .with_state(traces.clone()); + + let listener = tokio::net::TcpListener::bind("127.0.0.1:0") + .await + .expect("Failed to bind to random port"); + + let addr = listener.local_addr().expect("Failed to get local address"); + let address = format!("http://127.0.0.1:{}", addr.port()); + + let server_handle = tokio::spawn(async move { + axum::serve(listener, app) + .await + .expect("Server failed"); + }); + + // Give server a moment to start + tokio::time::sleep(tokio::time::Duration::from_millis(50)).await; + + Self { + address, + client: reqwest::Client::new(), + server_handle, + } + } + + /// Get the address of the collector + pub fn address(&self) -> &str { + &self.address + } + + /// GET /v1/traces - fetch all captured payloads + pub async fn get_traces(&self) -> Vec { + self.client + .get(format!("{}/v1/traces", self.address)) + .send() + .await + .expect("Failed to GET traces") + .json() + .await + .expect("Failed to parse traces JSON") + } +} diff --git a/crates/common/src/traces/tests/mod.rs b/crates/common/src/traces/tests/mod.rs new file mode 100644 index 00000000..7bba42f8 --- /dev/null +++ b/crates/common/src/traces/tests/mod.rs @@ -0,0 +1,4 @@ +mod mock_otel_collector; +mod trace_integration_test; + +pub use mock_otel_collector::MockOtelCollector; diff --git a/crates/common/src/traces/tests/trace_integration_test.rs b/crates/common/src/traces/tests/trace_integration_test.rs new file mode 100644 index 00000000..a3c8a6ba --- /dev/null +++ b/crates/common/src/traces/tests/trace_integration_test.rs @@ -0,0 +1,304 @@ +//! Integration tests for OpenTelemetry tracing in router.rs +//! +//! These tests validate that the spans created for LLM requests contain +//! all expected attributes and events by checking the raw JSON payloads +//! sent to the mock OTEL collector. +//! +//! ## Test Design +//! Each test creates its own MockOtelCollector and TraceCollector: +//! 1. Start MockOtelCollector on random port +//! 2. Create TraceCollector with 500ms flush interval +//! 3. Record spans using TraceCollector +//! 4. Flush and wait (500ms + 200ms buffer = 700ms total) for spans to arrive +//! 5. Get raw JSON payloads (GET /v1/traces) and validate structure +//! 6. Test cleanup happens automatically when collectors are dropped +//! +//! ## Serial Execution +//! Tests use the `#[serial]` attribute to run sequentially because they +//! use global environment variables (OTEL_COLLECTOR_URL, OTEL_TRACING_ENABLED, +//! TRACE_FLUSH_INTERVAL_MS). This ensures test isolation without requiring +//! the `--test-threads=1` command line flag. + +const FLUSH_INTERVAL_MS: u64 = 50; +const FLUSH_BUFFER_MS: u64 = 50; +const TOTAL_WAIT_MS: u64 = FLUSH_INTERVAL_MS + FLUSH_BUFFER_MS; + +use crate::traces::{SpanBuilder, SpanKind, TraceCollector}; +use serde_json::Value; +use serial_test::serial; +use std::sync::Arc; + +use super::MockOtelCollector; + +/// Helper to extract all spans from OTLP JSON payloads +fn extract_spans(payloads: &[Value]) -> Vec<&Value> { + let mut spans = Vec::new(); + for payload in payloads { + if let Some(resource_spans) = payload.get("resourceSpans").and_then(|v| v.as_array()) { + for resource_span in resource_spans { + if let Some(scope_spans) = resource_span.get("scopeSpans").and_then(|v| v.as_array()) { + for scope_span in scope_spans { + if let Some(span_list) = scope_span.get("spans").and_then(|v| v.as_array()) { + spans.extend(span_list.iter()); + } + } + } + } + } + } + spans +} + +/// Helper to get string attribute value from a span +fn get_string_attr<'a>(span: &'a Value, key: &str) -> Option<&'a str> { + span.get("attributes") + .and_then(|attrs| attrs.as_array()) + .and_then(|attrs| { + attrs.iter().find(|attr| { + attr.get("key").and_then(|k| k.as_str()) == Some(key) + }) + }) + .and_then(|attr| attr.get("value")) + .and_then(|v| v.get("stringValue")) + .and_then(|v| v.as_str()) +} + +#[tokio::test] +#[serial] +async fn test_llm_span_contains_basic_attributes() { + // Start mock OTEL collector + let mock_collector = MockOtelCollector::start().await; + + // Create TraceCollector pointing to mock with 500ms flush intervalc + std::env::set_var("OTEL_COLLECTOR_URL", format!("{}/v1/traces", mock_collector.address())); + std::env::set_var("OTEL_TRACING_ENABLED", "true"); + std::env::set_var("TRACE_FLUSH_INTERVAL_MS", "500"); + let trace_collector = Arc::new(TraceCollector::new(Some(true))); + + // Create a test span simulating router.rs behavior + let span = SpanBuilder::new("POST /v1/chat/completions >> /v1/chat/completions") + .with_kind(SpanKind::Client) + .with_trace_id("test-trace-123") + .with_attribute("http.method", "POST") + .with_attribute("http.target", "/v1/chat/completions") + .with_attribute("http.upstream_target", "/v1/chat/completions") + .with_attribute("llm.model", "gpt-4o") + .with_attribute("llm.provider", "openai") + .with_attribute("llm.is_streaming", "true") + .with_attribute("llm.temperature", "0.7") + .build(); + + trace_collector.record_span("archgw(llm)", span); + + // Flush and wait for spans to arrive (500ms flush interval + 200ms buffer) + trace_collector.flush().await.expect("Failed to flush"); + tokio::time::sleep(tokio::time::Duration::from_millis(TOTAL_WAIT_MS)).await; + + let payloads = mock_collector.get_traces().await; + let spans = extract_spans(&payloads); + + assert_eq!(spans.len(), 1, "Expected exactly one span"); + + let span = spans[0]; + // Validate HTTP attributes + assert_eq!(get_string_attr(span, "http.method"), Some("POST")); + assert_eq!(get_string_attr(span, "http.target"), Some("/v1/chat/completions")); + + // Validate LLM attributes + assert_eq!(get_string_attr(span, "llm.model"), Some("gpt-4o")); + assert_eq!(get_string_attr(span, "llm.provider"), Some("openai")); + assert_eq!(get_string_attr(span, "llm.is_streaming"), Some("true")); + assert_eq!(get_string_attr(span, "llm.temperature"), Some("0.7")); +} + +#[tokio::test] +#[serial] +async fn test_llm_span_contains_tool_information() { + let mock_collector = MockOtelCollector::start().await; + std::env::set_var("OTEL_COLLECTOR_URL", format!("{}/v1/traces", mock_collector.address())); + std::env::set_var("OTEL_TRACING_ENABLED", "true"); + std::env::set_var("TRACE_FLUSH_INTERVAL_MS", "500"); + let trace_collector = Arc::new(TraceCollector::new(Some(true))); + + let tools_formatted = "get_weather(...)\nsearch_web(...)\ncalculate(...)"; + + let span = SpanBuilder::new("POST /v1/chat/completions") + .with_trace_id("test-trace-tools") + .with_attribute("llm.request.tools", tools_formatted) + .with_attribute("llm.model", "gpt-4o") + .build(); + + trace_collector.record_span("archgw(llm)", span); + trace_collector.flush().await.expect("Failed to flush"); + tokio::time::sleep(tokio::time::Duration::from_millis(TOTAL_WAIT_MS)).await; + + let payloads = mock_collector.get_traces().await; + let spans = extract_spans(&payloads); + + assert!(!spans.is_empty(), "No spans captured"); + + let span = spans[0]; + let tools = get_string_attr(span, "llm.request.tools"); + + assert!(tools.is_some(), "Tools attribute missing"); + assert!(tools.unwrap().contains("get_weather(...)")); + assert!(tools.unwrap().contains("search_web(...)")); + assert!(tools.unwrap().contains("calculate(...)")); + assert!(tools.unwrap().contains('\n'), "Tools should be newline-separated"); +} + +#[tokio::test] +#[serial] +async fn test_llm_span_contains_user_message_preview() { + let mock_collector = MockOtelCollector::start().await; + std::env::set_var("OTEL_COLLECTOR_URL", format!("{}/v1/traces", mock_collector.address())); + std::env::set_var("OTEL_TRACING_ENABLED", "true"); + std::env::set_var("TRACE_FLUSH_INTERVAL_MS", "500"); + let trace_collector = Arc::new(TraceCollector::new(Some(true))); + + let long_message = "This is a very long user message that should be truncated to 50 characters in the span"; + let preview = if long_message.len() > 50 { + format!("{}...", &long_message[..50]) + } else { + long_message.to_string() + }; + + let span = SpanBuilder::new("POST /v1/messages") + .with_trace_id("test-trace-preview") + .with_attribute("llm.request.user_message_preview", &preview) + .build(); + + trace_collector.record_span("archgw(llm)", span); + trace_collector.flush().await.expect("Failed to flush"); + tokio::time::sleep(tokio::time::Duration::from_millis(TOTAL_WAIT_MS)).await; + + let payloads = mock_collector.get_traces().await; + let spans = extract_spans(&payloads); + let span = spans[0]; + + let message_preview = get_string_attr(span, "llm.request.user_message_preview"); + + assert!(message_preview.is_some()); + assert!(message_preview.unwrap().len() <= 53); // 50 chars + "..." + assert!(message_preview.unwrap().contains("...")); +} + +#[tokio::test] +#[serial] +async fn test_llm_span_contains_time_to_first_token() { + let mock_collector = MockOtelCollector::start().await; + std::env::set_var("OTEL_COLLECTOR_URL", format!("{}/v1/traces", mock_collector.address())); + std::env::set_var("OTEL_TRACING_ENABLED", "true"); + std::env::set_var("TRACE_FLUSH_INTERVAL_MS", "500"); + let trace_collector = Arc::new(TraceCollector::new(Some(true))); + + let ttft_ms = "245"; // milliseconds as string + + let span = SpanBuilder::new("POST /v1/chat/completions") + .with_trace_id("test-trace-ttft") + .with_attribute("llm.is_streaming", "true") + .with_attribute("llm.time_to_first_token_ms", ttft_ms) + .build(); + + trace_collector.record_span("archgw(llm)", span); + trace_collector.flush().await.expect("Failed to flush"); + tokio::time::sleep(tokio::time::Duration::from_millis(TOTAL_WAIT_MS)).await; + + let payloads = mock_collector.get_traces().await; + let spans = extract_spans(&payloads); + let span = spans[0]; + + // Check TTFT attribute + let ttft_attr = get_string_attr(span, "llm.time_to_first_token_ms"); + assert_eq!(ttft_attr, Some("245")); +} + +#[tokio::test] +#[serial] +async fn test_llm_span_contains_upstream_path() { + let mock_collector = MockOtelCollector::start().await; + std::env::set_var("OTEL_COLLECTOR_URL", format!("{}/v1/traces", mock_collector.address())); + std::env::set_var("OTEL_TRACING_ENABLED", "true"); + std::env::set_var("TRACE_FLUSH_INTERVAL_MS", "500"); + let trace_collector = Arc::new(TraceCollector::new(Some(true))); + + // Test Zhipu provider with path transformation + let span = SpanBuilder::new("POST /v1/chat/completions >> /api/paas/v4/chat/completions") + .with_trace_id("test-trace-upstream") + .with_attribute("http.upstream_target", "/api/paas/v4/chat/completions") + .with_attribute("llm.provider", "zhipu") + .with_attribute("llm.model", "glm-4") + .build(); + + trace_collector.record_span("archgw(llm)", span); + trace_collector.flush().await.expect("Failed to flush"); + tokio::time::sleep(tokio::time::Duration::from_millis(TOTAL_WAIT_MS)).await; + + let payloads = mock_collector.get_traces().await; + let spans = extract_spans(&payloads); + let span = spans[0]; + + // Operation name should show the transformation + let name = span.get("name").and_then(|v| v.as_str()); + assert!(name.is_some()); + assert!(name.unwrap().contains(">>"), "Operation name should show path transformation"); + + // Check upstream target attribute + let upstream = get_string_attr(span, "http.upstream_target"); + assert_eq!(upstream, Some("/api/paas/v4/chat/completions")); +} + +#[tokio::test] +#[serial] +async fn test_llm_span_multiple_services() { + let mock_collector = MockOtelCollector::start().await; + std::env::set_var("OTEL_COLLECTOR_URL", format!("{}/v1/traces", mock_collector.address())); + std::env::set_var("OTEL_TRACING_ENABLED", "true"); + std::env::set_var("TRACE_FLUSH_INTERVAL_MS", "500"); + let trace_collector = Arc::new(TraceCollector::new(Some(true))); + + // Create spans for different services + let llm_span = SpanBuilder::new("LLM Request") + .with_trace_id("test-multi") + .with_attribute("service", "llm") + .build(); + + let routing_span = SpanBuilder::new("Routing Decision") + .with_trace_id("test-multi") + .with_attribute("service", "routing") + .build(); + + trace_collector.record_span("archgw(llm)", llm_span); + trace_collector.record_span("archgw(routing)", routing_span); + trace_collector.flush().await.expect("Failed to flush"); + tokio::time::sleep(tokio::time::Duration::from_millis(TOTAL_WAIT_MS)).await; + + let payloads = mock_collector.get_traces().await; + let all_spans = extract_spans(&payloads); + + assert_eq!(all_spans.len(), 2, "Should have captured both spans"); +} + +#[tokio::test] +#[serial] +async fn test_tracing_disabled_produces_no_spans() { + let mock_collector = MockOtelCollector::start().await; + + // Create TraceCollector with tracing DISABLED + std::env::set_var("OTEL_COLLECTOR_URL", format!("{}/v1/traces", mock_collector.address())); + std::env::set_var("OTEL_TRACING_ENABLED", "false"); + std::env::set_var("TRACE_FLUSH_INTERVAL_MS", "500"); + let trace_collector = Arc::new(TraceCollector::new(Some(false))); + + let span = SpanBuilder::new("Test Span") + .with_trace_id("test-disabled") + .build(); + + trace_collector.record_span("archgw(llm)", span); + trace_collector.flush().await.ok(); // Should be no-op when disabled + tokio::time::sleep(tokio::time::Duration::from_millis(TOTAL_WAIT_MS)).await; + + let payloads = mock_collector.get_traces().await; + let all_spans = extract_spans(&payloads); + assert_eq!(all_spans.len(), 0, "No spans should be captured when tracing is disabled"); +} diff --git a/crates/hermesllm/Cargo.toml b/crates/hermesllm/Cargo.toml index ab2390bf..d877fc00 100644 --- a/crates/hermesllm/Cargo.toml +++ b/crates/hermesllm/Cargo.toml @@ -10,3 +10,5 @@ serde_with = {version = "3.12.0", features = ["base64"]} thiserror = "2.0.12" aws-smithy-eventstream = "0.60" bytes = "1.10" +uuid = { version = "1.11", features = ["v4"] } +log = "0.4" diff --git a/crates/hermesllm/src/apis/amazon_bedrock.rs b/crates/hermesllm/src/apis/amazon_bedrock.rs index eb1f3ddf..3c953850 100644 --- a/crates/hermesllm/src/apis/amazon_bedrock.rs +++ b/crates/hermesllm/src/apis/amazon_bedrock.rs @@ -7,7 +7,7 @@ use thiserror::Error; use super::ApiDefinition; use crate::providers::request::{ProviderRequest, ProviderRequestError}; -use crate::providers::response::ProviderStreamResponse; +use crate::providers::streaming_response::ProviderStreamResponse; // ============================================================================ // AMAZON BEDROCK CONVERSE API ENUMERATION @@ -200,6 +200,17 @@ impl ProviderRequest for ConverseRequest { }) } + fn get_tool_names(&self) -> Option> { + self.tool_config.as_ref()?.tools.as_ref().map(|tools| { + tools + .iter() + .filter_map(|tool| match tool { + Tool::ToolSpec { tool_spec } => Some(tool_spec.name.clone()), + }) + .collect() + }) + } + fn to_bytes(&self) -> Result, ProviderRequestError> { serde_json::to_vec(self).map_err(|e| ProviderRequestError { message: format!("Failed to serialize Bedrock request: {}", e), @@ -218,6 +229,108 @@ impl ProviderRequest for ConverseRequest { false } } + + fn get_temperature(&self) -> Option { + self.inference_config.as_ref()?.temperature + } + + fn get_messages(&self) -> Vec { + use crate::apis::openai::{Message, MessageContent, Role}; + + let mut openai_messages = Vec::new(); + + // Add system messages if present + if let Some(system) = &self.system { + for sys_block in system { + match sys_block { + SystemContentBlock::Text { text } => { + openai_messages.push(Message { + role: Role::System, + content: MessageContent::Text(text.clone()), + name: None, + tool_calls: None, + tool_call_id: None, + }); + } + _ => {} // Skip other system content types + } + } + } + + // Convert conversation messages + if let Some(messages) = &self.messages { + for msg in messages { + let role = match msg.role { + ConversationRole::User => Role::User, + ConversationRole::Assistant => Role::Assistant, + }; + + // Extract text from content blocks + let content = msg.content.iter() + .filter_map(|block| { + if let ContentBlock::Text { text } = block { + Some(text.clone()) + } else { + None + } + }) + .collect::>() + .join("\n"); + + openai_messages.push(Message { + role, + content: MessageContent::Text(content), + name: None, + tool_calls: None, + tool_call_id: None, + }); + } + } + + openai_messages + } + + fn set_messages(&mut self, messages: &[crate::apis::openai::Message]) { + // Convert OpenAI messages to Bedrock format + use crate::apis::amazon_bedrock::{ContentBlock, ConversationRole, SystemContentBlock}; + + let mut system_blocks = Vec::new(); + let mut bedrock_messages = Vec::new(); + + for msg in messages { + match msg.role { + crate::apis::openai::Role::System => { + if let crate::apis::openai::MessageContent::Text(text) = &msg.content { + system_blocks.push(SystemContentBlock::Text { text: text.clone() }); + } + } + crate::apis::openai::Role::User | crate::apis::openai::Role::Assistant => { + let role = match msg.role { + crate::apis::openai::Role::User => ConversationRole::User, + crate::apis::openai::Role::Assistant => ConversationRole::Assistant, + _ => continue, + }; + + let content = if let crate::apis::openai::MessageContent::Text(text) = &msg.content { + vec![ContentBlock::Text { text: text.clone() }] + } else { + vec![] + }; + + bedrock_messages.push(crate::apis::amazon_bedrock::Message { + role, + content, + }); + } + _ => {} + } + } + + if !system_blocks.is_empty() { + self.system = Some(system_blocks); + } + self.messages = Some(bedrock_messages); + } } // ============================================================================ diff --git a/crates/hermesllm/src/apis/anthropic.rs b/crates/hermesllm/src/apis/anthropic.rs index f91b381c..2e73e1c2 100644 --- a/crates/hermesllm/src/apis/anthropic.rs +++ b/crates/hermesllm/src/apis/anthropic.rs @@ -6,7 +6,8 @@ use std::collections::HashMap; use super::ApiDefinition; use crate::providers::request::{ProviderRequest, ProviderRequestError}; -use crate::providers::response::{ProviderResponse, ProviderStreamResponse}; +use crate::providers::response::ProviderResponse; +use crate::providers::streaming_response::ProviderStreamResponse; use crate::transforms::lib::ExtractText; use crate::MESSAGES_PATH; @@ -397,6 +398,8 @@ pub enum MessagesContentDelta { InputJsonDelta { partial_json: String }, #[serde(rename = "thinking_delta")] ThinkingDelta { thinking: String }, + #[serde(rename = "signature_delta")] + SignatureDelta { signature: String }, } #[skip_serializing_none] @@ -512,6 +515,12 @@ impl ProviderRequest for MessagesRequest { None } + fn get_tool_names(&self) -> Option> { + self.tools.as_ref().map(|tools| { + tools.iter().map(|tool| tool.name.clone()).collect() + }) + } + fn to_bytes(&self) -> Result, ProviderRequestError> { serde_json::to_vec(self).map_err(|e| ProviderRequestError { message: format!("Failed to serialize MessagesRequest: {}", e), @@ -530,6 +539,69 @@ impl ProviderRequest for MessagesRequest { false } } + + fn get_temperature(&self) -> Option { + self.temperature + } + + fn get_messages(&self) -> Vec { + use crate::apis::openai::Message; + + let mut openai_messages = Vec::new(); + + // Add system prompt as system message if present + if let Some(system) = &self.system { + openai_messages.push(system.clone().into()); + } + + // Convert each Anthropic message to OpenAI format + for msg in &self.messages { + if let Ok(converted_msgs) = TryInto::>::try_into(msg.clone()) { + openai_messages.extend(converted_msgs); + } + } + + openai_messages + } + + fn set_messages(&mut self, messages: &[crate::apis::openai::Message]) { + // Convert OpenAI messages to Anthropic format + // Separate system messages from regular messages + let mut system_messages = Vec::new(); + let mut regular_messages = Vec::new(); + + for msg in messages { + if msg.role == crate::apis::openai::Role::System { + system_messages.push(msg.clone()); + } else { + regular_messages.push(msg.clone()); + } + } + + // Set system prompt if there are system messages + if !system_messages.is_empty() { + // Combine all system messages into one + let system_text = system_messages.iter() + .filter_map(|msg| { + if let crate::apis::openai::MessageContent::Text(text) = &msg.content { + Some(text.as_str()) + } else { + None + } + }) + .collect::>() + .join("\n"); + + self.system = Some(crate::apis::anthropic::MessagesSystemPrompt::Single(system_text)); + } + + // Convert regular messages + self.messages = regular_messages.iter() + .filter_map(|msg| { + msg.clone().try_into().ok() + }) + .collect(); + } } impl MessagesResponse { diff --git a/crates/hermesllm/src/apis/mod.rs b/crates/hermesllm/src/apis/mod.rs index 7d84e3ab..ea056392 100644 --- a/crates/hermesllm/src/apis/mod.rs +++ b/crates/hermesllm/src/apis/mod.rs @@ -1,8 +1,8 @@ pub mod amazon_bedrock; -pub mod amazon_bedrock_binary_frame; pub mod anthropic; pub mod openai; -pub mod sse; +pub mod openai_responses; +pub mod streaming_shapes; // Explicit exports to avoid naming conflicts pub use amazon_bedrock::{AmazonBedrockApi, ConverseRequest, ConverseStreamRequest}; @@ -88,8 +88,9 @@ mod tests { fn test_all_variants_method() { // Test that all_variants returns the expected variants let openai_variants = OpenAIApi::all_variants(); - assert_eq!(openai_variants.len(), 1); + assert_eq!(openai_variants.len(), 2); assert!(openai_variants.contains(&OpenAIApi::ChatCompletions)); + assert!(openai_variants.contains(&OpenAIApi::Responses)); let anthropic_variants = AnthropicApi::all_variants(); assert_eq!(anthropic_variants.len(), 1); diff --git a/crates/hermesllm/src/apis/openai.rs b/crates/hermesllm/src/apis/openai.rs index 82c5d1a1..79154d39 100644 --- a/crates/hermesllm/src/apis/openai.rs +++ b/crates/hermesllm/src/apis/openai.rs @@ -7,9 +7,10 @@ use thiserror::Error; use super::ApiDefinition; use crate::providers::request::{ProviderRequest, ProviderRequestError}; -use crate::providers::response::{ProviderResponse, ProviderStreamResponse, TokenUsage}; +use crate::providers::response::{ProviderResponse, TokenUsage}; +use crate::providers::streaming_response::ProviderStreamResponse; use crate::transforms::lib::ExtractText; -use crate::CHAT_COMPLETIONS_PATH; +use crate::{CHAT_COMPLETIONS_PATH, OPENAI_RESPONSES_API_PATH}; // ============================================================================ // OPENAI API ENUMERATION @@ -19,6 +20,7 @@ use crate::CHAT_COMPLETIONS_PATH; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum OpenAIApi { ChatCompletions, + Responses, // Future APIs can be added here: // Embeddings, // FineTuning, @@ -29,12 +31,14 @@ impl ApiDefinition for OpenAIApi { fn endpoint(&self) -> &'static str { match self { OpenAIApi::ChatCompletions => CHAT_COMPLETIONS_PATH, + OpenAIApi::Responses => OPENAI_RESPONSES_API_PATH, } } fn from_endpoint(endpoint: &str) -> Option { match endpoint { CHAT_COMPLETIONS_PATH => Some(OpenAIApi::ChatCompletions), + OPENAI_RESPONSES_API_PATH => Some(OpenAIApi::Responses), _ => None, } } @@ -42,23 +46,26 @@ impl ApiDefinition for OpenAIApi { fn supports_streaming(&self) -> bool { match self { OpenAIApi::ChatCompletions => true, + OpenAIApi::Responses => true, } } fn supports_tools(&self) -> bool { match self { OpenAIApi::ChatCompletions => true, + OpenAIApi::Responses => true, } } fn supports_vision(&self) -> bool { match self { OpenAIApi::ChatCompletions => true, + OpenAIApi::Responses => true, } } fn all_variants() -> Vec { - vec![OpenAIApi::ChatCompletions] + vec![OpenAIApi::ChatCompletions, OpenAIApi::Responses] } } @@ -101,6 +108,12 @@ pub struct ChatCompletionsRequest { pub top_logprobs: Option, pub user: Option, // pub web_search: Option, // GOOD FIRST ISSUE: Future support for web search + + // VLLM-specific parameters (used by Arch-Function) + pub top_k: Option, + pub stop_token_ids: Option>, + pub continue_final_message: Option, + pub add_generation_prompt: Option, } impl ChatCompletionsRequest { @@ -385,6 +398,8 @@ pub struct ChatCompletionsResponse { pub usage: Usage, pub system_fingerprint: Option, pub service_tier: Option, + // This isn't a standard OpenAI field, but we include it for extensibility + pub metadata: Option>, } impl Default for ChatCompletionsResponse { @@ -398,6 +413,7 @@ impl Default for ChatCompletionsResponse { usage: Usage::default(), system_fingerprint: None, service_tier: None, + metadata: None, } } } @@ -671,6 +687,32 @@ impl ProviderRequest for ChatCompletionsRequest { }) } + fn get_tool_names(&self) -> Option> { + // First check the 'tools' field (current API) + if let Some(tools) = &self.tools { + let names: Vec = tools + .iter() + .map(|tool| tool.function.name.clone()) + .collect(); + if !names.is_empty() { + return Some(names); + } + } + + // Fallback to 'functions' field (deprecated but still supported) + if let Some(functions) = &self.functions { + let names: Vec = functions + .iter() + .map(|func| func.function.name.clone()) + .collect(); + if !names.is_empty() { + return Some(names); + } + } + + None + } + fn to_bytes(&self) -> Result, ProviderRequestError> { serde_json::to_vec(&self).map_err(|e| ProviderRequestError { message: format!("Failed to serialize OpenAI request: {}", e), @@ -689,6 +731,18 @@ impl ProviderRequest for ChatCompletionsRequest { false } } + + fn get_temperature(&self) -> Option { + self.temperature + } + + fn get_messages(&self) -> Vec { + self.messages.clone() + } + + fn set_messages(&mut self, messages: &[crate::apis::openai::Message]) { + self.messages = messages.to_vec(); + } } /// Implementation of ProviderResponse for ChatCompletionsResponse @@ -1068,8 +1122,9 @@ mod tests { // Test all_variants let all_variants = OpenAIApi::all_variants(); - assert_eq!(all_variants.len(), 1); - assert_eq!(all_variants[0], OpenAIApi::ChatCompletions); + assert_eq!(all_variants.len(), 2); + assert!(all_variants.contains(&OpenAIApi::ChatCompletions)); + assert!(all_variants.contains(&OpenAIApi::Responses)); } #[test] diff --git a/crates/hermesllm/src/apis/openai_responses.rs b/crates/hermesllm/src/apis/openai_responses.rs new file mode 100644 index 00000000..6afe9f09 --- /dev/null +++ b/crates/hermesllm/src/apis/openai_responses.rs @@ -0,0 +1,1573 @@ +use std::collections::HashMap; +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; +use crate::providers::request::{ProviderRequest, ProviderRequestError}; + +impl TryFrom<&[u8]> for ResponsesAPIRequest { + type Error = serde_json::Error; + + fn try_from(bytes: &[u8]) -> Result { + serde_json::from_slice(bytes) + } +} + +/// Parameterized conversion for ResponsesAPIResponse +impl TryFrom<&[u8]> for ResponsesAPIResponse { + type Error = crate::apis::openai::OpenAIStreamError; + + fn try_from(bytes: &[u8]) -> Result { + serde_json::from_slice(bytes).map_err(crate::apis::openai::OpenAIStreamError::from) + } +} + +// ============================================================================ +// Request Structs - CreateResponse +// ============================================================================ + +/// Request to create a model response +#[skip_serializing_none] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ResponsesAPIRequest { + /// The model to use for generating the response + pub model: String, + + /// Text, image, or file inputs to the model + pub input: InputParam, + + /// Specify additional output data to include in the model response + pub include: Option>, + + /// Whether to allow the model to run tool calls in parallel + pub parallel_tool_calls: Option, + + /// Whether to store the generated model response for later retrieval via API + pub store: Option, + + /// A system (or developer) message inserted into the model's context + pub instructions: Option, + + /// If set to true, the model response data will be streamed to the client + pub stream: Option, + + /// Stream options configuration + pub stream_options: Option, + + /// Conversation state + pub conversation: Option, + + /// Tools available to the model + pub tools: Option>, + + /// Tool choice option + pub tool_choice: Option, + + /// Maximum number of output tokens + pub max_output_tokens: Option, + + /// Temperature for sampling (0-2) + pub temperature: Option, + + /// Top-p nucleus sampling parameter + pub top_p: Option, + + /// Metadata for the response + pub metadata: Option>, + + /// Previous response ID for conversation continuation + pub previous_response_id: Option, + + /// Response modalities + pub modalities: Option>, + + /// Audio output configuration + pub audio: Option, + + /// Text output format configuration + pub text: Option, + + /// Reasoning effort level + pub reasoning_effort: Option, + + /// Truncation strategy + pub truncation: Option, + + /// User identifier + pub user: Option, + + /// Maximum number of tool calls + pub max_tool_calls: Option, + + /// Service tier + pub service_tier: Option, + + /// Whether to run in background + pub background: Option, + + /// Number of top logprobs to include + pub top_logprobs: Option, +} + +/// Input parameter - can be a simple string or array of input items +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum InputParam { + /// Simple text input + Text(String), + /// Array of input items (messages, references, outputs, etc.) + Items(Vec), +} + +/// Input item - can be a message, item reference, function call output, etc. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum InputItem { + /// Input message (role + content) + Message(InputMessage), + /// Item reference + ItemReference { + #[serde(rename = "type")] + item_type: String, + id: String, + }, + /// Function call output + FunctionCallOutput { + #[serde(rename = "type")] + item_type: String, + call_id: String, + output: String, + }, +} + +/// Input message with role and content +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct InputMessage { + /// Message role + pub role: MessageRole, + /// Message content - can be a string or array of InputContent + pub content: MessageContent, +} + +/// Message content - can be either a simple string or array of content items +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum MessageContent { + /// Simple text content + Text(String), + /// Array of content items + Items(Vec), +} + +/// Message roles +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum MessageRole { + User, + Assistant, + System, + Developer, +} + +/// Input content types +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "type", rename_all = "snake_case")] +pub enum InputContent { + /// Text input + InputText { + text: String, + }, + /// Image input via URL + InputImage { + image_url: String, + detail: Option, + }, + /// File input via URL + InputFile { + file_url: String, + }, + /// Audio input + InputAudio { + data: Option, + format: Option, + }, +} + +/// Modality options +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum Modality { + Text, + Audio, +} + +/// Audio configuration +#[skip_serializing_none] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AudioConfig { + /// Voice to use for audio output + pub voice: String, + /// Audio output format + pub format: Option, +} + +/// Text configuration +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TextConfig { + /// Text format configuration + pub format: TextFormat, +} + +/// Text format +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "type", rename_all = "snake_case")] +pub enum TextFormat { + Text, + JsonObject, + JsonSchema { + json_schema: serde_json::Value, + }, +} + +/// Reasoning effort levels +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum ReasoningEffort { + Low, + Medium, + High, +} + +/// Include enum for additional output data +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum IncludeEnum { + #[serde(rename = "web_search_call.action.sources")] + WebSearchCallActionSources, + #[serde(rename = "code_interpreter_call.outputs")] + CodeInterpreterCallOutputs, + #[serde(rename = "computer_call_output.output.image_url")] + ComputerCallOutputImageUrl, + #[serde(rename = "file_search_call.results")] + FileSearchCallResults, + #[serde(rename = "message.input_image.image_url")] + MessageInputImageImageUrl, + #[serde(rename = "message.output_text.logprobs")] + MessageOutputTextLogprobs, + #[serde(rename = "reasoning.encrypted_content")] + ReasoningEncryptedContent, +} + +/// Response stream options +#[skip_serializing_none] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ResponseStreamOptions { + /// Whether to include usage in stream + pub include_usage: Option, +} + +/// Conversation parameter +#[skip_serializing_none] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ConversationParam { + /// Conversation ID + pub id: Option, +} + +/// Tool definitions +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "type", rename_all = "snake_case")] +pub enum Tool { + /// Function tool - flat structure in Responses API + Function { + name: String, + description: Option, + parameters: Option, + strict: Option, + }, + /// File search tool + FileSearch { + vector_store_ids: Option>, + max_num_results: Option, + ranking_options: Option, + filters: Option, + }, + /// Web search tool + WebSearchPreview { + domains: Option>, + search_context_size: Option, + user_location: Option, + }, + /// Code interpreter tool + CodeInterpreter, + /// Computer tool + Computer { + display_width_px: Option, + display_height_px: Option, + display_number: Option, + }, +} + +/// Ranking options for file search +#[skip_serializing_none] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RankingOptions { + /// Ranker type + pub ranker: String, + /// Score threshold + pub score_threshold: Option, +} + +/// User location for web search +#[skip_serializing_none] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UserLocation { + #[serde(rename = "type")] + pub location_type: String, + pub city: Option, + pub country: Option, + pub region: Option, + pub timezone: Option, +} + +/// Tool choice options +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum ToolChoice { + /// Auto, none, or required + String(String), + /// Named tool choice + Named { + #[serde(rename = "type")] + tool_type: String, + function: NamedFunction, + }, +} + +/// Named function for tool choice +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct NamedFunction { + pub name: String, +} + +// ============================================================================ +// Response Structs - Response Object +// ============================================================================ + +/// The response object returned from the API +/// Request to create a model response +#[skip_serializing_none] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ResponsesAPIResponse { + /// Unique identifier for this Response + pub id: String, + + /// The object type - always "response" + pub object: String, + + /// Unix timestamp (in seconds) of when this Response was created + pub created_at: i64, + + /// The status of the response generation + pub status: ResponseStatus, + + /// Error information if the response failed + pub error: Option, + + /// Details about why the response is incomplete + pub incomplete_details: Option, + + /// System/developer instructions + pub instructions: Option, + + /// The model used + pub model: String, + + /// An array of content items generated by the model + pub output: Vec, + + /// Usage statistics + pub usage: Option, + + /// Whether to allow parallel tool calls + pub parallel_tool_calls: bool, + + /// Conversation state + pub conversation: Option, + + /// Previous response ID + pub previous_response_id: Option, + + /// Tools available + pub tools: Vec, + + /// Tool choice setting + pub tool_choice: String, + + /// Temperature setting + pub temperature: f32, + + /// Top-p setting + pub top_p: f32, + + /// Metadata + pub metadata: HashMap, + + /// Truncation setting + pub truncation: Option, + + /// Maximum output tokens + pub max_output_tokens: Option, + + /// Reasoning configuration + pub reasoning: Option, + + /// Whether response is stored + pub store: Option, + + /// Text configuration + pub text: Option, + + /// Audio configuration + pub audio: Option, + + /// Modalities + pub modalities: Option>, + + /// Service tier + pub service_tier: Option, + + /// Background execution + pub background: Option, + + /// Top logprobs count + pub top_logprobs: Option, + + /// Maximum tool calls + pub max_tool_calls: Option, +} + +/// Response status +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "snake_case")] +pub enum ResponseStatus { + Completed, + Failed, + InProgress, + Cancelled, + Queued, + Incomplete, +} + +/// Response error information +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ResponseError { + /// Error code + pub code: ResponseErrorCode, + /// Human-readable error message + pub message: String, +} + +/// Response error codes +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum ResponseErrorCode { + ServerError, + RateLimitExceeded, + InvalidPrompt, + VectorStoreTimeout, + InvalidImage, + InvalidImageFormat, + InvalidBase64Image, + InvalidImageUrl, + ImageTooLarge, + ImageTooSmall, + ImageParseError, + ImageContentPolicyViolation, + InvalidImageMode, + ImageFileTooLarge, + UnsupportedImageMediaType, + EmptyImageFile, + FailedToDownloadImage, + ImageFileNotFound, +} + +/// Incomplete details +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct IncompleteDetails { + /// The reason why the response is incomplete + pub reason: IncompleteReason, +} + +/// Incomplete reasons +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum IncompleteReason { + MaxOutputTokens, + ContentFilter, +} + +/// Output items from the model +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "type", rename_all = "snake_case")] +pub enum OutputItem { + /// Output message + Message { + id: String, + status: OutputItemStatus, + role: String, + content: Vec, + }, + /// Function tool call + FunctionCall { + id: String, + status: OutputItemStatus, + call_id: String, + name: Option, + arguments: Option, + }, + /// Function call output + FunctionCallOutput { + id: String, + call_id: String, + output: String, + status: Option, + }, + /// File search tool call + FileSearchCall { + id: String, + status: OutputItemStatus, + queries: Option>, + results: Option>, + }, + /// Web search tool call + WebSearchCall { + id: String, + status: OutputItemStatus, + }, + /// Code interpreter tool call + CodeInterpreterCall { + id: String, + status: OutputItemStatus, + code: Option, + outputs: Option>, + }, + /// Computer tool call + ComputerCall { + id: String, + status: OutputItemStatus, + action: Option, + }, + /// Computer call output + ComputerCallOutput { + id: String, + call_id: String, + output: Option, + status: Option, + }, + /// Custom tool call + CustomToolCall { + id: String, + status: OutputItemStatus, + call_id: String, + input: Option, + }, + /// Custom tool call output + CustomToolCallOutput { + id: String, + call_id: String, + output: String, + status: Option, + }, + /// Reasoning item + Reasoning { + id: String, + summary: Vec, + }, +} + +/// Output item status +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum OutputItemStatus { + InProgress, + Completed, + Incomplete, +} + +/// Output content types +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "type", rename_all = "snake_case")] +pub enum OutputContent { + /// Text output + OutputText { + text: String, + annotations: Vec, + logprobs: Option>, + }, + /// Audio output + OutputAudio { + data: Option, + transcript: Option, + }, + /// Refusal output + Refusal { + refusal: String, + }, +} + +/// Annotations for output text +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "type", rename_all = "snake_case")] +pub enum Annotation { + /// File citation + FileCitation { + index: i32, + file_id: String, + filename: String, + quote: Option, + }, + /// URL citation + UrlCitation { + start_index: i32, + end_index: i32, + url: String, + title: String, + }, +} + +/// Log probability information +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct LogProb { + /// The token + pub token: String, + /// Log probability value + pub logprob: f32, + /// Token bytes + pub bytes: Vec, +} + +/// File search result +#[skip_serializing_none] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct FileSearchResult { + /// File ID + pub file_id: String, + /// File name + pub filename: String, + /// Score + pub score: Option, + /// Content excerpt + pub content: Option, +} + +/// Code interpreter output +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "type", rename_all = "snake_case")] +pub enum CodeInterpreterOutput { + /// Text output + Text { + text: String, + }, + /// Image output + Image { + image: String, + }, +} + +/// Response usage statistics +#[skip_serializing_none] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ResponseUsage { + /// Input tokens used + pub input_tokens: i32, + /// Output tokens generated + pub output_tokens: i32, + /// Total tokens (input + output) + pub total_tokens: i32, + /// Input token details + pub input_tokens_details: Option, + /// Output token details + pub output_tokens_details: Option, +} + +impl crate::providers::response::TokenUsage for ResponseUsage { + fn completion_tokens(&self) -> usize { + self.output_tokens as usize + } + + fn prompt_tokens(&self) -> usize { + self.input_tokens as usize + } + + fn total_tokens(&self) -> usize { + self.total_tokens as usize + } +} + +/// Token details +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TokenDetails { + /// Cached tokens + pub cached_tokens: i32, +} + +/// Output token details +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct OutputTokenDetails { + /// Reasoning tokens + pub reasoning_tokens: i32, +} + +/// Reasoning configuration and summary +#[skip_serializing_none] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Reasoning { + /// Reasoning effort level + pub effort: Option, + /// Summary of reasoning + pub summary: Option, +} + +/// Conversation object +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Conversation { + /// Conversation ID + pub id: String, + /// Conversation object type + pub object: String, +} + +// ============================================================================ +// Streaming Response Events +// ============================================================================ + +/// Stream events for responses +#[skip_serializing_none] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "type", rename_all = "snake_case")] +pub enum ResponsesAPIStreamEvent { + /// Response created + #[serde(rename = "response.created")] + ResponseCreated { + response: ResponsesAPIResponse, + sequence_number: i32, + }, + + /// Response in progress + #[serde(rename = "response.in_progress")] + ResponseInProgress { + response: ResponsesAPIResponse, + sequence_number: i32, + }, + + /// Response completed + #[serde(rename = "response.completed")] + ResponseCompleted { + response: ResponsesAPIResponse, + sequence_number: i32, + }, + + /// Output item added + #[serde(rename = "response.output_item.added")] + ResponseOutputItemAdded { + output_index: i32, + item: OutputItem, + sequence_number: i32, + }, + + /// Output item done + #[serde(rename = "response.output_item.done")] + ResponseOutputItemDone { + output_index: i32, + item: OutputItem, + sequence_number: i32, + }, + + /// Content part added + #[serde(rename = "response.content_part.added")] + ResponseContentPartAdded { + item_id: String, + output_index: i32, + content_index: i32, + part: OutputContent, + sequence_number: i32, + }, + + /// Content part done + #[serde(rename = "response.content_part.done")] + ResponseContentPartDone { + item_id: String, + output_index: i32, + content_index: i32, + part: OutputContent, + sequence_number: i32, + }, + + /// Output text delta (incremental text streaming) + #[serde(rename = "response.output_text.delta")] + ResponseOutputTextDelta { + item_id: String, + output_index: i32, + content_index: i32, + delta: String, + logprobs: Vec, + obfuscation: Option, + sequence_number: i32, + }, + + /// Output text done (final complete text) + #[serde(rename = "response.output_text.done")] + ResponseOutputTextDone { + item_id: String, + output_index: i32, + content_index: i32, + text: String, + logprobs: Vec, + sequence_number: i32, + }, + + /// Audio delta + #[serde(rename = "response.audio.delta")] + ResponseAudioDelta { + item_id: Option, + output_index: Option, + content_index: Option, + delta: String, + sequence_number: i32, + }, + + /// Audio done + #[serde(rename = "response.audio.done")] + ResponseAudioDone { + item_id: Option, + output_index: Option, + content_index: Option, + sequence_number: i32, + }, + + /// Audio transcript delta + #[serde(rename = "response.audio_transcript.delta")] + ResponseAudioTranscriptDelta { + item_id: Option, + output_index: Option, + content_index: Option, + delta: String, + sequence_number: i32, + }, + + /// Audio transcript done + #[serde(rename = "response.audio_transcript.done")] + ResponseAudioTranscriptDone { + item_id: Option, + output_index: Option, + content_index: Option, + transcript: Option, + sequence_number: i32, + }, + + /// Function call arguments delta + #[serde(rename = "response.function_call_arguments.delta")] + ResponseFunctionCallArgumentsDelta { + output_index: i32, + item_id: String, + delta: String, + sequence_number: i32, + call_id: Option, + name: Option, + }, + + /// Function call arguments done + #[serde(rename = "response.function_call_arguments.done")] + ResponseFunctionCallArgumentsDone { + output_index: i32, + item_id: String, + arguments: String, + sequence_number: i32, + }, + + /// Code interpreter call code delta + #[serde(rename = "response.code_interpreter_call.code.delta")] + ResponseCodeInterpreterCallCodeDelta { + output_index: i32, + item_id: String, + delta: String, + sequence_number: i32, + }, + + /// Code interpreter call code done + #[serde(rename = "response.code_interpreter_call.code.done")] + ResponseCodeInterpreterCallCodeDone { + output_index: i32, + item_id: String, + code: String, + sequence_number: i32, + }, + + /// Code interpreter call in progress + #[serde(rename = "response.code_interpreter_call.in_progress")] + ResponseCodeInterpreterCallInProgress { + output_index: i32, + item_id: String, + sequence_number: i32, + }, + + /// Code interpreter call interpreting + #[serde(rename = "response.code_interpreter_call.interpreting")] + ResponseCodeInterpreterCallInterpreting { + output_index: i32, + item_id: String, + sequence_number: i32, + }, + + /// Code interpreter call completed + #[serde(rename = "response.code_interpreter_call.completed")] + ResponseCodeInterpreterCallCompleted { + output_index: i32, + item_id: String, + sequence_number: i32, + }, + + /// Custom tool call input delta + #[serde(rename = "response.custom_tool_call.input.delta")] + ResponseCustomToolCallInputDelta { + output_index: i32, + item_id: String, + delta: String, + sequence_number: i32, + }, + + /// Custom tool call input done + #[serde(rename = "response.custom_tool_call.input.done")] + ResponseCustomToolCallInputDone { + output_index: i32, + item_id: String, + input: String, + sequence_number: i32, + }, + + /// Error event + Error { + code: String, + message: String, + sequence_number: i32, + }, + + /// Done event (end of stream) + Done { + sequence_number: i32, + }, +} + +// ============================================================================ +// Additional Response Operations +// ============================================================================ + +/// Retrieve response request (GET /responses/{response_id}) +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GetResponseRequest { + /// Response ID to retrieve + pub response_id: String, +} + +/// Delete response request (DELETE /responses/{response_id}) +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DeleteResponseRequest { + /// Response ID to delete + pub response_id: String, +} + +/// Delete response response +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DeleteResponseResponse { + /// Response ID that was deleted + pub id: String, + /// Object type + pub object: String, + /// Whether deletion was successful + pub deleted: bool, +} + +/// Cancel response request (POST /responses/{response_id}/cancel) +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CancelResponseRequest { + /// Response ID to cancel + pub response_id: String, +} + +/// List input items request (GET /responses/{response_id}/input_items) +#[skip_serializing_none] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ListInputItemsRequest { + /// Response ID + pub response_id: String, + /// Limit for pagination + pub limit: Option, + /// Order for pagination + pub order: Option, + /// After cursor for pagination + pub after: Option, + /// Before cursor for pagination + pub before: Option, +} + +/// List input items response +#[skip_serializing_none] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ListInputItemsResponse { + /// Object type - always "list" + pub object: String, + /// Array of input items + pub data: Vec, + /// First ID in the list + pub first_id: Option, + /// Last ID in the list + pub last_id: Option, + /// Whether there are more items + pub has_more: bool, +} + +// ============================================================================ +// ProviderRequest Implementation +// ============================================================================ + +impl ProviderRequest for ResponsesAPIRequest { + fn model(&self) -> &str { + &self.model + } + + fn set_model(&mut self, model: String) { + self.model = model; + } + + fn is_streaming(&self) -> bool { + self.stream.unwrap_or_default() + } + + fn extract_messages_text(&self) -> String { + match &self.input { + InputParam::Text(text) => text.clone(), + InputParam::Items(items) => { + items.iter().fold(String::new(), |acc, item| { + match item { + InputItem::Message(msg) => { + let content_text = match &msg.content { + MessageContent::Text(text) => text.clone(), + MessageContent::Items(content_items) => { + content_items.iter().fold(String::new(), |acc, content| { + acc + " " + &match content { + InputContent::InputText { text } => text.clone(), + InputContent::InputImage { .. } => "[Image]".to_string(), + InputContent::InputFile { .. } => "[File]".to_string(), + InputContent::InputAudio { .. } => "[Audio]".to_string(), + } + }) + } + }; + acc + " " + &content_text + } + // Skip non-message items (references, outputs, etc.) + _ => acc, + } + }) + } + } + } + + fn get_recent_user_message(&self) -> Option { + match &self.input { + InputParam::Text(text) => Some(text.clone()), + InputParam::Items(items) => { + items.iter().rev().find_map(|item| { + match item { + InputItem::Message(msg) if matches!(msg.role, MessageRole::User) => { + // Extract text from content + match &msg.content { + MessageContent::Text(text) => Some(text.clone()), + MessageContent::Items(content_items) => { + content_items.iter().find_map(|content| { + match content { + InputContent::InputText { text } => Some(text.clone()), + _ => None, + } + }) + } + } + } + // Skip non-message items + _ => None, + } + }) + } + } + } + + fn get_tool_names(&self) -> Option> { + self.tools.as_ref().map(|tools| { + tools + .iter() + .filter_map(|tool| match tool { + Tool::Function { name, .. } => Some(name.clone()), + // Other tool types don't have user-defined names + _ => None, + }) + .collect() + }) + } + + fn to_bytes(&self) -> Result, ProviderRequestError> { + serde_json::to_vec(&self).map_err(|e| ProviderRequestError { + message: format!("Failed to serialize Responses API request: {}", e), + source: Some(Box::new(e)), + }) + } + + fn metadata(&self) -> &Option> { + &self.metadata + } + + fn remove_metadata_key(&mut self, key: &str) -> bool { + if let Some(ref mut metadata) = self.metadata { + metadata.remove(key).is_some() + } else { + false + } + } + + fn get_temperature(&self) -> Option { + self.temperature + } + + fn get_messages(&self) -> Vec { + use crate::apis::openai::{Message, MessageContent, Role}; + + let mut openai_messages = Vec::new(); + + // Add instructions as system message if present + if let Some(instructions) = &self.instructions { + openai_messages.push(Message { + role: Role::System, + content: MessageContent::Text(instructions.clone()), + name: None, + tool_calls: None, + tool_call_id: None, + }); + } + + // Convert input to messages + match &self.input { + InputParam::Text(text) => { + openai_messages.push(Message { + role: Role::User, + content: MessageContent::Text(text.clone()), + name: None, + tool_calls: None, + tool_call_id: None, + }); + } + InputParam::Items(items) => { + for item in items { + match item { + InputItem::Message(msg) => { + // Convert message role + let role = match msg.role { + MessageRole::User => Role::User, + MessageRole::Assistant => Role::Assistant, + MessageRole::System => Role::System, + MessageRole::Developer => Role::System, // Map developer to system + }; + + // Extract text from message content + let content = match &msg.content { + crate::apis::openai_responses::MessageContent::Text(text) => text.clone(), + crate::apis::openai_responses::MessageContent::Items(items) => { + items.iter() + .filter_map(|c| { + if let InputContent::InputText { text } = c { + Some(text.clone()) + } else { + None + } + }) + .collect::>() + .join("\n") + } + }; + + openai_messages.push(Message { + role, + content: MessageContent::Text(content), + name: None, + tool_calls: None, + tool_call_id: None, + }); + } + // Skip other input item types for now + InputItem::ItemReference { .. } | InputItem::FunctionCallOutput { .. } => { + // These are not yet supported in agent framework + } + } + } + } + } + + openai_messages + } + + fn set_messages(&mut self, messages: &[crate::apis::openai::Message]) { + // For ResponsesAPI, we need to convert messages back to input format + // Extract system messages as instructions + let system_text = messages.iter() + .filter(|msg| msg.role == crate::apis::openai::Role::System) + .filter_map(|msg| { + if let crate::apis::openai::MessageContent::Text(text) = &msg.content { + Some(text.as_str()) + } else { + None + } + }) + .collect::>() + .join("\n"); + + if !system_text.is_empty() { + self.instructions = Some(system_text); + } + + // Convert user/assistant messages to InputParam + // For simplicity, we'll use the last user message as the input + // or combine all non-system messages + let input_messages: Vec<_> = messages.iter() + .filter(|msg| msg.role != crate::apis::openai::Role::System) + .collect(); + + if !input_messages.is_empty() { + // If there's only one message, use Text format + if input_messages.len() == 1 { + if let crate::apis::openai::MessageContent::Text(text) = &input_messages[0].content { + self.input = crate::apis::openai_responses::InputParam::Text(text.clone()); + } + } else { + // Multiple messages - combine them as text for now + // A more sophisticated approach would use InputParam::Items + let combined_text = input_messages.iter() + .filter_map(|msg| { + if let crate::apis::openai::MessageContent::Text(text) = &msg.content { + Some(format!("{}: {}", + match msg.role { + crate::apis::openai::Role::User => "User", + crate::apis::openai::Role::Assistant => "Assistant", + _ => "Unknown", + }, + text + )) + } else { + None + } + }) + .collect::>() + .join("\n"); + + self.input = crate::apis::openai_responses::InputParam::Text(combined_text); + } + } + } +} + +// ============================================================================ +// Into Implementation for SSE Formatting +// ============================================================================ + +impl Into for ResponsesAPIStreamEvent { + fn into(self) -> String { + let transformed_json = serde_json::to_string(&self).unwrap_or_default(); + let event_type = match &self { + ResponsesAPIStreamEvent::ResponseCreated { .. } => "response.created", + ResponsesAPIStreamEvent::ResponseInProgress { .. } => "response.in_progress", + ResponsesAPIStreamEvent::ResponseCompleted { .. } => "response.completed", + ResponsesAPIStreamEvent::ResponseOutputItemAdded { .. } => "response.output_item.added", + ResponsesAPIStreamEvent::ResponseOutputItemDone { .. } => "response.output_item.done", + ResponsesAPIStreamEvent::ResponseContentPartAdded { .. } => { + "response.content_part.added" + } + ResponsesAPIStreamEvent::ResponseContentPartDone { .. } => "response.content_part.done", + ResponsesAPIStreamEvent::ResponseOutputTextDelta { .. } => "response.output_text.delta", + ResponsesAPIStreamEvent::ResponseOutputTextDone { .. } => "response.output_text.done", + ResponsesAPIStreamEvent::ResponseAudioDelta { .. } => "response.audio.delta", + ResponsesAPIStreamEvent::ResponseAudioDone { .. } => "response.audio.done", + ResponsesAPIStreamEvent::ResponseAudioTranscriptDelta { .. } => { + "response.audio_transcript.delta" + } + ResponsesAPIStreamEvent::ResponseAudioTranscriptDone { .. } => { + "response.audio_transcript.done" + } + ResponsesAPIStreamEvent::ResponseFunctionCallArgumentsDelta { .. } => { + "response.function_call_arguments.delta" + } + ResponsesAPIStreamEvent::ResponseFunctionCallArgumentsDone { .. } => { + "response.function_call_arguments.done" + } + ResponsesAPIStreamEvent::ResponseCodeInterpreterCallCodeDelta { .. } => { + "response.code_interpreter_call.code.delta" + } + ResponsesAPIStreamEvent::ResponseCodeInterpreterCallCodeDone { .. } => { + "response.code_interpreter_call.code.done" + } + ResponsesAPIStreamEvent::ResponseCodeInterpreterCallInProgress { .. } => { + "response.code_interpreter_call.in_progress" + } + ResponsesAPIStreamEvent::ResponseCodeInterpreterCallInterpreting { .. } => { + "response.code_interpreter_call.interpreting" + } + ResponsesAPIStreamEvent::ResponseCodeInterpreterCallCompleted { .. } => { + "response.code_interpreter_call.completed" + } + ResponsesAPIStreamEvent::ResponseCustomToolCallInputDelta { .. } => { + "response.custom_tool_call.input.delta" + } + ResponsesAPIStreamEvent::ResponseCustomToolCallInputDone { .. } => { + "response.custom_tool_call.input.done" + } + ResponsesAPIStreamEvent::Error { .. } => "error", + ResponsesAPIStreamEvent::Done { .. } => "done", + }; + + let event = format!("event: {}\n", event_type); + let data = format!("data: {}\n\n", transformed_json); + event + &data + } +} + +// ============================================================================ +// ProviderStreamResponse Implementation +// ============================================================================ + +impl crate::providers::streaming_response::ProviderStreamResponse for ResponsesAPIStreamEvent { + fn content_delta(&self) -> Option<&str> { + match self { + ResponsesAPIStreamEvent::ResponseOutputTextDelta { delta, .. } => Some(delta), + ResponsesAPIStreamEvent::ResponseAudioDelta { delta, .. } => Some(delta), + ResponsesAPIStreamEvent::ResponseAudioTranscriptDelta { delta, .. } => Some(delta), + ResponsesAPIStreamEvent::ResponseFunctionCallArgumentsDelta { delta, .. } => { + Some(delta) + } + ResponsesAPIStreamEvent::ResponseCodeInterpreterCallCodeDelta { delta, .. } => { + Some(delta) + } + ResponsesAPIStreamEvent::ResponseCustomToolCallInputDelta { delta, .. } => Some(delta), + _ => None, + } + } + + fn is_final(&self) -> bool { + matches!( + self, + ResponsesAPIStreamEvent::ResponseCompleted { .. } + | ResponsesAPIStreamEvent::Done { .. } + ) + } + + fn role(&self) -> Option<&str> { + match self { + ResponsesAPIStreamEvent::ResponseOutputItemDone { item, .. } => match item { + OutputItem::Message { role, .. } => Some(role.as_str()), + _ => None, + }, + _ => None, + } + } + + fn event_type(&self) -> Option<&str> { + Some(match self { + ResponsesAPIStreamEvent::ResponseCreated { .. } => "response.created", + ResponsesAPIStreamEvent::ResponseInProgress { .. } => "response.in_progress", + ResponsesAPIStreamEvent::ResponseCompleted { .. } => "response.completed", + ResponsesAPIStreamEvent::ResponseOutputItemAdded { .. } => "response.output_item.added", + ResponsesAPIStreamEvent::ResponseOutputItemDone { .. } => "response.output_item.done", + ResponsesAPIStreamEvent::ResponseContentPartAdded { .. } => { + "response.content_part.added" + } + ResponsesAPIStreamEvent::ResponseContentPartDone { .. } => "response.content_part.done", + ResponsesAPIStreamEvent::ResponseOutputTextDelta { .. } => "response.output_text.delta", + ResponsesAPIStreamEvent::ResponseOutputTextDone { .. } => "response.output_text.done", + ResponsesAPIStreamEvent::ResponseAudioDelta { .. } => "response.audio.delta", + ResponsesAPIStreamEvent::ResponseAudioDone { .. } => "response.audio.done", + ResponsesAPIStreamEvent::ResponseAudioTranscriptDelta { .. } => { + "response.audio_transcript.delta" + } + ResponsesAPIStreamEvent::ResponseAudioTranscriptDone { .. } => { + "response.audio_transcript.done" + } + ResponsesAPIStreamEvent::ResponseFunctionCallArgumentsDelta { .. } => { + "response.function_call_arguments.delta" + } + ResponsesAPIStreamEvent::ResponseFunctionCallArgumentsDone { .. } => { + "response.function_call_arguments.done" + } + ResponsesAPIStreamEvent::ResponseCodeInterpreterCallCodeDelta { .. } => { + "response.code_interpreter_call.code.delta" + } + ResponsesAPIStreamEvent::ResponseCodeInterpreterCallCodeDone { .. } => { + "response.code_interpreter_call.code.done" + } + ResponsesAPIStreamEvent::ResponseCodeInterpreterCallInProgress { .. } => { + "response.code_interpreter_call.in_progress" + } + ResponsesAPIStreamEvent::ResponseCodeInterpreterCallInterpreting { .. } => { + "response.code_interpreter_call.interpreting" + } + ResponsesAPIStreamEvent::ResponseCodeInterpreterCallCompleted { .. } => { + "response.code_interpreter_call.completed" + } + ResponsesAPIStreamEvent::ResponseCustomToolCallInputDelta { .. } => { + "response.custom_tool_call.input.delta" + } + ResponsesAPIStreamEvent::ResponseCustomToolCallInputDone { .. } => { + "response.custom_tool_call.input.done" + } + ResponsesAPIStreamEvent::Error { .. } => "error", + ResponsesAPIStreamEvent::Done { .. } => "done", + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_response_output_text_delta_deserialization() { + let json = r#"{ + "type":"response.output_text.delta", + "sequence_number":811, + "item_id":"msg_0d87415661475591006924ce5465748190bdc8874257743b5c", + "output_index":1, + "content_index":0, + "delta":" first", + "logprobs":[], + "obfuscation":"sRhca4PA06" + }"#; + + let event: ResponsesAPIStreamEvent = + serde_json::from_str(json).expect("Failed to deserialize"); + + match event { + ResponsesAPIStreamEvent::ResponseOutputTextDelta { + item_id, + output_index, + content_index, + delta, + sequence_number, + logprobs, + obfuscation, + } => { + assert_eq!( + item_id, + "msg_0d87415661475591006924ce5465748190bdc8874257743b5c" + ); + assert_eq!(output_index, 1); + assert_eq!(content_index, 0); + assert_eq!(delta, " first"); + assert_eq!(sequence_number, 811); + assert_eq!(logprobs.len(), 0); + assert_eq!(obfuscation, Some("sRhca4PA06".to_string())); + } + _ => panic!("Expected ResponseOutputTextDelta event"), + } + } + + #[test] + fn test_response_output_text_done_deserialization() { + let json = r#"{ + "type":"response.output_text.done", + "sequence_number":818, + "item_id":"msg_0d87415661475591006924ce5465748190bdc8874257743b5c", + "output_index":1, + "content_index":0, + "text":"The otters linked paws and laughed.", + "logprobs":[] + }"#; + + let event: ResponsesAPIStreamEvent = + serde_json::from_str(json).expect("Failed to deserialize"); + + match event { + ResponsesAPIStreamEvent::ResponseOutputTextDone { + item_id, + output_index, + content_index, + text, + sequence_number, + logprobs, + } => { + assert_eq!( + item_id, + "msg_0d87415661475591006924ce5465748190bdc8874257743b5c" + ); + assert_eq!(output_index, 1); + assert_eq!(content_index, 0); + assert_eq!(text, "The otters linked paws and laughed."); + assert_eq!(sequence_number, 818); + assert_eq!(logprobs.len(), 0); + } + _ => panic!("Expected ResponseOutputTextDone event"), + } + } + + #[test] + fn test_response_completed_deserialization() { + // Simplified response.completed event + let json = r#"{ + "type":"response.completed", + "sequence_number":821, + "response":{ + "id":"resp_test123", + "object":"response", + "created_at":1764019793, + "status":"completed", + "background":false, + "error":null, + "incomplete_details":null, + "instructions":null, + "max_output_tokens":null, + "max_tool_calls":null, + "model":"o3-2025-04-16", + "output":[], + "output_text":null, + "usage":{ + "input_tokens":17, + "output_tokens":946, + "total_tokens":963 + }, + "parallel_tool_calls":true, + "conversation":null, + "previous_response_id":null, + "tools":[], + "tool_choice":"auto", + "temperature":1.0, + "top_p":1.0, + "metadata":{}, + "truncation":null, + "user":null, + "reasoning":null, + "store":true, + "text":null, + "audio":null, + "modalities":null, + "service_tier":"default", + "top_logprobs":0 + } + }"#; + + let event: ResponsesAPIStreamEvent = + serde_json::from_str(json).expect("Failed to deserialize"); + + match event { + ResponsesAPIStreamEvent::ResponseCompleted { + response, + sequence_number, + } => { + assert_eq!(response.id, "resp_test123"); + assert_eq!(sequence_number, 821); + assert_eq!(response.model, "o3-2025-04-16"); + } + _ => panic!("Expected ResponseCompleted event"), + } + } +} diff --git a/crates/hermesllm/src/apis/amazon_bedrock_binary_frame.rs b/crates/hermesllm/src/apis/streaming_shapes/amazon_bedrock_binary_frame.rs similarity index 66% rename from crates/hermesllm/src/apis/amazon_bedrock_binary_frame.rs rename to crates/hermesllm/src/apis/streaming_shapes/amazon_bedrock_binary_frame.rs index bacbad62..7f68bb26 100644 --- a/crates/hermesllm/src/apis/amazon_bedrock_binary_frame.rs +++ b/crates/hermesllm/src/apis/streaming_shapes/amazon_bedrock_binary_frame.rs @@ -1,7 +1,6 @@ use aws_smithy_eventstream::frame::DecodedFrame; use aws_smithy_eventstream::frame::MessageFrameDecoder; use bytes::Buf; -use std::collections::HashSet; /// AWS Event Stream frame decoder wrapper pub struct BedrockBinaryFrameDecoder @@ -10,7 +9,6 @@ where { decoder: MessageFrameDecoder, buffer: B, - content_block_start_indices: HashSet, } impl BedrockBinaryFrameDecoder { @@ -20,7 +18,6 @@ impl BedrockBinaryFrameDecoder { Self { decoder: MessageFrameDecoder::new(), buffer, - content_block_start_indices: std::collections::HashSet::new(), } } } @@ -33,7 +30,6 @@ where Self { decoder: MessageFrameDecoder::new(), buffer, - content_block_start_indices: HashSet::new(), } } @@ -52,14 +48,4 @@ where pub fn has_remaining(&self) -> bool { self.buffer.has_remaining() } - - /// Check if a content_block_start event has been sent for the given index - pub fn has_content_block_start_been_sent(&self, index: i32) -> bool { - self.content_block_start_indices.contains(&index) - } - - /// Mark that a content_block_start event has been sent for the given index - pub fn set_content_block_start_sent(&mut self, index: i32) { - self.content_block_start_indices.insert(index); - } } diff --git a/crates/hermesllm/src/apis/streaming_shapes/anthropic_streaming_buffer.rs b/crates/hermesllm/src/apis/streaming_shapes/anthropic_streaming_buffer.rs new file mode 100644 index 00000000..818ee37d --- /dev/null +++ b/crates/hermesllm/src/apis/streaming_shapes/anthropic_streaming_buffer.rs @@ -0,0 +1,507 @@ +use crate::apis::streaming_shapes::sse::{SseEvent, SseStreamBufferTrait}; +use crate::apis::anthropic::MessagesStreamEvent; +use crate::providers::streaming_response::ProviderStreamResponseType; +use std::collections::HashSet; + +/// SSE Stream Buffer for Anthropic Messages API streaming. +/// +/// This buffer manages the wire format for Anthropic Messages API streaming, +/// handling the specific event sequencing requirements: +/// - MessageStart → ContentBlockStart → ContentBlockDelta(s) → ContentBlockStop → MessageDelta → MessageStop +/// +/// When converting from OpenAI to Anthropic format, this buffer injects the required +/// ContentBlockStart and ContentBlockStop events to maintain proper Anthropic protocol. +pub struct AnthropicMessagesStreamBuffer { + /// Buffered SSE events ready to be written to wire + buffered_events: Vec, + + /// Track if we've seen a message_start event + message_started: bool, + + /// Track content block indices that have received ContentBlockStart events + content_block_start_indices: HashSet, + + /// Track if we need to inject ContentBlockStop before message_delta + needs_content_block_stop: bool, + + /// Track if we've seen a MessageDelta (so we need to send MessageStop at the end) + seen_message_delta: bool, + + /// Model name to use when generating message_start events + model: Option, +} + +impl AnthropicMessagesStreamBuffer { + pub fn new() -> Self { + Self { + buffered_events: Vec::new(), + message_started: false, + content_block_start_indices: HashSet::new(), + needs_content_block_stop: false, + seen_message_delta: false, + model: None, + } + } + + /// Check if a content_block_start event has been sent for the given index + fn has_content_block_start_been_sent(&self, index: i32) -> bool { + self.content_block_start_indices.contains(&index) + } + + /// Mark that a content_block_start event has been sent for the given index + fn set_content_block_start_sent(&mut self, index: i32) { + self.content_block_start_indices.insert(index); + } + + /// Helper to create and format a ContentBlockStart SSE event + fn create_content_block_start_event() -> SseEvent { + let content_block_start = MessagesStreamEvent::ContentBlockStart { + index: 0, + content_block: crate::apis::anthropic::MessagesContentBlock::Text { + text: String::new(), + cache_control: None, + }, + }; + let sse_string: String = content_block_start.into(); + + SseEvent { + data: None, + event: Some("content_block_start".to_string()), + raw_line: sse_string.clone(), + sse_transformed_lines: sse_string, + provider_stream_response: None, + } + } + + /// Helper to create and format a MessageStart SSE event + fn create_message_start_event(model: &str) -> SseEvent { + let message_start = MessagesStreamEvent::MessageStart { + message: crate::apis::anthropic::MessagesStreamMessage { + id: format!("msg_{}", uuid::Uuid::new_v4().to_string().replace("-", "")), + obj_type: "message".to_string(), + role: crate::apis::anthropic::MessagesRole::Assistant, + content: vec![], + model: model.to_string(), + stop_reason: None, + stop_sequence: None, + usage: crate::apis::anthropic::MessagesUsage { + input_tokens: 0, + output_tokens: 0, + cache_creation_input_tokens: None, + cache_read_input_tokens: None, + }, + }, + }; + let sse_string: String = message_start.into(); + + SseEvent { + data: None, + event: Some("message_start".to_string()), + raw_line: sse_string.clone(), + sse_transformed_lines: sse_string, + provider_stream_response: None, + } + } + + /// Helper to create and format a ContentBlockStop SSE event + fn create_content_block_stop_event() -> SseEvent { + let content_block_stop = MessagesStreamEvent::ContentBlockStop { index: 0 }; + let sse_string: String = content_block_stop.into(); + + SseEvent { + data: None, + event: Some("content_block_stop".to_string()), + raw_line: sse_string.clone(), + sse_transformed_lines: sse_string, + provider_stream_response: None, + } + } +} + +impl SseStreamBufferTrait for AnthropicMessagesStreamBuffer { + fn add_transformed_event(&mut self, event: SseEvent) { + // Skip ping messages + if event.should_skip() { + return; + } + + // FIRST: Try to extract model name from the raw event data before transformation + // The provider_stream_response has already been transformed to Anthropic format, + // so we need to extract the model from the original raw data if available + if self.model.is_none() { + if let Some(data) = &event.data { + // Try to parse as JSON and extract model field + if let Ok(json) = serde_json::from_str::(data) { + if let Some(model) = json.get("model").and_then(|m| m.as_str()) { + self.model = Some(model.to_string()); + } + } + } + } + + // Match directly on the provider response type to handle event processing + // We match on a reference first to determine the type, then move the event + match &event.provider_stream_response { + Some(ProviderStreamResponseType::MessagesStreamEvent(evt)) => { + match evt { + MessagesStreamEvent::MessageStart { .. } => { + // Add the message_start event + self.buffered_events.push(event); + self.message_started = true; + } + MessagesStreamEvent::ContentBlockStart { index, .. } => { + let index = *index as i32; + // Inject message_start if needed + if !self.message_started { + let model = self.model.as_deref().unwrap_or("unknown"); + let message_start = AnthropicMessagesStreamBuffer::create_message_start_event(model); + self.buffered_events.push(message_start); + self.message_started = true; + } + + // Add the content_block_start event (from tool calls or other sources) + self.buffered_events.push(event); + self.set_content_block_start_sent(index); + self.needs_content_block_stop = true; + } + MessagesStreamEvent::ContentBlockDelta { index, .. } => { + let index = *index as i32; + // Inject message_start if needed + if !self.message_started { + let model = self.model.as_deref().unwrap_or("unknown"); + let message_start = AnthropicMessagesStreamBuffer::create_message_start_event(model); + self.buffered_events.push(message_start); + self.message_started = true; + } + + // Check if ContentBlockStart was sent for this index + if !self.has_content_block_start_been_sent(index) { + // Inject ContentBlockStart before delta + let content_block_start = AnthropicMessagesStreamBuffer::create_content_block_start_event(); + self.buffered_events.push(content_block_start); + self.set_content_block_start_sent(index); + self.needs_content_block_stop = true; + } + + // Content deltas are between ContentBlockStart and ContentBlockStop + self.buffered_events.push(event); + } + MessagesStreamEvent::MessageDelta { usage, .. } => { + // Inject ContentBlockStop before message_delta + if self.needs_content_block_stop { + let content_block_stop = AnthropicMessagesStreamBuffer::create_content_block_stop_event(); + self.buffered_events.push(content_block_stop); + self.needs_content_block_stop = false; + } + + // Check if the last event was also a MessageDelta - if so, merge them + // This handles Bedrock's split of stop_reason (MessageStop) and usage (Metadata) + if let Some(last_event) = self.buffered_events.last_mut() { + if let Some(ProviderStreamResponseType::MessagesStreamEvent( + MessagesStreamEvent::MessageDelta { + usage: last_usage, + .. + } + )) = &mut last_event.provider_stream_response { + // Merge: take stop_reason from first, usage from second (if non-zero) + if usage.input_tokens > 0 || usage.output_tokens > 0 { + *last_usage = usage.clone(); + } + // Mark that we've seen MessageDelta (need to send MessageStop later) + self.seen_message_delta = true; + // Don't push the new event, we've merged it + return; + } + } + + // No previous MessageDelta to merge with, add this one + self.buffered_events.push(event); + self.seen_message_delta = true; + } + MessagesStreamEvent::ContentBlockStop { .. } => { + // ContentBlockStop received from upstream (e.g., Bedrock) + // Clear the flag so we don't inject another one + self.needs_content_block_stop = false; + self.buffered_events.push(event); + } + MessagesStreamEvent::MessageStop => { + // MessageStop received from upstream (e.g., OpenAI via [DONE]) + // Clear the flag so we don't inject another one + self.seen_message_delta = false; + self.buffered_events.push(event); + } + _ => { + // Other Anthropic event types (Ping, etc.), just accumulate + self.buffered_events.push(event); + } + } + } + _ => { + // Non-Anthropic events or events without provider_stream_response, just accumulate + self.buffered_events.push(event); + } + } + } + + fn into_bytes(&mut self) -> Vec { + // Convert all accumulated events to bytes and clear buffer + // NOTE: We do NOT inject ContentBlockStop here because it's injected when we see MessageDelta + // or MessageStop. Injecting it here causes premature ContentBlockStop in the middle of streaming. + + // Inject MessageStop after MessageDelta if we've seen one + // This completes the Anthropic Messages API event sequence + if self.seen_message_delta { + let message_stop = MessagesStreamEvent::MessageStop; + let sse_string: String = message_stop.into(); + let message_stop_event = SseEvent { + data: None, + event: Some("message_stop".to_string()), + raw_line: sse_string.clone(), + sse_transformed_lines: sse_string, + provider_stream_response: None, + }; + self.buffered_events.push(message_stop_event); + self.seen_message_delta = false; + } + + let mut buffer = Vec::new(); + for event in self.buffered_events.drain(..) { + let event_bytes: Vec = event.into(); + buffer.extend_from_slice(&event_bytes); + } + buffer + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::clients::{SupportedAPIsFromClient, SupportedUpstreamAPIs}; + use crate::apis::anthropic::AnthropicApi; + use crate::apis::openai::OpenAIApi; + use crate::apis::streaming_shapes::sse::SseStreamIter; + + #[test] + fn test_openai_to_anthropic_complete_transformation() { + // OpenAI ChatCompletions input that will be transformed to Anthropic Messages API + let raw_input = r#"data: {"id":"chatcmpl-123","object":"chat.completion.chunk","created":1234567890,"model":"gpt-4o","choices":[{"index":0,"delta":{"role":"assistant","content":"Hello"},"finish_reason":null}]} + +data: {"id":"chatcmpl-123","object":"chat.completion.chunk","created":1234567890,"model":"gpt-4o","choices":[{"index":0,"delta":{"content":" world"},"finish_reason":null}]} + +data: {"id":"chatcmpl-123","object":"chat.completion.chunk","created":1234567890,"model":"gpt-4o","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]} + +data: [DONE]"#; + + println!("\n{}", "=".repeat(80)); + println!("TEST 1: OpenAI → Anthropic Messages API Complete Transformation"); + println!("{}", "=".repeat(80)); + println!("\nRAW INPUT (OpenAI ChatCompletions):"); + println!("{}", "-".repeat(80)); + println!("{}", raw_input); + + // Setup API configuration for transformation (client wants Anthropic, upstream is OpenAI) + let client_api = SupportedAPIsFromClient::AnthropicMessagesAPI(AnthropicApi::Messages); + let upstream_api = SupportedUpstreamAPIs::OpenAIChatCompletions(OpenAIApi::ChatCompletions); + + // Parse events and apply transformation + let stream_iter = SseStreamIter::try_from(raw_input.as_bytes()).unwrap(); + let mut buffer = AnthropicMessagesStreamBuffer::new(); + + for raw_event in stream_iter { + let transformed_event = SseEvent::try_from((raw_event, &client_api, &upstream_api)).unwrap(); + buffer.add_transformed_event(transformed_event); + } + + let output_bytes = buffer.into_bytes(); + let output = String::from_utf8_lossy(&output_bytes); + + println!("\nTRANSFORMED OUTPUT (Anthropic Messages API):"); + println!("{}", "-".repeat(80)); + println!("{}", output); + + // Assertions + assert!(!output_bytes.is_empty(), "Should have output"); + assert!(output.contains("event: message_start"), "Should have message_start"); + assert!(output.contains("event: content_block_start"), "Should have content_block_start (injected)"); + + let delta_count = output.matches("event: content_block_delta").count(); + assert_eq!(delta_count, 2, "Should have exactly 2 content_block_delta events"); + + // Verify both pieces of content are present + assert!(output.contains("\"text\":\"Hello\""), "Should have first content delta 'Hello'"); + assert!(output.contains("\"text\":\" world\""), "Should have second content delta ' world'"); + + assert!(output.contains("event: content_block_stop"), "Should have content_block_stop (injected)"); + assert!(output.contains("event: message_delta"), "Should have message_delta"); + assert!(output.contains("event: message_stop"), "Should have message_stop"); + + println!("\nVALIDATION SUMMARY:"); + println!("{}", "-".repeat(80)); + println!("✓ Complete transformation: OpenAI ChatCompletions → Anthropic Messages API"); + println!("✓ Injected lifecycle events: message_start, content_block_start, content_block_stop"); + println!("✓ Content deltas: {} events (BOTH 'Hello' and ' world' preserved!)", delta_count); + println!("✓ Complete stream with message_stop"); + println!("✓ Proper Anthropic protocol sequencing\n"); + } + + #[test] + fn test_openai_to_anthropic_partial_transformation() { + // Partial OpenAI ChatCompletions stream - no [DONE] + let raw_input = r#"data: {"id":"chatcmpl-456","object":"chat.completion.chunk","created":1234567890,"model":"gpt-4o","choices":[{"index":0,"delta":{"role":"assistant","content":"The weather"},"finish_reason":null}]} + +data: {"id":"chatcmpl-456","object":"chat.completion.chunk","created":1234567890,"model":"gpt-4o","choices":[{"index":0,"delta":{"content":" in San Francisco"},"finish_reason":null}]} + +data: {"id":"chatcmpl-456","object":"chat.completion.chunk","created":1234567890,"model":"gpt-4o","choices":[{"index":0,"delta":{"content":" is"},"finish_reason":null}]}"#; + + println!("\n{}", "=".repeat(80)); + println!("TEST 2: OpenAI → Anthropic Partial Transformation (NO [DONE])"); + println!("{}", "=".repeat(80)); + println!("\nRAW INPUT (OpenAI ChatCompletions - NO [DONE]):"); + println!("{}", "-".repeat(80)); + println!("{}", raw_input); + + // Setup API configuration for transformation + let client_api = SupportedAPIsFromClient::AnthropicMessagesAPI(AnthropicApi::Messages); + let upstream_api = SupportedUpstreamAPIs::OpenAIChatCompletions(OpenAIApi::ChatCompletions); + + // Parse and transform events + let stream_iter = SseStreamIter::try_from(raw_input.as_bytes()).unwrap(); + let mut buffer = AnthropicMessagesStreamBuffer::new(); + + for raw_event in stream_iter { + let transformed_event = SseEvent::try_from((raw_event, &client_api, &upstream_api)).unwrap(); + buffer.add_transformed_event(transformed_event); + } + + let output_bytes = buffer.into_bytes(); + let output = String::from_utf8_lossy(&output_bytes); + + println!("\nTRANSFORMED OUTPUT (Anthropic Messages API):"); + println!("{}", "-".repeat(80)); + println!("{}", output); + + // Assertions + assert!(!output_bytes.is_empty(), "Should have output"); + assert!(output.contains("event: message_start"), "Should have message_start"); + assert!(output.contains("event: content_block_start"), "Should have content_block_start (injected)"); + + let delta_count = output.matches("event: content_block_delta").count(); + assert_eq!(delta_count, 3, "Should have exactly 3 content_block_delta events"); + + // Verify all three pieces of content are present + assert!(output.contains("\"text\":\"The weather\""), "Should have first content delta"); + assert!(output.contains("\"text\":\" in San Francisco\""), "Should have second content delta"); + assert!(output.contains("\"text\":\" is\""), "Should have third content delta"); + + // For partial streams (no finish_reason, no [DONE]), we do NOT inject content_block_stop + // because the stream may continue. This is correct behavior - only inject lifecycle events + // when we have explicit signals from upstream (finish_reason, [DONE], etc.) + assert!(!output.contains("event: content_block_stop"), "Should NOT have content_block_stop for partial stream"); + + // Should NOT have completion events + assert!(!output.contains("event: message_delta"), "Should NOT have message_delta"); + assert!(!output.contains("event: message_stop"), "Should NOT have message_stop"); + + println!("\nVALIDATION SUMMARY:"); + println!("{}", "-".repeat(80)); + println!("✓ Partial transformation: OpenAI → Anthropic (stream interrupted)"); + println!("✓ Injected: message_start, content_block_start at beginning"); + println!("✓ Incremental deltas: {} events (ALL content preserved!)", delta_count); + println!("✓ NO completion events (partial stream, no [DONE])"); + println!("✓ Buffer maintains Anthropic protocol for active streams\n"); + } + + #[test] + fn test_openai_tool_calling_to_anthropic_transformation() { + // OpenAI ChatCompletions tool calling stream + let raw_input = r#"data: {"id":"chatcmpl-Cgx6pZPBgfLcMqfT0ILIH2mID2zWQ","object":"chat.completion.chunk","created":1764353027,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_7eeb46f068","choices":[{"index":0,"delta":{"role":"assistant","content":null,"tool_calls":[{"index":0,"id":"call_2Uzw0AEZQeOex2CP2TKjcLKc","type":"function","function":{"name":"get_weather","arguments":""}}],"refusal":null},"logprobs":null,"finish_reason":null}],"obfuscation":"uSpCcO"} + +data: {"id":"chatcmpl-Cgx6pZPBgfLcMqfT0ILIH2mID2zWQ","object":"chat.completion.chunk","created":1764353027,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_7eeb46f068","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\""}}]},"logprobs":null,"finish_reason":null}],"obfuscation":""} + +data: {"id":"chatcmpl-Cgx6pZPBgfLcMqfT0ILIH2mID2zWQ","object":"chat.completion.chunk","created":1764353027,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_7eeb46f068","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"location"}}]},"logprobs":null,"finish_reason":null}],"obfuscation":"24WSqt08jtf"} + +data: {"id":"chatcmpl-Cgx6pZPBgfLcMqfT0ILIH2mID2zWQ","object":"chat.completion.chunk","created":1764353027,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_7eeb46f068","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\":\""}}]},"logprobs":null,"finish_reason":null}],"obfuscation":"6CleV8twTxkKYg"} + +data: {"id":"chatcmpl-Cgx6pZPBgfLcMqfT0ILIH2mID2zWQ","object":"chat.completion.chunk","created":1764353027,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_7eeb46f068","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"San"}}]},"logprobs":null,"finish_reason":null}],"obfuscation":""} + +data: {"id":"chatcmpl-Cgx6pZPBgfLcMqfT0ILIH2mID2zWQ","object":"chat.completion.chunk","created":1764353027,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_7eeb46f068","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":" Francisco"}}]},"logprobs":null,"finish_reason":null}],"obfuscation":"1XLz89l3v"} + +data: {"id":"chatcmpl-Cgx6pZPBgfLcMqfT0ILIH2mID2zWQ","object":"chat.completion.chunk","created":1764353027,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_7eeb46f068","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":","}}]},"logprobs":null,"finish_reason":null}],"obfuscation":"sh"} + +data: {"id":"chatcmpl-Cgx6pZPBgfLcMqfT0ILIH2mID2zWQ","object":"chat.completion.chunk","created":1764353027,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_7eeb46f068","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":" CA"}}]},"logprobs":null,"finish_reason":null}],"obfuscation":""} + +data: {"id":"chatcmpl-Cgx6pZPBgfLcMqfT0ILIH2mID2zWQ","object":"chat.completion.chunk","created":1764353027,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_7eeb46f068","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\"}"}}]},"logprobs":null,"finish_reason":null}],"obfuscation":""} + +data: {"id":"chatcmpl-Cgx6pZPBgfLcMqfT0ILIH2mID2zWQ","object":"chat.completion.chunk","created":1764353027,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_7eeb46f068","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"tool_calls"}],"obfuscation":"I"} + +data: [DONE]"#; + + println!("\n{}", "=".repeat(80)); + println!("TEST 3: OpenAI Tool Calling → Anthropic Messages API Transformation"); + println!("{}", "=".repeat(80)); + println!("\nRAW INPUT (OpenAI ChatCompletions with Tool Calls):"); + println!("{}", "-".repeat(80)); + println!("{}", raw_input); + + // Setup API configuration for transformation + let client_api = SupportedAPIsFromClient::AnthropicMessagesAPI(AnthropicApi::Messages); + let upstream_api = SupportedUpstreamAPIs::OpenAIChatCompletions(OpenAIApi::ChatCompletions); + + // Parse and transform events + let stream_iter = SseStreamIter::try_from(raw_input.as_bytes()).unwrap(); + let mut buffer = AnthropicMessagesStreamBuffer::new(); + + for raw_event in stream_iter { + let transformed_event = SseEvent::try_from((raw_event, &client_api, &upstream_api)).unwrap(); + buffer.add_transformed_event(transformed_event); + } + + let output_bytes = buffer.into_bytes(); + let output = String::from_utf8_lossy(&output_bytes); + + println!("\nTRANSFORMED OUTPUT (Anthropic Messages API):"); + println!("{}", "-".repeat(80)); + println!("{}", output); + + // Assertions for tool calling transformation + assert!(!output_bytes.is_empty(), "Should have output"); + + // Should have lifecycle events (injected by buffer) + assert!(output.contains("event: message_start"), "Should have message_start (injected)"); + assert!(output.contains("event: content_block_start"), "Should have content_block_start"); + assert!(output.contains("event: content_block_stop"), "Should have content_block_stop (injected)"); + assert!(output.contains("event: message_delta"), "Should have message_delta"); + assert!(output.contains("event: message_stop"), "Should have message_stop"); + + // Should have tool_use content block + assert!(output.contains("\"type\":\"tool_use\""), "Should have tool_use type"); + assert!(output.contains("\"name\":\"get_weather\""), "Should have correct function name"); + assert!(output.contains("\"id\":\"call_2Uzw0AEZQeOex2CP2TKjcLKc\""), "Should have correct tool call ID"); + + // Count input_json_delta events - should match the number of argument chunks + let delta_count = output.matches("event: content_block_delta").count(); + assert!(delta_count >= 8, "Should have at least 8 input_json_delta events"); + + // Verify argument deltas are present + assert!(output.contains("\"type\":\"input_json_delta\""), "Should have input_json_delta type"); + assert!(output.contains("\"partial_json\":"), "Should have partial_json field"); + + // Verify the accumulated arguments contain the location + assert!(output.contains("San"), "Arguments should contain 'San'"); + assert!(output.contains("Francisco"), "Arguments should contain 'Francisco'"); + assert!(output.contains("CA"), "Arguments should contain 'CA'"); + + // Verify stop reason is tool_use + assert!(output.contains("\"stop_reason\":\"tool_use\""), "Should have stop_reason as tool_use"); + + println!("\nVALIDATION SUMMARY:"); + println!("{}", "-".repeat(80)); + println!("✓ Complete tool calling transformation: OpenAI → Anthropic Messages API"); + println!("✓ Injected lifecycle: message_start, content_block_stop"); + println!("✓ Tool metadata: name='get_weather', id='call_2Uzw0AEZQeOex2CP2TKjcLKc'"); + println!("✓ Argument deltas: {} events", delta_count); + println!("✓ Complete JSON arguments: '{{\"location\":\"San Francisco, CA\"}}'"); + println!("✓ Stop reason: tool_use"); + println!("✓ Proper Anthropic tool_use protocol\n"); + } +} diff --git a/crates/hermesllm/src/apis/streaming_shapes/chat_completions_streaming_buffer.rs b/crates/hermesllm/src/apis/streaming_shapes/chat_completions_streaming_buffer.rs new file mode 100644 index 00000000..0243a5cd --- /dev/null +++ b/crates/hermesllm/src/apis/streaming_shapes/chat_completions_streaming_buffer.rs @@ -0,0 +1,39 @@ +use crate::apis::streaming_shapes::sse::{SseEvent, SseStreamBufferTrait}; + +/// OpenAI Chat Completions SSE Stream Buffer for when client and upstream APIs match. +pub struct OpenAIChatCompletionsStreamBuffer { + /// Buffered SSE events ready to be written to wire + buffered_events: Vec, +} + +impl OpenAIChatCompletionsStreamBuffer { + pub fn new() -> Self { + Self { + buffered_events: Vec::new(), + } + } +} + +impl SseStreamBufferTrait for OpenAIChatCompletionsStreamBuffer { + fn add_transformed_event(&mut self, event: SseEvent) { + // Skip ping messages + if event.should_skip() { + return; + } + + // For OpenAI Chat Completions, events are already properly transformed + // Just accumulate them for later wire transmission + self.buffered_events.push(event); + } + + fn into_bytes(&mut self) -> Vec { + // No finalization needed for OpenAI Chat Completions + // The [DONE] marker is already handled by the transformation layer + let mut buffer = Vec::new(); + for event in self.buffered_events.drain(..) { + let event_bytes: Vec = event.into(); + buffer.extend_from_slice(&event_bytes); + } + buffer + } +} diff --git a/crates/hermesllm/src/apis/streaming_shapes/mod.rs b/crates/hermesllm/src/apis/streaming_shapes/mod.rs new file mode 100644 index 00000000..4db3b094 --- /dev/null +++ b/crates/hermesllm/src/apis/streaming_shapes/mod.rs @@ -0,0 +1,7 @@ +pub mod sse; +pub mod sse_chunk_processor; +pub mod amazon_bedrock_binary_frame; +pub mod anthropic_streaming_buffer; +pub mod chat_completions_streaming_buffer; +pub mod passthrough_streaming_buffer; +pub mod responses_api_streaming_buffer; diff --git a/crates/hermesllm/src/apis/streaming_shapes/passthrough_streaming_buffer.rs b/crates/hermesllm/src/apis/streaming_shapes/passthrough_streaming_buffer.rs new file mode 100644 index 00000000..2ac2a688 --- /dev/null +++ b/crates/hermesllm/src/apis/streaming_shapes/passthrough_streaming_buffer.rs @@ -0,0 +1,95 @@ +use crate::apis::streaming_shapes::sse::{SseEvent, SseStreamBufferTrait}; + +/// Passthrough SSE Stream Buffer for when client and upstream APIs match. +pub struct PassthroughStreamBuffer { + /// Buffered SSE events ready to be written to wire + buffered_events: Vec, +} + +impl PassthroughStreamBuffer { + pub fn new() -> Self { + Self { + buffered_events: Vec::new(), + } + } +} + +impl SseStreamBufferTrait for PassthroughStreamBuffer { + fn add_transformed_event(&mut self, event: SseEvent) { + // Skip ping messages + if event.should_skip() { + return; + } + + // Skip events with empty transformed lines (e.g., suppressed event-only lines) + if event.sse_transformed_lines.is_empty() { + return; + } + + // Just accumulate events as-is + self.buffered_events.push(event); + } + + fn into_bytes(&mut self) -> Vec { + // No finalization needed for passthrough - just convert accumulated events to bytes + let mut buffer = Vec::new(); + for event in self.buffered_events.drain(..) { + let event_bytes: Vec = event.into(); + buffer.extend_from_slice(&event_bytes); + } + buffer + } +} + +#[cfg(test)] +mod tests { + use crate::apis::streaming_shapes::passthrough_streaming_buffer::PassthroughStreamBuffer; + use crate::apis::streaming_shapes::sse::{SseStreamIter, SseStreamBufferTrait}; + + #[test] + fn test_chat_completions_passthrough_buffer() { + let raw_input = r#"data: {"id":"chatcmpl-123","object":"chat.completion.chunk","created":1234567890,"model":"gpt-4o","choices":[{"index":0,"delta":{"role":"assistant","content":null,"tool_calls":[{"index":0,"id":"call_abc","type":"function","function":{"name":"get_weather","arguments":""}}],"refusal":null},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-123","object":"chat.completion.chunk","created":1234567890,"model":"gpt-4o","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\""}}]},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-123","object":"chat.completion.chunk","created":1234567890,"model":"gpt-4o","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"location"}}]},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-123","object":"chat.completion.chunk","created":1234567890,"model":"gpt-4o","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"tool_calls"}]} + + data: [DONE]"#; + + println!("\n{}", "=".repeat(80)); + println!("TEST 1: ChatCompletions Passthrough Buffer"); + println!("{}", "=".repeat(80)); + println!("\nRAW INPUT (ChatCompletions):"); + println!("{}", "-".repeat(80)); + println!("{}", raw_input); + + // Parse and process through buffer + let stream_iter = SseStreamIter::try_from(raw_input.as_bytes()).unwrap(); + let mut buffer = PassthroughStreamBuffer::new(); + + for event in stream_iter { + buffer.add_transformed_event(event); + } + + let output_bytes = buffer.into_bytes(); + let output = String::from_utf8_lossy(&output_bytes); + + println!("\nTRANSFORMED OUTPUT (ChatCompletions - Passthrough):"); + println!("{}", "-".repeat(80)); + println!("{}", output); + + // Assertions + assert!(!output_bytes.is_empty()); + assert!(output.contains("chatcmpl-123")); + assert!(output.contains("[DONE]")); + assert_eq!(raw_input.trim(), output.trim(), "Passthrough should preserve input"); + + println!("\nVALIDATION SUMMARY:"); + println!("{}", "-".repeat(80)); + println!("✓ Passthrough buffer: input = output (no transformation)"); + println!("✓ All events preserved including [DONE]"); + println!("✓ Function calling events preserved\n"); + } +} diff --git a/crates/hermesllm/src/apis/streaming_shapes/responses_api_streaming_buffer.rs b/crates/hermesllm/src/apis/streaming_shapes/responses_api_streaming_buffer.rs new file mode 100644 index 00000000..ca8a9cfd --- /dev/null +++ b/crates/hermesllm/src/apis/streaming_shapes/responses_api_streaming_buffer.rs @@ -0,0 +1,649 @@ +use std::collections::HashMap; +use log::debug; +use crate::apis::openai_responses::{ + ResponsesAPIStreamEvent, ResponsesAPIResponse, OutputItem, OutputItemStatus, + ResponseStatus, TextConfig, TextFormat, Reasoning, +}; +use crate::apis::streaming_shapes::sse::{SseEvent, SseStreamBufferTrait}; + +/// Helper to convert ResponseAPIStreamEvent to SseEvent +fn event_to_sse(event: ResponsesAPIStreamEvent) -> SseEvent { + let event_type = match &event { + ResponsesAPIStreamEvent::ResponseCreated { .. } => "response.created", + ResponsesAPIStreamEvent::ResponseInProgress { .. } => "response.in_progress", + ResponsesAPIStreamEvent::ResponseCompleted { .. } => "response.completed", + ResponsesAPIStreamEvent::ResponseOutputItemAdded { .. } => "response.output_item.added", + ResponsesAPIStreamEvent::ResponseOutputItemDone { .. } => "response.output_item.done", + ResponsesAPIStreamEvent::ResponseOutputTextDelta { .. } => "response.output_text.delta", + ResponsesAPIStreamEvent::ResponseOutputTextDone { .. } => "response.output_text.done", + ResponsesAPIStreamEvent::ResponseFunctionCallArgumentsDelta { .. } => "response.function_call_arguments.delta", + ResponsesAPIStreamEvent::ResponseFunctionCallArgumentsDone { .. } => "response.function_call_arguments.done", + unknown => { + debug!("Unknown ResponsesAPIStreamEvent type encountered: {:?}", unknown); + "unknown" + } + }; + + let json_data = match serde_json::to_string(&event) { + Ok(data) => data, + Err(e) => { + debug!("Error serializing ResponsesAPIStreamEvent to JSON: {}", e); + String::new() + } + }; + let wire_format: String = event.into(); + + SseEvent { + data: Some(json_data), + event: Some(event_type.to_string()), + raw_line: wire_format.clone(), + sse_transformed_lines: wire_format, + provider_stream_response: None, + } +} + +/// SSE Stream Buffer for ResponsesAPIStreamEvent with full lifecycle management. +/// +/// This buffer manages the wire format for v1/responses streaming, handling +/// delta events and emitting complete lifecycle events. +/// +pub struct ResponsesAPIStreamBuffer { + /// Sequence number for events + sequence_number: i32, + + /// Track item IDs by output index + item_ids: HashMap, + + /// Response metadata + response_id: Option, + model: Option, + created_at: Option, + + /// Full response metadata from upstream (tools, temperature, etc.) + /// This is extracted from the first upstream event and used to build + /// complete response.created and response.in_progress events + upstream_response_metadata: Option, + + /// Lifecycle state flags + created_emitted: bool, + in_progress_emitted: bool, + + /// Track which output items we've added + output_items_added: HashMap, // output_index -> item_id + + /// Accumulated content by item_id + text_content: HashMap, + function_arguments: HashMap, + + /// Tool call metadata by output_index + tool_call_metadata: HashMap, // output_index -> (call_id, name) + + /// Final completed response (for logging/tracing/persistence) + completed_response: Option, + + /// Buffered SSE events ready to be written to wire + buffered_events: Vec, +} + +impl ResponsesAPIStreamBuffer { + pub fn new() -> Self { + Self { + sequence_number: 0, + item_ids: HashMap::new(), + response_id: None, + model: None, + created_at: None, + upstream_response_metadata: None, + created_emitted: false, + in_progress_emitted: false, + output_items_added: HashMap::new(), + text_content: HashMap::new(), + function_arguments: HashMap::new(), + tool_call_metadata: HashMap::new(), + completed_response: None, + buffered_events: Vec::new(), + } + } + + fn next_sequence_number(&mut self) -> i32 { + let seq = self.sequence_number; + self.sequence_number += 1; + seq + } + + fn generate_item_id(prefix: &str) -> String { + format!("{}_{}", prefix, uuid::Uuid::new_v4().to_string().replace("-", "")) + } + + fn get_or_create_item_id(&mut self, output_index: i32, prefix: &str) -> String { + if let Some(id) = self.item_ids.get(&output_index) { + return id.clone(); + } + let id = ResponsesAPIStreamBuffer::generate_item_id(prefix); + self.item_ids.insert(output_index, id.clone()); + id + } + + /// Create response.created event + fn create_response_created_event(&mut self) -> SseEvent { + let response = self.build_response(ResponseStatus::InProgress); + let event = ResponsesAPIStreamEvent::ResponseCreated { + response, + sequence_number: self.next_sequence_number(), + }; + event_to_sse(event) + } + + /// Create response.in_progress event + fn create_response_in_progress_event(&mut self) -> SseEvent { + let response = self.build_response(ResponseStatus::InProgress); + let event = ResponsesAPIStreamEvent::ResponseInProgress { + response, + sequence_number: self.next_sequence_number(), + }; + event_to_sse(event) + } + + /// Create output_item.added event for text + fn create_output_item_added_event(&mut self, output_index: i32, item_id: &str) -> SseEvent { + let event = ResponsesAPIStreamEvent::ResponseOutputItemAdded { + output_index, + item: OutputItem::Message { + id: item_id.to_string(), + status: OutputItemStatus::InProgress, + role: "assistant".to_string(), + content: vec![], + }, + sequence_number: self.next_sequence_number(), + }; + event_to_sse(event) + } + + /// Create output_item.added event for tool call + fn create_tool_call_added_event(&mut self, output_index: i32, item_id: &str, call_id: &str, name: &str) -> SseEvent { + let event = ResponsesAPIStreamEvent::ResponseOutputItemAdded { + output_index, + item: OutputItem::FunctionCall { + id: item_id.to_string(), + status: OutputItemStatus::InProgress, + call_id: call_id.to_string(), + name: Some(name.to_string()), + arguments: Some(String::new()), + }, + sequence_number: self.next_sequence_number(), + }; + event_to_sse(event) + } + + /// Build the base response object with current state + fn build_response(&self, status: ResponseStatus) -> ResponsesAPIResponse { + // If we have upstream metadata, use it as a base and update status/output + if let Some(upstream) = &self.upstream_response_metadata { + let mut response = upstream.clone(); + response.status = status; + // Don't update output here - will be set in finalize() + return response; + } + + // Fallback: build a minimal response from local state + ResponsesAPIResponse { + id: self.response_id.clone().unwrap_or_default(), + object: "response".to_string(), + created_at: self.created_at.unwrap_or(0), + status, + error: None, + incomplete_details: None, + instructions: None, + model: self.model.clone().unwrap_or_else(|| "unknown".to_string()), + output: vec![], + usage: None, + parallel_tool_calls: true, + conversation: None, + previous_response_id: None, + tools: vec![], + tool_choice: "auto".to_string(), + temperature: 1.0, + top_p: 1.0, + metadata: HashMap::new(), + truncation: Some("disabled".to_string()), + max_output_tokens: None, + reasoning: Some(Reasoning { + effort: None, + summary: None, + }), + store: Some(true), + text: Some(TextConfig { + format: TextFormat::Text, + }), + audio: None, + modalities: None, + service_tier: Some("auto".to_string()), + background: Some(false), + top_logprobs: Some(0), + max_tool_calls: None, + } + } + + /// Get the completed response after finalization (for logging/tracing/persistence) + pub fn get_completed_response(&self) -> Option<&ResponsesAPIResponse> { + self.completed_response.as_ref() + } + + /// Finalize the response by emitting all *.done events and response.completed. + /// Call this when the stream is complete (after seeing [DONE] or end_of_stream). + pub fn finalize(&mut self) { + let mut events = Vec::new(); + + // Emit done events for all accumulated content + + // Text content done events + let text_items: Vec<_> = self.text_content.iter().map(|(id, content)| (id.clone(), content.clone())).collect(); + for (item_id, content) in text_items { + let output_index = self.output_items_added.iter() + .find(|(_, id)| **id == item_id) + .map(|(idx, _)| *idx) + .unwrap_or(0); + + let seq1 = self.next_sequence_number(); + let text_done_event = ResponsesAPIStreamEvent::ResponseOutputTextDone { + item_id: item_id.clone(), + output_index, + content_index: 0, + text: content.clone(), + logprobs: vec![], + sequence_number: seq1, + }; + events.push(event_to_sse(text_done_event)); + + let seq2 = self.next_sequence_number(); + let item_done_event = ResponsesAPIStreamEvent::ResponseOutputItemDone { + output_index, + item: OutputItem::Message { + id: item_id.clone(), + status: OutputItemStatus::Completed, + role: "assistant".to_string(), + content: vec![], + }, + sequence_number: seq2, + }; + events.push(event_to_sse(item_done_event)); + } + + // Function call done events + let func_items: Vec<_> = self.function_arguments.iter().map(|(id, args)| (id.clone(), args.clone())).collect(); + for (item_id, arguments) in func_items { + let output_index = self.output_items_added.iter() + .find(|(_, id)| **id == item_id) + .map(|(idx, _)| *idx) + .unwrap_or(0); + + let seq1 = self.next_sequence_number(); + let args_done_event = ResponsesAPIStreamEvent::ResponseFunctionCallArgumentsDone { + output_index, + item_id: item_id.clone(), + arguments: arguments.clone(), + sequence_number: seq1, + }; + events.push(event_to_sse(args_done_event)); + + let (call_id, name) = self.tool_call_metadata.get(&output_index) + .cloned() + .unwrap_or_else(|| (format!("call_{}", uuid::Uuid::new_v4()), "unknown".to_string())); + + let seq2 = self.next_sequence_number(); + let item_done_event = ResponsesAPIStreamEvent::ResponseOutputItemDone { + output_index, + item: OutputItem::FunctionCall { + id: item_id.clone(), + status: OutputItemStatus::Completed, + call_id, + name: Some(name), + arguments: Some(arguments.clone()), + }, + sequence_number: seq2, + }; + events.push(event_to_sse(item_done_event)); + } + + // Build final response + let mut output_items = Vec::new(); + + // Build complete output array by iterating through all output indices in order + let max_output_index = self.output_items_added.keys().max().copied().unwrap_or(-1); + + for output_index in 0..=max_output_index { + if let Some(item_id) = self.output_items_added.get(&output_index) { + // Check if this is a function call + if let Some(arguments) = self.function_arguments.get(item_id) { + let (call_id, name) = self.tool_call_metadata.get(&output_index) + .cloned() + .unwrap_or_else(|| (format!("call_{}", uuid::Uuid::new_v4()), "unknown".to_string())); + + output_items.push(OutputItem::FunctionCall { + id: item_id.clone(), + status: OutputItemStatus::Completed, + call_id, + name: Some(name), + arguments: Some(arguments.clone()), + }); + } + // Check if this is a text message + else if let Some(text) = self.text_content.get(item_id) { + use crate::apis::openai_responses::OutputContent; + output_items.push(OutputItem::Message { + id: item_id.clone(), + status: OutputItemStatus::Completed, + role: "assistant".to_string(), + content: vec![OutputContent::OutputText { + text: text.clone(), + annotations: vec![], + logprobs: None, + }], + }); + } + } + } + + let mut final_response = self.build_response(ResponseStatus::Completed); + final_response.output = output_items; + + // Store completed response + self.completed_response = Some(final_response.clone()); + + // Emit response.completed + let seq_final = self.next_sequence_number(); + let completed_event = ResponsesAPIStreamEvent::ResponseCompleted { + response: final_response, + sequence_number: seq_final, + }; + events.push(event_to_sse(completed_event)); + + // Add all finalization events to the buffer + self.buffered_events.extend(events); + } +} + +impl SseStreamBufferTrait for ResponsesAPIStreamBuffer { + fn add_transformed_event(&mut self, event: SseEvent) { + // Skip ping messages + if event.should_skip() { + return; + } + + // Handle [DONE] marker - trigger finalization + if event.is_done() { + self.finalize(); + return; + } + + // Extract the ResponseAPIStreamEvent from the SseEvent's provider_stream_response + let provider_response = match event.provider_stream_response.as_ref() { + Some(response) => response, + None => { + eprintln!("Warning: Event missing provider_stream_response"); + return; + } + }; + + // Extract ResponseAPIStreamEvent from the enum + let stream_event = match provider_response { + crate::providers::streaming_response::ProviderStreamResponseType::ResponseAPIStreamEvent(evt) => evt, + _ => { + eprintln!("Warning: Expected ResponseAPIStreamEvent in provider_stream_response"); + return; + } + }; + + let mut events = Vec::new(); + + // Capture upstream metadata from ResponseCreated or ResponseInProgress if present + match stream_event { + ResponsesAPIStreamEvent::ResponseCreated { response, .. } | + ResponsesAPIStreamEvent::ResponseInProgress { response, .. } => { + if self.upstream_response_metadata.is_none() { + // Store the full upstream response as our metadata template + self.upstream_response_metadata = Some(response.clone()); + // Also extract basic fields + self.response_id = Some(response.id.clone()); + self.model = Some(response.model.clone()); + self.created_at = Some(response.created_at); + } + // Don't emit these - we'll generate our own lifecycle events + return; + } + _ => {} + } + + // Emit lifecycle events if not yet emitted + if !self.created_emitted { + // Initialize metadata from first event if needed + if self.response_id.is_none() { + self.response_id = Some(format!("resp_{}", uuid::Uuid::new_v4().to_string().replace("-", ""))); + self.created_at = Some(std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs() as i64); + self.model = Some("unknown".to_string()); // Will be set by caller if available + } + + events.push(self.create_response_created_event()); + self.created_emitted = true; + } + + if !self.in_progress_emitted { + events.push(self.create_response_in_progress_event()); + self.in_progress_emitted = true; + } + + // Process the delta event + match stream_event { + ResponsesAPIStreamEvent::ResponseOutputTextDelta { output_index, delta, .. } => { + let item_id = self.get_or_create_item_id(*output_index, "msg"); + + // Emit output_item.added if this is the first time we see this output index + if !self.output_items_added.contains_key(output_index) { + self.output_items_added.insert(*output_index, item_id.clone()); + events.push(self.create_output_item_added_event(*output_index, &item_id)); + } + + // Accumulate text content + self.text_content.entry(item_id.clone()) + .and_modify(|content| content.push_str(delta)) + .or_insert_with(|| delta.clone()); + + // Emit text delta with filled-in item_id and sequence_number + let mut delta_event = stream_event.clone(); + if let ResponsesAPIStreamEvent::ResponseOutputTextDelta { item_id: ref mut id, sequence_number: ref mut seq, .. } = delta_event { + *id = item_id; + *seq = self.next_sequence_number(); + } + events.push(event_to_sse(delta_event)); + } + ResponsesAPIStreamEvent::ResponseFunctionCallArgumentsDelta { output_index, delta, call_id, name, .. } => { + let item_id = self.get_or_create_item_id(*output_index, "fc"); + + // Store metadata if provided (from initial tool call event) + if let (Some(cid), Some(n)) = (call_id, name) { + self.tool_call_metadata.insert(*output_index, (cid.clone(), n.clone())); + } + + // Emit output_item.added if this is the first time we see this tool call + if !self.output_items_added.contains_key(output_index) { + self.output_items_added.insert(*output_index, item_id.clone()); + + // For tool calls, we need call_id and name from metadata + // These should now be populated from the event itself + let (call_id, name) = self.tool_call_metadata.get(output_index) + .cloned() + .unwrap_or_else(|| (format!("call_{}", uuid::Uuid::new_v4()), "unknown".to_string())); + + events.push(self.create_tool_call_added_event(*output_index, &item_id, &call_id, &name)); + } + + // Accumulate function arguments + self.function_arguments.entry(item_id.clone()) + .and_modify(|args| args.push_str(delta)) + .or_insert_with(|| delta.clone()); + + // Emit function call arguments delta with filled-in item_id and sequence_number + let mut delta_event = stream_event.clone(); + if let ResponsesAPIStreamEvent::ResponseFunctionCallArgumentsDelta { item_id: ref mut id, sequence_number: ref mut seq, .. } = delta_event { + *id = item_id; + *seq = self.next_sequence_number(); + } + events.push(event_to_sse(delta_event)); + } + _ => { + // For other event types, just pass through with sequence number + let other_event = stream_event.clone(); + // TODO: Add sequence number to other event types if needed + events.push(event_to_sse(other_event)); + } + } + + // Store all generated events in the buffer + self.buffered_events.extend(events); + } + + + fn into_bytes(&mut self) -> Vec { + // For Responses API, we need special handling: + // - Most events are already in buffered_events from add_transformed_event + // - We should NOT finalize here - finalization happens when we detect [DONE] or end of stream + // - Just flush the accumulated events and clear the buffer + + // Convert all accumulated events to bytes and clear buffer + let mut buffer = Vec::new(); + for event in self.buffered_events.drain(..) { + let event_bytes: Vec = event.into(); + buffer.extend_from_slice(&event_bytes); + } + buffer + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::clients::{SupportedAPIsFromClient, SupportedUpstreamAPIs}; + use crate::apis::openai::OpenAIApi; + use crate::apis::streaming_shapes::sse::SseStreamIter; + + #[test] + fn test_chat_completions_to_responses_api_transformation() { + // ChatCompletions input that will be transformed to ResponsesAPI + let raw_input = r#"data: {"id":"chatcmpl-123","object":"chat.completion.chunk","created":1234567890,"model":"gpt-4o","choices":[{"index":0,"delta":{"role":"assistant","content":"Hello"},"finish_reason":null}]} + + data: {"id":"chatcmpl-123","object":"chat.completion.chunk","created":1234567890,"model":"gpt-4o","choices":[{"index":0,"delta":{"content":" world"},"finish_reason":null}]} + + data: {"id":"chatcmpl-123","object":"chat.completion.chunk","created":1234567890,"model":"gpt-4o","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]} + + data: [DONE]"#; + + println!("\n{}", "=".repeat(80)); + println!("TEST 2: ChatCompletions → ResponsesAPI Transformation (with [DONE])"); + println!("{}", "=".repeat(80)); + println!("\nRAW INPUT (ChatCompletions):"); + println!("{}", "-".repeat(80)); + println!("{}", raw_input); + + // Setup API configuration for transformation + let client_api = SupportedAPIsFromClient::OpenAIResponsesAPI(OpenAIApi::Responses); + let upstream_api = SupportedUpstreamAPIs::OpenAIChatCompletions(OpenAIApi::ChatCompletions); + + // Parse events and apply transformation + let stream_iter = SseStreamIter::try_from(raw_input.as_bytes()).unwrap(); + let mut buffer = ResponsesAPIStreamBuffer::new(); + + for raw_event in stream_iter { + // Transform the event using the client/upstream APIs + let transformed_event = SseEvent::try_from((raw_event, &client_api, &upstream_api)).unwrap(); + buffer.add_transformed_event(transformed_event); + } + + let output_bytes = buffer.into_bytes(); + let output = String::from_utf8_lossy(&output_bytes); + + println!("\nTRANSFORMED OUTPUT (ResponsesAPI):"); + println!("{}", "-".repeat(80)); + println!("{}", output); + + // Assertions + assert!(!output_bytes.is_empty(), "Should have output"); + assert!(output.contains("response.created"), "Should have response.created"); + assert!(output.contains("response.in_progress"), "Should have response.in_progress"); + assert!(output.contains("response.output_item.added"), "Should have output_item.added"); + assert!(output.contains("response.output_text.delta"), "Should have text deltas"); + assert!(output.contains("response.output_text.done"), "Should have text.done"); + assert!(output.contains("response.output_item.done"), "Should have output_item.done"); + assert!(output.contains("response.completed"), "Should have response.completed"); + + println!("\nVALIDATION SUMMARY:"); + println!("{}", "-".repeat(80)); + println!("✓ Lifecycle events: response.created, response.in_progress, response.completed"); + println!("✓ Output item lifecycle: output_item.added, output_item.done"); + println!("✓ Text streaming: output_text.delta (2 deltas), output_text.done"); + println!("✓ Complete transformation with finalization ([DONE] processed)\n"); + } + + #[test] + fn test_partial_streaming_incremental_output() { + let raw_input = r#"data: {"id":"chatcmpl-CfpqklihniLRuuQfP7inMb2ghtGmT","object":"chat.completion.chunk","created":1764086794,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_7eeb46f068","choices":[{"index":0,"delta":{"role":"assistant","content":null,"tool_calls":[{"index":0,"id":"call_mD5ggLKk3SMKGPFqFdcpKg6q","type":"function","function":{"name":"get_weather","arguments":""}}],"refusal":null},"logprobs":null,"finish_reason":null}],"obfuscation":"PCFrpy"} + + data: {"id":"chatcmpl-CfpqklihniLRuuQfP7inMb2ghtGmT","object":"chat.completion.chunk","created":1764086794,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_7eeb46f068","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\""}}]},"logprobs":null,"finish_reason":null}],"obfuscation":""} + + data: {"id":"chatcmpl-CfpqklihniLRuuQfP7inMb2ghtGmT","object":"chat.completion.chunk","created":1764086794,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_7eeb46f068","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"location"}}]},"logprobs":null,"finish_reason":null}],"obfuscation":"TC58A3QEIx8"} + + data: {"id":"chatcmpl-CfpqklihniLRuuQfP7inMb2ghtGmT","object":"chat.completion.chunk","created":1764086794,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_7eeb46f068","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\":\""}}]},"logprobs":null,"finish_reason":null}],"obfuscation":"PK4oFzlVlGTUP5"}"#; + + println!("\n{}", "=".repeat(80)); + println!("TEST 3: Partial Streaming - Function Calling (NO [DONE])"); + println!("{}", "=".repeat(80)); + println!("\nRAW INPUT (ChatCompletions - NO [DONE]):"); + println!("{}", "-".repeat(80)); + println!("{}", raw_input); + + // Setup API configuration for transformation + let client_api = SupportedAPIsFromClient::OpenAIResponsesAPI(OpenAIApi::Responses); + let upstream_api = SupportedUpstreamAPIs::OpenAIChatCompletions(OpenAIApi::ChatCompletions); + + // Transform all events + let stream_iter = SseStreamIter::try_from(raw_input.as_bytes()).unwrap(); + let mut buffer = ResponsesAPIStreamBuffer::new(); + + for raw_event in stream_iter { + let transformed = SseEvent::try_from((raw_event, &client_api, &upstream_api)).unwrap(); + buffer.add_transformed_event(transformed); + } + + let output_bytes = buffer.into_bytes(); + let output = String::from_utf8_lossy(&output_bytes); + + println!("\nTRANSFORMED OUTPUT (ResponsesAPI):"); + println!("{}", "-".repeat(80)); + println!("{}", output); + + // Assertions + assert!(output.contains("response.created"), "Should have response.created"); + assert!(output.contains("response.in_progress"), "Should have response.in_progress"); + assert!(output.contains("response.output_item.added"), "Should have output_item.added"); + assert!(output.contains("\"type\":\"function_call\""), "Should be function_call type"); + assert!(output.contains("\"name\":\"get_weather\""), "Should have function name"); + assert!(output.contains("\"call_id\":\"call_mD5ggLKk3SMKGPFqFdcpKg6q\""), "Should have correct call_id"); + + let delta_count = output.matches("event: response.function_call_arguments.delta").count(); + assert_eq!(delta_count, 4, "Should have 4 delta events"); + + assert!(!output.contains("response.function_call_arguments.done"), "Should NOT have arguments.done"); + assert!(!output.contains("response.output_item.done"), "Should NOT have output_item.done"); + assert!(!output.contains("response.completed"), "Should NOT have response.completed"); + + println!("\nVALIDATION SUMMARY:"); + println!("{}", "-".repeat(80)); + println!("✓ Lifecycle events: response.created, response.in_progress"); + println!("✓ Function call metadata: name='get_weather', call_id='call_mD5ggLKk3SMKGPFqFdcpKg6q'"); + println!("✓ Incremental deltas: 4 events (1 initial + 3 argument chunks)"); + println!("✓ NO completion events (partial stream, no [DONE])"); + println!("✓ Arguments accumulated: '{{\"location\":\"'\n"); + } +} diff --git a/crates/hermesllm/src/apis/sse.rs b/crates/hermesllm/src/apis/streaming_shapes/sse.rs similarity index 50% rename from crates/hermesllm/src/apis/sse.rs rename to crates/hermesllm/src/apis/streaming_shapes/sse.rs index b8a9b492..05f0b296 100644 --- a/crates/hermesllm/src/apis/sse.rs +++ b/crates/hermesllm/src/apis/streaming_shapes/sse.rs @@ -1,10 +1,73 @@ -use crate::providers::response::ProviderStreamResponse; -use crate::providers::response::ProviderStreamResponseType; +use crate::providers::streaming_response::ProviderStreamResponse; +use crate::providers::streaming_response::ProviderStreamResponseType; +use crate::apis::streaming_shapes::chat_completions_streaming_buffer::OpenAIChatCompletionsStreamBuffer; +use crate::apis::streaming_shapes::anthropic_streaming_buffer::AnthropicMessagesStreamBuffer; +use crate::apis::streaming_shapes::passthrough_streaming_buffer::PassthroughStreamBuffer; +use crate::apis::streaming_shapes::responses_api_streaming_buffer::ResponsesAPIStreamBuffer; use serde::{Deserialize, Serialize}; use std::error::Error; use std::fmt; use std::str::FromStr; +/// Trait defining the interface for SSE stream buffers. +/// +/// This trait is implemented by both the enum `SseStreamBuffer` (for zero-cost dispatch) +/// and individual buffer implementations (for direct use). +/// +pub trait SseStreamBufferTrait: Send + Sync { + /// Add a transformed SSE event to the buffer. + /// + /// The buffer may inject additional events as needed based on internal state. + /// For example, Anthropic buffers inject ContentBlockStart before the first ContentBlockDelta. + /// + /// All events (original + injected) are accumulated internally for the next `into_bytes()` call. + /// + /// # Arguments + /// * `event` - A transformed SSE event to accumulate + fn add_transformed_event(&mut self, event: SseEvent); + + /// Get bytes for all accumulated events since the last call. + /// + /// This method: + /// - Converts all buffered events to wire format bytes + /// - Clears the internal event buffer + /// - Preserves state for subsequent `add_transformed_event()` calls + /// + /// Call this after processing each chunk of upstream events to get bytes for immediate transmission. + /// + /// # Returns + /// Bytes ready for wire transmission (may be empty if no events were accumulated) + fn into_bytes(&mut self) -> Vec; +} + +/// Unified SSE Stream Buffer enum that provides a zero-cost abstraction +pub enum SseStreamBuffer { + Passthrough(PassthroughStreamBuffer), + OpenAIChatCompletions(OpenAIChatCompletionsStreamBuffer), + AnthropicMessages(AnthropicMessagesStreamBuffer), + OpenAIResponses(ResponsesAPIStreamBuffer), +} + +impl SseStreamBufferTrait for SseStreamBuffer { + fn add_transformed_event(&mut self, event: SseEvent) { + match self { + Self::Passthrough(buffer) => buffer.add_transformed_event(event), + Self::OpenAIChatCompletions(buffer) => buffer.add_transformed_event(event), + Self::AnthropicMessages(buffer) => buffer.add_transformed_event(event), + Self::OpenAIResponses(buffer) => buffer.add_transformed_event(event), + } + } + + fn into_bytes(&mut self) -> Vec { + match self { + Self::Passthrough(buffer) => buffer.into_bytes(), + Self::OpenAIChatCompletions(buffer) => buffer.into_bytes(), + Self::AnthropicMessages(buffer) => buffer.into_bytes(), + Self::OpenAIResponses(buffer) => buffer.into_bytes(), + } + } +} + // ============================================================================ // SSE EVENT CONTAINER // ============================================================================ @@ -22,16 +85,31 @@ pub struct SseEvent { pub raw_line: String, // The complete line as received including "data: " prefix and "\n\n" #[serde(skip_serializing, skip_deserializing)] - pub sse_transform_buffer: String, // The complete line as received including "data: " prefix and "\n\n" + pub sse_transformed_lines: String, // The complete line as received including "data: " prefix and "\n\n" #[serde(skip_serializing, skip_deserializing)] pub provider_stream_response: Option, // Parsed provider stream response object } impl SseEvent { + /// Create an SseEvent from a ProviderStreamResponseType + /// This is useful for binary frame formats (like Bedrock) that need to be converted to SSE + pub fn from_provider_response(response: ProviderStreamResponseType) -> Self { + // Convert the provider response to SSE format string + let sse_string: String = response.clone().into(); + + SseEvent { + data: None, // Data is embedded in sse_transformed_lines + event: None, // Event type is embedded in sse_transformed_lines + raw_line: sse_string.clone(), + sse_transformed_lines: sse_string, + provider_stream_response: Some(response), + } + } + /// Check if this event represents the end of the stream pub fn is_done(&self) -> bool { - self.data == Some("[DONE]".into()) + self.data == Some("[DONE]".into()) || self.event == Some("message_stop".into()) } /// Check if this event should be skipped during processing @@ -61,23 +139,35 @@ impl FromStr for SseEvent { type Err = SseParseError; fn from_str(line: &str) -> Result { - if line.starts_with("data: ") { - let data: String = line[6..].to_string(); // Remove "data: " prefix - if data.is_empty() { + // Trim leading/trailing whitespace for parsing + let trimmed_line = line.trim(); + + // Skip empty or whitespace-only lines (SSE event separators) + if trimmed_line.is_empty() { + return Err(SseParseError { + message: "Empty line (SSE event separator)".to_string(), + }); + } + + if trimmed_line.starts_with("data: ") { + let data: String = trimmed_line[6..].to_string(); // Remove "data: " prefix + // Allow empty data content after "data: " prefix + // This handles cases like "data: " followed by newline + if data.trim().is_empty() { return Err(SseParseError { - message: "Empty data field is not a valid SSE event".to_string(), + message: "Empty data field after 'data: ' prefix".to_string(), }); } Ok(SseEvent { data: Some(data), event: None, raw_line: line.to_string(), - sse_transform_buffer: line.to_string(), + // Preserve original line format for passthrough, use trimmed for transformations + sse_transformed_lines: line.to_string(), provider_stream_response: None, }) - } else if line.starts_with("event: ") { - //used by Anthropic - let event_type = line[7..].to_string(); + } else if trimmed_line.starts_with("event: ") { + let event_type = trimmed_line[7..].to_string(); if event_type.is_empty() { return Err(SseParseError { message: "Empty event field is not a valid SSE event".to_string(), @@ -87,12 +177,13 @@ impl FromStr for SseEvent { data: None, event: Some(event_type), raw_line: line.to_string(), - sse_transform_buffer: line.to_string(), + // Preserve original line format for passthrough, use trimmed for transformations + sse_transformed_lines: line.to_string(), provider_stream_response: None, }) } else { Err(SseParseError { - message: format!("Line does not start with 'data: ' or 'event: ': {}", line), + message: format!("Line does not start with 'data: ' or 'event: ': {}", trimmed_line), }) } } @@ -100,14 +191,22 @@ impl FromStr for SseEvent { impl fmt::Display for SseEvent { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.sse_transform_buffer) + write!(f, "{}", self.sse_transformed_lines) } } // Into implementation to convert SseEvent to bytes for response buffer impl Into> for SseEvent { fn into(self) -> Vec { - format!("{}\n\n", self.sse_transform_buffer).into_bytes() + // For generated events (like ResponsesAPI), sse_transformed_lines already includes trailing \n\n + // For parsed events (like passthrough), we need to add the \n\n separator + if self.sse_transformed_lines.ends_with("\n\n") { + // Already properly formatted with trailing newlines + self.sse_transformed_lines.into_bytes() + } else { + // Add SSE event separator + format!("{}\n\n", self.sse_transformed_lines).into_bytes() + } } } diff --git a/crates/hermesllm/src/apis/streaming_shapes/sse_chunk_processor.rs b/crates/hermesllm/src/apis/streaming_shapes/sse_chunk_processor.rs new file mode 100644 index 00000000..c7d25527 --- /dev/null +++ b/crates/hermesllm/src/apis/streaming_shapes/sse_chunk_processor.rs @@ -0,0 +1,241 @@ +use crate::apis::streaming_shapes::sse::{SseEvent, SseStreamIter}; +use crate::clients::endpoints::{SupportedAPIsFromClient, SupportedUpstreamAPIs}; + +/// Stateful processor for handling SSE chunks that may contain incomplete events. +/// +/// This processor buffers incomplete SSE event bytes when transformation fails +/// (e.g., due to incomplete JSON) and prepends them to the next chunk for retry. +pub struct SseChunkProcessor { + /// Buffered bytes from incomplete SSE events across chunks + incomplete_event_buffer: Vec, +} + +impl SseChunkProcessor { + pub fn new() -> Self { + Self { + incomplete_event_buffer: Vec::new(), + } + } + + /// Process a chunk of SSE data, handling incomplete events across chunk boundaries. + /// + /// Returns successfully transformed events. Incomplete events are buffered internally + /// and will be retried when more data arrives in the next chunk. + /// + /// # Arguments + /// * `chunk` - Raw bytes from upstream SSE stream + /// * `client_api` - The API format the client expects + /// * `upstream_api` - The API format from the upstream provider + /// + /// # Returns + /// * `Ok(Vec)` - Successfully transformed events ready for client + /// * `Err(String)` - Fatal error that cannot be recovered by buffering + pub fn process_chunk( + &mut self, + chunk: &[u8], + client_api: &SupportedAPIsFromClient, + upstream_api: &SupportedUpstreamAPIs, + ) -> Result, String> { + // Combine buffered incomplete event with new chunk + let mut combined_data = std::mem::take(&mut self.incomplete_event_buffer); + combined_data.extend_from_slice(chunk); + + // Parse using SseStreamIter + let sse_iter = match SseStreamIter::try_from(combined_data.as_slice()) { + Ok(iter) => iter, + Err(e) => return Err(format!("Failed to create SSE iterator: {}", e)), + }; + + let mut transformed_events = Vec::new(); + + // Process each parsed SSE event + for sse_event in sse_iter { + // Try to transform the event (this is where incomplete JSON fails) + match SseEvent::try_from((sse_event.clone(), client_api, upstream_api)) { + Ok(transformed) => { + // Successfully transformed - add to results + transformed_events.push(transformed); + } + Err(e) => { + // Check if this is incomplete JSON (EOF while parsing) vs other errors + let error_str = e.to_string().to_lowercase(); + let is_incomplete_json = error_str.contains("eof while parsing") + || error_str.contains("unexpected end of json") + || error_str.contains("unexpected eof"); + + if is_incomplete_json { + // Incomplete JSON - buffer for retry with next chunk + self.incomplete_event_buffer = sse_event.raw_line.as_bytes().to_vec(); + break; + } else { + // Other error (unsupported event type, validation error, etc.) + // Skip this event and continue processing others + continue; + } + } + } + } + + Ok(transformed_events) + } + + /// Check if there are buffered incomplete bytes + pub fn has_buffered_data(&self) -> bool { + !self.incomplete_event_buffer.is_empty() + } + + /// Get the size of buffered incomplete data (for debugging/logging) + pub fn buffered_size(&self) -> usize { + self.incomplete_event_buffer.len() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::clients::endpoints::{SupportedAPIsFromClient, SupportedUpstreamAPIs}; + use crate::apis::openai::OpenAIApi; + + #[test] + fn test_complete_events_process_immediately() { + let mut processor = SseChunkProcessor::new(); + let client_api = SupportedAPIsFromClient::OpenAIChatCompletions(OpenAIApi::ChatCompletions); + let upstream_api = SupportedUpstreamAPIs::OpenAIChatCompletions(OpenAIApi::ChatCompletions); + + let chunk1 = b"data: {\"id\":\"chatcmpl-123\",\"object\":\"chat.completion.chunk\",\"created\":1234567890,\"model\":\"gpt-4o\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"Hello\"},\"finish_reason\":null}]}\n\n"; + + let events = processor.process_chunk(chunk1, &client_api, &upstream_api).unwrap(); + + assert_eq!(events.len(), 1); + assert!(!processor.has_buffered_data()); + } + + #[test] + fn test_incomplete_json_buffered_and_completed() { + let mut processor = SseChunkProcessor::new(); + let client_api = SupportedAPIsFromClient::OpenAIChatCompletions(OpenAIApi::ChatCompletions); + let upstream_api = SupportedUpstreamAPIs::OpenAIChatCompletions(OpenAIApi::ChatCompletions); + + // First chunk with incomplete JSON + let chunk1 = b"data: {\"id\":\"chatcmpl-123\",\"object\":\"chat.completion.chu"; + + let events1 = processor.process_chunk(chunk1, &client_api, &upstream_api).unwrap(); + + assert_eq!(events1.len(), 0, "Incomplete event should not be processed"); + assert!(processor.has_buffered_data(), "Incomplete data should be buffered"); + + // Second chunk completes the JSON + let chunk2 = b"nk\",\"created\":1234567890,\"model\":\"gpt-4o\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"Hello\"},\"finish_reason\":null}]}\n\n"; + + let events2 = processor.process_chunk(chunk2, &client_api, &upstream_api).unwrap(); + + assert_eq!(events2.len(), 1, "Complete event should be processed"); + assert!(!processor.has_buffered_data(), "Buffer should be cleared after completion"); + } + + #[test] + fn test_multiple_events_with_one_incomplete() { + let mut processor = SseChunkProcessor::new(); + let client_api = SupportedAPIsFromClient::OpenAIChatCompletions(OpenAIApi::ChatCompletions); + let upstream_api = SupportedUpstreamAPIs::OpenAIChatCompletions(OpenAIApi::ChatCompletions); + + // Chunk with 2 complete events and 1 incomplete + let chunk = b"data: {\"id\":\"chatcmpl-123\",\"object\":\"chat.completion.chunk\",\"created\":1234567890,\"model\":\"gpt-4o\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"A\"},\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-124\",\"object\":\"chat.completion.chunk\",\"created\":1234567890,\"model\":\"gpt-4o\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"B\"},\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-125\",\"object\":\"chat.completion.chu"; + + let events = processor.process_chunk(chunk, &client_api, &upstream_api).unwrap(); + + assert_eq!(events.len(), 2, "Two complete events should be processed"); + assert!(processor.has_buffered_data(), "Incomplete third event should be buffered"); + } + + #[test] + fn test_anthropic_signature_delta_from_production_logs() { + use crate::apis::anthropic::AnthropicApi; + + let mut processor = SseChunkProcessor::new(); + let client_api = SupportedAPIsFromClient::AnthropicMessagesAPI(AnthropicApi::Messages); + let upstream_api = SupportedUpstreamAPIs::AnthropicMessagesAPI(AnthropicApi::Messages); + + // Exact chunk from production logs - signature_delta event followed by content_block_stop + let chunk = br#"event: content_block_delta +data: {"type":"content_block_delta","index":0,"delta":{"type":"signature_delta","signature":"ErECCkYIChgCKkC7lAf/BOatd0I4NnANYNEDKl5/WSsjNK44AETnLoy3i5FfdYMAb0m4qMLJD6A04QnM4Hf3VpGqq/snA/9vvNxCEgw3CYcHcj0aTdqOisQaDOhlVBtAUKkoh3WopSIwAbJp4jG/41vVWBj63eaR7KFJ37OdY1byjlPkaGDUJRcWc/YfUWIDSAToomq2fB4VKpgBk+swVYxLZ709gQvyTCT+3vO/I+yexZpkx6eBl/+YCgQXTeviZ+hTxSoPVayf5vEQoc19ZA4MEkZ7yBInRgk8vUxAJITSf+vOvDIBsElpgkLfSjARCasjh78wONg39AkAoIbKzU+Q2l1htUwXcqQ2b+b5DrY9+Oxae4pBVGQlWU36XAHsa/KG+ejfdwhWJM7FNL3uphwAf0oYAQ=="}} + +event: content_block_stop +data: {"type":"content_block_stop","index":0} + +"#; + + let result = processor.process_chunk(chunk, &client_api, &upstream_api); + + match result { + Ok(events) => { + println!("Successfully processed {} events", events.len()); + for (i, event) in events.iter().enumerate() { + println!("Event {}: event={:?}, has_data={}", i, event.event, event.data.is_some()); + } + // Should successfully process both events (signature_delta + content_block_stop) + assert!(events.len() >= 2, "Should process at least 2 complete events (signature_delta + stop), got {}", events.len()); + assert!(!processor.has_buffered_data(), "Complete events should not be buffered"); + } + Err(e) => { + panic!("Failed to process signature_delta chunk - this means SignatureDelta is not properly handled: {}", e); + } + } + } + + #[test] + fn test_unsupported_event_does_not_block_subsequent_events() { + let mut processor = SseChunkProcessor::new(); + let client_api = SupportedAPIsFromClient::OpenAIChatCompletions(OpenAIApi::ChatCompletions); + let upstream_api = SupportedUpstreamAPIs::OpenAIChatCompletions(OpenAIApi::ChatCompletions); + + // Chunk with an unsupported/invalid event followed by a valid event + // First event has invalid JSON structure that will fail validation (not incomplete) + // Second event is valid and should be processed + let chunk = b"data: {\"id\":\"chatcmpl-123\",\"object\":\"chat.completion.chunk\",\"created\":1234567890,\"model\":\"gpt-4o\",\"choices\":[{\"index\":0,\"delta\":{\"unsupported_field_causing_validation_error\":true},\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-124\",\"object\":\"chat.completion.chunk\",\"created\":1234567890,\"model\":\"gpt-4o\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"Hello\"},\"finish_reason\":null}]}\n\n"; + + let events = processor.process_chunk(chunk, &client_api, &upstream_api).unwrap(); + + // Should skip the invalid event and process the valid one + // (If we were buffering all errors, we'd get 0 events and have buffered data) + assert!(events.len() >= 1, "Should process at least the valid event, got {} events", events.len()); + assert!(!processor.has_buffered_data(), "Invalid (non-incomplete) events should not be buffered"); + } + + #[test] + fn test_unknown_delta_type_skipped_others_processed() { + use crate::apis::anthropic::AnthropicApi; + + let mut processor = SseChunkProcessor::new(); + let client_api = SupportedAPIsFromClient::AnthropicMessagesAPI(AnthropicApi::Messages); + let upstream_api = SupportedUpstreamAPIs::AnthropicMessagesAPI(AnthropicApi::Messages); + + // Chunk with valid event, unsupported delta type, then another valid event + // This simulates a future API change where Anthropic adds a new delta type we don't support yet + let chunk = br#"event: content_block_delta +data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello"}} + +event: content_block_delta +data: {"type":"content_block_delta","index":0,"delta":{"type":"future_unsupported_delta","future_field":"some_value"}} + +event: content_block_delta +data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" World"}} + +"#; + + let result = processor.process_chunk(chunk, &client_api, &upstream_api); + + match result { + Ok(events) => { + println!("Processed {} events (unsupported event should be skipped)", events.len()); + // Should process the 2 valid text_delta events and skip the unsupported one + // We expect at least 2 events (the valid ones), unsupported should be skipped + assert!(events.len() >= 2, "Should process at least 2 valid events, got {}", events.len()); + assert!(!processor.has_buffered_data(), "Unsupported events should be skipped, not buffered"); + } + Err(e) => { + panic!("Should not fail on unsupported delta type, should skip it: {}", e); + } + } + } +} diff --git a/crates/hermesllm/src/clients/endpoints.rs b/crates/hermesllm/src/clients/endpoints.rs index e0ad47d3..5a923329 100644 --- a/crates/hermesllm/src/clients/endpoints.rs +++ b/crates/hermesllm/src/clients/endpoints.rs @@ -4,9 +4,10 @@ use std::fmt; /// Unified enum representing all supported API endpoints across providers #[derive(Debug, Clone, PartialEq)] -pub enum SupportedAPIs { +pub enum SupportedAPIsFromClient { OpenAIChatCompletions(OpenAIApi), AnthropicMessagesAPI(AnthropicApi), + OpenAIResponsesAPI(OpenAIApi), } #[derive(Debug, Clone, PartialEq)] @@ -15,17 +16,21 @@ pub enum SupportedUpstreamAPIs { AnthropicMessagesAPI(AnthropicApi), AmazonBedrockConverse(AmazonBedrockApi), AmazonBedrockConverseStream(AmazonBedrockApi), + OpenAIResponsesAPI(OpenAIApi), } -impl fmt::Display for SupportedAPIs { +impl fmt::Display for SupportedAPIsFromClient { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - SupportedAPIs::OpenAIChatCompletions(api) => { + SupportedAPIsFromClient::OpenAIChatCompletions(api) => { write!(f, "OpenAI ({})", api.endpoint()) } - SupportedAPIs::AnthropicMessagesAPI(api) => { + SupportedAPIsFromClient::AnthropicMessagesAPI(api) => { write!(f, "Anthropic AI ({})", api.endpoint()) } + SupportedAPIsFromClient::OpenAIResponsesAPI(api) => { + write!(f, "OpenAI Responses ({})", api.endpoint()) + } } } } @@ -45,19 +50,27 @@ impl fmt::Display for SupportedUpstreamAPIs { SupportedUpstreamAPIs::AmazonBedrockConverseStream(api) => { write!(f, "Amazon Bedrock ({})", api.endpoint()) } + SupportedUpstreamAPIs::OpenAIResponsesAPI(api) => { + write!(f, "OpenAI Responses ({})", api.endpoint()) + } } } } -impl SupportedAPIs { +impl SupportedAPIsFromClient { /// Create a SupportedApi from an endpoint path pub fn from_endpoint(endpoint: &str) -> Option { if let Some(openai_api) = OpenAIApi::from_endpoint(endpoint) { - return Some(SupportedAPIs::OpenAIChatCompletions(openai_api)); + // Check if this is the Responses API endpoint + if openai_api == OpenAIApi::Responses { + return Some(SupportedAPIsFromClient::OpenAIResponsesAPI(openai_api)); + } + // Otherwise it's ChatCompletions + return Some(SupportedAPIsFromClient::OpenAIChatCompletions(openai_api)); } if let Some(anthropic_api) = AnthropicApi::from_endpoint(endpoint) { - return Some(SupportedAPIs::AnthropicMessagesAPI(anthropic_api)); + return Some(SupportedAPIsFromClient::AnthropicMessagesAPI(anthropic_api)); } None @@ -66,8 +79,9 @@ impl SupportedAPIs { /// Get the endpoint path for this API pub fn endpoint(&self) -> &'static str { match self { - SupportedAPIs::OpenAIChatCompletions(api) => api.endpoint(), - SupportedAPIs::AnthropicMessagesAPI(api) => api.endpoint(), + SupportedAPIsFromClient::OpenAIChatCompletions(api) => api.endpoint(), + SupportedAPIsFromClient::AnthropicMessagesAPI(api) => api.endpoint(), + SupportedAPIsFromClient::OpenAIResponsesAPI(api) => api.endpoint(), } } @@ -94,8 +108,62 @@ impl SupportedAPIs { } }; + // Helper function to route based on provider with a specific endpoint suffix + let route_by_provider = |endpoint_suffix: &str| -> String { + match provider_id { + ProviderId::Groq => { + if request_path.starts_with("/v1/") { + build_endpoint("/openai", request_path) + } else { + build_endpoint("/v1", endpoint_suffix) + } + } + ProviderId::Zhipu => { + if request_path.starts_with("/v1/") { + build_endpoint("/api/paas/v4", endpoint_suffix) + } else { + build_endpoint("/v1", endpoint_suffix) + } + } + ProviderId::Qwen => { + if request_path.starts_with("/v1/") { + build_endpoint("/compatible-mode/v1", endpoint_suffix) + } else { + build_endpoint("/v1", endpoint_suffix) + } + } + ProviderId::AzureOpenAI => { + if request_path.starts_with("/v1/") { + let suffix = endpoint_suffix.trim_start_matches('/'); + build_endpoint("/openai/deployments", &format!("/{}/{}?api-version=2025-01-01-preview", model_id, suffix)) + } else { + build_endpoint("/v1", endpoint_suffix) + } + } + ProviderId::Gemini => { + if request_path.starts_with("/v1/") { + build_endpoint("/v1beta/openai", endpoint_suffix) + } else { + build_endpoint("/v1", endpoint_suffix) + } + } + ProviderId::AmazonBedrock => { + if request_path.starts_with("/v1/") { + if !is_streaming { + build_endpoint("", &format!("/model/{}/converse", model_id)) + } else { + build_endpoint("", &format!("/model/{}/converse-stream", model_id)) + } + } else { + build_endpoint("/v1", endpoint_suffix) + } + } + _ => build_endpoint("/v1", endpoint_suffix), + } + }; + match self { - SupportedAPIs::AnthropicMessagesAPI(AnthropicApi::Messages) => match provider_id { + SupportedAPIsFromClient::AnthropicMessagesAPI(AnthropicApi::Messages) => match provider_id { ProviderId::Anthropic => build_endpoint("/v1", "/messages"), ProviderId::AmazonBedrock => { if request_path.starts_with("/v1/") && !is_streaming { @@ -108,59 +176,57 @@ impl SupportedAPIs { } _ => build_endpoint("/v1", "/chat/completions"), }, - _ => match provider_id { - ProviderId::Groq => { - if request_path.starts_with("/v1/") { - build_endpoint("/openai", request_path) - } else { - build_endpoint("/v1", "/chat/completions") - } + SupportedAPIsFromClient::OpenAIResponsesAPI(_) => { + // For Responses API, check if provider supports it, otherwise translate to chat/completions + match provider_id { + // OpenAI and compatible providers that support /v1/responses + ProviderId::OpenAI => route_by_provider("/responses"), + // All other providers: translate to /chat/completions + _ => route_by_provider("/chat/completions"), } - ProviderId::Zhipu => { - if request_path.starts_with("/v1/") { - build_endpoint("/api/paas/v4", "/chat/completions") - } else { - build_endpoint("/v1", "/chat/completions") - } - } - ProviderId::Qwen => { - if request_path.starts_with("/v1/") { - build_endpoint("/compatible-mode/v1", "/chat/completions") - } else { - build_endpoint("/v1", "/chat/completions") - } - } - ProviderId::AzureOpenAI => { - if request_path.starts_with("/v1/") { - build_endpoint("/openai/deployments", &format!("/{}/chat/completions?api-version=2025-01-01-preview", model_id)) - } else { - build_endpoint("/v1", "/chat/completions") - } - } - ProviderId::Gemini => { - if request_path.starts_with("/v1/") { - build_endpoint("/v1beta/openai", "/chat/completions") - } else { - build_endpoint("/v1", "/chat/completions") - } - } - ProviderId::AmazonBedrock => { - if request_path.starts_with("/v1/") { - if !is_streaming { - build_endpoint("", &format!("/model/{}/converse", model_id)) - } else { - build_endpoint("", &format!("/model/{}/converse-stream", model_id)) - } - } else { - build_endpoint("/v1", "/chat/completions") - } - } - _ => build_endpoint("/v1", "/chat/completions"), - }, + } + SupportedAPIsFromClient::OpenAIChatCompletions(_) => { + // For Chat Completions API, use the standard chat/completions path + route_by_provider("/chat/completions") + } } } } + +impl SupportedUpstreamAPIs { + /// Create a SupportedUpstreamApi from an endpoint path + pub fn from_endpoint(endpoint: &str) -> Option { + if let Some(openai_api) = OpenAIApi::from_endpoint(endpoint) { + // Check if this is the Responses API endpoint + if openai_api == OpenAIApi::Responses { + return Some(SupportedUpstreamAPIs::OpenAIResponsesAPI(openai_api)); + } + // Otherwise it's ChatCompletions + return Some(SupportedUpstreamAPIs::OpenAIChatCompletions(openai_api)); + } + + if let Some(anthropic_api) = AnthropicApi::from_endpoint(endpoint) { + return Some(SupportedUpstreamAPIs::AnthropicMessagesAPI(anthropic_api)); + } + + if let Some(bedrock_api) = AmazonBedrockApi::from_endpoint(endpoint) { + match bedrock_api { + AmazonBedrockApi::Converse => { + return Some(SupportedUpstreamAPIs::AmazonBedrockConverse(bedrock_api)) + } + AmazonBedrockApi::ConverseStream => { + return Some(SupportedUpstreamAPIs::AmazonBedrockConverseStream(bedrock_api)) + } + } + } + + None + } + +} + + /// Get all supported endpoint paths pub fn supported_endpoints() -> Vec<&'static str> { let mut endpoints = Vec::new(); @@ -198,22 +264,23 @@ mod tests { #[test] fn test_is_supported_endpoint() { // OpenAI endpoints - assert!(SupportedAPIs::from_endpoint("/v1/chat/completions").is_some()); + assert!(SupportedAPIsFromClient::from_endpoint("/v1/chat/completions").is_some()); // Anthropic endpoints - assert!(SupportedAPIs::from_endpoint("/v1/messages").is_some()); + assert!(SupportedAPIsFromClient::from_endpoint("/v1/messages").is_some()); // Unsupported endpoints - assert!(!SupportedAPIs::from_endpoint("/v1/unknown").is_some()); - assert!(!SupportedAPIs::from_endpoint("/v2/chat").is_some()); - assert!(!SupportedAPIs::from_endpoint("").is_some()); + assert!(!SupportedAPIsFromClient::from_endpoint("/v1/unknown").is_some()); + assert!(!SupportedAPIsFromClient::from_endpoint("/v2/chat").is_some()); + assert!(!SupportedAPIsFromClient::from_endpoint("").is_some()); } #[test] fn test_supported_endpoints() { let endpoints = supported_endpoints(); - assert_eq!(endpoints.len(), 2); // We have 2 APIs defined + assert_eq!(endpoints.len(), 3); // We have 3 APIs defined assert!(endpoints.contains(&"/v1/chat/completions")); assert!(endpoints.contains(&"/v1/messages")); + assert!(endpoints.contains(&"/v1/responses")); } #[test] @@ -263,7 +330,7 @@ mod tests { #[test] fn test_target_endpoint_without_base_url_prefix() { - let api = SupportedAPIs::OpenAIChatCompletions(OpenAIApi::ChatCompletions); + let api = SupportedAPIsFromClient::OpenAIChatCompletions(OpenAIApi::ChatCompletions); // Test default OpenAI provider assert_eq!( @@ -340,7 +407,7 @@ mod tests { #[test] fn test_target_endpoint_with_base_url_prefix() { - let api = SupportedAPIs::OpenAIChatCompletions(OpenAIApi::ChatCompletions); + let api = SupportedAPIsFromClient::OpenAIChatCompletions(OpenAIApi::ChatCompletions); // Test Zhipu with custom base_url_path_prefix assert_eq!( @@ -405,7 +472,7 @@ mod tests { #[test] fn test_target_endpoint_with_empty_base_url_prefix() { - let api = SupportedAPIs::OpenAIChatCompletions(OpenAIApi::ChatCompletions); + let api = SupportedAPIsFromClient::OpenAIChatCompletions(OpenAIApi::ChatCompletions); // Test with just slashes - trims to empty, uses provider default assert_eq!( @@ -434,7 +501,7 @@ mod tests { #[test] fn test_amazon_bedrock_endpoints() { - let api = SupportedAPIs::AnthropicMessagesAPI(AnthropicApi::Messages); + let api = SupportedAPIsFromClient::AnthropicMessagesAPI(AnthropicApi::Messages); // Test Bedrock non-streaming without prefix assert_eq!( @@ -487,7 +554,7 @@ mod tests { #[test] fn test_anthropic_messages_endpoint() { - let api = SupportedAPIs::AnthropicMessagesAPI(AnthropicApi::Messages); + let api = SupportedAPIsFromClient::AnthropicMessagesAPI(AnthropicApi::Messages); // Test Anthropic without prefix assert_eq!( @@ -516,7 +583,7 @@ mod tests { #[test] fn test_non_v1_request_paths() { - let api = SupportedAPIs::OpenAIChatCompletions(OpenAIApi::ChatCompletions); + let api = SupportedAPIsFromClient::OpenAIChatCompletions(OpenAIApi::ChatCompletions); // Test Groq with non-v1 path (should use default) assert_eq!( @@ -557,7 +624,7 @@ mod tests { #[test] fn test_azure_openai_with_query_params() { - let api = SupportedAPIs::OpenAIChatCompletions(OpenAIApi::ChatCompletions); + let api = SupportedAPIsFromClient::OpenAIChatCompletions(OpenAIApi::ChatCompletions); // Test Azure without prefix - should include query params assert_eq!( diff --git a/crates/hermesllm/src/clients/mod.rs b/crates/hermesllm/src/clients/mod.rs index b93f910e..6841804f 100644 --- a/crates/hermesllm/src/clients/mod.rs +++ b/crates/hermesllm/src/clients/mod.rs @@ -1,9 +1,8 @@ pub mod endpoints; pub mod lib; -pub mod transformer; // Re-export the main items for easier access -pub use endpoints::{identify_provider, SupportedAPIs}; +pub use endpoints::*; pub use lib::*; // Note: transformer module contains TryFrom trait implementations that are automatically available diff --git a/crates/hermesllm/src/clients/transformer.rs b/crates/hermesllm/src/clients/transformer.rs deleted file mode 100644 index cbb9bbe7..00000000 --- a/crates/hermesllm/src/clients/transformer.rs +++ /dev/null @@ -1,694 +0,0 @@ -// Re-export new transformation modules for backward compatibility - -//KEEPING THE TESTS TO MAKE SURE ALL THE REFACTORING DIDN'T BREAK ANYTHING - -// ============================================================================ -// TESTS -// ============================================================================ - -#[cfg(test)] -mod tests { - use crate::apis::anthropic::*; - use crate::apis::openai::*; - use crate::transforms::*; - use serde_json::json; - type AnthropicMessagesRequest = MessagesRequest; - - #[test] - fn test_anthropic_to_openai_basic_request() { - let anthropic_req = AnthropicMessagesRequest { - model: "claude-3-sonnet-20240229".to_string(), - system: Some(MessagesSystemPrompt::Single("You are helpful".to_string())), - messages: vec![MessagesMessage { - role: MessagesRole::User, - content: MessagesMessageContent::Single("Hello, world!".to_string()), - }], - max_tokens: 1024, - container: None, - mcp_servers: None, - service_tier: None, - thinking: None, - temperature: Some(0.7), - top_p: Some(0.9), - top_k: Some(50), - stream: Some(false), - stop_sequences: Some(vec!["STOP".to_string()]), - tools: None, - tool_choice: None, - metadata: None, - }; - - let openai_req: ChatCompletionsRequest = anthropic_req.try_into().unwrap(); - - assert_eq!(openai_req.model, "claude-3-sonnet-20240229"); - assert_eq!(openai_req.messages.len(), 2); // system + user message - assert_eq!(openai_req.max_completion_tokens, Some(1024)); - assert_eq!(openai_req.temperature, Some(0.7)); - assert_eq!(openai_req.top_p, Some(0.9)); - assert_eq!(openai_req.stream, Some(false)); - assert_eq!(openai_req.stop, Some(vec!["STOP".to_string()])); - } - - #[test] - fn test_roundtrip_consistency() { - // Test that converting back and forth maintains consistency - let original_anthropic = AnthropicMessagesRequest { - model: "claude-3-sonnet".to_string(), - system: Some(MessagesSystemPrompt::Single("System prompt".to_string())), - messages: vec![MessagesMessage { - role: MessagesRole::User, - content: MessagesMessageContent::Single("User message".to_string()), - }], - max_tokens: 1000, - container: None, - mcp_servers: None, - service_tier: None, - thinking: None, - temperature: Some(0.5), - top_p: Some(1.0), - top_k: None, - stream: Some(false), - stop_sequences: None, - tools: None, - tool_choice: None, - metadata: None, - }; - - // Convert to OpenAI and back - let openai_req: ChatCompletionsRequest = original_anthropic.clone().try_into().unwrap(); - let roundtrip_anthropic: AnthropicMessagesRequest = openai_req.try_into().unwrap(); - - // Check key fields are preserved - assert_eq!(original_anthropic.model, roundtrip_anthropic.model); - assert_eq!( - original_anthropic.max_tokens, - roundtrip_anthropic.max_tokens - ); - assert_eq!( - original_anthropic.temperature, - roundtrip_anthropic.temperature - ); - assert_eq!(original_anthropic.top_p, roundtrip_anthropic.top_p); - assert_eq!(original_anthropic.stream, roundtrip_anthropic.stream); - assert_eq!( - original_anthropic.messages.len(), - roundtrip_anthropic.messages.len() - ); - } - - #[test] - fn test_tool_choice_auto() { - let anthropic_req = AnthropicMessagesRequest { - model: "claude-3".to_string(), - system: None, - messages: vec![], - max_tokens: 100, - container: None, - mcp_servers: None, - service_tier: None, - thinking: None, - temperature: None, - top_p: None, - top_k: None, - stream: None, - stop_sequences: None, - tools: Some(vec![MessagesTool { - name: "test_tool".to_string(), - description: Some("A test tool".to_string()), - input_schema: json!({"type": "object"}), - }]), - tool_choice: Some(MessagesToolChoice { - kind: MessagesToolChoiceType::Auto, - name: None, - disable_parallel_tool_use: Some(true), - }), - metadata: None, - }; - - let openai_req: ChatCompletionsRequest = anthropic_req.try_into().unwrap(); - - assert!(openai_req.tools.is_some()); - assert_eq!(openai_req.tools.as_ref().unwrap().len(), 1); - - if let Some(ToolChoice::Type(choice)) = openai_req.tool_choice { - assert_eq!(choice, ToolChoiceType::Auto); - } else { - panic!("Expected auto tool choice"); - } - - assert_eq!(openai_req.parallel_tool_calls, Some(false)); - } - - #[test] - fn test_default_max_tokens_used_when_openai_has_none() { - // Test that DEFAULT_MAX_TOKENS is used when OpenAI request has no max_tokens - let openai_req = ChatCompletionsRequest { - model: "gpt-4".to_string(), - messages: vec![Message { - role: Role::User, - content: MessageContent::Text("Hello".to_string()), - name: None, - tool_calls: None, - tool_call_id: None, - }], - max_tokens: None, // No max_tokens specified - ..Default::default() - }; - - let anthropic_req: AnthropicMessagesRequest = openai_req.try_into().unwrap(); - - assert_eq!(anthropic_req.max_tokens, DEFAULT_MAX_TOKENS); - } - - #[test] - fn test_anthropic_message_start_streaming() { - let event = MessagesStreamEvent::MessageStart { - message: MessagesStreamMessage { - id: "msg_stream_123".to_string(), - obj_type: "message".to_string(), - role: MessagesRole::Assistant, - content: vec![], - model: "claude-3".to_string(), - stop_reason: None, - stop_sequence: None, - usage: MessagesUsage { - input_tokens: 5, - output_tokens: 0, - cache_creation_input_tokens: None, - cache_read_input_tokens: None, - }, - }, - }; - - let openai_resp: ChatCompletionsStreamResponse = event.try_into().unwrap(); - - assert_eq!(openai_resp.id, "msg_stream_123"); - assert_eq!(openai_resp.object.as_deref(), Some("chat.completion.chunk")); - assert_eq!(openai_resp.model, "claude-3"); - assert_eq!(openai_resp.choices.len(), 1); - - let choice = &openai_resp.choices[0]; - assert_eq!(choice.index, 0); - assert_eq!(choice.delta.role, Some(Role::Assistant)); - assert_eq!(choice.delta.content, None); - assert_eq!(choice.finish_reason, None); - } - - #[test] - fn test_anthropic_content_block_delta_streaming() { - let event = MessagesStreamEvent::ContentBlockDelta { - index: 0, - delta: MessagesContentDelta::TextDelta { - text: "Hello, world!".to_string(), - }, - }; - - let openai_resp: ChatCompletionsStreamResponse = event.try_into().unwrap(); - - assert_eq!(openai_resp.object.as_deref(), Some("chat.completion.chunk")); - assert_eq!(openai_resp.choices.len(), 1); - - let choice = &openai_resp.choices[0]; - assert_eq!(choice.index, 0); - assert_eq!(choice.delta.content, Some("Hello, world!".to_string())); - assert_eq!(choice.delta.role, None); - assert_eq!(choice.finish_reason, None); - } - - #[test] - fn test_anthropic_tool_use_streaming() { - // Test tool use start - let tool_start = MessagesStreamEvent::ContentBlockStart { - index: 0, - content_block: MessagesContentBlock::ToolUse { - id: "call_123".to_string(), - name: "get_weather".to_string(), - input: json!({}), - cache_control: None, - }, - }; - - let openai_resp: ChatCompletionsStreamResponse = tool_start.try_into().unwrap(); - - assert_eq!(openai_resp.choices.len(), 1); - let choice = &openai_resp.choices[0]; - assert!(choice.delta.tool_calls.is_some()); - - let tool_calls = choice.delta.tool_calls.as_ref().unwrap(); - assert_eq!(tool_calls.len(), 1); - assert_eq!(tool_calls[0].id, Some("call_123".to_string())); - assert_eq!( - tool_calls[0].function.as_ref().unwrap().name, - Some("get_weather".to_string()) - ); - } - - #[test] - fn test_anthropic_tool_input_delta_streaming() { - let event = MessagesStreamEvent::ContentBlockDelta { - index: 0, - delta: MessagesContentDelta::InputJsonDelta { - partial_json: r#"{"location": "San Francisco"#.to_string(), - }, - }; - - let openai_resp: ChatCompletionsStreamResponse = event.try_into().unwrap(); - - assert_eq!(openai_resp.choices.len(), 1); - let choice = &openai_resp.choices[0]; - assert!(choice.delta.tool_calls.is_some()); - - let tool_calls = choice.delta.tool_calls.as_ref().unwrap(); - assert_eq!(tool_calls.len(), 1); - assert_eq!( - tool_calls[0].function.as_ref().unwrap().arguments, - Some(r#"{"location": "San Francisco"#.to_string()) - ); - } - - #[test] - fn test_anthropic_message_delta_with_usage() { - let event = MessagesStreamEvent::MessageDelta { - delta: MessagesMessageDelta { - stop_reason: MessagesStopReason::EndTurn, - stop_sequence: None, - }, - usage: MessagesUsage { - input_tokens: 10, - output_tokens: 25, - cache_creation_input_tokens: None, - cache_read_input_tokens: None, - }, - }; - - let openai_resp: ChatCompletionsStreamResponse = event.try_into().unwrap(); - - assert_eq!(openai_resp.choices.len(), 1); - let choice = &openai_resp.choices[0]; - assert_eq!(choice.finish_reason, Some(FinishReason::Stop)); - - assert!(openai_resp.usage.is_some()); - let usage = openai_resp.usage.unwrap(); - assert_eq!(usage.prompt_tokens, 10); - assert_eq!(usage.completion_tokens, 25); - assert_eq!(usage.total_tokens, 35); - } - - #[test] - fn test_anthropic_message_stop_streaming() { - let event = MessagesStreamEvent::MessageStop; - - let openai_resp: ChatCompletionsStreamResponse = event.try_into().unwrap(); - - assert_eq!(openai_resp.choices.len(), 1); - let choice = &openai_resp.choices[0]; - assert_eq!(choice.finish_reason, Some(FinishReason::Stop)); - } - - #[test] - fn test_anthropic_ping_streaming() { - let event = MessagesStreamEvent::Ping; - - let openai_resp: ChatCompletionsStreamResponse = event.try_into().unwrap(); - - assert_eq!(openai_resp.object.as_deref(), Some("chat.completion.chunk")); - assert_eq!(openai_resp.choices.len(), 0); // Ping has no choices - } - - #[test] - fn test_openai_to_anthropic_streaming_role_start() { - let openai_resp = ChatCompletionsStreamResponse { - id: "chatcmpl-123".to_string(), - object: Some("chat.completion.chunk".to_string()), - created: 1234567890, - model: "gpt-4".to_string(), - choices: vec![StreamChoice { - index: 0, - delta: MessageDelta { - role: Some(Role::Assistant), - content: None, - refusal: None, - function_call: None, - tool_calls: None, - }, - finish_reason: None, - logprobs: None, - }], - usage: None, - system_fingerprint: None, - service_tier: None, - }; - - let anthropic_event: MessagesStreamEvent = openai_resp.try_into().unwrap(); - - match anthropic_event { - MessagesStreamEvent::MessageStart { message } => { - assert_eq!(message.id, "chatcmpl-123"); - assert_eq!(message.role, MessagesRole::Assistant); - assert_eq!(message.model, "gpt-4"); - } - _ => panic!("Expected MessageStart event"), - } - } - - #[test] - fn test_openai_to_anthropic_streaming_content_delta() { - let openai_resp = ChatCompletionsStreamResponse { - id: "chatcmpl-123".to_string(), - object: Some("chat.completion.chunk".to_string()), - created: 1234567890, - model: "gpt-4".to_string(), - choices: vec![StreamChoice { - index: 0, - delta: MessageDelta { - role: None, - content: Some("Hello there!".to_string()), - refusal: None, - function_call: None, - tool_calls: None, - }, - finish_reason: None, - logprobs: None, - }], - usage: None, - system_fingerprint: None, - service_tier: None, - }; - - let anthropic_event: MessagesStreamEvent = openai_resp.try_into().unwrap(); - - match anthropic_event { - MessagesStreamEvent::ContentBlockDelta { index, delta } => { - assert_eq!(index, 0); - match delta { - MessagesContentDelta::TextDelta { text } => { - assert_eq!(text, "Hello there!"); - } - _ => panic!("Expected TextDelta"), - } - } - _ => panic!("Expected ContentBlockDelta event"), - } - } - - #[test] - fn test_openai_to_anthropic_streaming_tool_calls() { - let openai_resp = ChatCompletionsStreamResponse { - id: "chatcmpl-123".to_string(), - object: Some("chat.completion.chunk".to_string()), - created: 1234567890, - model: "gpt-4".to_string(), - choices: vec![StreamChoice { - index: 0, - delta: MessageDelta { - role: None, - content: None, - refusal: None, - function_call: None, - tool_calls: Some(vec![ToolCallDelta { - index: 0, - id: Some("call_abc123".to_string()), - call_type: Some("function".to_string()), - function: Some(FunctionCallDelta { - name: Some("get_current_weather".to_string()), - arguments: Some("".to_string()), - }), - }]), - }, - finish_reason: None, - logprobs: None, - }], - usage: None, - system_fingerprint: None, - service_tier: None, - }; - - let anthropic_event: MessagesStreamEvent = openai_resp.try_into().unwrap(); - - match anthropic_event { - MessagesStreamEvent::ContentBlockStart { - index, - content_block, - } => { - assert_eq!(index, 0); - match content_block { - MessagesContentBlock::ToolUse { id, name, .. } => { - assert_eq!(id, "call_abc123"); - assert_eq!(name, "get_current_weather"); - } - _ => panic!("Expected ToolUse content block"), - } - } - _ => panic!("Expected ContentBlockStart event"), - } - } - - #[test] - fn test_openai_to_anthropic_streaming_final_usage() { - let openai_resp = ChatCompletionsStreamResponse { - id: "chatcmpl-123".to_string(), - object: Some("chat.completion.chunk".to_string()), - created: 1234567890, - model: "gpt-4".to_string(), - choices: vec![StreamChoice { - index: 0, - delta: MessageDelta { - role: None, - content: None, - refusal: None, - function_call: None, - tool_calls: None, - }, - finish_reason: Some(FinishReason::Stop), - logprobs: None, - }], - usage: Some(Usage { - prompt_tokens: 15, - completion_tokens: 30, - total_tokens: 45, - prompt_tokens_details: None, - completion_tokens_details: None, - }), - system_fingerprint: None, - service_tier: None, - }; - - let anthropic_event: MessagesStreamEvent = openai_resp.try_into().unwrap(); - - match anthropic_event { - MessagesStreamEvent::MessageDelta { delta, usage } => { - assert_eq!(delta.stop_reason, MessagesStopReason::EndTurn); - assert_eq!(usage.input_tokens, 15); - assert_eq!(usage.output_tokens, 30); - } - _ => panic!("Expected MessageDelta event"), - } - } - - #[test] - fn test_openai_empty_choices_to_anthropic_ping() { - let openai_resp = ChatCompletionsStreamResponse { - id: "chatcmpl-123".to_string(), - object: Some("chat.completion.chunk".to_string()), - created: 1234567890, - model: "gpt-4".to_string(), - choices: vec![], // Empty choices - usage: None, - system_fingerprint: None, - service_tier: None, - }; - - let anthropic_event: MessagesStreamEvent = openai_resp.try_into().unwrap(); - - match anthropic_event { - MessagesStreamEvent::Ping => { - // Expected behavior - } - _ => panic!("Expected Ping event for empty choices"), - } - } - - #[test] - fn test_streaming_roundtrip_consistency() { - // Test that streaming events can roundtrip through conversions - let original_event = MessagesStreamEvent::ContentBlockDelta { - index: 0, - delta: MessagesContentDelta::TextDelta { - text: "Test message".to_string(), - }, - }; - - // Convert to OpenAI and back - let openai_resp: ChatCompletionsStreamResponse = original_event.try_into().unwrap(); - let roundtrip_event: MessagesStreamEvent = openai_resp.try_into().unwrap(); - - // Verify the roundtrip maintains the essential information - match roundtrip_event { - MessagesStreamEvent::ContentBlockDelta { index, delta } => { - assert_eq!(index, 0); - match delta { - MessagesContentDelta::TextDelta { text } => { - assert_eq!(text, "Test message"); - } - _ => panic!("Expected TextDelta after roundtrip"), - } - } - _ => panic!("Expected ContentBlockDelta after roundtrip"), - } - } - - #[test] - fn test_streaming_tool_argument_accumulation() { - // Test multiple tool argument deltas that should accumulate - let tool_start = MessagesStreamEvent::ContentBlockStart { - index: 0, - content_block: MessagesContentBlock::ToolUse { - id: "call_weather".to_string(), - name: "get_weather".to_string(), - input: json!({}), - cache_control: None, - }, - }; - - let arg_delta1 = MessagesStreamEvent::ContentBlockDelta { - index: 0, - delta: MessagesContentDelta::InputJsonDelta { - partial_json: r#"{"location": "#.to_string(), - }, - }; - - let arg_delta2 = MessagesStreamEvent::ContentBlockDelta { - index: 0, - delta: MessagesContentDelta::InputJsonDelta { - partial_json: r#"San Francisco", "unit": "fahrenheit"}"#.to_string(), - }, - }; - - // Test that each delta converts properly to OpenAI format - let openai_start: ChatCompletionsStreamResponse = tool_start.try_into().unwrap(); - let openai_delta1: ChatCompletionsStreamResponse = arg_delta1.try_into().unwrap(); - let openai_delta2: ChatCompletionsStreamResponse = arg_delta2.try_into().unwrap(); - - // Verify tool start - let tool_calls = &openai_start.choices[0].delta.tool_calls.as_ref().unwrap(); - assert_eq!(tool_calls[0].id, Some("call_weather".to_string())); - assert_eq!( - tool_calls[0].function.as_ref().unwrap().name, - Some("get_weather".to_string()) - ); - - // Verify argument deltas - let args1 = &openai_delta1.choices[0].delta.tool_calls.as_ref().unwrap()[0] - .function - .as_ref() - .unwrap() - .arguments; - assert_eq!(args1, &Some(r#"{"location": "#.to_string())); - - let args2 = &openai_delta2.choices[0].delta.tool_calls.as_ref().unwrap()[0] - .function - .as_ref() - .unwrap() - .arguments; - assert_eq!( - args2, - &Some(r#"San Francisco", "unit": "fahrenheit"}"#.to_string()) - ); - } - - #[test] - fn test_streaming_multiple_finish_reasons() { - // Test different finish reasons in streaming - let test_cases = vec![ - (MessagesStopReason::EndTurn, FinishReason::Stop), - (MessagesStopReason::MaxTokens, FinishReason::Length), - (MessagesStopReason::ToolUse, FinishReason::ToolCalls), - (MessagesStopReason::StopSequence, FinishReason::Stop), - ]; - - for (anthropic_reason, expected_openai_reason) in test_cases { - let event = MessagesStreamEvent::MessageDelta { - delta: MessagesMessageDelta { - stop_reason: anthropic_reason.clone(), - stop_sequence: None, - }, - usage: MessagesUsage { - input_tokens: 10, - output_tokens: 20, - cache_creation_input_tokens: None, - cache_read_input_tokens: None, - }, - }; - - let openai_resp: ChatCompletionsStreamResponse = event.try_into().unwrap(); - assert_eq!( - openai_resp.choices[0].finish_reason, - Some(expected_openai_reason) - ); - - // Test reverse conversion - let roundtrip_event: MessagesStreamEvent = openai_resp.try_into().unwrap(); - match roundtrip_event { - MessagesStreamEvent::MessageDelta { delta, .. } => { - // Note: Some precision may be lost in roundtrip due to mapping differences - assert!(matches!( - delta.stop_reason, - MessagesStopReason::EndTurn - | MessagesStopReason::MaxTokens - | MessagesStopReason::ToolUse - | MessagesStopReason::StopSequence - )); - } - _ => panic!("Expected MessageDelta after roundtrip"), - } - } - } - - #[test] - fn test_streaming_error_handling() { - // Test that malformed streaming events are handled gracefully - let openai_resp_with_missing_data = ChatCompletionsStreamResponse { - id: "test".to_string(), - object: Some("chat.completion.chunk".to_string()), - created: 1234567890, - model: "test".to_string(), - choices: vec![StreamChoice { - index: 0, - delta: MessageDelta { - role: None, - content: None, - refusal: None, - function_call: None, - tool_calls: None, - }, - finish_reason: None, - logprobs: None, - }], - usage: None, - system_fingerprint: None, - service_tier: None, - }; - - // Should convert to Ping when no meaningful content - let anthropic_event: MessagesStreamEvent = - openai_resp_with_missing_data.try_into().unwrap(); - assert!(matches!(anthropic_event, MessagesStreamEvent::Ping)); - } - - #[test] - fn test_streaming_content_block_stop() { - let event = MessagesStreamEvent::ContentBlockStop { index: 0 }; - - let openai_resp: ChatCompletionsStreamResponse = event.try_into().unwrap(); - - // ContentBlockStop should produce an empty chunk - assert_eq!(openai_resp.object.as_deref(), Some("chat.completion.chunk")); - assert_eq!(openai_resp.choices.len(), 1); - - let choice = &openai_resp.choices[0]; - assert_eq!(choice.delta.role, None); - assert_eq!(choice.delta.content, None); - assert_eq!(choice.delta.tool_calls, None); - assert_eq!(choice.finish_reason, None); - } -} diff --git a/crates/hermesllm/src/lib.rs b/crates/hermesllm/src/lib.rs index 77289f4b..918fd4e9 100644 --- a/crates/hermesllm/src/lib.rs +++ b/crates/hermesllm/src/lib.rs @@ -6,18 +6,21 @@ pub mod clients; pub mod providers; pub mod transforms; // Re-export important types and traits -pub use apis::amazon_bedrock_binary_frame::BedrockBinaryFrameDecoder; -pub use apis::sse::{SseEvent, SseStreamIter}; +pub use apis::streaming_shapes::amazon_bedrock_binary_frame::BedrockBinaryFrameDecoder; +pub use apis::streaming_shapes::sse::{SseEvent, SseStreamIter}; pub use aws_smithy_eventstream::frame::DecodedFrame; pub use providers::id::ProviderId; pub use providers::request::{ProviderRequest, ProviderRequestError, ProviderRequestType}; pub use providers::response::{ - ProviderResponse, ProviderResponseError, ProviderResponseType, ProviderStreamResponse, - ProviderStreamResponseType, TokenUsage, + ProviderResponse, ProviderResponseType, TokenUsage, ProviderResponseError +}; +pub use providers::streaming_response::{ + ProviderStreamResponse, ProviderStreamResponseType }; //TODO: Refactor such that commons doesn't depend on Hermes. For now this will clean up strings pub const CHAT_COMPLETIONS_PATH: &str = "/v1/chat/completions"; +pub const OPENAI_RESPONSES_API_PATH: &str = "/v1/responses"; pub const MESSAGES_PATH: &str = "/v1/messages"; #[cfg(test)] @@ -42,9 +45,9 @@ mod tests { data: [DONE] "#; - use crate::clients::endpoints::SupportedAPIs; + use crate::clients::endpoints::SupportedAPIsFromClient; let client_api = - SupportedAPIs::OpenAIChatCompletions(crate::apis::OpenAIApi::ChatCompletions); + SupportedAPIsFromClient::OpenAIChatCompletions(crate::apis::OpenAIApi::ChatCompletions); let upstream_api = SupportedUpstreamAPIs::OpenAIChatCompletions(crate::apis::OpenAIApi::ChatCompletions); @@ -79,9 +82,16 @@ mod tests { assert_eq!(stream_response.content_delta(), Some("Hello")); assert!(!stream_response.is_final()); - // Test that stream ends properly with [DONE] (SseStreamIter should stop before [DONE]) + // Test that stream ends properly with [DONE] + // The iterator should return the [DONE] event, then None + let done_event = streaming_iter.next(); + assert!(done_event.is_some(), "Should get [DONE] event"); + let done_event = done_event.unwrap(); + assert!(done_event.is_done(), "[DONE] event should be marked as done"); + + // After [DONE], iterator should return None let final_event = streaming_iter.next(); - assert!(final_event.is_none()); // Should be None because iterator stops at [DONE] + assert!(final_event.is_none(), "Iterator should return None after [DONE]"); } /// Test AWS Event Stream decoding for Bedrock ConverseStream responses. diff --git a/crates/hermesllm/src/providers/id.rs b/crates/hermesllm/src/providers/id.rs index 94a6205a..344a795f 100644 --- a/crates/hermesllm/src/providers/id.rs +++ b/crates/hermesllm/src/providers/id.rs @@ -1,5 +1,5 @@ use crate::apis::{AmazonBedrockApi, AnthropicApi, OpenAIApi}; -use crate::clients::endpoints::{SupportedAPIs, SupportedUpstreamAPIs}; +use crate::clients::endpoints::{SupportedAPIsFromClient, SupportedUpstreamAPIs}; use std::fmt::Display; /// Provider identifier enum - simple enum for identifying providers @@ -51,19 +51,24 @@ impl ProviderId { /// Given a client API, return the compatible upstream API for this provider pub fn compatible_api_for_client( &self, - client_api: &SupportedAPIs, + client_api: &SupportedAPIsFromClient, is_streaming: bool, ) -> SupportedUpstreamAPIs { match (self, client_api) { // Claude/Anthropic providers natively support Anthropic APIs - (ProviderId::Anthropic, SupportedAPIs::AnthropicMessagesAPI(_)) => { + (ProviderId::Anthropic, SupportedAPIsFromClient::AnthropicMessagesAPI(_)) => { SupportedUpstreamAPIs::AnthropicMessagesAPI(AnthropicApi::Messages) } ( ProviderId::Anthropic, - SupportedAPIs::OpenAIChatCompletions(OpenAIApi::ChatCompletions), + SupportedAPIsFromClient::OpenAIChatCompletions(_), ) => SupportedUpstreamAPIs::OpenAIChatCompletions(OpenAIApi::ChatCompletions), + // Anthropic doesn't support Responses API, fall back to chat completions + (ProviderId::Anthropic, SupportedAPIsFromClient::OpenAIResponsesAPI(_)) => { + SupportedUpstreamAPIs::OpenAIChatCompletions(OpenAIApi::ChatCompletions) + } + // OpenAI-compatible providers only support OpenAI chat completions ( ProviderId::OpenAI @@ -80,7 +85,7 @@ impl ProviderId { | ProviderId::Moonshotai | ProviderId::Zhipu | ProviderId::Qwen, - SupportedAPIs::AnthropicMessagesAPI(_), + SupportedAPIsFromClient::AnthropicMessagesAPI(_), ) => SupportedUpstreamAPIs::OpenAIChatCompletions(OpenAIApi::ChatCompletions), ( @@ -98,11 +103,16 @@ impl ProviderId { | ProviderId::Moonshotai | ProviderId::Zhipu | ProviderId::Qwen, - SupportedAPIs::OpenAIChatCompletions(_), + SupportedAPIsFromClient::OpenAIChatCompletions(_), ) => SupportedUpstreamAPIs::OpenAIChatCompletions(OpenAIApi::ChatCompletions), + // OpenAI Responses API - only OpenAI supports this + (ProviderId::OpenAI, SupportedAPIsFromClient::OpenAIResponsesAPI(_)) => { + SupportedUpstreamAPIs::OpenAIResponsesAPI(OpenAIApi::Responses) + } + // Amazon Bedrock natively supports Bedrock APIs - (ProviderId::AmazonBedrock, SupportedAPIs::OpenAIChatCompletions(_)) => { + (ProviderId::AmazonBedrock, SupportedAPIsFromClient::OpenAIChatCompletions(_)) => { if is_streaming { SupportedUpstreamAPIs::AmazonBedrockConverseStream( AmazonBedrockApi::ConverseStream, @@ -111,7 +121,7 @@ impl ProviderId { SupportedUpstreamAPIs::AmazonBedrockConverse(AmazonBedrockApi::Converse) } } - (ProviderId::AmazonBedrock, SupportedAPIs::AnthropicMessagesAPI(_)) => { + (ProviderId::AmazonBedrock, SupportedAPIsFromClient::AnthropicMessagesAPI(_)) => { if is_streaming { SupportedUpstreamAPIs::AmazonBedrockConverseStream( AmazonBedrockApi::ConverseStream, @@ -120,6 +130,20 @@ impl ProviderId { SupportedUpstreamAPIs::AmazonBedrockConverse(AmazonBedrockApi::Converse) } } + (ProviderId::AmazonBedrock, SupportedAPIsFromClient::OpenAIResponsesAPI(_)) => { + if is_streaming { + SupportedUpstreamAPIs::AmazonBedrockConverseStream( + AmazonBedrockApi::ConverseStream, + ) + } else { + SupportedUpstreamAPIs::AmazonBedrockConverse(AmazonBedrockApi::Converse) + } + } + + // Non-OpenAI providers: if client requested the Responses API, fall back to Chat Completions + (_, SupportedAPIsFromClient::OpenAIResponsesAPI(_)) => { + SupportedUpstreamAPIs::OpenAIChatCompletions(OpenAIApi::ChatCompletions) + } } } } diff --git a/crates/hermesllm/src/providers/mod.rs b/crates/hermesllm/src/providers/mod.rs index 97b14285..4343022f 100644 --- a/crates/hermesllm/src/providers/mod.rs +++ b/crates/hermesllm/src/providers/mod.rs @@ -6,7 +6,9 @@ pub mod id; pub mod request; pub mod response; +pub mod streaming_response; pub use id::ProviderId; pub use request::{ProviderRequest, ProviderRequestError, ProviderRequestType}; -pub use response::{ProviderResponse, ProviderResponseType, ProviderStreamResponse, TokenUsage}; +pub use response::{ProviderResponse, ProviderResponseType, TokenUsage}; +pub use streaming_response::{ProviderStreamResponse, ProviderStreamResponseType}; diff --git a/crates/hermesllm/src/providers/request.rs b/crates/hermesllm/src/providers/request.rs index a8bcfa29..7cd951c3 100644 --- a/crates/hermesllm/src/providers/request.rs +++ b/crates/hermesllm/src/providers/request.rs @@ -2,19 +2,21 @@ use crate::apis::anthropic::MessagesRequest; use crate::apis::openai::ChatCompletionsRequest; use crate::apis::amazon_bedrock::{ConverseRequest, ConverseStreamRequest}; -use crate::clients::endpoints::SupportedAPIs; +use crate::apis::openai_responses::ResponsesAPIRequest; +use crate::clients::endpoints::SupportedAPIsFromClient; use crate::clients::endpoints::SupportedUpstreamAPIs; use serde_json::Value; use std::collections::HashMap; use std::error::Error; use std::fmt; -#[derive(Clone)] +#[derive(Clone, Debug)] pub enum ProviderRequestType { ChatCompletionsRequest(ChatCompletionsRequest), MessagesRequest(MessagesRequest), BedrockConverse(ConverseRequest), BedrockConverseStream(ConverseStreamRequest), + ResponsesAPIRequest(ResponsesAPIRequest), //add more request types here } pub trait ProviderRequest: Send + Sync { @@ -33,6 +35,9 @@ pub trait ProviderRequest: Send + Sync { /// Extract the user message for tracing/logging purposes fn get_recent_user_message(&self) -> Option; + /// Get tool names if tools are defined in the request + fn get_tool_names(&self) -> Option>; + /// Convert the request to bytes for transmission fn to_bytes(&self) -> Result, ProviderRequestError>; @@ -40,6 +45,30 @@ pub trait ProviderRequest: Send + Sync { /// Remove a metadata key from the request and return true if the key was present fn remove_metadata_key(&mut self, key: &str) -> bool; + + fn get_temperature(&self) -> Option; + + /// Get message history as OpenAI Message format + /// This is useful for processing chat history across different provider formats + fn get_messages(&self) -> Vec; + + /// Set message history from OpenAI Message format + /// This converts OpenAI messages to the appropriate format for each provider type + fn set_messages(&mut self, messages: &[crate::apis::openai::Message]); +} + +impl ProviderRequestType { + /// Set message history from OpenAI Message format + /// This converts OpenAI messages to the appropriate format for each provider type + pub fn set_messages(&mut self, messages: &[crate::apis::openai::Message]) { + match self { + Self::ChatCompletionsRequest(r) => r.set_messages(messages), + Self::MessagesRequest(r) => r.set_messages(messages), + Self::BedrockConverse(r) => r.set_messages(messages), + Self::BedrockConverseStream(r) => r.set_messages(messages), + Self::ResponsesAPIRequest(r) => r.set_messages(messages), + } + } } impl ProviderRequest for ProviderRequestType { @@ -49,6 +78,7 @@ impl ProviderRequest for ProviderRequestType { Self::MessagesRequest(r) => r.model(), Self::BedrockConverse(r) => r.model(), Self::BedrockConverseStream(r) => r.model(), + Self::ResponsesAPIRequest(r) => r.model(), } } @@ -58,6 +88,7 @@ impl ProviderRequest for ProviderRequestType { Self::MessagesRequest(r) => r.set_model(model), Self::BedrockConverse(r) => r.set_model(model), Self::BedrockConverseStream(r) => r.set_model(model), + Self::ResponsesAPIRequest(r) => r.set_model(model), } } @@ -67,6 +98,7 @@ impl ProviderRequest for ProviderRequestType { Self::MessagesRequest(r) => r.is_streaming(), Self::BedrockConverse(_) => false, Self::BedrockConverseStream(_) => true, + Self::ResponsesAPIRequest(r) => r.is_streaming(), } } @@ -76,6 +108,7 @@ impl ProviderRequest for ProviderRequestType { Self::MessagesRequest(r) => r.extract_messages_text(), Self::BedrockConverse(r) => r.extract_messages_text(), Self::BedrockConverseStream(r) => r.extract_messages_text(), + Self::ResponsesAPIRequest(r) => r.extract_messages_text(), } } @@ -85,6 +118,17 @@ impl ProviderRequest for ProviderRequestType { Self::MessagesRequest(r) => r.get_recent_user_message(), Self::BedrockConverse(r) => r.get_recent_user_message(), Self::BedrockConverseStream(r) => r.get_recent_user_message(), + Self::ResponsesAPIRequest(r) => r.get_recent_user_message(), + } + } + + fn get_tool_names(&self) -> Option> { + match self { + Self::ChatCompletionsRequest(r) => r.get_tool_names(), + Self::MessagesRequest(r) => r.get_tool_names(), + Self::BedrockConverse(r) => r.get_tool_names(), + Self::BedrockConverseStream(r) => r.get_tool_names(), + Self::ResponsesAPIRequest(r) => r.get_tool_names(), } } @@ -94,6 +138,7 @@ impl ProviderRequest for ProviderRequestType { Self::MessagesRequest(r) => r.to_bytes(), Self::BedrockConverse(r) => r.to_bytes(), Self::BedrockConverseStream(r) => r.to_bytes(), + Self::ResponsesAPIRequest(r) => r.to_bytes(), } } @@ -103,6 +148,7 @@ impl ProviderRequest for ProviderRequestType { Self::MessagesRequest(r) => r.metadata(), Self::BedrockConverse(r) => r.metadata(), Self::BedrockConverseStream(r) => r.metadata(), + Self::ResponsesAPIRequest(r) => r.metadata(), } } @@ -112,18 +158,49 @@ impl ProviderRequest for ProviderRequestType { Self::MessagesRequest(r) => r.remove_metadata_key(key), Self::BedrockConverse(r) => r.remove_metadata_key(key), Self::BedrockConverseStream(r) => r.remove_metadata_key(key), + Self::ResponsesAPIRequest(r) => r.remove_metadata_key(key), + } + } + + fn get_temperature(&self) -> Option { + match self { + Self::ChatCompletionsRequest(r) => r.get_temperature(), + Self::MessagesRequest(r) => r.get_temperature(), + Self::BedrockConverse(r) => r.get_temperature(), + Self::BedrockConverseStream(r) => r.get_temperature(), + Self::ResponsesAPIRequest(r) => r.get_temperature(), + } + } + + fn get_messages(&self) -> Vec { + match self { + Self::ChatCompletionsRequest(r) => r.get_messages(), + Self::MessagesRequest(r) => r.get_messages(), + Self::BedrockConverse(r) => r.get_messages(), + Self::BedrockConverseStream(r) => r.get_messages(), + Self::ResponsesAPIRequest(r) => r.get_messages(), + } + } + + fn set_messages(&mut self, messages: &[crate::apis::openai::Message]) { + match self { + Self::ChatCompletionsRequest(r) => r.set_messages(messages), + Self::MessagesRequest(r) => r.set_messages(messages), + Self::BedrockConverse(r) => r.set_messages(messages), + Self::BedrockConverseStream(r) => r.set_messages(messages), + Self::ResponsesAPIRequest(r) => r.set_messages(messages), } } } /// Parse the client API from a byte slice. -impl TryFrom<(&[u8], &SupportedAPIs)> for ProviderRequestType { +impl TryFrom<(&[u8], &SupportedAPIsFromClient)> for ProviderRequestType { type Error = std::io::Error; - fn try_from((bytes, client_api): (&[u8], &SupportedAPIs)) -> Result { + fn try_from((bytes, client_api): (&[u8], &SupportedAPIsFromClient)) -> Result { // Use SupportedApi to determine the appropriate request type match client_api { - SupportedAPIs::OpenAIChatCompletions(_) => { + SupportedAPIsFromClient::OpenAIChatCompletions(_) => { let chat_completion_request: ChatCompletionsRequest = ChatCompletionsRequest::try_from(bytes) .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?; @@ -131,11 +208,20 @@ impl TryFrom<(&[u8], &SupportedAPIs)> for ProviderRequestType { chat_completion_request, )) } - SupportedAPIs::AnthropicMessagesAPI(_) => { + SupportedAPIsFromClient::AnthropicMessagesAPI(_) => { let messages_request: MessagesRequest = MessagesRequest::try_from(bytes) .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?; Ok(ProviderRequestType::MessagesRequest(messages_request)) } + + SupportedAPIsFromClient::OpenAIResponsesAPI(_) => { + let responses_apirequest: ResponsesAPIRequest = + ResponsesAPIRequest::try_from(bytes) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?; + Ok(ProviderRequestType::ResponsesAPIRequest( + responses_apirequest, + )) + } } } } @@ -148,17 +234,13 @@ impl TryFrom<(ProviderRequestType, &SupportedUpstreamAPIs)> for ProviderRequestT (client_request, upstream_api): (ProviderRequestType, &SupportedUpstreamAPIs), ) -> Result { match (client_request, upstream_api) { - // Same API - no conversion needed, just clone the reference + // ============================================================================ + // ChatCompletionsRequest conversions + // ============================================================================ ( ProviderRequestType::ChatCompletionsRequest(chat_req), SupportedUpstreamAPIs::OpenAIChatCompletions(_), ) => Ok(ProviderRequestType::ChatCompletionsRequest(chat_req)), - ( - ProviderRequestType::MessagesRequest(messages_req), - SupportedUpstreamAPIs::AnthropicMessagesAPI(_), - ) => Ok(ProviderRequestType::MessagesRequest(messages_req)), - - // Cross-API conversion - cloning is necessary for transformation ( ProviderRequestType::ChatCompletionsRequest(chat_req), SupportedUpstreamAPIs::AnthropicMessagesAPI(_), @@ -173,7 +255,45 @@ impl TryFrom<(ProviderRequestType, &SupportedUpstreamAPIs)> for ProviderRequestT })?; Ok(ProviderRequestType::MessagesRequest(messages_req)) } + ( + ProviderRequestType::ChatCompletionsRequest(chat_req), + SupportedUpstreamAPIs::AmazonBedrockConverse(_), + ) => { + let bedrock_req = ConverseRequest::try_from(chat_req) + .map_err(|e| ProviderRequestError { + message: format!("Failed to convert ChatCompletionsRequest to Amazon Bedrock request: {}", e), + source: Some(Box::new(e)) + })?; + Ok(ProviderRequestType::BedrockConverse(bedrock_req)) + } + ( + ProviderRequestType::ChatCompletionsRequest(chat_req), + SupportedUpstreamAPIs::AmazonBedrockConverseStream(_), + ) => { + let bedrock_req = ConverseStreamRequest::try_from(chat_req) + .map_err(|e| ProviderRequestError { + message: format!("Failed to convert ChatCompletionsRequest to Amazon Bedrock Stream request: {}", e), + source: Some(Box::new(e)) + })?; + Ok(ProviderRequestType::BedrockConverseStream(bedrock_req)) + } + ( + ProviderRequestType::ChatCompletionsRequest(_), + SupportedUpstreamAPIs::OpenAIResponsesAPI(_), + ) => { + Err(ProviderRequestError { + message: "Conversion from ChatCompletionsRequest to ResponsesAPIRequest is not supported. ResponsesAPI can only be used as a client API, not as an upstream API.".to_string(), + source: None, + }) + } + // ============================================================================ + // MessagesRequest conversions + // ============================================================================ + ( + ProviderRequestType::MessagesRequest(messages_req), + SupportedUpstreamAPIs::AnthropicMessagesAPI(_), + ) => Ok(ProviderRequestType::MessagesRequest(messages_req)), ( ProviderRequestType::MessagesRequest(messages_req), SupportedUpstreamAPIs::OpenAIChatCompletions(_), @@ -189,31 +309,6 @@ impl TryFrom<(ProviderRequestType, &SupportedUpstreamAPIs)> for ProviderRequestT })?; Ok(ProviderRequestType::ChatCompletionsRequest(chat_req)) } - - // Cross-API conversions: OpenAI/Anthropic to Amazon Bedrock - ( - ProviderRequestType::ChatCompletionsRequest(chat_req), - SupportedUpstreamAPIs::AmazonBedrockConverse(_), - ) => { - let bedrock_req = ConverseRequest::try_from(chat_req) - .map_err(|e| ProviderRequestError { - message: format!("Failed to convert ChatCompletionsRequest to Amazon Bedrock request: {}", e), - source: Some(Box::new(e)) - })?; - Ok(ProviderRequestType::BedrockConverse(bedrock_req)) - } - - ( - ProviderRequestType::ChatCompletionsRequest(chat_req), - SupportedUpstreamAPIs::AmazonBedrockConverseStream(_), - ) => { - let bedrock_req = ConverseStreamRequest::try_from(chat_req) - .map_err(|e| ProviderRequestError { - message: format!("Failed to convert ChatCompletionsRequest to Amazon Bedrock request: {}", e), - source: Some(Box::new(e)) - })?; - Ok(ProviderRequestType::BedrockConverse(bedrock_req)) - } ( ProviderRequestType::MessagesRequest(messages_req), SupportedUpstreamAPIs::AmazonBedrockConverse(_), @@ -235,7 +330,97 @@ impl TryFrom<(ProviderRequestType, &SupportedUpstreamAPIs)> for ProviderRequestT let bedrock_req = ConverseStreamRequest::try_from(messages_req).map_err(|e| { ProviderRequestError { message: format!( - "Failed to convert MessagesRequest to Amazon Bedrock request: {}", + "Failed to convert MessagesRequest to Amazon Bedrock Stream request: {}", + e + ), + source: Some(Box::new(e)), + } + })?; + Ok(ProviderRequestType::BedrockConverseStream(bedrock_req)) + } + ( + ProviderRequestType::MessagesRequest(_), + SupportedUpstreamAPIs::OpenAIResponsesAPI(_), + ) => { + Err(ProviderRequestError { + message: "Conversion from MessagesRequest to ResponsesAPIRequest is not supported. ResponsesAPI can only be used as a client API, not as an upstream API.".to_string(), + source: None, + }) + } + + // ============================================================================ + // ResponsesAPIRequest conversions (only converts TO other formats) + // ============================================================================ + ( + ProviderRequestType::ResponsesAPIRequest(responses_req), + SupportedUpstreamAPIs::OpenAIResponsesAPI(_), + ) => Ok(ProviderRequestType::ResponsesAPIRequest(responses_req)), + + // ResponsesAPI -> ChatCompletions (direct conversion) + ( + ProviderRequestType::ResponsesAPIRequest(responses_req), + SupportedUpstreamAPIs::OpenAIChatCompletions(_), + ) => { + let chat_req = ChatCompletionsRequest::try_from(responses_req).map_err(|e| { + ProviderRequestError { + message: format!( + "Failed to convert ResponsesAPIRequest to ChatCompletionsRequest: {}", + e + ), + source: Some(Box::new(e)), + } + })?; + Ok(ProviderRequestType::ChatCompletionsRequest(chat_req)) + } + + // ResponsesAPI -> Anthropic Messages (via ChatCompletions) + ( + ProviderRequestType::ResponsesAPIRequest(responses_req), + SupportedUpstreamAPIs::AnthropicMessagesAPI(_), + ) => { + // Chain: ResponsesAPI -> ChatCompletions -> MessagesRequest + let chat_req = ChatCompletionsRequest::try_from(responses_req).map_err(|e| { + ProviderRequestError { + message: format!( + "Failed to convert ResponsesAPIRequest to ChatCompletionsRequest: {}", + e + ), + source: Some(Box::new(e)), + } + })?; + + let messages_req = MessagesRequest::try_from(chat_req).map_err(|e| { + ProviderRequestError { + message: format!( + "Failed to convert ChatCompletionsRequest to MessagesRequest: {}", + e + ), + source: Some(Box::new(e)), + } + })?; + Ok(ProviderRequestType::MessagesRequest(messages_req)) + } + + // ResponsesAPI -> Bedrock Converse (via ChatCompletions) + ( + ProviderRequestType::ResponsesAPIRequest(responses_req), + SupportedUpstreamAPIs::AmazonBedrockConverse(_), + ) => { + // Chain: ResponsesAPI -> ChatCompletions -> ConverseRequest + let chat_req = ChatCompletionsRequest::try_from(responses_req).map_err(|e| { + ProviderRequestError { + message: format!( + "Failed to convert ResponsesAPIRequest to ChatCompletionsRequest: {}", + e + ), + source: Some(Box::new(e)), + } + })?; + + let bedrock_req = ConverseRequest::try_from(chat_req).map_err(|e| { + ProviderRequestError { + message: format!( + "Failed to convert ChatCompletionsRequest to Amazon Bedrock request: {}", e ), source: Some(Box::new(e)), @@ -244,13 +429,50 @@ impl TryFrom<(ProviderRequestType, &SupportedUpstreamAPIs)> for ProviderRequestT Ok(ProviderRequestType::BedrockConverse(bedrock_req)) } - // Amazon Bedrock to other APIs conversions + // ResponsesAPI -> Bedrock Converse Stream (via ChatCompletions) + ( + ProviderRequestType::ResponsesAPIRequest(responses_req), + SupportedUpstreamAPIs::AmazonBedrockConverseStream(_), + ) => { + // Chain: ResponsesAPI -> ChatCompletions -> ConverseStreamRequest + let chat_req = ChatCompletionsRequest::try_from(responses_req).map_err(|e| { + ProviderRequestError { + message: format!( + "Failed to convert ResponsesAPIRequest to ChatCompletionsRequest: {}", + e + ), + source: Some(Box::new(e)), + } + })?; + + let bedrock_req = ConverseStreamRequest::try_from(chat_req).map_err(|e| { + ProviderRequestError { + message: format!( + "Failed to convert ChatCompletionsRequest to Amazon Bedrock Stream request: {}", + e + ), + source: Some(Box::new(e)), + } + })?; + Ok(ProviderRequestType::BedrockConverseStream(bedrock_req)) + } + + // ============================================================================ + // Amazon Bedrock conversions (not supported as client API) + // ============================================================================ + (ProviderRequestType::BedrockConverse(_), _) => { - todo!("Amazon Bedrock to ChatCompletionsRequest conversion not implemented yet") + Err(ProviderRequestError { + message: "Amazon Bedrock Converse is not supported as a client API. Only OpenAI ChatCompletions, Anthropic Messages, and OpenAI Responses APIs are supported as client APIs.".to_string(), + source: None, + }) } (ProviderRequestType::BedrockConverseStream(_), _) => { - todo!("Amazon Bedrock Stream to ChatCompletionsRequest conversion not implemented yet") + Err(ProviderRequestError { + message: "Amazon Bedrock Converse Stream is not supported as a client API. Only OpenAI ChatCompletions, Anthropic Messages, and OpenAI Responses APIs are supported as client APIs.".to_string(), + source: None, + }) } } } @@ -284,7 +506,7 @@ mod tests { use crate::apis::anthropic::MessagesRequest as AnthropicMessagesRequest; use crate::apis::openai::ChatCompletionsRequest; use crate::apis::openai::OpenAIApi::ChatCompletions; - use crate::clients::endpoints::SupportedAPIs; + use crate::clients::endpoints::SupportedAPIsFromClient; use crate::transforms::lib::ExtractText; use serde_json::json; @@ -298,7 +520,7 @@ mod tests { ] }); let bytes = serde_json::to_vec(&req).unwrap(); - let api = SupportedAPIs::OpenAIChatCompletions(ChatCompletions); + let api = SupportedAPIsFromClient::OpenAIChatCompletions(ChatCompletions); let result = ProviderRequestType::try_from((bytes.as_slice(), &api)); assert!(result.is_ok()); match result.unwrap() { @@ -321,7 +543,7 @@ mod tests { ] }); let bytes = serde_json::to_vec(&req).unwrap(); - let endpoint = SupportedAPIs::AnthropicMessagesAPI(Messages); + let endpoint = SupportedAPIsFromClient::AnthropicMessagesAPI(Messages); let result = ProviderRequestType::try_from((bytes.as_slice(), &endpoint)); assert!(result.is_ok()); match result.unwrap() { @@ -343,7 +565,7 @@ mod tests { ] }); let bytes = serde_json::to_vec(&req).unwrap(); - let endpoint = SupportedAPIs::OpenAIChatCompletions(ChatCompletions); + let endpoint = SupportedAPIsFromClient::OpenAIChatCompletions(ChatCompletions); let result = ProviderRequestType::try_from((bytes.as_slice(), &endpoint)); assert!(result.is_ok()); match result.unwrap() { @@ -366,7 +588,7 @@ mod tests { }); let bytes = serde_json::to_vec(&req).unwrap(); // Intentionally use OpenAI endpoint for Anthropic payload - let endpoint = SupportedAPIs::OpenAIChatCompletions(ChatCompletions); + let endpoint = SupportedAPIsFromClient::OpenAIChatCompletions(ChatCompletions); let result = ProviderRequestType::try_from((bytes.as_slice(), &endpoint)); // Should parse as ChatCompletionsRequest, not error assert!(result.is_ok()); @@ -486,4 +708,399 @@ mod tests { let roundtrip_max_tokens = openai_req2.max_completion_tokens.or(openai_req2.max_tokens); assert_eq!(original_max_tokens, roundtrip_max_tokens); } + + #[test] + fn test_responses_api_request_from_bytes() { + use crate::apis::openai::OpenAIApi::Responses; + + let req = json!({ + "model": "gpt-4o", + "input": "Hello, how are you?" + }); + let bytes = serde_json::to_vec(&req).unwrap(); + let api = SupportedAPIsFromClient::OpenAIResponsesAPI(Responses); + let result = ProviderRequestType::try_from((bytes.as_slice(), &api)); + assert!(result.is_ok()); + match result.unwrap() { + ProviderRequestType::ResponsesAPIRequest(r) => { + assert_eq!(r.model, "gpt-4o"); + } + _ => panic!("Expected ResponsesAPIRequest variant"), + } + } + + #[test] + fn test_responses_api_to_chat_completions_conversion() { + use crate::apis::openai::OpenAIApi::ChatCompletions; + use crate::apis::openai_responses::{InputParam, ResponsesAPIRequest}; + + let responses_req = ResponsesAPIRequest { + model: "gpt-4o".to_string(), + input: InputParam::Text("Hello, world!".to_string()), + temperature: Some(0.7), + top_p: Some(0.9), + max_output_tokens: Some(100), + stream: Some(false), + metadata: None, + tools: None, + tool_choice: None, + parallel_tool_calls: None, + instructions: None, + modalities: None, + user: None, + store: None, + reasoning_effort: None, + include: None, + audio: None, + text: None, + service_tier: None, + top_logprobs: None, + stream_options: None, + truncation: None, + conversation: None, + previous_response_id: None, + max_tool_calls: None, + background: None, + }; + + let upstream_api = SupportedUpstreamAPIs::OpenAIChatCompletions(ChatCompletions); + let result = ProviderRequestType::try_from(( + ProviderRequestType::ResponsesAPIRequest(responses_req), + &upstream_api, + )); + + assert!(result.is_ok()); + match result.unwrap() { + ProviderRequestType::ChatCompletionsRequest(chat_req) => { + assert_eq!(chat_req.model, "gpt-4o"); + assert_eq!(chat_req.temperature, Some(0.7)); + assert_eq!(chat_req.top_p, Some(0.9)); + assert_eq!(chat_req.max_completion_tokens, Some(100)); + assert_eq!(chat_req.messages.len(), 1); + } + _ => panic!("Expected ChatCompletionsRequest variant"), + } + } + + #[test] + fn test_responses_api_to_anthropic_messages_conversion() { + use crate::apis::anthropic::AnthropicApi::Messages; + use crate::apis::openai_responses::{InputParam, ResponsesAPIRequest}; + + let responses_req = ResponsesAPIRequest { + model: "gpt-4o".to_string(), + input: InputParam::Text("Hello, Claude!".to_string()), + temperature: Some(0.8), + max_output_tokens: Some(150), + stream: Some(false), + metadata: None, + tools: None, + tool_choice: None, + parallel_tool_calls: None, + instructions: Some("You are a helpful assistant".to_string()), + modalities: None, + user: None, + store: None, + reasoning_effort: None, + include: None, + audio: None, + text: None, + service_tier: None, + top_p: None, + top_logprobs: None, + stream_options: None, + truncation: None, + conversation: None, + previous_response_id: None, + max_tool_calls: None, + background: None, + }; + + let upstream_api = SupportedUpstreamAPIs::AnthropicMessagesAPI(Messages); + let result = ProviderRequestType::try_from(( + ProviderRequestType::ResponsesAPIRequest(responses_req), + &upstream_api, + )); + + assert!(result.is_ok()); + match result.unwrap() { + ProviderRequestType::MessagesRequest(messages_req) => { + assert_eq!(messages_req.model, "gpt-4o"); + assert_eq!(messages_req.temperature, Some(0.8)); + assert_eq!(messages_req.max_tokens, 150); + // Instructions should be converted to system prompt via ChatCompletions conversion + // The conversion chain: ResponsesAPI -> ChatCompletions (system message) -> Anthropic (system prompt) + // But we need to check if the system prompt was actually set + assert_eq!(messages_req.messages.len(), 1); + } + _ => panic!("Expected MessagesRequest variant"), + } + } + + #[test] + fn test_responses_api_to_bedrock_conversion() { + use crate::apis::amazon_bedrock::AmazonBedrockApi::Converse; + use crate::apis::openai_responses::{InputParam, ResponsesAPIRequest}; + + let responses_req = ResponsesAPIRequest { + model: "gpt-4o".to_string(), + input: InputParam::Text("Hello, Bedrock!".to_string()), + temperature: Some(0.5), + max_output_tokens: Some(200), + stream: Some(false), + metadata: None, + tools: None, + tool_choice: None, + parallel_tool_calls: None, + instructions: None, + modalities: None, + user: None, + store: None, + reasoning_effort: None, + include: None, + audio: None, + text: None, + service_tier: None, + top_p: None, + top_logprobs: None, + stream_options: None, + truncation: None, + conversation: None, + previous_response_id: None, + max_tool_calls: None, + background: None, + }; + + let upstream_api = SupportedUpstreamAPIs::AmazonBedrockConverse(Converse); + let result = ProviderRequestType::try_from(( + ProviderRequestType::ResponsesAPIRequest(responses_req), + &upstream_api, + )); + + assert!(result.is_ok()); + match result.unwrap() { + ProviderRequestType::BedrockConverse(bedrock_req) => { + assert_eq!(bedrock_req.model_id, "gpt-4o"); + // Bedrock receives the converted request through ChatCompletions + assert!(!bedrock_req.messages.is_none()); + } + _ => panic!("Expected BedrockConverse variant"), + } + } + + #[test] + fn test_chat_completions_to_responses_api_not_supported() { + use crate::apis::openai::OpenAIApi::Responses; + use crate::apis::openai::{Message, MessageContent, Role}; + + let chat_req = ChatCompletionsRequest { + model: "gpt-4".to_string(), + messages: vec![Message { + role: Role::User, + content: MessageContent::Text("Hello!".to_string()), + name: None, + tool_calls: None, + tool_call_id: None, + }], + ..Default::default() + }; + + let upstream_api = SupportedUpstreamAPIs::OpenAIResponsesAPI(Responses); + let result = ProviderRequestType::try_from(( + ProviderRequestType::ChatCompletionsRequest(chat_req), + &upstream_api, + )); + + assert!(result.is_err()); + let err = result.unwrap_err(); + assert!(err.message.contains("ResponsesAPI can only be used as a client API")); + } + + #[test] + fn test_anthropic_messages_to_responses_api_not_supported() { + use crate::apis::anthropic::MessagesRequest as AnthropicMessagesRequest; + use crate::apis::openai::OpenAIApi::Responses; + + let messages_req = AnthropicMessagesRequest { + model: "claude-3-sonnet".to_string(), + messages: vec![crate::apis::anthropic::MessagesMessage { + role: crate::apis::anthropic::MessagesRole::User, + content: crate::apis::anthropic::MessagesMessageContent::Single( + "Hello!".to_string(), + ), + }], + max_tokens: 100, + container: None, + mcp_servers: None, + service_tier: None, + thinking: None, + temperature: None, + top_p: None, + top_k: None, + stream: None, + stop_sequences: None, + system: None, + tools: None, + tool_choice: None, + metadata: None, + }; + + let upstream_api = SupportedUpstreamAPIs::OpenAIResponsesAPI(Responses); + let result = ProviderRequestType::try_from(( + ProviderRequestType::MessagesRequest(messages_req), + &upstream_api, + )); + + assert!(result.is_err()); + let err = result.unwrap_err(); + assert!(err.message.contains("ResponsesAPI can only be used as a client API")); + } + + #[test] + fn test_bedrock_as_client_api_not_supported() { + use crate::apis::openai::OpenAIApi::ChatCompletions; + + // Create a simple Bedrock request (we'll use Default if available, or minimal construction) + let bedrock_req = ConverseRequest::default(); + + let upstream_api = SupportedUpstreamAPIs::OpenAIChatCompletions(ChatCompletions); + let result = ProviderRequestType::try_from(( + ProviderRequestType::BedrockConverse(bedrock_req), + &upstream_api, + )); + + assert!(result.is_err()); + let err = result.unwrap_err(); + assert!(err.message.contains("not supported as a client API")); + assert!(err + .message + .contains("OpenAI ChatCompletions, Anthropic Messages, and OpenAI Responses")); + } + + #[test] + fn test_get_message_history_chat_completions() { + use crate::apis::openai::{Message, MessageContent, Role}; + + let chat_req = ChatCompletionsRequest { + model: "gpt-4".to_string(), + messages: vec![ + Message { + role: Role::System, + content: MessageContent::Text("You are helpful".to_string()), + name: None, + tool_calls: None, + tool_call_id: None, + }, + Message { + role: Role::User, + content: MessageContent::Text("Hello!".to_string()), + name: None, + tool_calls: None, + tool_call_id: None, + }, + ], + ..Default::default() + }; + + let provider_req = ProviderRequestType::ChatCompletionsRequest(chat_req); + let messages = provider_req.get_messages(); + + assert_eq!(messages.len(), 2); + assert_eq!(messages[0].role, Role::System); + assert_eq!(messages[1].role, Role::User); + } + + #[test] + fn test_get_message_history_anthropic_messages() { + use crate::apis::anthropic::{ + MessagesMessage, MessagesMessageContent, MessagesRequest, MessagesRole, + MessagesSystemPrompt, + }; + + let anthropic_req = MessagesRequest { + model: "claude-3-sonnet".to_string(), + messages: vec![MessagesMessage { + role: MessagesRole::User, + content: MessagesMessageContent::Single("Hello!".to_string()), + }], + system: Some(MessagesSystemPrompt::Single( + "You are helpful".to_string(), + )), + max_tokens: 100, + container: None, + mcp_servers: None, + metadata: None, + service_tier: None, + thinking: None, + temperature: None, + top_p: None, + top_k: None, + stream: None, + stop_sequences: None, + tools: None, + tool_choice: None, + }; + + let provider_req = ProviderRequestType::MessagesRequest(anthropic_req); + let messages = provider_req.get_messages(); + + // Should have system message + user message + assert_eq!(messages.len(), 2); + assert_eq!( + messages[0].role, + crate::apis::openai::Role::System + ); + assert_eq!( + messages[1].role, + crate::apis::openai::Role::User + ); + } + + #[test] + fn test_get_message_history_responses_api() { + use crate::apis::openai_responses::{InputParam, ResponsesAPIRequest}; + + let responses_req = ResponsesAPIRequest { + model: "gpt-4o".to_string(), + input: InputParam::Text("Hello, world!".to_string()), + instructions: Some("Be helpful".to_string()), + temperature: None, + max_output_tokens: None, + stream: None, + metadata: None, + tools: None, + tool_choice: None, + parallel_tool_calls: None, + modalities: None, + user: None, + store: None, + reasoning_effort: None, + include: None, + audio: None, + text: None, + service_tier: None, + top_p: None, + top_logprobs: None, + stream_options: None, + truncation: None, + conversation: None, + previous_response_id: None, + max_tool_calls: None, + background: None, + }; + + let provider_req = ProviderRequestType::ResponsesAPIRequest(responses_req); + let messages = provider_req.get_messages(); + + // Should have system message (instructions) + user message (input) + assert_eq!(messages.len(), 2); + assert_eq!( + messages[0].role, + crate::apis::openai::Role::System + ); + assert_eq!( + messages[1].role, + crate::apis::openai::Role::User + ); + } } diff --git a/crates/hermesllm/src/providers/response.rs b/crates/hermesllm/src/providers/response.rs index f09b2c04..a2494c6d 100644 --- a/crates/hermesllm/src/providers/response.rs +++ b/crates/hermesllm/src/providers/response.rs @@ -2,38 +2,28 @@ use serde::Serialize; use std::convert::TryFrom; use std::error::Error; use std::fmt; - use crate::apis::amazon_bedrock::ConverseResponse; -use crate::apis::amazon_bedrock::ConverseStreamEvent; use crate::apis::anthropic::MessagesResponse; -use crate::apis::anthropic::MessagesStreamEvent; use crate::apis::openai::ChatCompletionsResponse; -use crate::apis::openai::ChatCompletionsStreamResponse; -use crate::apis::sse::SseEvent; -use crate::clients::endpoints::SupportedAPIs; +use crate::apis::openai_responses::ResponsesAPIResponse; +use crate::clients::endpoints::SupportedAPIsFromClient; use crate::clients::endpoints::SupportedUpstreamAPIs; use crate::providers::id::ProviderId; -/// Trait for token usage information -pub trait TokenUsage { - fn completion_tokens(&self) -> usize; - fn prompt_tokens(&self) -> usize; - fn total_tokens(&self) -> usize; -} #[derive(Serialize, Debug, Clone)] #[serde(untagged)] pub enum ProviderResponseType { ChatCompletionsResponse(ChatCompletionsResponse), MessagesResponse(MessagesResponse), + ResponsesAPIResponse(ResponsesAPIResponse), } -#[derive(Serialize, Debug, Clone)] -#[serde(untagged)] -pub enum ProviderStreamResponseType { - ChatCompletionsStreamResponse(ChatCompletionsStreamResponse), - MessagesStreamEvent(MessagesStreamEvent), - ConverseStreamEvent(ConverseStreamEvent), +/// Trait for token usage information +pub trait TokenUsage { + fn completion_tokens(&self) -> usize; + fn prompt_tokens(&self) -> usize; + fn total_tokens(&self) -> usize; } pub trait ProviderResponse: Send + Sync { @@ -52,6 +42,7 @@ impl ProviderResponse for ProviderResponseType { match self { ProviderResponseType::ChatCompletionsResponse(resp) => resp.usage(), ProviderResponseType::MessagesResponse(resp) => resp.usage(), + ProviderResponseType::ResponsesAPIResponse(resp) => resp.usage.as_ref().map(|u| u as &dyn TokenUsage), } } @@ -59,89 +50,27 @@ impl ProviderResponse for ProviderResponseType { match self { ProviderResponseType::ChatCompletionsResponse(resp) => resp.extract_usage_counts(), ProviderResponseType::MessagesResponse(resp) => resp.extract_usage_counts(), - } - } -} -pub trait ProviderStreamResponse: Send + Sync { - /// Get the content delta for this chunk - fn content_delta(&self) -> Option<&str>; - - /// Check if this is the final chunk in the stream - fn is_final(&self) -> bool; - - /// Get role information if available - fn role(&self) -> Option<&str>; - - /// Get event type for SSE streaming (used by Anthropic) - fn event_type(&self) -> Option<&str>; -} - -impl ProviderStreamResponse for ProviderStreamResponseType { - fn content_delta(&self) -> Option<&str> { - match self { - ProviderStreamResponseType::ChatCompletionsStreamResponse(resp) => resp.content_delta(), - ProviderStreamResponseType::MessagesStreamEvent(resp) => resp.content_delta(), - ProviderStreamResponseType::ConverseStreamEvent(resp) => resp.content_delta(), - } - } - - fn is_final(&self) -> bool { - match self { - ProviderStreamResponseType::ChatCompletionsStreamResponse(resp) => resp.is_final(), - ProviderStreamResponseType::MessagesStreamEvent(resp) => resp.is_final(), - ProviderStreamResponseType::ConverseStreamEvent(resp) => resp.is_final(), - } - } - - fn role(&self) -> Option<&str> { - match self { - ProviderStreamResponseType::ChatCompletionsStreamResponse(resp) => resp.role(), - ProviderStreamResponseType::MessagesStreamEvent(resp) => resp.role(), - ProviderStreamResponseType::ConverseStreamEvent(resp) => resp.role(), - } - } - - fn event_type(&self) -> Option<&str> { - match self { - ProviderStreamResponseType::ChatCompletionsStreamResponse(_resp) => None, // OpenAI doesn't use event types - ProviderStreamResponseType::MessagesStreamEvent(resp) => resp.event_type(), - ProviderStreamResponseType::ConverseStreamEvent(resp) => resp.event_type(), // Bedrock doesn't use event types - } - } -} - -impl Into for ProviderStreamResponseType { - fn into(self) -> String { - match self { - ProviderStreamResponseType::MessagesStreamEvent(event) => { - // Use the Into implementation for proper SSE formatting with event lines - event.into() - } - ProviderStreamResponseType::ConverseStreamEvent(event) => { - // Use the Into implementation for proper SSE formatting with event lines - event.into() - } - ProviderStreamResponseType::ChatCompletionsStreamResponse(_) => { - // For OpenAI, use simple data line format - let json = serde_json::to_string(&self).unwrap_or_default(); - format!("data: {}\n\n", json) + ProviderResponseType::ResponsesAPIResponse(resp) => { + resp.usage.as_ref().map(|u| { + (u.input_tokens as usize, u.output_tokens as usize, u.total_tokens as usize) + }) } } } } // --- Response transformation logic for client API compatibility --- -impl TryFrom<(&[u8], &SupportedAPIs, &ProviderId)> for ProviderResponseType { +impl TryFrom<(&[u8], &SupportedAPIsFromClient, &ProviderId)> for ProviderResponseType { type Error = std::io::Error; fn try_from( - (bytes, client_api, provider_id): (&[u8], &SupportedAPIs, &ProviderId), + (bytes, client_api, provider_id): (&[u8], &SupportedAPIsFromClient, &ProviderId), ) -> Result { let upstream_api = provider_id.compatible_api_for_client(client_api, false); match (&upstream_api, client_api) { ( SupportedUpstreamAPIs::OpenAIChatCompletions(_), - SupportedAPIs::OpenAIChatCompletions(_), + SupportedAPIsFromClient::OpenAIChatCompletions(_), ) => { let resp: ChatCompletionsResponse = ChatCompletionsResponse::try_from(bytes) .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?; @@ -149,7 +78,7 @@ impl TryFrom<(&[u8], &SupportedAPIs, &ProviderId)> for ProviderResponseType { } ( SupportedUpstreamAPIs::AnthropicMessagesAPI(_), - SupportedAPIs::AnthropicMessagesAPI(_), + SupportedAPIsFromClient::AnthropicMessagesAPI(_), ) => { let resp: MessagesResponse = serde_json::from_slice(bytes) .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?; @@ -157,7 +86,7 @@ impl TryFrom<(&[u8], &SupportedAPIs, &ProviderId)> for ProviderResponseType { } ( SupportedUpstreamAPIs::AnthropicMessagesAPI(_), - SupportedAPIs::OpenAIChatCompletions(_), + SupportedAPIsFromClient::OpenAIChatCompletions(_), ) => { let anthropic_resp: MessagesResponse = serde_json::from_slice(bytes) .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?; @@ -174,7 +103,7 @@ impl TryFrom<(&[u8], &SupportedAPIs, &ProviderId)> for ProviderResponseType { } ( SupportedUpstreamAPIs::OpenAIChatCompletions(_), - SupportedAPIs::AnthropicMessagesAPI(_), + SupportedAPIsFromClient::AnthropicMessagesAPI(_), ) => { let openai_resp: ChatCompletionsResponse = ChatCompletionsResponse::try_from(bytes) .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?; @@ -191,7 +120,7 @@ impl TryFrom<(&[u8], &SupportedAPIs, &ProviderId)> for ProviderResponseType { // Amazon Bedrock transformations ( SupportedUpstreamAPIs::AmazonBedrockConverse(_), - SupportedAPIs::OpenAIChatCompletions(_), + SupportedAPIsFromClient::OpenAIChatCompletions(_), ) => { let bedrock_resp: ConverseResponse = serde_json::from_slice(bytes) .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?; @@ -207,7 +136,7 @@ impl TryFrom<(&[u8], &SupportedAPIs, &ProviderId)> for ProviderResponseType { } ( SupportedUpstreamAPIs::AmazonBedrockConverse(_), - SupportedAPIs::AnthropicMessagesAPI(_), + SupportedAPIsFromClient::AnthropicMessagesAPI(_), ) => { let bedrock_resp: ConverseResponse = serde_json::from_slice(bytes) .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?; @@ -221,6 +150,80 @@ impl TryFrom<(&[u8], &SupportedAPIs, &ProviderId)> for ProviderResponseType { })?; Ok(ProviderResponseType::MessagesResponse(messages_resp)) } + ( + SupportedUpstreamAPIs::OpenAIResponsesAPI(_), + SupportedAPIsFromClient::OpenAIResponsesAPI(_), + ) => { + let resp: ResponsesAPIResponse = ResponsesAPIResponse::try_from(bytes) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?; + Ok(ProviderResponseType::ResponsesAPIResponse(resp)) + } + ( + SupportedUpstreamAPIs::OpenAIChatCompletions(_), + SupportedAPIsFromClient::OpenAIResponsesAPI(_), + ) => { + let chat_completions_response: ChatCompletionsResponse = ChatCompletionsResponse::try_from(bytes) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?; + + // Transform to ResponsesAPI format using the transformer + let responses_resp: ResponsesAPIResponse = chat_completions_response.try_into().map_err(|e| { + std::io::Error::new( + std::io::ErrorKind::InvalidData, + format!("Transformation error: {}", e), + ) + })?; + Ok(ProviderResponseType::ResponsesAPIResponse(responses_resp)) + } + ( + SupportedUpstreamAPIs::AnthropicMessagesAPI(_), + SupportedAPIsFromClient::OpenAIResponsesAPI(_), + ) => { + + //Chain transform: Anthropic Messages -> OpenAI ChatCompletions -> ResponsesAPI + let anthropic_resp: MessagesResponse = serde_json::from_slice(bytes) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?; + + // Transform to ChatCompletions format using the transformer + let chat_resp: ChatCompletionsResponse = anthropic_resp.try_into().map_err(|e| { + std::io::Error::new( + std::io::ErrorKind::InvalidData, + format!("Transformation error: {}", e), + ) + })?; + + let response_api: ResponsesAPIResponse = chat_resp.try_into().map_err(|e| { + std::io::Error::new( + std::io::ErrorKind::InvalidData, + format!("Transformation error: {}", e), + ) + })?; + Ok(ProviderResponseType::ResponsesAPIResponse(response_api)) + } + ( + SupportedUpstreamAPIs::AmazonBedrockConverse(_), + SupportedAPIsFromClient::OpenAIResponsesAPI(_), + ) => { + // Chain transform: Bedrock Converse -> ChatCompletions -> ResponsesAPI + let bedrock_resp: ConverseResponse = serde_json::from_slice(bytes) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?; + + // Transform to ChatCompletions format + let chat_resp: ChatCompletionsResponse = bedrock_resp.try_into().map_err(|e| { + std::io::Error::new( + std::io::ErrorKind::InvalidData, + format!("Bedrock to ChatCompletions transformation error: {}", e), + ) + })?; + + // Transform to ResponsesAPI format + let response_api: ResponsesAPIResponse = chat_resp.try_into().map_err(|e| { + std::io::Error::new( + std::io::ErrorKind::InvalidData, + format!("ChatCompletions to ResponsesAPI transformation error: {}", e), + ) + })?; + Ok(ProviderResponseType::ResponsesAPIResponse(response_api)) + } _ => Err(std::io::Error::new( std::io::ErrorKind::InvalidData, "Unsupported API combination for response transformation", @@ -229,236 +232,6 @@ impl TryFrom<(&[u8], &SupportedAPIs, &ProviderId)> for ProviderResponseType { } } -// Stream response transformation logic for client API compatibility -impl TryFrom<(&[u8], &SupportedAPIs, &SupportedUpstreamAPIs)> for ProviderStreamResponseType { - type Error = Box; - - fn try_from( - (bytes, client_api, upstream_api): (&[u8], &SupportedAPIs, &SupportedUpstreamAPIs), - ) -> Result { - // Special case: Handle [DONE] marker for OpenAI -> Anthropic conversion - if bytes == b"[DONE]" && matches!(client_api, SupportedAPIs::AnthropicMessagesAPI(_)) { - return Ok(ProviderStreamResponseType::MessagesStreamEvent( - crate::apis::anthropic::MessagesStreamEvent::MessageStop, - )); - } - match (upstream_api, client_api) { - // OpenAI upstream - ( - SupportedUpstreamAPIs::OpenAIChatCompletions(_), - SupportedAPIs::OpenAIChatCompletions(_), - ) => { - let resp = serde_json::from_slice(bytes)?; - Ok(ProviderStreamResponseType::ChatCompletionsStreamResponse( - resp, - )) - } - ( - SupportedUpstreamAPIs::OpenAIChatCompletions(_), - SupportedAPIs::AnthropicMessagesAPI(_), - ) => { - let openai_resp: crate::apis::openai::ChatCompletionsStreamResponse = - serde_json::from_slice(bytes)?; - let anthropic_resp = openai_resp.try_into()?; - Ok(ProviderStreamResponseType::MessagesStreamEvent( - anthropic_resp, - )) - } - - // Anthropic upstream - ( - SupportedUpstreamAPIs::AnthropicMessagesAPI(_), - SupportedAPIs::AnthropicMessagesAPI(_), - ) => { - let resp = serde_json::from_slice(bytes)?; - Ok(ProviderStreamResponseType::MessagesStreamEvent(resp)) - } - ( - SupportedUpstreamAPIs::AnthropicMessagesAPI(_), - SupportedAPIs::OpenAIChatCompletions(_), - ) => { - let anthropic_resp: crate::apis::anthropic::MessagesStreamEvent = - serde_json::from_slice(bytes)?; - let openai_resp = anthropic_resp.try_into()?; - Ok(ProviderStreamResponseType::ChatCompletionsStreamResponse( - openai_resp, - )) - } - - // Amazon Bedrock ConverseStream upstream - ( - SupportedUpstreamAPIs::AmazonBedrockConverseStream(_), - SupportedAPIs::AnthropicMessagesAPI(_), - ) => { - let bedrock_resp: crate::apis::amazon_bedrock::ConverseStreamEvent = - serde_json::from_slice(bytes)?; - let anthropic_resp = bedrock_resp.try_into()?; - Ok(ProviderStreamResponseType::MessagesStreamEvent( - anthropic_resp, - )) - } - _ => Err(std::io::Error::new( - std::io::ErrorKind::InvalidData, - "Unsupported API combination for response transformation", - ) - .into()), - } - } -} - -// TryFrom implementation to convert raw bytes to SseEvent with parsed provider response -impl TryFrom<(SseEvent, &SupportedAPIs, &SupportedUpstreamAPIs)> for SseEvent { - type Error = Box; - - fn try_from( - (sse_event, client_api, upstream_api): (SseEvent, &SupportedAPIs, &SupportedUpstreamAPIs), - ) -> Result { - // Create a new transformed event based on the original - let mut transformed_event = sse_event; - - // If has data, parse the data as a provider stream response (business logic layer) - if transformed_event.data.is_some() { - let data_str = transformed_event.data.as_ref().unwrap(); - let data_bytes = data_str.as_bytes(); - let transformed_response: ProviderStreamResponseType = - ProviderStreamResponseType::try_from((data_bytes, client_api, upstream_api))?; - - // Convert to SSE string explicitly to avoid type ambiguity - let sse_string: String = transformed_response.clone().into(); - transformed_event.sse_transform_buffer = sse_string; - transformed_event.provider_stream_response = Some(transformed_response); - } - - match (client_api, upstream_api) { - ( - SupportedAPIs::AnthropicMessagesAPI(_), - SupportedUpstreamAPIs::OpenAIChatCompletions(_), - ) => { - if let Some(provider_response) = &transformed_event.provider_stream_response { - if let Some(event_type) = provider_response.event_type() { - // This ensures the required Anthropic sequence: MessageStart → ContentBlockStart → ContentBlockDelta(s) - if event_type == "message_start" { - // Create ContentBlockStart event and format it using Into - let content_block_start = MessagesStreamEvent::ContentBlockStart { - index: 0, - content_block: crate::apis::anthropic::MessagesContentBlock::Text { - text: String::new(), - cache_control: None, - }, - }; - let content_block_start_sse: String = content_block_start.into(); - - // Format as proper SSE: MessageStart first, then ContentBlockStart - // The sse_transform_buffer already contains the properly formatted MessageStart - transformed_event.sse_transform_buffer = format!( - "{}{}", - transformed_event.sse_transform_buffer, content_block_start_sse, - ); - } else if event_type == "message_delta" { - // Create ContentBlockStop event and format it using Into - let content_block_stop = - MessagesStreamEvent::ContentBlockStop { index: 0 }; - let content_block_stop_sse: String = content_block_stop.into(); - - // Format as proper SSE: ContentBlockStop first, then MessageDelta - transformed_event.sse_transform_buffer = format!( - "{}{}", - content_block_stop_sse, transformed_event.sse_transform_buffer - ); - } - // For other event types, the sse_transform_buffer already has the correct format from Into - } - // If event_type is None, we just keep the data line as-is without an event line - // This handles cases where the transformation might not produce a valid event type - } - } - ( - SupportedAPIs::OpenAIChatCompletions(_), - SupportedUpstreamAPIs::AnthropicMessagesAPI(_), - ) => { - if transformed_event.is_event_only() && transformed_event.event.is_some() { - transformed_event.sse_transform_buffer = format!("\n"); // suppress the event upstream for OpenAI - } - } - ( - SupportedAPIs::AnthropicMessagesAPI(_), - SupportedUpstreamAPIs::AnthropicMessagesAPI(_), - ) => { - // When both client and upstream are Anthropic, suppress event-only lines - // because the data line transformation already includes the event line - if transformed_event.is_event_only() && transformed_event.event.is_some() { - transformed_event.sse_transform_buffer = String::new(); // suppress duplicate event line - } - } - _ => { - // Other combinations can be handled here as needed - } - } - - Ok(transformed_event) - } -} - -// TryFrom implementation to convert AWS Event Stream DecodedFrame to ProviderStreamResponseType -impl - TryFrom<( - &aws_smithy_eventstream::frame::DecodedFrame, - &SupportedAPIs, - &SupportedUpstreamAPIs, - )> for ProviderStreamResponseType -{ - type Error = Box; - - fn try_from( - (frame, client_api, upstream_api): ( - &aws_smithy_eventstream::frame::DecodedFrame, - &SupportedAPIs, - &SupportedUpstreamAPIs, - ), - ) -> Result { - use aws_smithy_eventstream::frame::DecodedFrame; - - match frame { - DecodedFrame::Complete(_) => { - // We have a complete frame - parse it based on upstream API - match (upstream_api, client_api) { - ( - SupportedUpstreamAPIs::AmazonBedrockConverseStream(_), - SupportedAPIs::AnthropicMessagesAPI(_), - ) => { - // Parse the DecodedFrame into ConverseStreamEvent - let bedrock_event = - crate::apis::amazon_bedrock::ConverseStreamEvent::try_from(frame)?; - let anthropic_event: crate::apis::anthropic::MessagesStreamEvent = - bedrock_event.try_into()?; - - Ok(ProviderStreamResponseType::MessagesStreamEvent( - anthropic_event, - )) - } - ( - SupportedUpstreamAPIs::AmazonBedrockConverseStream(_), - SupportedAPIs::OpenAIChatCompletions(_), - ) => { - // Parse the DecodedFrame into ConverseStreamEvent - let bedrock_event = - crate::apis::amazon_bedrock::ConverseStreamEvent::try_from(frame)?; - let openai_event: crate::apis::openai::ChatCompletionsStreamResponse = - bedrock_event.try_into()?; - Ok(ProviderStreamResponseType::ChatCompletionsStreamResponse( - openai_event, - )) - } - _ => Err("Unsupported API combination for event-stream decoding".into()), - } - } - DecodedFrame::Incomplete => { - Err("Cannot convert incomplete frame to provider response".into()) - } - } - } -} - #[derive(Debug)] pub struct ProviderResponseError { pub message: String, @@ -482,11 +255,9 @@ impl Error for ProviderResponseError { #[cfg(test)] mod tests { use super::*; - use crate::apis::amazon_bedrock_binary_frame::BedrockBinaryFrameDecoder; - use crate::apis::anthropic::AnthropicApi; use crate::apis::openai::OpenAIApi; - use crate::apis::sse::SseStreamIter; - use crate::clients::endpoints::SupportedAPIs; + use crate::apis::anthropic::AnthropicApi; + use crate::clients::endpoints::SupportedAPIsFromClient; use crate::providers::id::ProviderId; use serde_json::json; @@ -510,7 +281,7 @@ mod tests { let bytes = serde_json::to_vec(&resp).unwrap(); let result = ProviderResponseType::try_from(( bytes.as_slice(), - &SupportedAPIs::OpenAIChatCompletions(OpenAIApi::ChatCompletions), + &SupportedAPIsFromClient::OpenAIChatCompletions(OpenAIApi::ChatCompletions), &ProviderId::OpenAI, )); assert!(result.is_ok()); @@ -539,7 +310,7 @@ mod tests { let bytes = serde_json::to_vec(&resp).unwrap(); let result = ProviderResponseType::try_from(( bytes.as_slice(), - &SupportedAPIs::AnthropicMessagesAPI(AnthropicApi::Messages), + &SupportedAPIsFromClient::AnthropicMessagesAPI(AnthropicApi::Messages), &ProviderId::Anthropic, )); assert!(result.is_ok()); @@ -573,7 +344,7 @@ mod tests { let bytes = serde_json::to_vec(&resp).unwrap(); let result = ProviderResponseType::try_from(( bytes.as_slice(), - &SupportedAPIs::AnthropicMessagesAPI(AnthropicApi::Messages), + &SupportedAPIsFromClient::AnthropicMessagesAPI(AnthropicApi::Messages), &ProviderId::OpenAI, )); assert!(result.is_ok()); @@ -615,7 +386,7 @@ mod tests { let bytes = serde_json::to_vec(&resp).unwrap(); let result = ProviderResponseType::try_from(( bytes.as_slice(), - &SupportedAPIs::OpenAIChatCompletions(OpenAIApi::ChatCompletions), + &SupportedAPIsFromClient::OpenAIChatCompletions(OpenAIApi::ChatCompletions), &ProviderId::Anthropic, )); assert!(result.is_ok()); @@ -628,951 +399,4 @@ mod tests { _ => panic!("Expected ChatCompletionsResponse variant"), } } - - #[test] - fn test_sse_event_parsing() { - // Test valid SSE data line - let line = "data: {\"id\":\"test\",\"object\":\"chat.completion.chunk\"}\n\n"; - let event: Result = line.parse(); - assert!(event.is_ok()); - let event = event.unwrap(); - assert_eq!( - event.data, - Some("{\"id\":\"test\",\"object\":\"chat.completion.chunk\"}\n\n".to_string()) - ); - - // Test conversion back to line using Display trait - let wire_format = event.to_string(); - assert_eq!( - wire_format, - "data: {\"id\":\"test\",\"object\":\"chat.completion.chunk\"}\n\n" - ); - - // Test [DONE] marker - should be valid SSE event - let done_line = "data: [DONE]"; - let done_result: Result = done_line.parse(); - assert!(done_result.is_ok()); - let done_event = done_result.unwrap(); - assert_eq!(done_event.data, Some("[DONE]".to_string())); - assert!(done_event.is_done()); // Test the helper method - - // Test non-DONE event - assert!(!event.is_done()); - - // Test empty data - should return error - let empty_line = "data: "; - let empty_result: Result = empty_line.parse(); - assert!(empty_result.is_err()); - - // Test non-data line - should return error - let comment_line = ": this is a comment"; - let comment_result: Result = comment_line.parse(); - assert!(comment_result.is_err()); - } - - #[test] - fn test_sse_event_serde() { - // Test serialization and deserialization with serde - let event = SseEvent { - data: Some(r#"{"id":"test","object":"chat.completion.chunk"}"#.to_string()), - event: None, - raw_line: r#"data: {"id":"test","object":"chat.completion.chunk"} - - "# - .to_string(), - sse_transform_buffer: r#"data: {"id":"test","object":"chat.completion.chunk"} - - "# - .to_string(), - provider_stream_response: None, - }; - - // Test JSON serialization - raw_line should be skipped - let json = serde_json::to_string(&event).unwrap(); - assert!(json.contains("test")); - assert!(json.contains("chat.completion.chunk")); - assert!(!json.contains("raw_line")); // Should be excluded from serialization - - // Test JSON deserialization - let deserialized: SseEvent = serde_json::from_str(&json).unwrap(); - assert_eq!(deserialized.data, event.data); - assert_eq!(deserialized.raw_line, ""); // Should be empty since it's skipped - - // Test round trip for data field only - assert_eq!(event.data, deserialized.data); - } - - #[test] - fn test_sse_event_should_skip() { - // Test ping message should be skipped - let ping_event = SseEvent { - data: Some(r#"{"type": "ping"}"#.to_string()), - event: None, - raw_line: r#"data: {"type": "ping"}"#.to_string(), - sse_transform_buffer: r#"data: {"type": "ping"}"#.to_string(), - provider_stream_response: None, - }; - assert!(ping_event.should_skip()); - assert!(!ping_event.is_done()); - - // Test normal event should not be skipped - let normal_event = SseEvent { - data: Some(r#"{"id": "test", "object": "chat.completion.chunk"}"#.to_string()), - event: Some("content_block_delta".to_string()), - raw_line: r#"data: {"id": "test", "object": "chat.completion.chunk"}"#.to_string(), - sse_transform_buffer: r#"data: {"id": "test", "object": "chat.completion.chunk"}"# - .to_string(), - provider_stream_response: None, - }; - assert!(!normal_event.should_skip()); - assert!(!normal_event.is_done()); - - // Test [DONE] event should not be skipped (but is handled separately) - let done_event = SseEvent { - data: Some("[DONE]".to_string()), - event: None, - raw_line: "data: [DONE]".to_string(), - sse_transform_buffer: "data: [DONE]".to_string(), - provider_stream_response: None, - }; - assert!(!done_event.should_skip()); - assert!(done_event.is_done()); - } - - #[test] - fn test_sse_stream_iter_filters_ping_messages() { - // Create test data with ping messages mixed in - let test_lines = vec![ - "data: {\"id\": \"msg1\", \"object\": \"chat.completion.chunk\"}".to_string(), - "data: {\"type\": \"ping\"}".to_string(), // This should be filtered out - "data: {\"id\": \"msg2\", \"object\": \"chat.completion.chunk\"}".to_string(), - "data: {\"type\": \"ping\"}".to_string(), // This should be filtered out - "data: [DONE]".to_string(), // This should end the stream - ]; - - let mut iter = SseStreamIter::new(test_lines.into_iter()); - - // First event should be msg1 (ping filtered out) - let event1 = iter.next().unwrap(); - assert!(event1.data.as_ref().unwrap().contains("msg1")); - assert!(!event1.should_skip()); - - // Second event should be msg2 (ping filtered out) - let event2 = iter.next().unwrap(); - assert!(event2.data.as_ref().unwrap().contains("msg2")); - assert!(!event2.should_skip()); - - // Third event should be [DONE] - let done_event = iter.next().unwrap(); - assert!(done_event.is_done()); - - // Iterator should end after [DONE] - assert!(iter.next().is_none()); - } - - #[test] - fn test_sse_stream_iter_handles_anthropic_events() { - // Create test data with Anthropic-style event/data pairs - let test_lines = vec![ - "event: message_start".to_string(), - "data: {\"type\":\"message_start\",\"message\":{\"id\":\"msg_123\"}}".to_string(), - "event: content_block_delta".to_string(), - "data: {\"type\":\"content_block_delta\",\"delta\":{\"text\":\"Hello\"}}".to_string(), - "data: [DONE]".to_string(), - ]; - - let mut iter = SseStreamIter::new(test_lines.into_iter()); - - // First event should be the event: line - let event1 = iter.next().unwrap(); - assert!(event1.is_event_only()); - assert_eq!(event1.event, Some("message_start".to_string())); - assert_eq!(event1.data, None); - - // Second event should be the data: line - let event2 = iter.next().unwrap(); - assert!(!event2.is_event_only()); - assert_eq!(event2.event, None); - assert!(event2.data.as_ref().unwrap().contains("message_start")); - - // Third event should be another event: line - let event3 = iter.next().unwrap(); - assert!(event3.is_event_only()); - assert_eq!(event3.event, Some("content_block_delta".to_string())); - - // Fourth event should be the content delta data - let event4 = iter.next().unwrap(); - assert!(!event4.is_event_only()); - assert!(event4.data.as_ref().unwrap().contains("Hello")); - - // Fifth event should be [DONE] - let done_event = iter.next().unwrap(); - assert!(done_event.is_done()); - - // Iterator should end after [DONE] - assert!(iter.next().is_none()); - } - - #[test] - fn test_provider_stream_response_event_type() { - use crate::apis::anthropic::{MessagesContentDelta, MessagesStreamEvent}; - use crate::apis::openai::ChatCompletionsStreamResponse; - - // Test Anthropic event type - let anthropic_event = MessagesStreamEvent::ContentBlockDelta { - index: 0, - delta: MessagesContentDelta::TextDelta { - text: "Hello".to_string(), - }, - }; - let provider_type = ProviderStreamResponseType::MessagesStreamEvent(anthropic_event); - assert_eq!(provider_type.event_type(), Some("content_block_delta")); - - // Test OpenAI event type (should be None) - let openai_event = ChatCompletionsStreamResponse { - id: "test".to_string(), - object: Some("chat.completion.chunk".to_string()), - created: 123456789, - model: "gpt-4".to_string(), - choices: vec![], - usage: None, - system_fingerprint: None, - service_tier: None, - }; - let provider_type = ProviderStreamResponseType::ChatCompletionsStreamResponse(openai_event); - assert_eq!(provider_type.event_type(), None); - } - - #[test] - fn test_done_marker_handled_in_stream_response_transformation() { - use crate::apis::anthropic::AnthropicApi; - - // Test that [DONE] marker is properly converted to MessageStop in the transformation layer - let done_bytes = b"[DONE]"; - let client_api = SupportedAPIs::AnthropicMessagesAPI(AnthropicApi::Messages); - let upstream_api = SupportedUpstreamAPIs::OpenAIChatCompletions( - crate::apis::openai::OpenAIApi::ChatCompletions, - ); - - let result = ProviderStreamResponseType::try_from(( - done_bytes.as_slice(), - &client_api, - &upstream_api, - )); - assert!(result.is_ok()); - - if let Ok(ProviderStreamResponseType::MessagesStreamEvent(event)) = result { - // Verify it's a MessageStop event - assert_eq!(event.event_type(), Some("message_stop")); - assert!(matches!( - event, - crate::apis::anthropic::MessagesStreamEvent::MessageStop - )); - } else { - panic!("Expected MessagesStreamEvent::MessageStop"); - } - } - - #[test] - fn test_bedrock_event_stream_decoder_basic() { - use bytes::BytesMut; - - // Create a simple test with minimal data - let mut buffer = BytesMut::new(); - - // Add some arbitrary bytes (not a real event-stream frame, just for testing the decoder) - buffer.extend_from_slice(b"test data"); - - let mut decoder = BedrockBinaryFrameDecoder::new(&mut buffer); - - // The decoder should return Incomplete for incomplete/invalid data - // This signals the caller to wait for more data - let result = decoder.decode_frame(); - assert!(result.is_some()); - assert!(matches!( - result.unwrap(), - aws_smithy_eventstream::frame::DecodedFrame::Incomplete - )); - - // Verify we can still access the buffer - assert!(decoder.has_remaining()); - } - - #[test] - fn test_bedrock_event_stream_decoder_with_real_frames() { - use bytes::BytesMut; - use std::fs; - use std::path::PathBuf; - - // Read the actual response.hex file from tests/e2e directory - let test_file = - PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../tests/e2e/response.hex"); - - // Only run this test if the file exists - if !test_file.exists() { - println!("Skipping test - response.hex not found"); - return; - } - - let response_data = fs::read(&test_file).unwrap(); - let mut buffer = BytesMut::from(&response_data[..]); - - let mut decoder = BedrockBinaryFrameDecoder::new(&mut buffer); - let mut frame_count = 0; - - // Decode all frames - loop { - match decoder.decode_frame() { - Some(aws_smithy_eventstream::frame::DecodedFrame::Complete(message)) => { - frame_count += 1; - - // Verify we can access headers - let event_type = message - .headers() - .iter() - .find(|h| h.name().as_str() == ":event-type") - .and_then(|h| h.value().as_string().ok()); - - assert!(event_type.is_some(), "Frame should have :event-type header"); - } - Some(aws_smithy_eventstream::frame::DecodedFrame::Incomplete) => { - // End of buffer, no more complete frames available - break; - } - None => { - // Decode error - panic!("Decode error encountered"); - } - } - } - - // We should have decoded multiple frames - assert!(frame_count > 0, "Should have decoded at least one frame"); - } - - #[test] - fn test_bedrock_event_stream_decoder_chunked_data() { - use bytes::BytesMut; - use std::fs; - use std::path::PathBuf; - - // Read the actual response.hex file - let test_file = - PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../tests/e2e/response.hex"); - - if !test_file.exists() { - println!("Skipping test - response.hex not found"); - return; - } - - let response_data = fs::read(&test_file).unwrap(); - - // Simulate chunked network arrivals with realistic chunk sizes - // Using varying chunk sizes to test partial frame handling - let mut buffer = BytesMut::new(); - let chunk_size_pattern = vec![500, 1000, 750, 1200, 800, 1500]; - let mut offset = 0; - let mut total_frames = 0; - let mut chunk_num = 0; - - // CRITICAL: Create ONE decoder and reuse it across chunks - // The MessageFrameDecoder maintains state about partial frames - let mut decoder = BedrockBinaryFrameDecoder::new(&mut buffer); - - // Process all data in chunks - while offset < response_data.len() { - let chunk_size = chunk_size_pattern[chunk_num % chunk_size_pattern.len()]; - chunk_num += 1; - - let end = (offset + chunk_size).min(response_data.len()); - let chunk = &response_data[offset..end]; - - // Add new data to the buffer (accessing via buffer_mut()) - decoder.buffer_mut().extend_from_slice(chunk); - offset = end; - - // Process all available complete frames from this chunk - loop { - match decoder.decode_frame() { - Some(aws_smithy_eventstream::frame::DecodedFrame::Complete(_)) => { - total_frames += 1; - } - Some(aws_smithy_eventstream::frame::DecodedFrame::Incomplete) => { - // Need more data - wait for next chunk - break; - } - None => { - // Decode error - panic!("Decode error in chunked test"); - } - } - } - } - - assert!( - total_frames > 0, - "Should have decoded frames from chunked data" - ); - } - - #[test] - fn test_bedrock_decoded_frame_to_provider_response() { - test_bedrock_conversion(false); - } - - #[test] - #[ignore] // Run with: cargo test -- --ignored --nocapture - fn test_bedrock_decoded_frame_to_provider_response_verbose() { - test_bedrock_conversion(true); - } - - #[test] - fn test_bedrock_decoded_frame_with_tool_use() { - test_bedrock_conversion_with_tools(false); - } - - #[test] - #[ignore] // Run with: cargo test -- --ignored --nocapture - fn test_bedrock_decoded_frame_with_tool_use_verbose() { - test_bedrock_conversion_with_tools(true); - } - - fn test_bedrock_conversion(verbose: bool) { - use bytes::BytesMut; - use std::fs; - use std::path::PathBuf; - - // Read the actual response.hex file from tests/e2e directory - let test_file = - PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../tests/e2e/response.hex"); - - // Only run this test if the file exists - if !test_file.exists() { - println!("Skipping test - response.hex not found"); - return; - } - - let response_data = fs::read(&test_file).unwrap(); - let mut buffer = BytesMut::from(&response_data[..]); - - let mut decoder = BedrockBinaryFrameDecoder::new(&mut buffer); - - let client_api = - SupportedAPIs::AnthropicMessagesAPI(crate::apis::anthropic::AnthropicApi::Messages); - let upstream_api = SupportedUpstreamAPIs::AmazonBedrockConverseStream( - crate::apis::amazon_bedrock::AmazonBedrockApi::ConverseStream, - ); - - let mut conversion_count = 0; - let mut message_start_seen = false; - - // Decode and convert frames - loop { - match decoder.decode_frame() { - Some(frame @ aws_smithy_eventstream::frame::DecodedFrame::Complete(_)) => { - // Convert DecodedFrame to ProviderStreamResponseType - let result = - ProviderStreamResponseType::try_from((&frame, &client_api, &upstream_api)); - - match result { - Ok(provider_response) => { - conversion_count += 1; - - // Verify we got a MessagesStreamEvent - assert!(matches!( - provider_response, - ProviderStreamResponseType::MessagesStreamEvent(_) - )); - - if verbose { - // Print the SSE string output - let sse_string: String = provider_response.clone().into(); - println!("{}", sse_string); - } - - // Check for MessageStart event - if let ProviderStreamResponseType::MessagesStreamEvent(ref event) = - provider_response - { - if matches!( - event, - crate::apis::anthropic::MessagesStreamEvent::MessageStart { .. } - ) { - message_start_seen = true; - } - } - } - Err(e) => { - println!("Conversion error (frame {}): {}", conversion_count, e); - } - } - } - Some(aws_smithy_eventstream::frame::DecodedFrame::Incomplete) => { - // End of buffer - break; - } - None => { - panic!("Decode error"); - } - } - } - - assert!( - conversion_count > 0, - "Should have converted at least one frame" - ); - assert!(message_start_seen, "Should have seen MessageStart event"); - } - - fn test_bedrock_conversion_with_tools(verbose: bool) { - use bytes::BytesMut; - use std::fs; - use std::path::PathBuf; - - // Read the actual response_with_tools.hex file from tests/e2e directory - let test_file = PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("../../tests/e2e/response_with_tools.hex"); - - // Only run this test if the file exists - if !test_file.exists() { - println!("Skipping test - response_with_tools.hex not found"); - return; - } - - let response_data = fs::read(&test_file).unwrap(); - let mut buffer = BytesMut::from(&response_data[..]); - - let mut decoder = BedrockBinaryFrameDecoder::new(&mut buffer); - - let client_api = - SupportedAPIs::AnthropicMessagesAPI(crate::apis::anthropic::AnthropicApi::Messages); - let upstream_api = SupportedUpstreamAPIs::AmazonBedrockConverseStream( - crate::apis::amazon_bedrock::AmazonBedrockApi::ConverseStream, - ); - - let mut conversion_count = 0; - let mut message_start_seen = false; - let mut content_block_start_seen = false; - let mut content_block_delta_tool_use_seen = false; - - // Decode and convert frames - loop { - match decoder.decode_frame() { - Some(frame @ aws_smithy_eventstream::frame::DecodedFrame::Complete(_)) => { - // Convert DecodedFrame to ProviderStreamResponseType - let result = - ProviderStreamResponseType::try_from((&frame, &client_api, &upstream_api)); - - match result { - Ok(provider_response) => { - conversion_count += 1; - - // Verify we got a MessagesStreamEvent - assert!(matches!( - provider_response, - ProviderStreamResponseType::MessagesStreamEvent(_) - )); - - if verbose { - // Print the SSE string output - let sse_string: String = provider_response.clone().into(); - println!("{}", sse_string); - } - - // Check for specific events related to tool use - if let ProviderStreamResponseType::MessagesStreamEvent(ref event) = - provider_response - { - match event { - crate::apis::anthropic::MessagesStreamEvent::MessageStart { .. } => { - message_start_seen = true; - } - crate::apis::anthropic::MessagesStreamEvent::ContentBlockStart { .. } => { - content_block_start_seen = true; - } - crate::apis::anthropic::MessagesStreamEvent::ContentBlockDelta { delta, .. } => { - if matches!(delta, crate::apis::anthropic::MessagesContentDelta::InputJsonDelta { .. }) { - content_block_delta_tool_use_seen = true; - } - } - _ => {} - } - } - } - Err(e) => { - println!("Conversion error (frame {}): {}", conversion_count, e); - } - } - } - Some(aws_smithy_eventstream::frame::DecodedFrame::Incomplete) => { - // End of buffer - break; - } - None => { - panic!("Decode error"); - } - } - } - - assert!( - conversion_count > 0, - "Should have converted at least one frame" - ); - assert!(message_start_seen, "Should have seen MessageStart event"); - assert!( - content_block_start_seen, - "Should have seen ContentBlockStart event for tool use" - ); - assert!( - content_block_delta_tool_use_seen, - "Should have seen ContentBlockDelta with ToolUseDelta" - ); - } - - #[test] - fn test_sse_event_transformation_openai_to_anthropic_message_start() { - use crate::apis::anthropic::AnthropicApi; - use crate::apis::openai::OpenAIApi; - - // Create an OpenAI stream response that represents a role start (which becomes message_start in Anthropic) - let openai_stream_chunk = json!({ - "id": "chatcmpl-123", - "object": "chat.completion.chunk", - "created": 1234567890, - "model": "gpt-4", - "choices": [{ - "index": 0, - "delta": {"role": "assistant"}, - "finish_reason": null - }] - }); - - // Create SSE event with this data - let sse_event = SseEvent { - data: Some(openai_stream_chunk.to_string()), - event: None, - raw_line: format!("data: {}", openai_stream_chunk.to_string()), - sse_transform_buffer: format!("data: {}", openai_stream_chunk.to_string()), - provider_stream_response: None, - }; - - let client_api = SupportedAPIs::AnthropicMessagesAPI(AnthropicApi::Messages); - let upstream_api = SupportedUpstreamAPIs::OpenAIChatCompletions(OpenAIApi::ChatCompletions); - - // Transform the event - let result = SseEvent::try_from((sse_event, &client_api, &upstream_api)); - assert!(result.is_ok()); - - let transformed = result.unwrap(); - - // Verify the transformation includes both message_start and content_block_start - let buffer = transformed.sse_transform_buffer; - assert!( - buffer.contains("event: message_start"), - "Should contain message_start event" - ); - assert!( - buffer.contains("event: content_block_start"), - "Should contain content_block_start event" - ); - - // Verify proper SSE format with event lines before data lines - assert!(buffer.find("event: message_start").unwrap() < buffer.find("data:").unwrap()); - assert!(buffer.find("content_block_start").is_some()); - } - - #[test] - fn test_sse_event_transformation_openai_to_anthropic_message_delta() { - use crate::apis::anthropic::AnthropicApi; - use crate::apis::openai::OpenAIApi; - - // Create an OpenAI stream response with finish_reason (which becomes message_delta in Anthropic) - let openai_stream_chunk = json!({ - "id": "chatcmpl-123", - "object": "chat.completion.chunk", - "created": 1234567890, - "model": "gpt-4", - "choices": [{ - "index": 0, - "delta": {}, - "finish_reason": "stop" - }], - "usage": { - "prompt_tokens": 10, - "completion_tokens": 25, - "total_tokens": 35 - } - }); - - // Create SSE event with this data - let sse_event = SseEvent { - data: Some(openai_stream_chunk.to_string()), - event: None, - raw_line: format!("data: {}", openai_stream_chunk.to_string()), - sse_transform_buffer: format!("data: {}", openai_stream_chunk.to_string()), - provider_stream_response: None, - }; - - let client_api = SupportedAPIs::AnthropicMessagesAPI(AnthropicApi::Messages); - let upstream_api = SupportedUpstreamAPIs::OpenAIChatCompletions(OpenAIApi::ChatCompletions); - - // Transform the event - let result = SseEvent::try_from((sse_event, &client_api, &upstream_api)); - assert!(result.is_ok()); - - let transformed = result.unwrap(); - - // Verify the transformation includes both content_block_stop and message_delta - let buffer = transformed.sse_transform_buffer; - assert!( - buffer.contains("event: content_block_stop"), - "Should contain content_block_stop event" - ); - assert!( - buffer.contains("event: message_delta"), - "Should contain message_delta event" - ); - - // Verify content_block_stop comes before message_delta - let stop_pos = buffer.find("content_block_stop").unwrap(); - let delta_pos = buffer.find("message_delta").unwrap(); - assert!( - stop_pos < delta_pos, - "content_block_stop should come before message_delta" - ); - } - - #[test] - fn test_sse_event_transformation_openai_to_anthropic_content_delta() { - use crate::apis::anthropic::AnthropicApi; - use crate::apis::openai::OpenAIApi; - - // Create an OpenAI stream response with content (which becomes content_block_delta in Anthropic) - let openai_stream_chunk = json!({ - "id": "chatcmpl-123", - "object": "chat.completion.chunk", - "created": 1234567890, - "model": "gpt-4", - "choices": [{ - "index": 0, - "delta": {"content": "Hello"}, - "finish_reason": null - }] - }); - - // Create SSE event with this data - let sse_event = SseEvent { - data: Some(openai_stream_chunk.to_string()), - event: None, - raw_line: format!("data: {}", openai_stream_chunk.to_string()), - sse_transform_buffer: format!("data: {}", openai_stream_chunk.to_string()), - provider_stream_response: None, - }; - - let client_api = SupportedAPIs::AnthropicMessagesAPI(AnthropicApi::Messages); - let upstream_api = SupportedUpstreamAPIs::OpenAIChatCompletions(OpenAIApi::ChatCompletions); - - // Transform the event - let result = SseEvent::try_from((sse_event, &client_api, &upstream_api)); - assert!(result.is_ok()); - - let transformed = result.unwrap(); - - // Verify the transformation is a content_block_delta (no extra events injected) - let buffer = transformed.sse_transform_buffer; - assert!( - buffer.contains("event: content_block_delta"), - "Should contain content_block_delta event" - ); - assert!( - !buffer.contains("content_block_start"), - "Should not inject content_block_start for content delta" - ); - assert!( - !buffer.contains("content_block_stop"), - "Should not inject content_block_stop for content delta" - ); - - // Verify the content is preserved - assert!(buffer.contains("Hello"), "Should preserve the content text"); - } - - #[test] - fn test_sse_event_transformation_anthropic_to_openai_suppresses_event_lines() { - use crate::apis::anthropic::AnthropicApi; - use crate::apis::openai::OpenAIApi; - - // Create an Anthropic event-only SSE line (no data) - let sse_event = SseEvent { - data: None, - event: Some("message_start".to_string()), - raw_line: "event: message_start".to_string(), - sse_transform_buffer: "event: message_start".to_string(), - provider_stream_response: None, - }; - - let client_api = SupportedAPIs::OpenAIChatCompletions(OpenAIApi::ChatCompletions); - let upstream_api = SupportedUpstreamAPIs::AnthropicMessagesAPI(AnthropicApi::Messages); - - // Transform the event - let result = SseEvent::try_from((sse_event, &client_api, &upstream_api)); - assert!(result.is_ok()); - - let transformed = result.unwrap(); - - // Verify the event line is suppressed (replaced with just newline) - assert_eq!( - transformed.sse_transform_buffer, "\n", - "Event-only lines should be suppressed to newline for OpenAI" - ); - assert!( - transformed.is_event_only(), - "Should still be marked as event-only" - ); - } - - #[test] - fn test_sse_event_transformation_anthropic_to_openai_preserves_data() { - use crate::apis::anthropic::AnthropicApi; - use crate::apis::openai::OpenAIApi; - - // Create an Anthropic message_start event with data - let anthropic_event = json!({ - "type": "message_start", - "message": { - "id": "msg_123", - "type": "message", - "role": "assistant", - "content": [], - "model": "claude-3-sonnet", - "stop_reason": null, - "usage": {"input_tokens": 10, "output_tokens": 0} - } - }); - - let sse_event = SseEvent { - data: Some(anthropic_event.to_string()), - event: None, - raw_line: format!("data: {}", anthropic_event.to_string()), - sse_transform_buffer: format!("data: {}", anthropic_event.to_string()), - provider_stream_response: None, - }; - - let client_api = SupportedAPIs::OpenAIChatCompletions(OpenAIApi::ChatCompletions); - let upstream_api = SupportedUpstreamAPIs::AnthropicMessagesAPI(AnthropicApi::Messages); - - // Transform the event - let result = SseEvent::try_from((sse_event, &client_api, &upstream_api)); - assert!(result.is_ok()); - - let transformed = result.unwrap(); - - // Verify data is transformed to OpenAI format - let buffer = transformed.sse_transform_buffer; - assert!(buffer.starts_with("data: "), "Should have data: prefix"); - assert!( - !buffer.contains("event:"), - "Should not have event: lines for OpenAI" - ); - - // Verify provider response was parsed - assert!(transformed.provider_stream_response.is_some()); - } - - #[test] - fn test_sse_event_transformation_no_change_for_matching_apis() { - use crate::apis::openai::OpenAIApi; - - // Create an OpenAI stream response - let openai_stream_chunk = json!({ - "id": "chatcmpl-123", - "object": "chat.completion.chunk", - "created": 1234567890, - "model": "gpt-4", - "choices": [{ - "index": 0, - "delta": {"content": "Hello"}, - "finish_reason": null - }] - }); - - let original_data = openai_stream_chunk.to_string(); - let sse_event = SseEvent { - data: Some(original_data.clone()), - event: None, - raw_line: format!("data: {}", original_data), - sse_transform_buffer: format!("data: {}\n\n", original_data), - provider_stream_response: None, - }; - - let client_api = SupportedAPIs::OpenAIChatCompletions(OpenAIApi::ChatCompletions); - let upstream_api = SupportedUpstreamAPIs::OpenAIChatCompletions(OpenAIApi::ChatCompletions); - - // Transform the event - let result = SseEvent::try_from((sse_event, &client_api, &upstream_api)); - assert!(result.is_ok()); - - let transformed = result.unwrap(); - - // Verify minimal transformation - just SSE formatting, no API conversion - let buffer = transformed.sse_transform_buffer; - assert!(buffer.starts_with("data: "), "Should preserve data: prefix"); - assert!(!buffer.contains("event:"), "Should not add event: lines"); - - // Verify provider response was parsed - assert!(transformed.provider_stream_response.is_some()); - } - - #[test] - fn test_sse_event_transformation_preserves_provider_response() { - use crate::apis::anthropic::AnthropicApi; - use crate::apis::openai::OpenAIApi; - - // Create an OpenAI stream response - let openai_stream_chunk = json!({ - "id": "chatcmpl-123", - "object": "chat.completion.chunk", - "created": 1234567890, - "model": "gpt-4", - "choices": [{ - "index": 0, - "delta": {"content": "Test"}, - "finish_reason": null - }] - }); - - let sse_event = SseEvent { - data: Some(openai_stream_chunk.to_string()), - event: None, - raw_line: format!("data: {}", openai_stream_chunk.to_string()), - sse_transform_buffer: format!("data: {}", openai_stream_chunk.to_string()), - provider_stream_response: None, - }; - - let client_api = SupportedAPIs::AnthropicMessagesAPI(AnthropicApi::Messages); - let upstream_api = SupportedUpstreamAPIs::OpenAIChatCompletions(OpenAIApi::ChatCompletions); - - // Transform the event - let result = SseEvent::try_from((sse_event, &client_api, &upstream_api)); - assert!(result.is_ok()); - - let transformed = result.unwrap(); - - // Verify provider_stream_response is populated - assert!( - transformed.provider_stream_response.is_some(), - "Should parse and store provider response" - ); - - // Verify we can access the provider response - let provider_response = transformed.provider_response(); - assert!( - provider_response.is_ok(), - "Should be able to access provider response" - ); - - // Verify the content delta is accessible - let content = provider_response.unwrap().content_delta(); - assert_eq!(content, Some("Test"), "Should preserve content delta"); - } } diff --git a/crates/hermesllm/src/providers/streaming_response.rs b/crates/hermesllm/src/providers/streaming_response.rs new file mode 100644 index 00000000..55e52f3d --- /dev/null +++ b/crates/hermesllm/src/providers/streaming_response.rs @@ -0,0 +1,1348 @@ +use serde::Serialize; +use std::convert::TryFrom; + +use crate::apis::openai::ChatCompletionsStreamResponse; +use crate::apis::openai_responses::ResponsesAPIStreamEvent; +use crate::apis::streaming_shapes::sse::SseEvent; +use crate::apis::amazon_bedrock::ConverseStreamEvent; +use crate::apis::anthropic::MessagesStreamEvent; +use crate::apis::streaming_shapes::sse::SseStreamBuffer; +use crate::apis::streaming_shapes::{ + anthropic_streaming_buffer::AnthropicMessagesStreamBuffer, + chat_completions_streaming_buffer::OpenAIChatCompletionsStreamBuffer, + passthrough_streaming_buffer::PassthroughStreamBuffer, + responses_api_streaming_buffer::ResponsesAPIStreamBuffer, + }; + +use crate::clients::endpoints::SupportedAPIsFromClient; +use crate::clients::endpoints::SupportedUpstreamAPIs; + +// ============================================================================ +// SSE STREAM BUFFER FACTORY +// ============================================================================ + +/// Check if streaming buffering is needed based on client and upstream API combination. +pub fn needs_buffering( + client_api: &SupportedAPIsFromClient, + upstream_api: &SupportedUpstreamAPIs, +) -> bool { + match (client_api, upstream_api) { + // Same APIs - no buffering needed + (SupportedAPIsFromClient::OpenAIChatCompletions(_), SupportedUpstreamAPIs::OpenAIChatCompletions(_)) => false, + (SupportedAPIsFromClient::AnthropicMessagesAPI(_), SupportedUpstreamAPIs::AnthropicMessagesAPI(_)) => false, + (SupportedAPIsFromClient::OpenAIResponsesAPI(_), SupportedUpstreamAPIs::OpenAIResponsesAPI(_)) => false, + + // Different APIs - buffering needed + _ => true, + } +} + +/// Factory pattern for creating SSE stream buffers based on client and upstream API combination. +/// # Example +/// ```ignore +/// use hermesllm::clients::endpoints::{SupportedAPIsFromClient, SupportedUpstreamAPIs}; +/// use hermesllm::apis::streaming_shapes::sse::SseStreamBuffer; +/// +/// // Transformation needed: OpenAI upstream -> Anthropic client +/// let mut buffer = SseStreamBuffer::try_from((&client_api, &upstream_api))?; +/// +/// // Add transformed events +/// let transformed = SseEvent::try_from((raw_event, &client_api, &upstream_api))?; +/// buffer.add_transformed_event(transformed); +/// +/// // Flush to wire +/// let bytes = buffer.into_bytes(); +/// ``` +impl TryFrom<(&SupportedAPIsFromClient, &SupportedUpstreamAPIs)> + for SseStreamBuffer +{ + type Error = Box; + + fn try_from( + (client_api, upstream_api): (&SupportedAPIsFromClient, &SupportedUpstreamAPIs), + ) -> Result { + + // If APIs match, use passthrough - no buffering/transformation needed + if !needs_buffering(client_api, upstream_api) { + return Ok(SseStreamBuffer::Passthrough(PassthroughStreamBuffer::new())); + } + + // APIs differ - use appropriate buffer for client API + match client_api { + SupportedAPIsFromClient::OpenAIChatCompletions(_) => { + Ok(SseStreamBuffer::OpenAIChatCompletions(OpenAIChatCompletionsStreamBuffer::new())) + } + SupportedAPIsFromClient::AnthropicMessagesAPI(_) => { + Ok(SseStreamBuffer::AnthropicMessages(AnthropicMessagesStreamBuffer::new())) + } + SupportedAPIsFromClient::OpenAIResponsesAPI(_) => { + Ok(SseStreamBuffer::OpenAIResponses(ResponsesAPIStreamBuffer::new())) + } + } + } +} + +// ============================================================================ +// PROVIDER STREAM RESPONSE TYPES +// ============================================================================ + +#[derive(Serialize, Debug, Clone)] +#[serde(untagged)] +pub enum ProviderStreamResponseType { + ChatCompletionsStreamResponse(ChatCompletionsStreamResponse), + MessagesStreamEvent(MessagesStreamEvent), + ConverseStreamEvent(ConverseStreamEvent), + ResponseAPIStreamEvent(ResponsesAPIStreamEvent) +} + +pub trait ProviderStreamResponse: Send + Sync { + /// Get the content delta for this chunk + fn content_delta(&self) -> Option<&str>; + + /// Check if this is the final chunk in the stream + fn is_final(&self) -> bool; + + /// Get role information if available + fn role(&self) -> Option<&str>; + + /// Get event type for SSE streaming (used by Anthropic) + fn event_type(&self) -> Option<&str>; +} + +impl ProviderStreamResponse for ProviderStreamResponseType { + fn content_delta(&self) -> Option<&str> { + match self { + ProviderStreamResponseType::ChatCompletionsStreamResponse(resp) => resp.content_delta(), + ProviderStreamResponseType::MessagesStreamEvent(resp) => resp.content_delta(), + ProviderStreamResponseType::ConverseStreamEvent(resp) => resp.content_delta(), + ProviderStreamResponseType::ResponseAPIStreamEvent(_resp) => None, // ResponsesAPI does not have content deltas + } + } + + fn is_final(&self) -> bool { + match self { + ProviderStreamResponseType::ChatCompletionsStreamResponse(resp) => resp.is_final(), + ProviderStreamResponseType::MessagesStreamEvent(resp) => resp.is_final(), + ProviderStreamResponseType::ConverseStreamEvent(resp) => resp.is_final(), + ProviderStreamResponseType::ResponseAPIStreamEvent(resp) => resp.is_final(), + } + } + + fn role(&self) -> Option<&str> { + match self { + ProviderStreamResponseType::ChatCompletionsStreamResponse(resp) => resp.role(), + ProviderStreamResponseType::MessagesStreamEvent(resp) => resp.role(), + ProviderStreamResponseType::ConverseStreamEvent(resp) => resp.role(), + ProviderStreamResponseType::ResponseAPIStreamEvent(resp) => resp.role(), + } + } + + fn event_type(&self) -> Option<&str> { + match self { + ProviderStreamResponseType::ChatCompletionsStreamResponse(_resp) => None, // OpenAI doesn't use event types + ProviderStreamResponseType::MessagesStreamEvent(resp) => resp.event_type(), + ProviderStreamResponseType::ConverseStreamEvent(resp) => resp.event_type(), // Bedrock doesn't use event types + ProviderStreamResponseType::ResponseAPIStreamEvent(resp) => resp.event_type(), + } + } + +} + +impl Into for ProviderStreamResponseType { + fn into(self) -> String { + match self { + ProviderStreamResponseType::MessagesStreamEvent(event) => { + // Use the Into implementation for proper SSE formatting with event lines + event.into() + } + ProviderStreamResponseType::ConverseStreamEvent(event) => { + // Use the Into implementation for proper SSE formatting with event lines + event.into() + } + ProviderStreamResponseType::ResponseAPIStreamEvent(event) => { + // Use the Into implementation for proper SSE formatting with event lines + event.into() + } + ProviderStreamResponseType::ChatCompletionsStreamResponse(_) => { + // For OpenAI, use simple data line format + let json = serde_json::to_string(&self).unwrap_or_default(); + format!("data: {}\n\n", json) + } + } + } +} + + +// Stream response transformation logic for client API compatibility +impl TryFrom<(&[u8], &SupportedAPIsFromClient, &SupportedUpstreamAPIs)> for ProviderStreamResponseType { + type Error = Box; + + fn try_from( + (bytes, client_api, upstream_api): (&[u8], &SupportedAPIsFromClient, &SupportedUpstreamAPIs), + ) -> Result { + // Special case: Handle [DONE] marker for OpenAI -> Anthropic conversion + if bytes == b"[DONE]" && matches!(client_api, SupportedAPIsFromClient::AnthropicMessagesAPI(_)) { + return Ok(ProviderStreamResponseType::MessagesStreamEvent( + crate::apis::anthropic::MessagesStreamEvent::MessageStop, + )); + } + match (upstream_api, client_api) { + // OpenAI upstream + ( + SupportedUpstreamAPIs::OpenAIChatCompletions(_), + SupportedAPIsFromClient::OpenAIChatCompletions(_), + ) => { + let resp = serde_json::from_slice(bytes)?; + Ok(ProviderStreamResponseType::ChatCompletionsStreamResponse( + resp, + )) + } + ( + SupportedUpstreamAPIs::OpenAIChatCompletions(_), + SupportedAPIsFromClient::AnthropicMessagesAPI(_), + ) => { + let openai_resp: crate::apis::openai::ChatCompletionsStreamResponse = + serde_json::from_slice(bytes)?; + let anthropic_resp = openai_resp.try_into()?; + Ok(ProviderStreamResponseType::MessagesStreamEvent( + anthropic_resp, + )) + } + ( + SupportedUpstreamAPIs::OpenAIChatCompletions(_), + SupportedAPIsFromClient::OpenAIResponsesAPI(_), + ) => { + let openai_resp: crate::apis::openai::ChatCompletionsStreamResponse = + serde_json::from_slice(bytes)?; + let responses_resp = openai_resp.try_into()?; + Ok(ProviderStreamResponseType::ResponseAPIStreamEvent( + responses_resp, + )) + } + + // OpenAI ResponsesAPI upstream + ( + SupportedUpstreamAPIs::OpenAIResponsesAPI(_), + SupportedAPIsFromClient::OpenAIResponsesAPI(_), + ) => { + let resp = serde_json::from_slice(bytes)?; + Ok(ProviderStreamResponseType::ResponseAPIStreamEvent(resp)) + } + // Anthropic upstream + ( + SupportedUpstreamAPIs::AnthropicMessagesAPI(_), + SupportedAPIsFromClient::AnthropicMessagesAPI(_), + ) => { + let resp = serde_json::from_slice(bytes)?; + Ok(ProviderStreamResponseType::MessagesStreamEvent(resp)) + } + ( + SupportedUpstreamAPIs::AnthropicMessagesAPI(_), + SupportedAPIsFromClient::OpenAIChatCompletions(_), + ) => { + let anthropic_resp: crate::apis::anthropic::MessagesStreamEvent = + serde_json::from_slice(bytes)?; + let openai_resp = anthropic_resp.try_into()?; + Ok(ProviderStreamResponseType::ChatCompletionsStreamResponse( + openai_resp, + )) + } + + // Amazon Bedrock ConverseStream upstream + ( + SupportedUpstreamAPIs::AmazonBedrockConverseStream(_), + SupportedAPIsFromClient::AnthropicMessagesAPI(_), + ) => { + let bedrock_resp: crate::apis::amazon_bedrock::ConverseStreamEvent = + serde_json::from_slice(bytes)?; + let anthropic_resp = bedrock_resp.try_into()?; + Ok(ProviderStreamResponseType::MessagesStreamEvent( + anthropic_resp, + )) + } + ( + SupportedUpstreamAPIs::AmazonBedrockConverseStream(_), + SupportedAPIsFromClient::OpenAIResponsesAPI(_), + ) => { + // Chain: Bedrock -> ChatCompletions -> ResponsesAPI + let bedrock_resp: crate::apis::amazon_bedrock::ConverseStreamEvent = + serde_json::from_slice(bytes)?; + let chat_resp: crate::apis::openai::ChatCompletionsStreamResponse = bedrock_resp.try_into()?; + let responses_resp = chat_resp.try_into()?; + Ok(ProviderStreamResponseType::ResponseAPIStreamEvent( + responses_resp, + )) + } + _ => Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + "Unsupported API combination for response transformation", + ) + .into()), + } + } +} + +// TryFrom implementation to convert raw bytes to SseEvent with parsed provider response +impl TryFrom<(SseEvent, &SupportedAPIsFromClient, &SupportedUpstreamAPIs)> for SseEvent { + type Error = Box; + + fn try_from( + (sse_event, client_api, upstream_api): (SseEvent, &SupportedAPIsFromClient, &SupportedUpstreamAPIs), + ) -> Result { + // Create a new transformed event based on the original + let mut transformed_event = sse_event; + + // Handle [DONE] marker early - don't try to parse as JSON + if transformed_event.is_done() { + // For OpenAI client APIs (ChatCompletions and ResponsesAPI), keep [DONE] as-is + // For Anthropic client API, it will be transformed via ProviderStreamResponseType + if matches!(client_api, SupportedAPIsFromClient::OpenAIChatCompletions(_) | SupportedAPIsFromClient::OpenAIResponsesAPI(_)) { + // Keep the [DONE] marker as-is for OpenAI clients + transformed_event.sse_transformed_lines = "data: [DONE]".to_string(); + return Ok(transformed_event); + } + } + + // If has data, parse the data as a provider stream response (business logic layer) + if transformed_event.data.is_some() { + let data_str = transformed_event.data.as_ref().unwrap(); + let data_bytes = data_str.as_bytes(); + let transformed_response: ProviderStreamResponseType = + ProviderStreamResponseType::try_from((data_bytes, client_api, upstream_api))?; + + // Convert to SSE string explicitly to avoid type ambiguity + let sse_string: String = transformed_response.clone().into(); + transformed_event.sse_transformed_lines = sse_string; + transformed_event.provider_stream_response = Some(transformed_response); + } + + // Apply wire format adjustments for cross-API transformations + // Note: When APIs match (passthrough mode), these adjustments are skipped + // since PassthroughStreamBuffer will handle events as-is + if needs_buffering(client_api, upstream_api) { + match (client_api, upstream_api) { + ( + SupportedAPIsFromClient::OpenAIChatCompletions(_), + SupportedUpstreamAPIs::AnthropicMessagesAPI(_), + ) => { + // OpenAI clients don't expect separate event: lines + // Suppress upstream Anthropic event-only lines + if transformed_event.is_event_only() && transformed_event.event.is_some() { + transformed_event.sse_transformed_lines = format!("\n"); + } + } + _ => { + // Other cross-API combinations can be handled here as needed + } + } + } else { + // Passthrough mode: APIs match, no transformation needed + // For Anthropic and ResponsesAPI SSE formats, event-only lines are redundant because + // the Into implementation for MessagesStreamEvent and ResponsesAPIStreamEvent + // couples event and data lines together. We suppress event-only events to + // avoid duplicate event: lines in the output. + match (client_api, upstream_api) { + ( + SupportedAPIsFromClient::AnthropicMessagesAPI(_), + SupportedUpstreamAPIs::AnthropicMessagesAPI(_), + ) | ( + SupportedAPIsFromClient::OpenAIResponsesAPI(_), + SupportedUpstreamAPIs::OpenAIResponsesAPI(_), + ) => { + if transformed_event.is_event_only() && transformed_event.event.is_some() { + // Mark as should-skip by clearing sse_transformed_lines + // The event line is already included when the data line is transformed + transformed_event.sse_transformed_lines = String::new(); + } + } + _ => { + // Other passthrough combinations (OpenAI ChatCompletions, etc.) don't have this issue + } + } + } + + Ok(transformed_event) + } +} + +// TryFrom implementation to convert AWS Event Stream DecodedFrame to ProviderStreamResponseType +impl + TryFrom<( + &aws_smithy_eventstream::frame::DecodedFrame, + &SupportedAPIsFromClient, + &SupportedUpstreamAPIs, + )> for ProviderStreamResponseType +{ + type Error = Box; + + fn try_from( + (frame, client_api, upstream_api): ( + &aws_smithy_eventstream::frame::DecodedFrame, + &SupportedAPIsFromClient, + &SupportedUpstreamAPIs, + ), + ) -> Result { + use aws_smithy_eventstream::frame::DecodedFrame; + + match frame { + DecodedFrame::Complete(_) => { + // We have a complete frame - parse it based on upstream API + match (upstream_api, client_api) { + ( + SupportedUpstreamAPIs::AmazonBedrockConverseStream(_), + SupportedAPIsFromClient::AnthropicMessagesAPI(_), + ) => { + // Parse the DecodedFrame into ConverseStreamEvent + let bedrock_event = + crate::apis::amazon_bedrock::ConverseStreamEvent::try_from(frame)?; + let anthropic_event: crate::apis::anthropic::MessagesStreamEvent = + bedrock_event.try_into()?; + + Ok(ProviderStreamResponseType::MessagesStreamEvent( + anthropic_event, + )) + } + ( + SupportedUpstreamAPIs::AmazonBedrockConverseStream(_), + SupportedAPIsFromClient::OpenAIChatCompletions(_), + ) => { + // Parse the DecodedFrame into ConverseStreamEvent + let bedrock_event = + crate::apis::amazon_bedrock::ConverseStreamEvent::try_from(frame)?; + let openai_event: crate::apis::openai::ChatCompletionsStreamResponse = + bedrock_event.try_into()?; + Ok(ProviderStreamResponseType::ChatCompletionsStreamResponse( + openai_event, + )) + } + ( + SupportedUpstreamAPIs::AmazonBedrockConverseStream(_), + SupportedAPIsFromClient::OpenAIResponsesAPI(_), + ) => { + // Parse the DecodedFrame into ConverseStreamEvent + let bedrock_event = + crate::apis::amazon_bedrock::ConverseStreamEvent::try_from(frame)?; + let openai_chat_completions_event: crate::apis::openai::ChatCompletionsStreamResponse = + bedrock_event.try_into()?; + let openai_responses_api_event: crate::apis::openai_responses::ResponsesAPIStreamEvent = + openai_chat_completions_event.try_into()?; + + Ok(ProviderStreamResponseType::ResponseAPIStreamEvent( + openai_responses_api_event, + )) + } + _ => Err("Unsupported API combination for event-stream decoding".into()), + } + } + DecodedFrame::Incomplete => { + Err("Cannot convert incomplete frame to provider response".into()) + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::apis::streaming_shapes::amazon_bedrock_binary_frame::BedrockBinaryFrameDecoder; + use crate::clients::endpoints::SupportedAPIsFromClient; + use crate::apis::streaming_shapes::sse::SseStreamIter; + use serde_json::json; + + #[test] + fn test_sse_event_parsing() { + // Test valid SSE data line + let line = "data: {\"id\":\"test\",\"object\":\"chat.completion.chunk\"}\n\n"; + let event: Result = line.parse(); + assert!(event.is_ok()); + let event = event.unwrap(); + // The data field should contain only the JSON content, not the trailing newlines + assert_eq!( + event.data, + Some("{\"id\":\"test\",\"object\":\"chat.completion.chunk\"}".to_string()) + ); + + // Test conversion back to line using Display trait + // The sse_transformed_lines preserves the original format including trailing newlines + let wire_format = event.to_string(); + assert_eq!( + wire_format, + "data: {\"id\":\"test\",\"object\":\"chat.completion.chunk\"}\n\n" + ); + + // Test [DONE] marker - should be valid SSE event + let done_line = "data: [DONE]"; + let done_result: Result = done_line.parse(); + assert!(done_result.is_ok()); + let done_event = done_result.unwrap(); + assert_eq!(done_event.data, Some("[DONE]".to_string())); + assert!(done_event.is_done()); // Test the helper method + + // Test non-DONE event + assert!(!event.is_done()); + + // Test empty data - should return error + let empty_line = "data: "; + let empty_result: Result = empty_line.parse(); + assert!(empty_result.is_err()); + + // Test non-data line - should return error + let comment_line = ": this is a comment"; + let comment_result: Result = comment_line.parse(); + assert!(comment_result.is_err()); + } + + #[test] + fn test_sse_event_serde() { + // Test serialization and deserialization with serde + let event = SseEvent { + data: Some(r#"{"id":"test","object":"chat.completion.chunk"}"#.to_string()), + event: None, + raw_line: r#"data: {"id":"test","object":"chat.completion.chunk"} + + "# + .to_string(), + sse_transformed_lines: r#"data: {"id":"test","object":"chat.completion.chunk"} + + "# + .to_string(), + provider_stream_response: None, + }; + + // Test JSON serialization - raw_line should be skipped + let json = serde_json::to_string(&event).unwrap(); + assert!(json.contains("test")); + assert!(json.contains("chat.completion.chunk")); + assert!(!json.contains("raw_line")); // Should be excluded from serialization + + // Test JSON deserialization + let deserialized: SseEvent = serde_json::from_str(&json).unwrap(); + assert_eq!(deserialized.data, event.data); + assert_eq!(deserialized.raw_line, ""); // Should be empty since it's skipped + + // Test round trip for data field only + assert_eq!(event.data, deserialized.data); + } + + #[test] + fn test_sse_event_should_skip() { + // Test ping message should be skipped + let ping_event = SseEvent { + data: Some(r#"{"type": "ping"}"#.to_string()), + event: None, + raw_line: r#"data: {"type": "ping"}"#.to_string(), + sse_transformed_lines: r#"data: {"type": "ping"}"#.to_string(), + provider_stream_response: None, + }; + assert!(ping_event.should_skip()); + assert!(!ping_event.is_done()); + + // Test normal event should not be skipped + let normal_event = SseEvent { + data: Some(r#"{"id": "test", "object": "chat.completion.chunk"}"#.to_string()), + event: Some("content_block_delta".to_string()), + raw_line: r#"data: {"id": "test", "object": "chat.completion.chunk"}"#.to_string(), + sse_transformed_lines: r#"data: {"id": "test", "object": "chat.completion.chunk"}"# + .to_string(), + provider_stream_response: None, + }; + assert!(!normal_event.should_skip()); + assert!(!normal_event.is_done()); + + // Test [DONE] event should not be skipped (but is handled separately) + let done_event = SseEvent { + data: Some("[DONE]".to_string()), + event: None, + raw_line: "data: [DONE]".to_string(), + sse_transformed_lines: "data: [DONE]".to_string(), + provider_stream_response: None, + }; + assert!(!done_event.should_skip()); + assert!(done_event.is_done()); + } + + #[test] + fn test_sse_stream_iter_filters_ping_messages() { + // Create test data with ping messages mixed in + let test_lines = vec![ + "data: {\"id\": \"msg1\", \"object\": \"chat.completion.chunk\"}".to_string(), + "data: {\"type\": \"ping\"}".to_string(), // This should be filtered out + "data: {\"id\": \"msg2\", \"object\": \"chat.completion.chunk\"}".to_string(), + "data: {\"type\": \"ping\"}".to_string(), // This should be filtered out + "data: [DONE]".to_string(), // This should end the stream + ]; + + let mut iter = SseStreamIter::new(test_lines.into_iter()); + + // First event should be msg1 (ping filtered out) + let event1 = iter.next().unwrap(); + assert!(event1.data.as_ref().unwrap().contains("msg1")); + assert!(!event1.should_skip()); + + // Second event should be msg2 (ping filtered out) + let event2 = iter.next().unwrap(); + assert!(event2.data.as_ref().unwrap().contains("msg2")); + assert!(!event2.should_skip()); + + // Third event should be [DONE] + let done_event = iter.next().unwrap(); + assert!(done_event.is_done()); + + // Iterator should end after [DONE] + assert!(iter.next().is_none()); + } + + #[test] + fn test_sse_stream_iter_handles_anthropic_events() { + // Create test data with Anthropic-style event/data pairs + let test_lines = vec![ + "event: message_start".to_string(), + "data: {\"type\":\"message_start\",\"message\":{\"id\":\"msg_123\"}}".to_string(), + "event: content_block_delta".to_string(), + "data: {\"type\":\"content_block_delta\",\"delta\":{\"text\":\"Hello\"}}".to_string(), + "data: [DONE]".to_string(), + ]; + + let mut iter = SseStreamIter::new(test_lines.into_iter()); + + // First event should be the event: line + let event1 = iter.next().unwrap(); + assert!(event1.is_event_only()); + assert_eq!(event1.event, Some("message_start".to_string())); + assert_eq!(event1.data, None); + + // Second event should be the data: line + let event2 = iter.next().unwrap(); + assert!(!event2.is_event_only()); + assert_eq!(event2.event, None); + assert!(event2.data.as_ref().unwrap().contains("message_start")); + + // Third event should be another event: line + let event3 = iter.next().unwrap(); + assert!(event3.is_event_only()); + assert_eq!(event3.event, Some("content_block_delta".to_string())); + + // Fourth event should be the content delta data + let event4 = iter.next().unwrap(); + assert!(!event4.is_event_only()); + assert!(event4.data.as_ref().unwrap().contains("Hello")); + + // Fifth event should be [DONE] + let done_event = iter.next().unwrap(); + assert!(done_event.is_done()); + + // Iterator should end after [DONE] + assert!(iter.next().is_none()); + } + + #[test] + fn test_provider_stream_response_event_type() { + use crate::apis::anthropic::{MessagesContentDelta, MessagesStreamEvent}; + use crate::apis::openai::ChatCompletionsStreamResponse; + + // Test Anthropic event type + let anthropic_event = MessagesStreamEvent::ContentBlockDelta { + index: 0, + delta: MessagesContentDelta::TextDelta { + text: "Hello".to_string(), + }, + }; + let provider_type = ProviderStreamResponseType::MessagesStreamEvent(anthropic_event); + assert_eq!(provider_type.event_type(), Some("content_block_delta")); + + // Test OpenAI event type (should be None) + let openai_event = ChatCompletionsStreamResponse { + id: "test".to_string(), + object: Some("chat.completion.chunk".to_string()), + created: 123456789, + model: "gpt-4".to_string(), + choices: vec![], + usage: None, + system_fingerprint: None, + service_tier: None, + }; + let provider_type = ProviderStreamResponseType::ChatCompletionsStreamResponse(openai_event); + assert_eq!(provider_type.event_type(), None); + } + + #[test] + fn test_done_marker_handled_in_stream_response_transformation() { + use crate::apis::anthropic::AnthropicApi; + + // Test that [DONE] marker is properly converted to MessageStop in the transformation layer + let done_bytes = b"[DONE]"; + let client_api = SupportedAPIsFromClient::AnthropicMessagesAPI(AnthropicApi::Messages); + let upstream_api = SupportedUpstreamAPIs::OpenAIChatCompletions( + crate::apis::openai::OpenAIApi::ChatCompletions, + ); + + let result = ProviderStreamResponseType::try_from(( + done_bytes.as_slice(), + &client_api, + &upstream_api, + )); + assert!(result.is_ok()); + + if let Ok(ProviderStreamResponseType::MessagesStreamEvent(event)) = result { + // Verify it's a MessageStop event + assert_eq!(event.event_type(), Some("message_stop")); + assert!(matches!( + event, + crate::apis::anthropic::MessagesStreamEvent::MessageStop + )); + } else { + panic!("Expected MessagesStreamEvent::MessageStop"); + } + } + + #[test] + fn test_bedrock_event_stream_decoder_basic() { + use bytes::BytesMut; + + // Create a simple test with minimal data + let mut buffer = BytesMut::new(); + + // Add some arbitrary bytes (not a real event-stream frame, just for testing the decoder) + buffer.extend_from_slice(b"test data"); + + let mut decoder = BedrockBinaryFrameDecoder::new(&mut buffer); + + // The decoder should return Incomplete for incomplete/invalid data + // This signals the caller to wait for more data + let result = decoder.decode_frame(); + assert!(result.is_some()); + assert!(matches!( + result.unwrap(), + aws_smithy_eventstream::frame::DecodedFrame::Incomplete + )); + + // Verify we can still access the buffer + assert!(decoder.has_remaining()); + } + + #[test] + fn test_bedrock_event_stream_decoder_with_real_frames() { + use bytes::BytesMut; + use std::fs; + use std::path::PathBuf; + + // Read the actual response.hex file from tests/e2e directory + let test_file = + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../tests/e2e/response.hex"); + + // Only run this test if the file exists + if !test_file.exists() { + println!("Skipping test - response.hex not found"); + return; + } + + let response_data = fs::read(&test_file).unwrap(); + let mut buffer = BytesMut::from(&response_data[..]); + + let mut decoder = BedrockBinaryFrameDecoder::new(&mut buffer); + let mut frame_count = 0; + + // Decode all frames + loop { + match decoder.decode_frame() { + Some(aws_smithy_eventstream::frame::DecodedFrame::Complete(message)) => { + frame_count += 1; + + // Verify we can access headers + let event_type = message + .headers() + .iter() + .find(|h| h.name().as_str() == ":event-type") + .and_then(|h| h.value().as_string().ok()); + + assert!(event_type.is_some(), "Frame should have :event-type header"); + } + Some(aws_smithy_eventstream::frame::DecodedFrame::Incomplete) => { + // End of buffer, no more complete frames available + break; + } + None => { + // Decode error + panic!("Decode error encountered"); + } + } + } + + // We should have decoded multiple frames + assert!(frame_count > 0, "Should have decoded at least one frame"); + } + + #[test] + fn test_bedrock_event_stream_decoder_chunked_data() { + use bytes::BytesMut; + use std::fs; + use std::path::PathBuf; + + // Read the actual response.hex file + let test_file = + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../tests/e2e/response.hex"); + + if !test_file.exists() { + println!("Skipping test - response.hex not found"); + return; + } + + let response_data = fs::read(&test_file).unwrap(); + + // Simulate chunked network arrivals with realistic chunk sizes + // Using varying chunk sizes to test partial frame handling + let mut buffer = BytesMut::new(); + let chunk_size_pattern = vec![500, 1000, 750, 1200, 800, 1500]; + let mut offset = 0; + let mut total_frames = 0; + let mut chunk_num = 0; + + // CRITICAL: Create ONE decoder and reuse it across chunks + // The MessageFrameDecoder maintains state about partial frames + let mut decoder = BedrockBinaryFrameDecoder::new(&mut buffer); + + // Process all data in chunks + while offset < response_data.len() { + let chunk_size = chunk_size_pattern[chunk_num % chunk_size_pattern.len()]; + chunk_num += 1; + + let end = (offset + chunk_size).min(response_data.len()); + let chunk = &response_data[offset..end]; + + // Add new data to the buffer (accessing via buffer_mut()) + decoder.buffer_mut().extend_from_slice(chunk); + offset = end; + + // Process all available complete frames from this chunk + loop { + match decoder.decode_frame() { + Some(aws_smithy_eventstream::frame::DecodedFrame::Complete(_)) => { + total_frames += 1; + } + Some(aws_smithy_eventstream::frame::DecodedFrame::Incomplete) => { + // Need more data - wait for next chunk + break; + } + None => { + // Decode error + panic!("Decode error in chunked test"); + } + } + } + } + + assert!( + total_frames > 0, + "Should have decoded frames from chunked data" + ); + } + + #[test] + fn test_bedrock_decoded_frame_to_provider_response() { + test_bedrock_conversion(false); + } + + #[test] + #[ignore] // Run with: cargo test -- --ignored --nocapture + fn test_bedrock_decoded_frame_to_provider_response_verbose() { + test_bedrock_conversion(true); + } + + #[test] + fn test_bedrock_decoded_frame_with_tool_use() { + test_bedrock_conversion_with_tools(false); + } + + #[test] + #[ignore] // Run with: cargo test -- --ignored --nocapture + fn test_bedrock_decoded_frame_with_tool_use_verbose() { + test_bedrock_conversion_with_tools(true); + } + + fn test_bedrock_conversion(verbose: bool) { + use bytes::BytesMut; + use std::fs; + use std::path::PathBuf; + + // Read the actual response.hex file from tests/e2e directory + let test_file = + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../tests/e2e/response.hex"); + + // Only run this test if the file exists + if !test_file.exists() { + println!("Skipping test - response.hex not found"); + return; + } + + let response_data = fs::read(&test_file).unwrap(); + let mut buffer = BytesMut::from(&response_data[..]); + + let mut decoder = BedrockBinaryFrameDecoder::new(&mut buffer); + + let client_api = + SupportedAPIsFromClient::AnthropicMessagesAPI(crate::apis::anthropic::AnthropicApi::Messages); + let upstream_api = SupportedUpstreamAPIs::AmazonBedrockConverseStream( + crate::apis::amazon_bedrock::AmazonBedrockApi::ConverseStream, + ); + + let mut conversion_count = 0; + let mut message_start_seen = false; + + // Decode and convert frames + loop { + match decoder.decode_frame() { + Some(frame @ aws_smithy_eventstream::frame::DecodedFrame::Complete(_)) => { + // Convert DecodedFrame to ProviderStreamResponseType + let result = + ProviderStreamResponseType::try_from((&frame, &client_api, &upstream_api)); + + match result { + Ok(provider_response) => { + conversion_count += 1; + + // Verify we got a MessagesStreamEvent + assert!(matches!( + provider_response, + ProviderStreamResponseType::MessagesStreamEvent(_) + )); + + if verbose { + // Print the SSE string output + let sse_string: String = provider_response.clone().into(); + println!("{}", sse_string); + } + + // Check for MessageStart event + if let ProviderStreamResponseType::MessagesStreamEvent(ref event) = + provider_response + { + if matches!( + event, + crate::apis::anthropic::MessagesStreamEvent::MessageStart { .. } + ) { + message_start_seen = true; + } + } + } + Err(e) => { + println!("Conversion error (frame {}): {}", conversion_count, e); + } + } + } + Some(aws_smithy_eventstream::frame::DecodedFrame::Incomplete) => { + // End of buffer + break; + } + None => { + panic!("Decode error"); + } + } + } + + assert!( + conversion_count > 0, + "Should have converted at least one frame" + ); + assert!(message_start_seen, "Should have seen MessageStart event"); + } + + fn test_bedrock_conversion_with_tools(verbose: bool) { + use bytes::BytesMut; + use std::fs; + use std::path::PathBuf; + + // Read the actual response_with_tools.hex file from tests/e2e directory + let test_file = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/e2e/response_with_tools.hex"); + + // Only run this test if the file exists + if !test_file.exists() { + println!("Skipping test - response_with_tools.hex not found"); + return; + } + + let response_data = fs::read(&test_file).unwrap(); + let mut buffer = BytesMut::from(&response_data[..]); + + let mut decoder = BedrockBinaryFrameDecoder::new(&mut buffer); + + let client_api = + SupportedAPIsFromClient::AnthropicMessagesAPI(crate::apis::anthropic::AnthropicApi::Messages); + let upstream_api = SupportedUpstreamAPIs::AmazonBedrockConverseStream( + crate::apis::amazon_bedrock::AmazonBedrockApi::ConverseStream, + ); + + let mut conversion_count = 0; + let mut message_start_seen = false; + let mut content_block_start_seen = false; + let mut content_block_delta_tool_use_seen = false; + + // Decode and convert frames + loop { + match decoder.decode_frame() { + Some(frame @ aws_smithy_eventstream::frame::DecodedFrame::Complete(_)) => { + // Convert DecodedFrame to ProviderStreamResponseType + let result = + ProviderStreamResponseType::try_from((&frame, &client_api, &upstream_api)); + + match result { + Ok(provider_response) => { + conversion_count += 1; + + // Verify we got a MessagesStreamEvent + assert!(matches!( + provider_response, + ProviderStreamResponseType::MessagesStreamEvent(_) + )); + + if verbose { + // Print the SSE string output + let sse_string: String = provider_response.clone().into(); + println!("{}", sse_string); + } + + // Check for specific events related to tool use + if let ProviderStreamResponseType::MessagesStreamEvent(ref event) = + provider_response + { + match event { + crate::apis::anthropic::MessagesStreamEvent::MessageStart { .. } => { + message_start_seen = true; + } + crate::apis::anthropic::MessagesStreamEvent::ContentBlockStart { .. } => { + content_block_start_seen = true; + } + crate::apis::anthropic::MessagesStreamEvent::ContentBlockDelta { delta, .. } => { + if matches!(delta, crate::apis::anthropic::MessagesContentDelta::InputJsonDelta { .. }) { + content_block_delta_tool_use_seen = true; + } + } + _ => {} + } + } + } + Err(e) => { + println!("Conversion error (frame {}): {}", conversion_count, e); + } + } + } + Some(aws_smithy_eventstream::frame::DecodedFrame::Incomplete) => { + // End of buffer + break; + } + None => { + panic!("Decode error"); + } + } + } + + assert!( + conversion_count > 0, + "Should have converted at least one frame" + ); + assert!(message_start_seen, "Should have seen MessageStart event"); + assert!( + content_block_start_seen, + "Should have seen ContentBlockStart event for tool use" + ); + assert!( + content_block_delta_tool_use_seen, + "Should have seen ContentBlockDelta with ToolUseDelta" + ); + } + + + #[test] + fn test_sse_event_transformation_openai_to_anthropic_message_delta() { + use crate::apis::anthropic::AnthropicApi; + use crate::apis::openai::OpenAIApi; + + // Create an OpenAI stream response with finish_reason (which becomes message_delta in Anthropic) + let openai_stream_chunk = json!({ + "id": "chatcmpl-123", + "object": "chat.completion.chunk", + "created": 1234567890, + "model": "gpt-4", + "choices": [{ + "index": 0, + "delta": {}, + "finish_reason": "stop" + }], + "usage": { + "prompt_tokens": 10, + "completion_tokens": 25, + "total_tokens": 35 + } + }); + + // Create SSE event with this data + let sse_event = SseEvent { + data: Some(openai_stream_chunk.to_string()), + event: None, + raw_line: format!("data: {}", openai_stream_chunk.to_string()), + sse_transformed_lines: format!("data: {}", openai_stream_chunk.to_string()), + provider_stream_response: None, + }; + + let client_api = SupportedAPIsFromClient::AnthropicMessagesAPI(AnthropicApi::Messages); + let upstream_api = SupportedUpstreamAPIs::OpenAIChatCompletions(OpenAIApi::ChatCompletions); + + // Transform the event + let result = SseEvent::try_from((sse_event, &client_api, &upstream_api)); + assert!(result.is_ok()); + + let transformed = result.unwrap(); + + // NOTE: This test now verifies single-event transformation only. + // Multi-event injection (content_block_stop + message_delta) is now handled + // by AnthropicMessagesStreamBuffer, not by TryFrom transformation. + let buffer = transformed.sse_transformed_lines; + + // Verify the event was transformed to Anthropic format + // This should contain message_delta with stop_reason and usage + assert!( + buffer.contains("event: message_delta") || buffer.contains("\"type\":\"message_delta\""), + "Should contain message_delta in transformed event" + ); + + // Verify usage information is present + assert!( + buffer.contains("\"usage\""), + "Should contain usage information" + ); + } + + #[test] + fn test_sse_event_transformation_openai_to_anthropic_content_delta() { + use crate::apis::anthropic::AnthropicApi; + use crate::apis::openai::OpenAIApi; + + // Create an OpenAI stream response with content (which becomes content_block_delta in Anthropic) + let openai_stream_chunk = json!({ + "id": "chatcmpl-123", + "object": "chat.completion.chunk", + "created": 1234567890, + "model": "gpt-4", + "choices": [{ + "index": 0, + "delta": {"content": "Hello"}, + "finish_reason": null + }] + }); + + // Create SSE event with this data + let sse_event = SseEvent { + data: Some(openai_stream_chunk.to_string()), + event: None, + raw_line: format!("data: {}", openai_stream_chunk.to_string()), + sse_transformed_lines: format!("data: {}", openai_stream_chunk.to_string()), + provider_stream_response: None, + }; + + let client_api = SupportedAPIsFromClient::AnthropicMessagesAPI(AnthropicApi::Messages); + let upstream_api = SupportedUpstreamAPIs::OpenAIChatCompletions(OpenAIApi::ChatCompletions); + + // Transform the event + let result = SseEvent::try_from((sse_event, &client_api, &upstream_api)); + assert!(result.is_ok()); + + let transformed = result.unwrap(); + + // Verify the transformation is a content_block_delta (no extra events injected) + let buffer = transformed.sse_transformed_lines; + assert!( + buffer.contains("event: content_block_delta"), + "Should contain content_block_delta event" + ); + assert!( + !buffer.contains("content_block_start"), + "Should not inject content_block_start for content delta" + ); + assert!( + !buffer.contains("content_block_stop"), + "Should not inject content_block_stop for content delta" + ); + + // Verify the content is preserved + assert!(buffer.contains("Hello"), "Should preserve the content text"); + } + + #[test] + fn test_sse_event_transformation_anthropic_to_openai_suppresses_event_lines() { + use crate::apis::anthropic::AnthropicApi; + use crate::apis::openai::OpenAIApi; + + // Create an Anthropic event-only SSE line (no data) + let sse_event = SseEvent { + data: None, + event: Some("message_start".to_string()), + raw_line: "event: message_start".to_string(), + sse_transformed_lines: "event: message_start".to_string(), + provider_stream_response: None, + }; + + let client_api = SupportedAPIsFromClient::OpenAIChatCompletions(OpenAIApi::ChatCompletions); + let upstream_api = SupportedUpstreamAPIs::AnthropicMessagesAPI(AnthropicApi::Messages); + + // Transform the event + let result = SseEvent::try_from((sse_event, &client_api, &upstream_api)); + assert!(result.is_ok()); + + let transformed = result.unwrap(); + + // Verify the event line is suppressed (replaced with just newline) + assert_eq!( + transformed.sse_transformed_lines, "\n", + "Event-only lines should be suppressed to newline for OpenAI" + ); + assert!( + transformed.is_event_only(), + "Should still be marked as event-only" + ); + } + + #[test] + fn test_sse_event_transformation_anthropic_to_openai_preserves_data() { + use crate::apis::anthropic::AnthropicApi; + use crate::apis::openai::OpenAIApi; + + // Create an Anthropic message_start event with data + let anthropic_event = json!({ + "type": "message_start", + "message": { + "id": "msg_123", + "type": "message", + "role": "assistant", + "content": [], + "model": "claude-3-sonnet", + "stop_reason": null, + "usage": {"input_tokens": 10, "output_tokens": 0} + } + }); + + let sse_event = SseEvent { + data: Some(anthropic_event.to_string()), + event: None, + raw_line: format!("data: {}", anthropic_event.to_string()), + sse_transformed_lines: format!("data: {}", anthropic_event.to_string()), + provider_stream_response: None, + }; + + let client_api = SupportedAPIsFromClient::OpenAIChatCompletions(OpenAIApi::ChatCompletions); + let upstream_api = SupportedUpstreamAPIs::AnthropicMessagesAPI(AnthropicApi::Messages); + + // Transform the event + let result = SseEvent::try_from((sse_event, &client_api, &upstream_api)); + assert!(result.is_ok()); + + let transformed = result.unwrap(); + + // Verify data is transformed to OpenAI format + let buffer = transformed.sse_transformed_lines; + assert!(buffer.starts_with("data: "), "Should have data: prefix"); + assert!( + !buffer.contains("event:"), + "Should not have event: lines for OpenAI" + ); + + // Verify provider response was parsed + assert!(transformed.provider_stream_response.is_some()); + } + + #[test] + fn test_sse_event_transformation_no_change_for_matching_apis() { + use crate::apis::openai::OpenAIApi; + + // Create an OpenAI stream response + let openai_stream_chunk = json!({ + "id": "chatcmpl-123", + "object": "chat.completion.chunk", + "created": 1234567890, + "model": "gpt-4", + "choices": [{ + "index": 0, + "delta": {"content": "Hello"}, + "finish_reason": null + }] + }); + + let original_data = openai_stream_chunk.to_string(); + let sse_event = SseEvent { + data: Some(original_data.clone()), + event: None, + raw_line: format!("data: {}", original_data), + sse_transformed_lines: format!("data: {}\n\n", original_data), + provider_stream_response: None, + }; + + let client_api = SupportedAPIsFromClient::OpenAIChatCompletions(OpenAIApi::ChatCompletions); + let upstream_api = SupportedUpstreamAPIs::OpenAIChatCompletions(OpenAIApi::ChatCompletions); + + // Transform the event + let result = SseEvent::try_from((sse_event, &client_api, &upstream_api)); + assert!(result.is_ok()); + + let transformed = result.unwrap(); + + // Verify minimal transformation - just SSE formatting, no API conversion + let buffer = transformed.sse_transformed_lines; + assert!(buffer.starts_with("data: "), "Should preserve data: prefix"); + assert!(!buffer.contains("event:"), "Should not add event: lines"); + + // Verify provider response was parsed + assert!(transformed.provider_stream_response.is_some()); + } + + #[test] + fn test_sse_event_transformation_preserves_provider_response() { + use crate::apis::anthropic::AnthropicApi; + use crate::apis::openai::OpenAIApi; + + // Create an OpenAI stream response + let openai_stream_chunk = json!({ + "id": "chatcmpl-123", + "object": "chat.completion.chunk", + "created": 1234567890, + "model": "gpt-4", + "choices": [{ + "index": 0, + "delta": {"content": "Test"}, + "finish_reason": null + }] + }); + + let sse_event = SseEvent { + data: Some(openai_stream_chunk.to_string()), + event: None, + raw_line: format!("data: {}", openai_stream_chunk.to_string()), + sse_transformed_lines: format!("data: {}", openai_stream_chunk.to_string()), + provider_stream_response: None, + }; + + let client_api = SupportedAPIsFromClient::AnthropicMessagesAPI(AnthropicApi::Messages); + let upstream_api = SupportedUpstreamAPIs::OpenAIChatCompletions(OpenAIApi::ChatCompletions); + + // Transform the event + let result = SseEvent::try_from((sse_event, &client_api, &upstream_api)); + assert!(result.is_ok()); + + let transformed = result.unwrap(); + + // Verify provider_stream_response is populated + assert!( + transformed.provider_stream_response.is_some(), + "Should parse and store provider response" + ); + + // Verify we can access the provider response + let provider_response = transformed.provider_response(); + assert!( + provider_response.is_ok(), + "Should be able to access provider response" + ); + + // Verify the content delta is accessible + let content = provider_response.unwrap().content_delta(); + assert_eq!(content, Some("Test"), "Should preserve content delta"); + } +} diff --git a/crates/hermesllm/src/transforms/mod.rs b/crates/hermesllm/src/transforms/mod.rs index 3fb4e397..ebb4bf20 100644 --- a/crates/hermesllm/src/transforms/mod.rs +++ b/crates/hermesllm/src/transforms/mod.rs @@ -11,11 +11,13 @@ pub mod lib; pub mod request; pub mod response; +pub mod response_streaming; // Re-export commonly used items for convenience pub use lib::*; pub use request::*; pub use response::*; +pub use response_streaming::*; // ============================================================================ // CONSTANTS diff --git a/crates/hermesllm/src/transforms/request/from_openai.rs b/crates/hermesllm/src/transforms/request/from_openai.rs index df4a9557..27366f4d 100644 --- a/crates/hermesllm/src/transforms/request/from_openai.rs +++ b/crates/hermesllm/src/transforms/request/from_openai.rs @@ -12,6 +12,10 @@ use crate::apis::anthropic::{ use crate::apis::openai::{ ChatCompletionsRequest, Message, MessageContent, Role, Tool, ToolChoice, ToolChoiceType, }; + +use crate::apis::openai_responses::{ + ResponsesAPIRequest, InputContent, InputItem, InputParam, MessageRole, Modality, ReasoningEffort, Tool as ResponsesTool, ToolChoice as ResponsesToolChoice +}; use crate::clients::TransformError; use crate::transforms::lib::ExtractText; use crate::transforms::lib::*; @@ -244,6 +248,212 @@ impl TryFrom for BedrockMessage { } } +impl TryFrom for ChatCompletionsRequest { + type Error = TransformError; + + fn try_from(req: ResponsesAPIRequest) -> Result { + + // Convert input to messages + let messages = match req.input { + InputParam::Text(text) => { + // Simple text input becomes a user message + vec![Message { + role: Role::User, + content: MessageContent::Text(text), + name: None, + tool_call_id: None, + tool_calls: None, + }] + } + InputParam::Items(items) => { + // Convert input items to messages + let mut converted_messages = Vec::new(); + + // Add instructions as system message if present + if let Some(instructions) = &req.instructions { + converted_messages.push(Message { + role: Role::System, + content: MessageContent::Text(instructions.clone()), + name: None, + tool_call_id: None, + tool_calls: None, + }); + } + + // Convert each input item + for item in items { + match item { + InputItem::Message(input_msg) => { + let role = match input_msg.role { + MessageRole::User => Role::User, + MessageRole::Assistant => Role::Assistant, + MessageRole::System => Role::System, + MessageRole::Developer => Role::System, // Map developer to system + }; + + // Convert content based on MessageContent type + let content = match &input_msg.content { + crate::apis::openai_responses::MessageContent::Text(text) => { + // Simple text content + MessageContent::Text(text.clone()) + } + crate::apis::openai_responses::MessageContent::Items(content_items) => { + // Check if it's a single text item (can use simple text format) + if content_items.len() == 1 { + if let InputContent::InputText { text } = &content_items[0] { + MessageContent::Text(text.clone()) + } else { + // Single non-text item - use parts format + MessageContent::Parts( + content_items.iter() + .filter_map(|c| match c { + InputContent::InputText { text } => { + Some(crate::apis::openai::ContentPart::Text { text: text.clone() }) + } + InputContent::InputImage { image_url, .. } => { + Some(crate::apis::openai::ContentPart::ImageUrl { + image_url: crate::apis::openai::ImageUrl { + url: image_url.clone(), + detail: None, + } + }) + } + InputContent::InputFile { .. } => None, // Skip files for now + InputContent::InputAudio { .. } => None, // Skip audio for now + }) + .collect() + ) + } + } else { + // Multiple content items - convert to parts + MessageContent::Parts( + content_items.iter() + .filter_map(|c| match c { + InputContent::InputText { text } => { + Some(crate::apis::openai::ContentPart::Text { text: text.clone() }) + } + InputContent::InputImage { image_url, .. } => { + Some(crate::apis::openai::ContentPart::ImageUrl { + image_url: crate::apis::openai::ImageUrl { + url: image_url.clone(), + detail: None, + } + }) + } + InputContent::InputFile { .. } => None, // Skip files for now + InputContent::InputAudio { .. } => None, // Skip audio for now + }) + .collect() + ) + } + } + }; + + converted_messages.push(Message { + role, + content, + name: None, + tool_call_id: None, + tool_calls: None, + }); + } + // Skip non-message items (references, outputs) for now + // These would need special handling in chat completions format + _ => {} + } + } + + converted_messages + } + }; + + // Build the ChatCompletionsRequest + Ok(ChatCompletionsRequest { + model: req.model, + messages, + temperature: req.temperature, + top_p: req.top_p, + max_completion_tokens: req.max_output_tokens.map(|t| t as u32), + stream: req.stream, + metadata: req.metadata, + user: req.user, + store: req.store, + service_tier: req.service_tier, + top_logprobs: req.top_logprobs.map(|t| t as u32), + modalities: req.modalities.map(|mods| { + mods.into_iter().map(|m| { + match m { + Modality::Text => "text".to_string(), + Modality::Audio => "audio".to_string(), + } + }).collect() + }), + stream_options: req.stream_options.map(|opts| { + crate::apis::openai::StreamOptions { + include_usage: opts.include_usage, + } + }), + reasoning_effort: req.reasoning_effort.map(|effort| { + match effort { + ReasoningEffort::Low => "low".to_string(), + ReasoningEffort::Medium => "medium".to_string(), + ReasoningEffort::High => "high".to_string(), + } + }), + tools: req.tools.map(|tools| { + tools.into_iter().map(|tool| { + + // Only convert Function tools - other types are not supported in ChatCompletions + match tool { + ResponsesTool::Function { name, description, parameters, strict } => Ok(Tool { + tool_type: "function".to_string(), + function: crate::apis::openai::Function { + name, + description, + parameters: parameters.unwrap_or_else(|| serde_json::json!({ + "type": "object", + "properties": {} + })), + strict, + } + }), + ResponsesTool::FileSearch { .. } => Err(TransformError::UnsupportedConversion( + "FileSearch tool is not supported in ChatCompletions API. Only function tools are supported.".to_string() + )), + ResponsesTool::WebSearchPreview { .. } => Err(TransformError::UnsupportedConversion( + "WebSearchPreview tool is not supported in ChatCompletions API. Only function tools are supported.".to_string() + )), + ResponsesTool::CodeInterpreter => Err(TransformError::UnsupportedConversion( + "CodeInterpreter tool is not supported in ChatCompletions API. Only function tools are supported.".to_string() + )), + ResponsesTool::Computer { .. } => Err(TransformError::UnsupportedConversion( + "Computer tool is not supported in ChatCompletions API. Only function tools are supported.".to_string() + )), + } + }).collect::, _>>() + }).transpose()?, + tool_choice: req.tool_choice.map(|choice| { + match choice { + ResponsesToolChoice::String(s) => { + match s.as_str() { + "auto" => ToolChoice::Type(ToolChoiceType::Auto), + "required" => ToolChoice::Type(ToolChoiceType::Required), + "none" => ToolChoice::Type(ToolChoiceType::None), + _ => ToolChoice::Type(ToolChoiceType::Auto), // Default to auto for unknown strings + } + } + ResponsesToolChoice::Named { function, .. } => ToolChoice::Function { + choice_type: "function".to_string(), + function: crate::apis::openai::FunctionChoice { name: function.name } + } + } + }), + parallel_tool_calls: req.parallel_tool_calls, + ..Default::default() + }) + } +} + impl TryFrom for AnthropicMessagesRequest { type Error = TransformError; diff --git a/crates/hermesllm/src/transforms/response/mod.rs b/crates/hermesllm/src/transforms/response/mod.rs index 3ce75123..1dd0d4ea 100644 --- a/crates/hermesllm/src/transforms/response/mod.rs +++ b/crates/hermesllm/src/transforms/response/mod.rs @@ -1,3 +1,4 @@ //! Response transformation modules +pub mod output_to_input; pub mod to_anthropic; pub mod to_openai; diff --git a/crates/hermesllm/src/transforms/response/output_to_input.rs b/crates/hermesllm/src/transforms/response/output_to_input.rs new file mode 100644 index 00000000..8ab08205 --- /dev/null +++ b/crates/hermesllm/src/transforms/response/output_to_input.rs @@ -0,0 +1,178 @@ +//! Conversions from response outputs to request inputs for conversation continuation +//! +//! This module provides utilities for converting OutputItem types from API responses +//! into InputItem types that can be used in subsequent requests. This is primarily used +//! for maintaining conversation history in the v1/responses API. + +use crate::apis::openai_responses::{ + InputContent, InputItem, InputMessage, MessageContent, MessageRole, OutputContent, OutputItem, +}; + +/// Converts an OutputItem from a response into an InputItem for the next request +/// This is used to build conversation history from previous responses +pub fn convert_responses_output_to_input_items(output: &OutputItem) -> Option { + match output { + // Convert output messages to input messages + OutputItem::Message { + role, content, .. + } => { + let input_content: Vec = content + .iter() + .filter_map(|c| match c { + OutputContent::OutputText { text, .. } => Some(InputContent::InputText { + text: text.clone(), + }), + OutputContent::OutputAudio { + data, .. + } => Some(InputContent::InputAudio { + data: data.clone(), + format: None, // Format not preserved in output + }), + OutputContent::Refusal { .. } => None, // Skip refusals + }) + .collect(); + + if input_content.is_empty() { + return None; + } + + // Map role string to MessageRole enum + let message_role = match role.as_str() { + "user" => MessageRole::User, + "assistant" => MessageRole::Assistant, + "system" => MessageRole::System, + "developer" => MessageRole::Developer, + _ => MessageRole::Assistant, // Default to assistant + }; + + Some(InputItem::Message(InputMessage { + role: message_role, + content: MessageContent::Items(input_content), + })) + } + // For function calls, we'll create an assistant message with the tool call info + // This matches how conversation history is typically built + OutputItem::FunctionCall { + name, arguments, .. + } => { + let tool_call_text = if let (Some(n), Some(args)) = (name, arguments) { + format!("Called function: {} with arguments: {}", n, args) + } else { + "Called a function".to_string() + }; + + Some(InputItem::Message(InputMessage { + role: MessageRole::Assistant, + content: MessageContent::Items(vec![InputContent::InputText { + text: tool_call_text, + }]), + })) + } + // Skip other output types (tool outputs, etc.) as they don't convert to input + _ => None, + } +} + +/// Converts a Vec of OutputItems into InputItems for conversation continuation +pub fn outputs_to_inputs(outputs: &[OutputItem]) -> Vec { + outputs + .iter() + .filter_map(convert_responses_output_to_input_items) + .collect() +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::apis::openai_responses::{OutputItemStatus}; + + #[test] + fn test_output_message_to_input() { + let output = OutputItem::Message { + id: "msg_123".to_string(), + status: OutputItemStatus::Completed, + role: "assistant".to_string(), + content: vec![OutputContent::OutputText { + text: "Hello!".to_string(), + annotations: vec![], + logprobs: None, + }], + }; + + let input = convert_responses_output_to_input_items(&output).unwrap(); + + match input { + InputItem::Message(msg) => { + assert!(matches!(msg.role, MessageRole::Assistant)); + match &msg.content { + MessageContent::Items(items) => { + assert_eq!(items.len(), 1); + match &items[0] { + InputContent::InputText { text } => assert_eq!(text, "Hello!"), + _ => panic!("Expected InputText"), + } + } + _ => panic!("Expected MessageContent::Items"), + } + } + _ => panic!("Expected Message variant"), + } + } + + #[test] + fn test_function_call_to_input() { + let output = OutputItem::FunctionCall { + id: "fc_123".to_string(), + status: OutputItemStatus::Completed, + call_id: "call_123".to_string(), + name: Some("get_weather".to_string()), + arguments: Some(r#"{"location":"SF"}"#.to_string()), + }; + + let input = convert_responses_output_to_input_items(&output).unwrap(); + + match input { + InputItem::Message(msg) => { + assert!(matches!(msg.role, MessageRole::Assistant)); + match &msg.content { + MessageContent::Items(items) => { + match &items[0] { + InputContent::InputText { text } => { + assert!(text.contains("get_weather")); + } + _ => panic!("Expected InputText"), + } + } + _ => panic!("Expected MessageContent::Items"), + } + } + _ => panic!("Expected Message variant"), + } + } + + #[test] + fn test_outputs_to_inputs() { + let outputs = vec![ + OutputItem::Message { + id: "msg_1".to_string(), + status: OutputItemStatus::Completed, + role: "assistant".to_string(), + content: vec![OutputContent::OutputText { + text: "Hello".to_string(), + annotations: vec![], + logprobs: None, + }], + }, + OutputItem::FunctionCall { + id: "fc_1".to_string(), + status: OutputItemStatus::Completed, + call_id: "call_1".to_string(), + name: Some("test".to_string()), + arguments: Some("{}".to_string()), + }, + ]; + + let inputs = outputs_to_inputs(&outputs); + assert_eq!(inputs.len(), 2); + } +} diff --git a/crates/hermesllm/src/transforms/response/to_anthropic.rs b/crates/hermesllm/src/transforms/response/to_anthropic.rs index 1c6ce238..0326fdb3 100644 --- a/crates/hermesllm/src/transforms/response/to_anthropic.rs +++ b/crates/hermesllm/src/transforms/response/to_anthropic.rs @@ -1,16 +1,11 @@ -use crate::apis::amazon_bedrock::{ - ContentBlockDelta, ConverseOutput, ConverseResponse, ConverseStreamEvent, StopReason, -}; +use crate::apis::amazon_bedrock::{ConverseOutput, ConverseResponse, StopReason}; use crate::apis::anthropic::{ - MessagesContentBlock, MessagesContentDelta, MessagesMessageDelta, MessagesResponse, - MessagesRole, MessagesStopReason, MessagesStreamEvent, MessagesStreamMessage, MessagesUsage, -}; -use crate::apis::openai::{ - ChatCompletionsResponse, ChatCompletionsStreamResponse, Role, ToolCallDelta, + MessagesContentBlock, MessagesResponse, + MessagesRole, MessagesStopReason, MessagesUsage, }; +use crate::apis::openai::ChatCompletionsResponse; use crate::clients::TransformError; use crate::transforms::lib::*; -use serde_json::Value; // ============================================================================ // STANDARD RUST TRAIT IMPLEMENTATIONS - Using Into/TryFrom for convenience @@ -120,289 +115,6 @@ impl TryFrom for MessagesResponse { } } -impl TryFrom for MessagesStreamEvent { - type Error = TransformError; - - fn try_from(resp: ChatCompletionsStreamResponse) -> Result { - if resp.choices.is_empty() { - return Ok(MessagesStreamEvent::Ping); - } - - let choice = &resp.choices[0]; - - // Handle final chunk with usage - let has_usage = resp.usage.is_some(); - if let Some(usage) = resp.usage { - if let Some(finish_reason) = &choice.finish_reason { - let anthropic_stop_reason: MessagesStopReason = finish_reason.clone().into(); - return Ok(MessagesStreamEvent::MessageDelta { - delta: MessagesMessageDelta { - stop_reason: anthropic_stop_reason, - stop_sequence: None, - }, - usage: usage.into(), - }); - } - } - - // Handle role start - if let Some(Role::Assistant) = choice.delta.role { - return Ok(MessagesStreamEvent::MessageStart { - message: MessagesStreamMessage { - id: resp.id, - obj_type: "message".to_string(), - role: MessagesRole::Assistant, - content: vec![], - model: resp.model, - stop_reason: None, - stop_sequence: None, - usage: MessagesUsage { - input_tokens: 0, - output_tokens: 0, - cache_creation_input_tokens: None, - cache_read_input_tokens: None, - }, - }, - }); - } - - // Handle content delta - if let Some(content) = &choice.delta.content { - if !content.is_empty() { - return Ok(MessagesStreamEvent::ContentBlockDelta { - index: 0, - delta: MessagesContentDelta::TextDelta { - text: content.clone(), - }, - }); - } - } - - // Handle tool calls - if let Some(tool_calls) = &choice.delta.tool_calls { - return convert_tool_call_deltas(tool_calls.clone()); - } - - // Handle finish reason - generate MessageDelta only (MessageStop comes later) - if let Some(finish_reason) = &choice.finish_reason { - // If we have usage data, it was already handled above - // If not, we need to generate MessageDelta with default usage - if !has_usage { - let anthropic_stop_reason: MessagesStopReason = finish_reason.clone().into(); - return Ok(MessagesStreamEvent::MessageDelta { - delta: MessagesMessageDelta { - stop_reason: anthropic_stop_reason, - stop_sequence: None, - }, - usage: MessagesUsage { - input_tokens: 0, - output_tokens: 0, - cache_creation_input_tokens: None, - cache_read_input_tokens: None, - }, - }); - } - // If usage was already handled above, we don't need to do anything more here - // MessageStop will be handled when [DONE] is encountered - } - - // Default to ping for unhandled cases - Ok(MessagesStreamEvent::Ping) - } -} - -impl Into for MessagesStreamEvent { - fn into(self) -> String { - let transformed_json = serde_json::to_string(&self).unwrap_or_default(); - let event_type = match &self { - MessagesStreamEvent::MessageStart { .. } => "message_start", - MessagesStreamEvent::ContentBlockStart { .. } => "content_block_start", - MessagesStreamEvent::ContentBlockDelta { .. } => "content_block_delta", - MessagesStreamEvent::ContentBlockStop { .. } => "content_block_stop", - MessagesStreamEvent::MessageDelta { .. } => "message_delta", - MessagesStreamEvent::MessageStop => "message_stop", - MessagesStreamEvent::Ping => "ping", - }; - - let event = format!("event: {}\n", event_type); - let data = format!("data: {}\n\n", transformed_json); - event + &data - } -} - -impl TryFrom for MessagesStreamEvent { - type Error = TransformError; - - fn try_from(event: ConverseStreamEvent) -> Result { - match event { - // MessageStart - convert to Anthropic MessageStart - ConverseStreamEvent::MessageStart(start_event) => { - let role = match start_event.role { - crate::apis::amazon_bedrock::ConversationRole::User => MessagesRole::User, - crate::apis::amazon_bedrock::ConversationRole::Assistant => { - MessagesRole::Assistant - } - }; - - Ok(MessagesStreamEvent::MessageStart { - message: MessagesStreamMessage { - id: format!( - "bedrock-stream-{}", - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() - .as_nanos() - ), - obj_type: "message".to_string(), - role, - content: vec![], - model: "bedrock-model".to_string(), - stop_reason: None, - stop_sequence: None, - usage: MessagesUsage { - input_tokens: 0, - output_tokens: 0, - cache_creation_input_tokens: None, - cache_read_input_tokens: None, - }, - }, - }) - } - - // ContentBlockStart - convert to Anthropic ContentBlockStart - ConverseStreamEvent::ContentBlockStart(start_event) => { - // Note: Bedrock sends tool_use_id and name at start, with input coming in subsequent deltas - // Anthropic expects the same pattern, so we initialize with an empty input object - match start_event.start { - crate::apis::amazon_bedrock::ContentBlockStart::ToolUse { tool_use } => { - Ok(MessagesStreamEvent::ContentBlockStart { - index: start_event.content_block_index as u32, - content_block: MessagesContentBlock::ToolUse { - id: tool_use.tool_use_id, - name: tool_use.name, - input: Value::Object(serde_json::Map::new()), // Empty - will be filled by deltas - cache_control: None, - }, - }) - } - } - } - - // ContentBlockDelta - convert to Anthropic ContentBlockDelta - ConverseStreamEvent::ContentBlockDelta(delta_event) => { - let delta = match delta_event.delta { - ContentBlockDelta::Text { text } => MessagesContentDelta::TextDelta { text }, - ContentBlockDelta::ToolUse { tool_use } => { - MessagesContentDelta::InputJsonDelta { - partial_json: tool_use.input, - } - } - }; - - Ok(MessagesStreamEvent::ContentBlockDelta { - index: delta_event.content_block_index as u32, - delta, - }) - } - - // ContentBlockStop - convert to Anthropic ContentBlockStop - ConverseStreamEvent::ContentBlockStop(stop_event) => { - Ok(MessagesStreamEvent::ContentBlockStop { - index: stop_event.content_block_index as u32, - }) - } - - // MessageStop - convert to Anthropic MessageDelta with stop reason + MessageStop - ConverseStreamEvent::MessageStop(stop_event) => { - let anthropic_stop_reason = match stop_event.stop_reason { - StopReason::EndTurn => MessagesStopReason::EndTurn, - StopReason::ToolUse => MessagesStopReason::ToolUse, - StopReason::MaxTokens => MessagesStopReason::MaxTokens, - StopReason::StopSequence => MessagesStopReason::EndTurn, - StopReason::GuardrailIntervened => MessagesStopReason::Refusal, - StopReason::ContentFiltered => MessagesStopReason::Refusal, - }; - - // Return MessageDelta (MessageStop will be sent separately by the streaming handler) - Ok(MessagesStreamEvent::MessageDelta { - delta: MessagesMessageDelta { - stop_reason: anthropic_stop_reason, - stop_sequence: None, - }, - usage: MessagesUsage { - input_tokens: 0, - output_tokens: 0, - cache_creation_input_tokens: None, - cache_read_input_tokens: None, - }, - }) - } - - // Metadata - convert usage information to MessageDelta - ConverseStreamEvent::Metadata(metadata_event) => { - Ok(MessagesStreamEvent::MessageDelta { - delta: MessagesMessageDelta { - stop_reason: MessagesStopReason::EndTurn, - stop_sequence: None, - }, - usage: MessagesUsage { - input_tokens: metadata_event.usage.input_tokens, - output_tokens: metadata_event.usage.output_tokens, - cache_creation_input_tokens: metadata_event.usage.cache_write_input_tokens, - cache_read_input_tokens: metadata_event.usage.cache_read_input_tokens, - }, - }) - } - - // Exception events - convert to Ping (could be enhanced to return error events) - ConverseStreamEvent::InternalServerException(_) - | ConverseStreamEvent::ModelStreamErrorException(_) - | ConverseStreamEvent::ServiceUnavailableException(_) - | ConverseStreamEvent::ThrottlingException(_) - | ConverseStreamEvent::ValidationException(_) => { - // TODO: Consider adding proper error handling/events - Ok(MessagesStreamEvent::Ping) - } - } - } -} - -/// Convert tool call deltas to Anthropic stream events -fn convert_tool_call_deltas( - tool_calls: Vec, -) -> Result { - for tool_call in tool_calls { - if let Some(id) = &tool_call.id { - // Tool call start - if let Some(function) = &tool_call.function { - if let Some(name) = &function.name { - return Ok(MessagesStreamEvent::ContentBlockStart { - index: tool_call.index, - content_block: MessagesContentBlock::ToolUse { - id: id.clone(), - name: name.clone(), - input: Value::Object(serde_json::Map::new()), - cache_control: None, - }, - }); - } - } - } else if let Some(function) = &tool_call.function { - if let Some(arguments) = &function.arguments { - // Tool arguments delta - return Ok(MessagesStreamEvent::ContentBlockDelta { - index: tool_call.index, - delta: MessagesContentDelta::InputJsonDelta { - partial_json: arguments.clone(), - }, - }); - } - } - } - - // Fallback to ping if no valid tool call found - Ok(MessagesStreamEvent::Ping) -} /// Convert Bedrock Message to Anthropic content blocks /// diff --git a/crates/hermesllm/src/transforms/response/to_openai.rs b/crates/hermesllm/src/transforms/response/to_openai.rs index acbdb420..ee526c71 100644 --- a/crates/hermesllm/src/transforms/response/to_openai.rs +++ b/crates/hermesllm/src/transforms/response/to_openai.rs @@ -1,15 +1,13 @@ use crate::apis::amazon_bedrock::{ - ConverseOutput, ConverseResponse, ConverseStreamEvent, StopReason, + ConverseOutput, ConverseResponse, StopReason, }; use crate::apis::anthropic::{ - MessagesContentBlock, MessagesContentDelta, MessagesResponse, MessagesStopReason, - MessagesStreamEvent, MessagesUsage, + MessagesContentBlock, MessagesResponse, MessagesUsage, }; use crate::apis::openai::{ - ChatCompletionsResponse, ChatCompletionsStreamResponse, Choice, FinishReason, - FunctionCallDelta, MessageContent, MessageDelta, ResponseMessage, Role, StreamChoice, - ToolCallDelta, Usage, + ChatCompletionsResponse, Choice, FinishReason, MessageContent, ResponseMessage, Role, Usage, }; +use crate::apis::openai_responses::ResponsesAPIResponse; use crate::clients::TransformError; use crate::transforms::lib::*; @@ -30,6 +28,182 @@ impl Into for MessagesUsage { } } +impl TryFrom for ResponsesAPIResponse { + type Error = TransformError; + + fn try_from(resp: ChatCompletionsResponse) -> Result { + use crate::apis::openai_responses::{ + IncompleteDetails, IncompleteReason, OutputContent, OutputItem, OutputItemStatus, + ResponseStatus, ResponseUsage, ResponsesAPIResponse, + }; + + // Convert the first choice's message to output items + let output = if let Some(choice) = resp.choices.first() { + let mut items = Vec::new(); + + // Create a message output item from the response message + let mut content = Vec::new(); + + // Add text content if present + if let Some(text) = &choice.message.content { + content.push(OutputContent::OutputText { + text: text.clone(), + annotations: vec![], + logprobs: None, + }); + } + + // Add audio content if present (audio is a Value, need to handle it carefully) + if let Some(audio) = &choice.message.audio { + // Audio is serde_json::Value, try to extract data and transcript + if let Some(audio_obj) = audio.as_object() { + let data = audio_obj + .get("data") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()); + let transcript = audio_obj + .get("transcript") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()); + + content.push(OutputContent::OutputAudio { data, transcript }); + } + } + + // Add refusal content if present + if let Some(refusal) = &choice.message.refusal { + content.push(OutputContent::Refusal { + refusal: refusal.clone(), + }); + } + + // Only add the message item if there's actual content (text, audio, or refusal) + // Don't add empty message items when there are only tool calls + if !content.is_empty() { + // Generate message ID: strip common prefixes to avoid double-prefixing + let message_id = if resp.id.starts_with("msg_") { + resp.id.clone() + } else if resp.id.starts_with("resp_") { + format!("msg_{}", &resp.id[5..]) // Strip "resp_" prefix + } else if resp.id.starts_with("chatcmpl-") { + format!("msg_{}", &resp.id[9..]) // Strip "chatcmpl-" prefix + } else { + format!("msg_{}", resp.id) + }; + + items.push(OutputItem::Message { + id: message_id, + status: OutputItemStatus::Completed, + role: match choice.message.role { + Role::User => "user".to_string(), + Role::Assistant => "assistant".to_string(), + Role::System => "system".to_string(), + Role::Tool => "tool".to_string(), + }, + content, + }); + } + + // Add tool calls as function call items if present + if let Some(tool_calls) = &choice.message.tool_calls { + for tool_call in tool_calls { + items.push(OutputItem::FunctionCall { + id: format!("func_{}", tool_call.id), + status: OutputItemStatus::Completed, + call_id: tool_call.id.clone(), + name: Some(tool_call.function.name.clone()), + arguments: Some(tool_call.function.arguments.clone()), + }); + } + } + + items + } else { + vec![] + }; + + // Convert finish_reason to status + let status = if let Some(choice) = resp.choices.first() { + match choice.finish_reason { + Some(FinishReason::Stop) => ResponseStatus::Completed, + Some(FinishReason::ToolCalls) => ResponseStatus::Completed, + Some(FinishReason::Length) => ResponseStatus::Incomplete, + Some(FinishReason::ContentFilter) => ResponseStatus::Failed, + _ => ResponseStatus::Completed, + } + } else { + ResponseStatus::Completed + }; + + // Convert usage + let usage = ResponseUsage { + input_tokens: resp.usage.prompt_tokens as i32, + output_tokens: resp.usage.completion_tokens as i32, + total_tokens: resp.usage.total_tokens as i32, + input_tokens_details: resp.usage.prompt_tokens_details.map(|details| { + crate::apis::openai_responses::TokenDetails { + cached_tokens: details.cached_tokens.unwrap_or(0) as i32, + } + }), + output_tokens_details: resp.usage.completion_tokens_details.map(|details| { + crate::apis::openai_responses::OutputTokenDetails { + reasoning_tokens: details.reasoning_tokens.unwrap_or(0) as i32, + } + }), + }; + + // Set incomplete_details if status is incomplete + let incomplete_details = if matches!(status, ResponseStatus::Incomplete) { + Some(IncompleteDetails { + reason: IncompleteReason::MaxOutputTokens, + }) + } else { + None + }; + + Ok(ResponsesAPIResponse { + // Generate proper resp_ prefixed ID if not already present + id: if resp.id.starts_with("resp_") { + resp.id + } else { + format!("resp_{}", uuid::Uuid::new_v4().to_string().replace("-", "")) + }, + object: "response".to_string(), + created_at: resp.created as i64, + status, + background: Some(false), + error: None, + incomplete_details, + instructions: None, + max_output_tokens: None, + max_tool_calls: None, + model: resp.model, + output, + usage: Some(usage), + parallel_tool_calls: true, + conversation: None, + previous_response_id: None, + tools: vec![], + tool_choice: "auto".to_string(), + temperature: 1.0, + top_p: 1.0, + metadata: resp.metadata.unwrap_or_default(), + truncation: None, + reasoning: Some(crate::apis::openai_responses::Reasoning { + effort: None, + summary: None, + }), + store: None, + text: None, + audio: None, + modalities: None, + service_tier: resp.service_tier, + top_logprobs: None, + }) + } +} + + impl TryFrom for ChatCompletionsResponse { type Error = TransformError; @@ -83,8 +257,7 @@ impl TryFrom for ChatCompletionsResponse { model: resp.model, choices: vec![choice], usage, - system_fingerprint: None, - service_tier: None, + ..Default::default() }) } } @@ -169,422 +342,11 @@ impl TryFrom for ChatCompletionsResponse { model, choices: vec![choice], usage, - system_fingerprint: None, - service_tier: None, + ..Default::default() }) } } -// ============================================================================ -// STREAMING TRANSFORMATIONS -// ============================================================================ - -impl TryFrom for ChatCompletionsStreamResponse { - type Error = TransformError; - - fn try_from(event: MessagesStreamEvent) -> Result { - match event { - MessagesStreamEvent::MessageStart { message } => Ok(create_openai_chunk( - &message.id, - &message.model, - MessageDelta { - role: Some(Role::Assistant), - content: None, - refusal: None, - function_call: None, - tool_calls: None, - }, - None, - None, - )), - - MessagesStreamEvent::ContentBlockStart { content_block, .. } => { - convert_content_block_start(content_block) - } - - MessagesStreamEvent::ContentBlockDelta { delta, .. } => convert_content_delta(delta), - - MessagesStreamEvent::ContentBlockStop { .. } => Ok(create_empty_openai_chunk()), - - MessagesStreamEvent::MessageDelta { delta, usage } => { - let finish_reason: Option = Some(delta.stop_reason.into()); - let openai_usage: Option = Some(usage.into()); - - Ok(create_openai_chunk( - "stream", - "unknown", - MessageDelta { - role: None, - content: None, - refusal: None, - function_call: None, - tool_calls: None, - }, - finish_reason, - openai_usage, - )) - } - - MessagesStreamEvent::MessageStop => Ok(create_openai_chunk( - "stream", - "unknown", - MessageDelta { - role: None, - content: None, - refusal: None, - function_call: None, - tool_calls: None, - }, - Some(FinishReason::Stop), - None, - )), - - MessagesStreamEvent::Ping => Ok(ChatCompletionsStreamResponse { - id: "stream".to_string(), - object: Some("chat.completion.chunk".to_string()), - created: current_timestamp(), - model: "unknown".to_string(), - choices: vec![], - usage: None, - system_fingerprint: None, - service_tier: None, - }), - } - } -} - -impl TryFrom for ChatCompletionsStreamResponse { - type Error = TransformError; - - fn try_from(event: ConverseStreamEvent) -> Result { - match event { - ConverseStreamEvent::MessageStart(start_event) => { - let role = match start_event.role { - crate::apis::amazon_bedrock::ConversationRole::User => Role::User, - crate::apis::amazon_bedrock::ConversationRole::Assistant => Role::Assistant, - }; - - Ok(create_openai_chunk( - "stream", - "unknown", - MessageDelta { - role: Some(role), - content: None, - refusal: None, - function_call: None, - tool_calls: None, - }, - None, - None, - )) - } - - ConverseStreamEvent::ContentBlockStart(start_event) => { - use crate::apis::amazon_bedrock::ContentBlockStart; - - match start_event.start { - ContentBlockStart::ToolUse { tool_use } => Ok(create_openai_chunk( - "stream", - "unknown", - MessageDelta { - role: None, - content: None, - refusal: None, - function_call: None, - tool_calls: Some(vec![ToolCallDelta { - index: start_event.content_block_index as u32, - id: Some(tool_use.tool_use_id), - call_type: Some("function".to_string()), - function: Some(FunctionCallDelta { - name: Some(tool_use.name), - arguments: Some("".to_string()), - }), - }]), - }, - None, - None, - )), - } - } - - ConverseStreamEvent::ContentBlockDelta(delta_event) => { - use crate::apis::amazon_bedrock::ContentBlockDelta; - - match delta_event.delta { - ContentBlockDelta::Text { text } => Ok(create_openai_chunk( - "stream", - "unknown", - MessageDelta { - role: None, - content: Some(text), - refusal: None, - function_call: None, - tool_calls: None, - }, - None, - None, - )), - ContentBlockDelta::ToolUse { tool_use } => Ok(create_openai_chunk( - "stream", - "unknown", - MessageDelta { - role: None, - content: None, - refusal: None, - function_call: None, - tool_calls: Some(vec![ToolCallDelta { - index: delta_event.content_block_index as u32, - id: None, - call_type: None, - function: Some(FunctionCallDelta { - name: None, - arguments: Some(tool_use.input), - }), - }]), - }, - None, - None, - )), - } - } - - ConverseStreamEvent::ContentBlockStop(_) => Ok(create_empty_openai_chunk()), - - ConverseStreamEvent::MessageStop(stop_event) => { - let finish_reason = match stop_event.stop_reason { - StopReason::EndTurn => FinishReason::Stop, - StopReason::ToolUse => FinishReason::ToolCalls, - StopReason::MaxTokens => FinishReason::Length, - StopReason::StopSequence => FinishReason::Stop, - StopReason::GuardrailIntervened => FinishReason::ContentFilter, - StopReason::ContentFiltered => FinishReason::ContentFilter, - }; - - Ok(create_openai_chunk( - "stream", - "unknown", - MessageDelta { - role: None, - content: None, - refusal: None, - function_call: None, - tool_calls: None, - }, - Some(finish_reason), - None, - )) - } - - ConverseStreamEvent::Metadata(metadata_event) => { - let usage = Usage { - prompt_tokens: metadata_event.usage.input_tokens, - completion_tokens: metadata_event.usage.output_tokens, - total_tokens: metadata_event.usage.total_tokens, - prompt_tokens_details: None, - completion_tokens_details: None, - }; - - Ok(create_openai_chunk( - "stream", - "unknown", - MessageDelta { - role: None, - content: None, - refusal: None, - function_call: None, - tool_calls: None, - }, - None, - Some(usage), - )) - } - - // Error events - convert to empty chunks (errors should be handled elsewhere) - ConverseStreamEvent::InternalServerException(_) - | ConverseStreamEvent::ModelStreamErrorException(_) - | ConverseStreamEvent::ServiceUnavailableException(_) - | ConverseStreamEvent::ThrottlingException(_) - | ConverseStreamEvent::ValidationException(_) => Ok(create_empty_openai_chunk()), - } - } -} - -/// Convert content block start to OpenAI chunk -fn convert_content_block_start( - content_block: MessagesContentBlock, -) -> Result { - match content_block { - MessagesContentBlock::Text { .. } => { - // No immediate output for text block start - Ok(create_empty_openai_chunk()) - } - MessagesContentBlock::ToolUse { id, name, .. } - | MessagesContentBlock::ServerToolUse { id, name, .. } - | MessagesContentBlock::McpToolUse { id, name, .. } => { - // Tool use start → OpenAI chunk with tool_calls - Ok(create_openai_chunk( - "stream", - "unknown", - MessageDelta { - role: None, - content: None, - refusal: None, - function_call: None, - tool_calls: Some(vec![ToolCallDelta { - index: 0, - id: Some(id), - call_type: Some("function".to_string()), - function: Some(FunctionCallDelta { - name: Some(name), - arguments: Some("".to_string()), - }), - }]), - }, - None, - None, - )) - } - _ => Err(TransformError::UnsupportedContent( - "Unsupported content block type in stream start".to_string(), - )), - } -} - -/// Convert content delta to OpenAI chunk -fn convert_content_delta( - delta: MessagesContentDelta, -) -> Result { - match delta { - MessagesContentDelta::TextDelta { text } => Ok(create_openai_chunk( - "stream", - "unknown", - MessageDelta { - role: None, - content: Some(text), - refusal: None, - function_call: None, - tool_calls: None, - }, - None, - None, - )), - MessagesContentDelta::ThinkingDelta { thinking } => Ok(create_openai_chunk( - "stream", - "unknown", - MessageDelta { - role: None, - content: Some(format!("thinking: {}", thinking)), - refusal: None, - function_call: None, - tool_calls: None, - }, - None, - None, - )), - MessagesContentDelta::InputJsonDelta { partial_json } => Ok(create_openai_chunk( - "stream", - "unknown", - MessageDelta { - role: None, - content: None, - refusal: None, - function_call: None, - tool_calls: Some(vec![ToolCallDelta { - index: 0, - id: None, - call_type: None, - function: Some(FunctionCallDelta { - name: None, - arguments: Some(partial_json), - }), - }]), - }, - None, - None, - )), - } -} - -/// Helper to create OpenAI streaming chunk -fn create_openai_chunk( - id: &str, - model: &str, - delta: MessageDelta, - finish_reason: Option, - usage: Option, -) -> ChatCompletionsStreamResponse { - ChatCompletionsStreamResponse { - id: id.to_string(), - object: Some("chat.completion.chunk".to_string()), - created: current_timestamp(), - model: model.to_string(), - choices: vec![StreamChoice { - index: 0, - delta, - finish_reason, - logprobs: None, - }], - usage, - system_fingerprint: None, - service_tier: None, - } -} - -/// Helper to create empty OpenAI streaming chunk -fn create_empty_openai_chunk() -> ChatCompletionsStreamResponse { - create_openai_chunk( - "stream", - "unknown", - MessageDelta { - role: None, - content: None, - refusal: None, - function_call: None, - tool_calls: None, - }, - None, - None, - ) -} - -/// Convert Anthropic content blocks to OpenAI message content -fn convert_anthropic_content_to_openai( - content: &[MessagesContentBlock], -) -> Result { - let mut text_parts = Vec::new(); - - for block in content { - match block { - MessagesContentBlock::Text { text, .. } => { - text_parts.push(text.clone()); - } - MessagesContentBlock::Thinking { thinking, .. } => { - text_parts.push(format!("thinking: {}", thinking)); - } - _ => { - // Skip other content types for basic text conversion - continue; - } - } - } - - Ok(MessageContent::Text(text_parts.join("\n"))) -} - -// Stop Reason Conversions -impl Into for MessagesStopReason { - fn into(self) -> FinishReason { - match self { - MessagesStopReason::EndTurn => FinishReason::Stop, - MessagesStopReason::MaxTokens => FinishReason::Length, - MessagesStopReason::StopSequence => FinishReason::Stop, - MessagesStopReason::ToolUse => FinishReason::ToolCalls, - MessagesStopReason::PauseTurn => FinishReason::Stop, - MessagesStopReason::Refusal => FinishReason::ContentFilter, - } - } -} - /// Convert Bedrock Message to OpenAI content and tool calls /// This function extracts text content and tool calls from a Bedrock message fn convert_bedrock_message_to_openai( @@ -629,6 +391,31 @@ fn convert_bedrock_message_to_openai( Ok((content, tool_calls)) } +/// Convert Anthropic content blocks to OpenAI message content +fn convert_anthropic_content_to_openai( + content: &[MessagesContentBlock], +) -> Result { + let mut text_parts = Vec::new(); + + for block in content { + match block { + MessagesContentBlock::Text { text, .. } => { + text_parts.push(text.clone()); + } + MessagesContentBlock::Thinking { thinking, .. } => { + text_parts.push(format!("thinking: {}", thinking)); + } + _ => { + // Skip other content types for basic text conversion + continue; + } + } + } + + Ok(MessageContent::Text(text_parts.join("\n"))) +} + + #[cfg(test)] mod tests { use super::*; @@ -1168,4 +955,214 @@ mod tests { assert!(content.contains("Here's the analysis:")); // Note: Image blocks are not converted to text in the current implementation } + + #[test] + fn test_chat_completions_to_responses_api_basic() { + use crate::apis::openai_responses::{OutputContent, OutputItem, ResponsesAPIResponse}; + + let chat_response = ChatCompletionsResponse { + id: "resp_6de5512800cf4375a329a473a4f02879".to_string(), + object: Some("chat.completion".to_string()), + created: 1677652288, + model: "gpt-4".to_string(), + choices: vec![Choice { + index: 0, + message: crate::apis::openai::ResponseMessage { + role: Role::Assistant, + content: Some("Hello! How can I help you?".to_string()), + refusal: None, + annotations: None, + audio: None, + function_call: None, + tool_calls: None, + }, + finish_reason: Some(FinishReason::Stop), + logprobs: None, + }], + usage: Usage { + prompt_tokens: 10, + completion_tokens: 20, + total_tokens: 30, + prompt_tokens_details: None, + completion_tokens_details: None, + }, + system_fingerprint: None, + service_tier: Some("default".to_string()), + metadata: None, + }; + + let responses_api: ResponsesAPIResponse = chat_response.try_into().unwrap(); + + // Response ID should be generated with resp_ prefix + assert!(responses_api.id.starts_with("resp_"), "Response ID should start with 'resp_'"); + assert_eq!(responses_api.id.len(), 37, "Response ID should be resp_ + 32 char UUID"); + assert_eq!(responses_api.object, "response"); + assert_eq!(responses_api.model, "gpt-4"); + + // Check usage conversion + let usage = responses_api.usage.unwrap(); + assert_eq!(usage.input_tokens, 10); + assert_eq!(usage.output_tokens, 20); + assert_eq!(usage.total_tokens, 30); + + // Check output items + assert_eq!(responses_api.output.len(), 1); + match &responses_api.output[0] { + OutputItem::Message { + role, + content, + .. + } => { + assert_eq!(role, "assistant"); + assert_eq!(content.len(), 1); + match &content[0] { + OutputContent::OutputText { text, .. } => { + assert_eq!(text, "Hello! How can I help you?"); + } + _ => panic!("Expected OutputText content"), + } + } + _ => panic!("Expected Message output item"), + } + } + + #[test] + fn test_chat_completions_to_responses_api_with_tool_calls() { + use crate::apis::openai::{FunctionCall, ToolCall}; + use crate::apis::openai_responses::{OutputItem, ResponsesAPIResponse}; + + let chat_response = ChatCompletionsResponse { + id: "chatcmpl-456".to_string(), + object: Some("chat.completion".to_string()), + created: 1677652300, + model: "gpt-4".to_string(), + choices: vec![Choice { + index: 0, + message: crate::apis::openai::ResponseMessage { + role: Role::Assistant, + content: Some("Let me check the weather.".to_string()), + refusal: None, + annotations: None, + audio: None, + function_call: None, + tool_calls: Some(vec![ToolCall { + id: "call_abc123".to_string(), + call_type: "function".to_string(), + function: FunctionCall { + name: "get_weather".to_string(), + arguments: r#"{"location":"San Francisco"}"#.to_string(), + }, + }]), + }, + finish_reason: Some(FinishReason::ToolCalls), + logprobs: None, + }], + usage: Usage { + prompt_tokens: 15, + completion_tokens: 25, + total_tokens: 40, + prompt_tokens_details: None, + completion_tokens_details: None, + }, + system_fingerprint: None, + service_tier: None, + metadata: None, + }; + + let responses_api: ResponsesAPIResponse = chat_response.try_into().unwrap(); + + // Should have 2 output items: message + function call + assert_eq!(responses_api.output.len(), 2); + + // Check message item + match &responses_api.output[0] { + OutputItem::Message { content, .. } => { + assert_eq!(content.len(), 1); + } + _ => panic!("Expected Message output item"), + } + + // Check function call item + match &responses_api.output[1] { + OutputItem::FunctionCall { + call_id, + name, + arguments, + .. + } => { + assert_eq!(call_id, "call_abc123"); + assert_eq!(name.as_ref().unwrap(), "get_weather"); + assert!(arguments.as_ref().unwrap().contains("San Francisco")); + } + _ => panic!("Expected FunctionCall output item"), + } + } + + #[test] + fn test_chat_completions_to_responses_api_tool_calls_only() { + use crate::apis::openai::{FunctionCall, ToolCall}; + use crate::apis::openai_responses::{OutputItem, ResponsesAPIResponse}; + + // Test the real-world case where content is null and there are only tool calls + let chat_response = ChatCompletionsResponse { + id: "chatcmpl-789".to_string(), + object: Some("chat.completion".to_string()), + created: 1764023939, + model: "gpt-4o-2024-08-06".to_string(), + choices: vec![Choice { + index: 0, + message: crate::apis::openai::ResponseMessage { + role: Role::Assistant, + content: None, // No text content, only tool calls + refusal: None, + annotations: None, + audio: None, + function_call: None, + tool_calls: Some(vec![ToolCall { + id: "call_oJBtqTJmRfBGlFS55QhMfUUV".to_string(), + call_type: "function".to_string(), + function: FunctionCall { + name: "get_weather".to_string(), + arguments: r#"{"location":"San Francisco, CA"}"#.to_string(), + }, + }]), + }, + finish_reason: Some(FinishReason::ToolCalls), + logprobs: None, + }], + usage: Usage { + prompt_tokens: 84, + completion_tokens: 17, + total_tokens: 101, + prompt_tokens_details: None, + completion_tokens_details: None, + }, + system_fingerprint: Some("fp_7eeb46f068".to_string()), + service_tier: Some("default".to_string()), + metadata: None, + }; + + let responses_api: ResponsesAPIResponse = chat_response.try_into().unwrap(); + + // Should have only 1 output item: function call (no empty message item) + assert_eq!(responses_api.output.len(), 1); + + // Check function call item + match &responses_api.output[0] { + OutputItem::FunctionCall { + call_id, + name, + arguments, + .. + } => { + assert_eq!(call_id, "call_oJBtqTJmRfBGlFS55QhMfUUV"); + assert_eq!(name.as_ref().unwrap(), "get_weather"); + assert!(arguments.as_ref().unwrap().contains("San Francisco, CA")); + } + _ => panic!("Expected FunctionCall output item as first item"), + } + + // Verify status is Completed for tool_calls finish reason + assert!(matches!(responses_api.status, crate::apis::openai_responses::ResponseStatus::Completed)); + } } diff --git a/crates/hermesllm/src/transforms/response_streaming/mod.rs b/crates/hermesllm/src/transforms/response_streaming/mod.rs new file mode 100644 index 00000000..fb06cce3 --- /dev/null +++ b/crates/hermesllm/src/transforms/response_streaming/mod.rs @@ -0,0 +1,2 @@ +pub mod to_anthropic_streaming; +pub mod to_openai_streaming; diff --git a/crates/hermesllm/src/transforms/response_streaming/to_anthropic_streaming.rs b/crates/hermesllm/src/transforms/response_streaming/to_anthropic_streaming.rs new file mode 100644 index 00000000..b8cac631 --- /dev/null +++ b/crates/hermesllm/src/transforms/response_streaming/to_anthropic_streaming.rs @@ -0,0 +1,281 @@ +use crate::apis::amazon_bedrock::{ + ContentBlockDelta, ConverseStreamEvent, +}; +use crate::apis::anthropic::{ + MessagesContentBlock, MessagesContentDelta, MessagesMessageDelta, + MessagesRole, MessagesStopReason, MessagesStreamEvent, MessagesStreamMessage, MessagesUsage, +}; +use crate::apis::openai::{ ChatCompletionsStreamResponse, ToolCallDelta, +}; +use crate::clients::TransformError; +use serde_json::Value; + +impl TryFrom for MessagesStreamEvent { + type Error = TransformError; + + fn try_from(resp: ChatCompletionsStreamResponse) -> Result { + if resp.choices.is_empty() { + return Ok(MessagesStreamEvent::Ping); + } + + let choice = &resp.choices[0]; + + // Handle final chunk with usage + let has_usage = resp.usage.is_some(); + if let Some(usage) = resp.usage { + if let Some(finish_reason) = &choice.finish_reason { + let anthropic_stop_reason: MessagesStopReason = finish_reason.clone().into(); + return Ok(MessagesStreamEvent::MessageDelta { + delta: MessagesMessageDelta { + stop_reason: anthropic_stop_reason, + stop_sequence: None, + }, + usage: usage.into(), + }); + } + } + + // NOTE: We do NOT emit MessageStart here anymore! + // The AnthropicMessagesStreamBuffer will inject message_start and content_block_start + // when it sees the first content_block_delta. This solves the problem where OpenAI + // sends both role and content in the same chunk - we can only return one event here, + // so we prioritize the content and let the buffer handle lifecycle events. + + // Handle content delta (even if role is present in the same chunk) + if let Some(content) = &choice.delta.content { + if !content.is_empty() { + return Ok(MessagesStreamEvent::ContentBlockDelta { + index: 0, + delta: MessagesContentDelta::TextDelta { + text: content.clone(), + }, + }); + } + } + + // Handle tool calls + if let Some(tool_calls) = &choice.delta.tool_calls { + return convert_tool_call_deltas(tool_calls.clone()); + } + + // Handle finish reason - generate MessageDelta only (MessageStop comes later) + if let Some(finish_reason) = &choice.finish_reason { + // If we have usage data, it was already handled above + // If not, we need to generate MessageDelta with default usage + if !has_usage { + let anthropic_stop_reason: MessagesStopReason = finish_reason.clone().into(); + return Ok(MessagesStreamEvent::MessageDelta { + delta: MessagesMessageDelta { + stop_reason: anthropic_stop_reason, + stop_sequence: None, + }, + usage: MessagesUsage { + input_tokens: 0, + output_tokens: 0, + cache_creation_input_tokens: None, + cache_read_input_tokens: None, + }, + }); + } + // If usage was already handled above, we don't need to do anything more here + // MessageStop will be handled when [DONE] is encountered + } + + // Default to ping for unhandled cases + Ok(MessagesStreamEvent::Ping) + } +} + +impl Into for MessagesStreamEvent { + fn into(self) -> String { + let transformed_json = serde_json::to_string(&self).unwrap_or_default(); + let event_type = match &self { + MessagesStreamEvent::MessageStart { .. } => "message_start", + MessagesStreamEvent::ContentBlockStart { .. } => "content_block_start", + MessagesStreamEvent::ContentBlockDelta { .. } => "content_block_delta", + MessagesStreamEvent::ContentBlockStop { .. } => "content_block_stop", + MessagesStreamEvent::MessageDelta { .. } => "message_delta", + MessagesStreamEvent::MessageStop => "message_stop", + MessagesStreamEvent::Ping => "ping", + }; + + let event = format!("event: {}\n", event_type); + let data = format!("data: {}\n\n", transformed_json); + event + &data + } +} + +impl TryFrom for MessagesStreamEvent { + type Error = TransformError; + + fn try_from(event: ConverseStreamEvent) -> Result { + match event { + // MessageStart - convert to Anthropic MessageStart + ConverseStreamEvent::MessageStart(start_event) => { + let role = match start_event.role { + crate::apis::amazon_bedrock::ConversationRole::User => MessagesRole::User, + crate::apis::amazon_bedrock::ConversationRole::Assistant => { + MessagesRole::Assistant + } + }; + + Ok(MessagesStreamEvent::MessageStart { + message: MessagesStreamMessage { + id: format!( + "bedrock-stream-{}", + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_nanos() + ), + obj_type: "message".to_string(), + role, + content: vec![], + model: "bedrock-model".to_string(), + stop_reason: None, + stop_sequence: None, + usage: MessagesUsage { + input_tokens: 0, + output_tokens: 0, + cache_creation_input_tokens: None, + cache_read_input_tokens: None, + }, + }, + }) + } + + // ContentBlockStart - convert to Anthropic ContentBlockStart + ConverseStreamEvent::ContentBlockStart(start_event) => { + // Note: Bedrock sends tool_use_id and name at start, with input coming in subsequent deltas + // Anthropic expects the same pattern, so we initialize with an empty input object + match start_event.start { + crate::apis::amazon_bedrock::ContentBlockStart::ToolUse { tool_use } => { + Ok(MessagesStreamEvent::ContentBlockStart { + index: start_event.content_block_index as u32, + content_block: MessagesContentBlock::ToolUse { + id: tool_use.tool_use_id, + name: tool_use.name, + input: Value::Object(serde_json::Map::new()), // Empty - will be filled by deltas + cache_control: None, + }, + }) + } + } + } + + // ContentBlockDelta - convert to Anthropic ContentBlockDelta + ConverseStreamEvent::ContentBlockDelta(delta_event) => { + let delta = match delta_event.delta { + ContentBlockDelta::Text { text } => MessagesContentDelta::TextDelta { text }, + ContentBlockDelta::ToolUse { tool_use } => { + MessagesContentDelta::InputJsonDelta { + partial_json: tool_use.input, + } + } + }; + + Ok(MessagesStreamEvent::ContentBlockDelta { + index: delta_event.content_block_index as u32, + delta, + }) + } + + // ContentBlockStop - convert to Anthropic ContentBlockStop + ConverseStreamEvent::ContentBlockStop(stop_event) => { + Ok(MessagesStreamEvent::ContentBlockStop { + index: stop_event.content_block_index as u32, + }) + } + + // MessageStop - convert to Anthropic MessageDelta with stop reason + // Note: Bedrock sends Metadata separately with usage info, creating a second MessageDelta + // The client should merge these or use the final one with complete usage + ConverseStreamEvent::MessageStop(stop_event) => { + let anthropic_stop_reason = match stop_event.stop_reason { + crate::apis::amazon_bedrock::StopReason::EndTurn => MessagesStopReason::EndTurn, + crate::apis::amazon_bedrock::StopReason::ToolUse => MessagesStopReason::ToolUse, + crate::apis::amazon_bedrock::StopReason::MaxTokens => MessagesStopReason::MaxTokens, + crate::apis::amazon_bedrock::StopReason::StopSequence => MessagesStopReason::EndTurn, + crate::apis::amazon_bedrock::StopReason::GuardrailIntervened => MessagesStopReason::Refusal, + crate::apis::amazon_bedrock::StopReason::ContentFiltered => MessagesStopReason::Refusal, + }; + + Ok(MessagesStreamEvent::MessageDelta { + delta: MessagesMessageDelta { + stop_reason: anthropic_stop_reason, + stop_sequence: None, + }, + usage: MessagesUsage { + input_tokens: 0, + output_tokens: 0, + cache_creation_input_tokens: None, + cache_read_input_tokens: None, + }, + }) + } + + // Metadata - convert usage information to MessageDelta + ConverseStreamEvent::Metadata(metadata_event) => { + Ok(MessagesStreamEvent::MessageDelta { + delta: MessagesMessageDelta { + stop_reason: MessagesStopReason::EndTurn, + stop_sequence: None, + }, + usage: MessagesUsage { + input_tokens: metadata_event.usage.input_tokens, + output_tokens: metadata_event.usage.output_tokens, + cache_creation_input_tokens: metadata_event.usage.cache_write_input_tokens, + cache_read_input_tokens: metadata_event.usage.cache_read_input_tokens, + }, + }) + } + + // Exception events - convert to Ping (could be enhanced to return error events) + ConverseStreamEvent::InternalServerException(_) + | ConverseStreamEvent::ModelStreamErrorException(_) + | ConverseStreamEvent::ServiceUnavailableException(_) + | ConverseStreamEvent::ThrottlingException(_) + | ConverseStreamEvent::ValidationException(_) => { + // TODO: Consider adding proper error handling/events + Ok(MessagesStreamEvent::Ping) + } + } + } +} + +/// Convert tool call deltas to Anthropic stream events +fn convert_tool_call_deltas( + tool_calls: Vec, +) -> Result { + for tool_call in tool_calls { + if let Some(id) = &tool_call.id { + // Tool call start + if let Some(function) = &tool_call.function { + if let Some(name) = &function.name { + return Ok(MessagesStreamEvent::ContentBlockStart { + index: tool_call.index, + content_block: MessagesContentBlock::ToolUse { + id: id.clone(), + name: name.clone(), + input: Value::Object(serde_json::Map::new()), + cache_control: None, + }, + }); + } + } + } else if let Some(function) = &tool_call.function { + if let Some(arguments) = &function.arguments { + // Tool arguments delta + return Ok(MessagesStreamEvent::ContentBlockDelta { + index: tool_call.index, + delta: MessagesContentDelta::InputJsonDelta { + partial_json: arguments.clone(), + }, + }); + } + } + } + + // Fallback to ping if no valid tool call found + Ok(MessagesStreamEvent::Ping) +} diff --git a/crates/hermesllm/src/transforms/response_streaming/to_openai_streaming.rs b/crates/hermesllm/src/transforms/response_streaming/to_openai_streaming.rs new file mode 100644 index 00000000..ca3d049b --- /dev/null +++ b/crates/hermesllm/src/transforms/response_streaming/to_openai_streaming.rs @@ -0,0 +1,546 @@ +use crate::apis::amazon_bedrock::{ ConverseStreamEvent, StopReason}; +use crate::apis::anthropic::{ + MessagesContentBlock, MessagesContentDelta, MessagesStopReason, MessagesStreamEvent}; +use crate::apis::openai::{ ChatCompletionsStreamResponse,FinishReason, + FunctionCallDelta, MessageDelta, Role, StreamChoice, ToolCallDelta, Usage, +}; +use crate::apis::openai_responses::ResponsesAPIStreamEvent; + +use crate::clients::TransformError; +use crate::transforms::lib::*; + +// ============================================================================ +// PROVIDER STREAMING TRANSFORMATIONS TO OPENAI FORMAT +// ============================================================================ +// +// This module handles business logic for converting streaming events from +// various providers (Anthropic, Bedrock, etc.) into OpenAI's ChatCompletions format. +// +// # Architecture Separation +// +// **Provider Transformations** (this module): +// - Business logic for converting between provider formats +// - Uses Rust traits (TryFrom, Into) for type-safe conversions +// - Stateless event-by-event transformation +// - Example: MessagesStreamEvent → ChatCompletionsStreamResponse +// +// **Wire Format Buffering** (`apis/streaming_shapes/`): +// - SSE protocol handling (data:, event: lines) +// - State accumulation and lifecycle management +// - Buffering for stateful APIs (v1/responses) +// - Example: ChatCompletionsToResponsesTransformer +// +// # Flow +// +// ```text +// Anthropic Event → [Provider Transform] → OpenAI Event → [Wire Buffer] → SSE Wire Format +// (business) (this module) (protocol) (streaming_shapes) (network) +// ``` +// +// ============================================================================ + +impl TryFrom for ChatCompletionsStreamResponse { + type Error = TransformError; + + fn try_from(event: MessagesStreamEvent) -> Result { + match event { + MessagesStreamEvent::MessageStart { message } => Ok(create_openai_chunk( + &message.id, + &message.model, + MessageDelta { + role: Some(Role::Assistant), + content: None, + refusal: None, + function_call: None, + tool_calls: None, + }, + None, + None, + )), + + MessagesStreamEvent::ContentBlockStart { content_block, index } => { + convert_content_block_start(content_block, index) + } + + MessagesStreamEvent::ContentBlockDelta { delta, index } => convert_content_delta(delta, index), + + MessagesStreamEvent::ContentBlockStop { .. } => Ok(create_empty_openai_chunk()), + + MessagesStreamEvent::MessageDelta { delta, usage } => { + let finish_reason: Option = Some(delta.stop_reason.into()); + let openai_usage: Option = Some(usage.into()); + + Ok(create_openai_chunk( + "stream", + "unknown", + MessageDelta { + role: None, + content: None, + refusal: None, + function_call: None, + tool_calls: None, + }, + finish_reason, + openai_usage, + )) + } + + MessagesStreamEvent::MessageStop => Ok(create_openai_chunk( + "stream", + "unknown", + MessageDelta { + role: None, + content: None, + refusal: None, + function_call: None, + tool_calls: None, + }, + Some(FinishReason::Stop), + None, + )), + + MessagesStreamEvent::Ping => Ok(ChatCompletionsStreamResponse { + id: "stream".to_string(), + object: Some("chat.completion.chunk".to_string()), + created: current_timestamp(), + model: "unknown".to_string(), + choices: vec![], + usage: None, + system_fingerprint: None, + service_tier: None, + }), + } + } +} + +impl TryFrom for ChatCompletionsStreamResponse { + type Error = TransformError; + + fn try_from(event: ConverseStreamEvent) -> Result { + match event { + ConverseStreamEvent::MessageStart(start_event) => { + let role = match start_event.role { + crate::apis::amazon_bedrock::ConversationRole::User => Role::User, + crate::apis::amazon_bedrock::ConversationRole::Assistant => Role::Assistant, + }; + + Ok(create_openai_chunk( + "stream", + "unknown", + MessageDelta { + role: Some(role), + content: None, + refusal: None, + function_call: None, + tool_calls: None, + }, + None, + None, + )) + } + + ConverseStreamEvent::ContentBlockStart(start_event) => { + use crate::apis::amazon_bedrock::ContentBlockStart; + + match start_event.start { + ContentBlockStart::ToolUse { tool_use } => Ok(create_openai_chunk( + "stream", + "unknown", + MessageDelta { + role: None, + content: None, + refusal: None, + function_call: None, + tool_calls: Some(vec![ToolCallDelta { + index: start_event.content_block_index as u32, + id: Some(tool_use.tool_use_id), + call_type: Some("function".to_string()), + function: Some(FunctionCallDelta { + name: Some(tool_use.name), + arguments: Some("".to_string()), + }), + }]), + }, + None, + None, + )), + } + } + + ConverseStreamEvent::ContentBlockDelta(delta_event) => { + use crate::apis::amazon_bedrock::ContentBlockDelta; + + match delta_event.delta { + ContentBlockDelta::Text { text } => Ok(create_openai_chunk( + "stream", + "unknown", + MessageDelta { + role: None, + content: Some(text), + refusal: None, + function_call: None, + tool_calls: None, + }, + None, + None, + )), + ContentBlockDelta::ToolUse { tool_use } => Ok(create_openai_chunk( + "stream", + "unknown", + MessageDelta { + role: None, + content: None, + refusal: None, + function_call: None, + tool_calls: Some(vec![ToolCallDelta { + index: delta_event.content_block_index as u32, + id: None, + call_type: None, + function: Some(FunctionCallDelta { + name: None, + arguments: Some(tool_use.input), + }), + }]), + }, + None, + None, + )), + } + } + + ConverseStreamEvent::ContentBlockStop(_) => Ok(create_empty_openai_chunk()), + + ConverseStreamEvent::MessageStop(stop_event) => { + let finish_reason = match stop_event.stop_reason { + StopReason::EndTurn => FinishReason::Stop, + StopReason::ToolUse => FinishReason::ToolCalls, + StopReason::MaxTokens => FinishReason::Length, + StopReason::StopSequence => FinishReason::Stop, + StopReason::GuardrailIntervened => FinishReason::ContentFilter, + StopReason::ContentFiltered => FinishReason::ContentFilter, + }; + + Ok(create_openai_chunk( + "stream", + "unknown", + MessageDelta { + role: None, + content: None, + refusal: None, + function_call: None, + tool_calls: None, + }, + Some(finish_reason), + None, + )) + } + + ConverseStreamEvent::Metadata(metadata_event) => { + let usage = Usage { + prompt_tokens: metadata_event.usage.input_tokens, + completion_tokens: metadata_event.usage.output_tokens, + total_tokens: metadata_event.usage.total_tokens, + prompt_tokens_details: None, + completion_tokens_details: None, + }; + + Ok(create_openai_chunk( + "stream", + "unknown", + MessageDelta { + role: None, + content: None, + refusal: None, + function_call: None, + tool_calls: None, + }, + None, + Some(usage), + )) + } + + // Error events - convert to empty chunks (errors should be handled elsewhere) + ConverseStreamEvent::InternalServerException(_) + | ConverseStreamEvent::ModelStreamErrorException(_) + | ConverseStreamEvent::ServiceUnavailableException(_) + | ConverseStreamEvent::ThrottlingException(_) + | ConverseStreamEvent::ValidationException(_) => Ok(create_empty_openai_chunk()), + } + } +} + +/// Convert content block start to OpenAI chunk +fn convert_content_block_start( + content_block: MessagesContentBlock, + index: u32, +) -> Result { + match content_block { + MessagesContentBlock::Text { .. } => { + // No immediate output for text block start + Ok(create_empty_openai_chunk()) + } + MessagesContentBlock::ToolUse { id, name, .. } + | MessagesContentBlock::ServerToolUse { id, name, .. } + | MessagesContentBlock::McpToolUse { id, name, .. } => { + // Tool use start → OpenAI chunk with tool_calls + Ok(create_openai_chunk( + "stream", + "unknown", + MessageDelta { + role: None, + content: None, + refusal: None, + function_call: None, + tool_calls: Some(vec![ToolCallDelta { + index, + id: Some(id), + call_type: Some("function".to_string()), + function: Some(FunctionCallDelta { + name: Some(name), + arguments: Some("".to_string()), + }), + }]), + }, + None, + None, + )) + } + _ => Err(TransformError::UnsupportedContent( + "Unsupported content block type in stream start".to_string(), + )), + } +} + +/// Convert content delta to OpenAI chunk +fn convert_content_delta( + delta: MessagesContentDelta, + index: u32, +) -> Result { + match delta { + MessagesContentDelta::TextDelta { text } => Ok(create_openai_chunk( + "stream", + "unknown", + MessageDelta { + role: None, + content: Some(text), + refusal: None, + function_call: None, + tool_calls: None, + }, + None, + None, + )), + MessagesContentDelta::ThinkingDelta { thinking } => Ok(create_openai_chunk( + "stream", + "unknown", + MessageDelta { + role: None, + content: Some(format!("thinking: {}", thinking)), + refusal: None, + function_call: None, + tool_calls: None, + }, + None, + None, + )), + MessagesContentDelta::InputJsonDelta { partial_json } => Ok(create_openai_chunk( + "stream", + "unknown", + MessageDelta { + role: None, + content: None, + refusal: None, + function_call: None, + tool_calls: Some(vec![ToolCallDelta { + index, + id: None, + call_type: None, + function: Some(FunctionCallDelta { + name: None, + arguments: Some(partial_json), + }), + }]), + }, + None, + None, + )), + MessagesContentDelta::SignatureDelta { signature: _ } => { + // Signature delta is cryptographic verification metadata, not content + // Create an empty delta chunk to maintain stream continuity + Ok(create_openai_chunk( + "stream", + "unknown", + MessageDelta { + role: None, + content: None, + refusal: None, + function_call: None, + tool_calls: None, + }, + None, + None, + )) + } + } +} + +/// Helper to create OpenAI streaming chunk +fn create_openai_chunk( + id: &str, + model: &str, + delta: MessageDelta, + finish_reason: Option, + usage: Option, +) -> ChatCompletionsStreamResponse { + ChatCompletionsStreamResponse { + id: id.to_string(), + object: Some("chat.completion.chunk".to_string()), + created: current_timestamp(), + model: model.to_string(), + choices: vec![StreamChoice { + index: 0, + delta, + finish_reason, + logprobs: None, + }], + usage, + system_fingerprint: None, + service_tier: None, + } +} + +/// Helper to create empty OpenAI streaming chunk +fn create_empty_openai_chunk() -> ChatCompletionsStreamResponse { + create_openai_chunk( + "stream", + "unknown", + MessageDelta { + role: None, + content: None, + refusal: None, + function_call: None, + tool_calls: None, + }, + None, + None, + ) +} + +// Stop Reason Conversions +impl Into for MessagesStopReason { + fn into(self) -> FinishReason { + match self { + MessagesStopReason::EndTurn => FinishReason::Stop, + MessagesStopReason::MaxTokens => FinishReason::Length, + MessagesStopReason::StopSequence => FinishReason::Stop, + MessagesStopReason::ToolUse => FinishReason::ToolCalls, + MessagesStopReason::PauseTurn => FinishReason::Stop, + MessagesStopReason::Refusal => FinishReason::ContentFilter, + } + } +} + +impl TryFrom for ResponsesAPIStreamEvent { + type Error = TransformError; + + fn try_from(chunk: ChatCompletionsStreamResponse) -> Result { + // Stateless conversion - just extract the delta information + // The buffer will manage state, item IDs, and sequence numbers + + // Extract first choice if available + if let Some(choice) = chunk.choices.first() { + let delta = &choice.delta; + + // Tool call with function name and/or arguments + if let Some(tool_calls) = &delta.tool_calls { + if let Some(tool_call) = tool_calls.first() { + // Extract call_id and name if available (metadata from initial event) + let call_id = tool_call.id.clone(); + let function_name = tool_call.function.as_ref() + .and_then(|f| f.name.clone()); + + // Check if we have function metadata (name, id) + if let Some(function) = &tool_call.function { + // If we have arguments delta, return that + if let Some(args) = &function.arguments { + return Ok(ResponsesAPIStreamEvent::ResponseFunctionCallArgumentsDelta { + output_index: choice.index as i32, + item_id: "".to_string(), // Buffer will fill this + delta: args.clone(), + sequence_number: 0, // Buffer will fill this + call_id, + name: function_name, + }); + } + + // If we have function name but no arguments yet (initial tool call event) + // Return an empty arguments delta so the buffer knows to create the item + if function.name.is_some() { + return Ok(ResponsesAPIStreamEvent::ResponseFunctionCallArgumentsDelta { + output_index: choice.index as i32, + item_id: "".to_string(), // Buffer will fill this + delta: "".to_string(), // Empty delta signals this is the initial event + sequence_number: 0, // Buffer will fill this + call_id, + name: function_name, + }); + } + } + } + } + + // Text content delta + if let Some(content) = &delta.content { + if !content.is_empty() { + return Ok(ResponsesAPIStreamEvent::ResponseOutputTextDelta { + item_id: "".to_string(), // Buffer will fill this + output_index: choice.index as i32, + content_index: 0, + delta: content.clone(), + logprobs: vec![], + obfuscation: None, + sequence_number: 0, // Buffer will fill this + }); + } + } + + // Handle finish_reason - this is a completion signal + // Return an empty delta that the buffer can use to detect completion + if choice.finish_reason.is_some() { + // Return a minimal text delta to signal completion + // The buffer will handle the finish_reason and generate response.completed + return Ok(ResponsesAPIStreamEvent::ResponseOutputTextDelta { + item_id: "".to_string(), // Buffer will fill this + output_index: choice.index as i32, + content_index: 0, + delta: "".to_string(), // Empty delta signals completion + logprobs: vec![], + obfuscation: None, + sequence_number: 0, // Buffer will fill this + }); + } + + // Empty delta with role only (common at stream start) + if delta.role.is_some() { + // This is typically the first chunk establishing the assistant role + // Return an empty text delta that the buffer can use to initialize state + return Ok(ResponsesAPIStreamEvent::ResponseOutputTextDelta { + item_id: "".to_string(), + output_index: choice.index as i32, + content_index: 0, + delta: "".to_string(), + logprobs: vec![], + obfuscation: None, + sequence_number: 0, + }); + } + } + + // Empty chunk or no convertible content (e.g., keep-alive chunks with delta: {}) + // These are valid in OpenAI streaming and should be silently ignored + // Return error so the caller can skip these chunks without warnings + Err(TransformError::UnsupportedConversion( + "Empty or keep-alive chunk with no convertible content".to_string(), + )) + } +} diff --git a/crates/llm_gateway/src/filter_context.rs b/crates/llm_gateway/src/filter_context.rs index 2b8e1a95..3a1f7b84 100644 --- a/crates/llm_gateway/src/filter_context.rs +++ b/crates/llm_gateway/src/filter_context.rs @@ -2,26 +2,18 @@ use crate::metrics::Metrics; use crate::stream_context::StreamContext; use common::configuration::Configuration; use common::configuration::Overrides; -use common::consts::OTEL_COLLECTOR_HTTP; -use common::consts::OTEL_POST_PATH; -use common::http::CallArgs; use common::http::Client; use common::llm_providers::LlmProviders; use common::ratelimit; use common::stats::Gauge; -use common::tracing::TraceData; use log::trace; -use log::warn; use proxy_wasm::traits::*; use proxy_wasm::types::*; use std::cell::RefCell; use std::collections::HashMap; -use std::collections::VecDeque; use std::rc::Rc; use std::time::Duration; -use std::sync::{Arc, Mutex}; - #[derive(Debug)] pub struct CallContext {} @@ -31,7 +23,6 @@ pub struct FilterContext { // callouts stores token_id to request mapping that we use during #on_http_call_response to match the response to the request. callouts: RefCell>, llm_providers: Option>, - traces_queue: Arc>>, overrides: Rc>, } @@ -41,7 +32,6 @@ impl FilterContext { callouts: RefCell::new(HashMap::new()), metrics: Rc::new(Metrics::new()), llm_providers: None, - traces_queue: Arc::new(Mutex::new(VecDeque::new())), overrides: Rc::new(None), } } @@ -95,7 +85,6 @@ impl RootContext for FilterContext { .as_ref() .expect("LLM Providers must exist when Streams are being created"), ), - Arc::clone(&self.traces_queue), Rc::clone(&self.overrides), ))) } @@ -108,34 +97,6 @@ impl RootContext for FilterContext { self.set_tick_period(Duration::from_secs(1)); true } - - fn on_tick(&mut self) { - let _ = self.traces_queue.try_lock().map(|mut traces_queue| { - while let Some(trace) = traces_queue.pop_front() { - let trace_str = serde_json::to_string(&trace).unwrap(); - trace!("trace details: {}", trace_str); - let call_args = CallArgs::new( - OTEL_COLLECTOR_HTTP, - OTEL_POST_PATH, - vec![ - (":method", http::Method::POST.as_str()), - (":path", OTEL_POST_PATH), - (":authority", OTEL_COLLECTOR_HTTP), - ("content-type", "application/json"), - ], - Some(trace_str.as_bytes()), - vec![], - Duration::from_secs(60), - ); - if let Err(error) = self.http_call(call_args, CallContext {}) { - warn!( - "failed to schedule http call to otel-collector: {:?}", - error - ); - } - } - }); - } } impl Context for FilterContext { diff --git a/crates/llm_gateway/src/stream_context.rs b/crates/llm_gateway/src/stream_context.rs index 1098185d..1fa1a418 100644 --- a/crates/llm_gateway/src/stream_context.rs +++ b/crates/llm_gateway/src/stream_context.rs @@ -4,10 +4,8 @@ use log::{debug, info, warn}; use proxy_wasm::hostcalls::get_current_time; use proxy_wasm::traits::*; use proxy_wasm::types::*; -use std::collections::VecDeque; use std::num::NonZero; use std::rc::Rc; -use std::sync::{Arc, Mutex}; use std::time::{Duration, SystemTime, UNIX_EPOCH}; use crate::metrics::Metrics; @@ -20,13 +18,13 @@ use common::errors::ServerError; use common::llm_providers::LlmProviders; use common::ratelimit::Header; use common::stats::{IncrementingMetric, RecordingMetric}; -use common::tracing::{Event, Span, TraceData, Traceparent}; use common::{ratelimit, routing, tokenizer}; -use hermesllm::apis::amazon_bedrock_binary_frame::BedrockBinaryFrameDecoder; -use hermesllm::apis::anthropic::{MessagesContentBlock, MessagesStreamEvent}; -use hermesllm::apis::sse::{SseEvent, SseStreamIter}; -use hermesllm::clients::endpoints::SupportedAPIs; +use hermesllm::apis::streaming_shapes::amazon_bedrock_binary_frame::BedrockBinaryFrameDecoder; +use hermesllm::apis::streaming_shapes::sse::{SseEvent, SseStreamBuffer, SseStreamBufferTrait}; +use hermesllm::apis::streaming_shapes::sse_chunk_processor::SseChunkProcessor; +use hermesllm::clients::endpoints::SupportedAPIsFromClient; use hermesllm::providers::response::ProviderResponse; +use hermesllm::providers::streaming_response::ProviderStreamResponse; use hermesllm::{ DecodedFrame, ProviderId, ProviderRequest, ProviderRequestType, ProviderResponseType, ProviderStreamResponseType, @@ -38,7 +36,7 @@ pub struct StreamContext { streaming_response: bool, response_tokens: usize, /// The API that is requested by the client (before compatibility mapping) - client_api: Option, + client_api: Option, /// The API that should be used for the upstream provider (after compatibility mapping) resolved_api: Option, llm_providers: Rc, @@ -49,20 +47,20 @@ pub struct StreamContext { ttft_time: Option, traceparent: Option, request_body_sent_time: Option, - traces_queue: Arc>>, overrides: Rc>, user_message: Option, upstream_status_code: Option, binary_frame_decoder: Option>, http_method: Option, http_protocol: Option, + sse_buffer: Option, + sse_chunk_processor: Option, } impl StreamContext { pub fn new( metrics: Rc, llm_providers: Rc, - traces_queue: Arc>>, overrides: Rc>, ) -> Self { StreamContext { @@ -80,13 +78,14 @@ impl StreamContext { ttft_duration: None, traceparent: None, ttft_time: None, - traces_queue, request_body_sent_time: None, user_message: None, upstream_status_code: None, binary_frame_decoder: None, http_method: None, http_protocol: None, + sse_buffer: None, + sse_chunk_processor: None, } } @@ -140,7 +139,7 @@ impl StreamContext { )); info!( - "[ARCHGW_REQ_ID:{}] PROVIDER_SELECTION: Hint='{}' -> Selected='{}'", + "[PLANO_REQ_ID:{}] PROVIDER_SELECTION: Hint='{}' -> Selected='{}'", self.request_identifier(), self.get_http_request_header(ARCH_PROVIDER_HINT_HEADER) .unwrap_or("none".to_string()), @@ -172,7 +171,8 @@ impl StreamContext { Some( SupportedUpstreamAPIs::OpenAIChatCompletions(_) | SupportedUpstreamAPIs::AmazonBedrockConverse(_) - | SupportedUpstreamAPIs::AmazonBedrockConverseStream(_), + | SupportedUpstreamAPIs::AmazonBedrockConverseStream(_) + | SupportedUpstreamAPIs::OpenAIResponsesAPI(_), ) | None => { // OpenAI and default: use Authorization Bearer token @@ -224,7 +224,7 @@ impl StreamContext { let token_count = tokenizer::token_count(model, json_string).unwrap_or(0); debug!( - "[ARCHGW_REQ_ID:{}] TOKEN_COUNT: model='{}' input_tokens={}", + "[PLANO_REQ_ID:{}] TOKEN_COUNT: model='{}' input_tokens={}", self.request_identifier(), model, token_count @@ -238,7 +238,7 @@ impl StreamContext { // Check if rate limiting needs to be applied. if let Some(selector) = self.ratelimit_selector.take() { info!( - "[ARCHGW_REQ_ID:{}] RATELIMIT_CHECK: model='{}' selector='{}:{}'", + "[PLANO_REQ_ID:{}] RATELIMIT_CHECK: model='{}' selector='{}:{}'", self.request_identifier(), model, selector.key, @@ -251,7 +251,7 @@ impl StreamContext { )?; } else { debug!( - "[ARCHGW_REQ_ID:{}] RATELIMIT_SKIP: model='{}' (no selector)", + "[PLANO_REQ_ID:{}] RATELIMIT_SKIP: model='{}' (no selector)", self.request_identifier(), model ); @@ -270,7 +270,7 @@ impl StreamContext { Ok(duration) => { let duration_ms = duration.as_millis(); info!( - "[ARCHGW_REQ_ID:{}] TIME_TO_FIRST_TOKEN: {}ms", + "[PLANO_REQ_ID:{}] TIME_TO_FIRST_TOKEN: {}ms", self.request_identifier(), duration_ms ); @@ -279,7 +279,7 @@ impl StreamContext { } Err(e) => { warn!( - "[ARCHGW_REQ_ID:{}] TIME_MEASUREMENT_ERROR: {:?}", + "[PLANO_REQ_ID:{}] TIME_MEASUREMENT_ERROR: {:?}", self.request_identifier(), e ); @@ -295,7 +295,7 @@ impl StreamContext { // Convert the duration to milliseconds let duration_ms = duration.as_millis(); info!( - "[ARCHGW_REQ_ID:{}] REQUEST_COMPLETE: latency={}ms tokens={}", + "[PLANO_REQ_ID:{}] REQUEST_COMPLETE: latency={}ms tokens={}", self.request_identifier(), duration_ms, self.response_tokens @@ -311,7 +311,7 @@ impl StreamContext { self.metrics.time_per_output_token.record(tpot); info!( - "[ARCHGW_REQ_ID:{}] TOKEN_THROUGHPUT: time_per_token={}ms tokens_per_second={}", + "[PLANO_REQ_ID:{}] TOKEN_THROUGHPUT: time_per_token={}ms tokens_per_second={}", self.request_identifier(), tpot, 1000 / tpot @@ -328,75 +328,13 @@ impl StreamContext { self.metrics .output_sequence_length .record(self.response_tokens as u64); - - if let Some(traceparent) = self.traceparent.as_ref() { - let current_time_ns = current_time_ns(); - - match Traceparent::try_from(traceparent.to_string()) { - Err(e) => { - warn!("traceparent header is invalid: {}", e); - } - Ok(traceparent) => { - let service_name = match &self.resolved_api { - Some(api) => { - let api_display = api.to_string(); - format!("archgw.{}", api_display) - } - None => "archgw".to_string(), - }; - - let mut trace_data = - common::tracing::TraceData::new_with_service_name(service_name); - let mut llm_span = Span::new( - self.llm_provider().name.to_string(), - Some(traceparent.trace_id), - Some(traceparent.parent_id), - self.request_body_sent_time.unwrap(), - current_time_ns, - ); - llm_span - .add_attribute("model".to_string(), self.llm_provider().name.to_string()); - - if let Some(user_message) = &self.user_message { - llm_span.add_attribute("message".to_string(), user_message.clone()); - } - - // Add HTTP attributes - if let Some(method) = &self.http_method { - llm_span.add_attribute("http.method".to_string(), method.clone()); - } - if let Some(protocol) = &self.http_protocol { - llm_span.add_attribute("http.protocol".to_string(), protocol.clone()); - } - if let Some(status_code) = &self.upstream_status_code { - llm_span.add_attribute( - "http.status_code".to_string(), - status_code.as_u16().to_string(), - ); - } - - // Add request ID attribute - llm_span - .add_attribute("http.request_id".to_string(), self.request_identifier()); - - if self.ttft_time.is_some() { - llm_span.add_event(Event::new( - "time_to_first_token".to_string(), - self.ttft_time.unwrap(), - )); - } - trace_data.add_span(llm_span); - self.traces_queue.lock().unwrap().push_back(trace_data); - } - }; - } } fn read_raw_response_body(&mut self, body_size: usize) -> Result, Action> { if self.streaming_response { let chunk_size = body_size; debug!( - "[ARCHGW_REQ_ID:{}] UPSTREAM_RESPONSE_CHUNK: streaming=true chunk_size={}", + "[PLANO_REQ_ID:{}] UPSTREAM_RESPONSE_CHUNK: streaming=true chunk_size={}", self.request_identifier(), chunk_size ); @@ -404,7 +342,7 @@ impl StreamContext { Some(chunk) => chunk, None => { warn!( - "[ARCHGW_REQ_ID:{}] UPSTREAM_RESPONSE_ERROR: empty chunk, size={}", + "[PLANO_REQ_ID:{}] UPSTREAM_RESPONSE_ERROR: empty chunk, size={}", self.request_identifier(), chunk_size ); @@ -414,7 +352,7 @@ impl StreamContext { if streaming_chunk.len() != chunk_size { warn!( - "[ARCHGW_REQ_ID:{}] UPSTREAM_RESPONSE_MISMATCH: expected={} actual={}", + "[PLANO_REQ_ID:{}] UPSTREAM_RESPONSE_MISMATCH: expected={} actual={}", self.request_identifier(), chunk_size, streaming_chunk.len() @@ -426,7 +364,7 @@ impl StreamContext { return Err(Action::Continue); } debug!( - "[ARCHGW_REQ_ID:{}] UPSTREAM_RESPONSE_COMPLETE: streaming=false body_size={}", + "[PLANO_REQ_ID:{}] UPSTREAM_RESPONSE_COMPLETE: streaming=false body_size={}", self.request_identifier(), body_size ); @@ -446,7 +384,7 @@ impl StreamContext { provider_id: ProviderId, ) -> Result, Action> { debug!( - "[ARCHGW_REQ_ID:{}] STREAMING_PROCESS: client={:?} provider_id={:?} chunk_size={}", + "[PLANO_REQ_ID:{}] STREAMING_PROCESS: client={:?} provider_id={:?} chunk_size={}", self.request_identifier(), self.client_api, provider_id, @@ -466,30 +404,59 @@ impl StreamContext { return self.handle_bedrock_binary_stream(body, &client_api, &upstream_api); } - // Parse body into SSE iterator using TryFrom - let sse_iter: SseStreamIter> = - match SseStreamIter::try_from(body) { - Ok(iter) => iter, + // Initialize SSE chunk processor if not present + if self.sse_chunk_processor.is_none() { + self.sse_chunk_processor = Some(SseChunkProcessor::new()); + } + + // Initialize SSE buffer if not present + if self.sse_buffer.is_none() { + self.sse_buffer = match SseStreamBuffer::try_from((&client_api, &upstream_api)) + { + Ok(buffer) => Some(buffer), Err(e) => { - warn!("Failed to parse body into SSE iterator: {}", e); + warn!("Failed to create SSE buffer: {}", e); return Err(Action::Continue); } }; + } - let mut response_buffer = Vec::new(); + // Process chunk through SSE processor (handles incomplete events) + let transformed_events = match self.sse_chunk_processor.as_mut() { + Some(processor) => { + let result = processor.process_chunk(body, &client_api, &upstream_api); + let has_buffered = processor.has_buffered_data(); + let buffered_size = processor.buffered_size(); - // Process each SSE event - for sse_event in sse_iter { - // Transform event if upstream API != client API - let transformed_event: SseEvent = - match SseEvent::try_from((sse_event, &client_api, &upstream_api)) { - Ok(event) => event, + match result { + Ok(events) => { + if has_buffered { + debug!( + "[PLANO_REQ_ID:{}] SSE_INCOMPLETE_BUFFERED: {} bytes buffered for next chunk", + self.request_identifier(), + buffered_size + ); + } + events + } Err(e) => { - warn!("Failed to transform SSE event: {}", e); + warn!( + "[PLANO_REQ_ID:{}] SSE_CHUNK_PROCESS_ERROR: {}", + self.request_identifier(), + e + ); return Err(Action::Continue); } - }; + } + } + None => { + warn!("SSE chunk processor unexpectedly missing"); + return Err(Action::Continue); + } + }; + // Process each successfully transformed SSE event + for transformed_event in transformed_events { // Extract ProviderStreamResponse for processing (token counting, etc.) if !transformed_event.is_done() && !transformed_event.is_event_only() { match transformed_event.provider_response() { @@ -498,7 +465,7 @@ impl StreamContext { if provider_response.is_final() { debug!( - "[ARCHGW_REQ_ID:{}] STREAMING_FINAL_CHUNK: total_tokens={}", + "[PLANO_REQ_ID:{}] STREAMING_FINAL_CHUNK: total_tokens={}", self.request_identifier(), self.response_tokens ); @@ -508,7 +475,7 @@ impl StreamContext { let estimated_tokens = content.len() / 4; self.response_tokens += estimated_tokens.max(1); debug!( - "[ARCHGW_REQ_ID:{}] STREAMING_TOKEN_UPDATE: delta_chars={} estimated_tokens={} total_tokens={}", + "[PLANO_REQ_ID:{}] STREAMING_TOKEN_UPDATE: delta_chars={} estimated_tokens={} total_tokens={}", self.request_identifier(), content.len(), estimated_tokens.max(1), @@ -518,7 +485,7 @@ impl StreamContext { } Err(e) => { warn!( - "[ARCHGW_REQ_ID:{}] STREAMING_CHUNK_ERROR: {}", + "[PLANO_REQ_ID:{}] STREAMING_CHUNK_ERROR: {}", self.request_identifier(), e ); @@ -527,12 +494,32 @@ impl StreamContext { } } - // Add transformed event to response buffer - let bytes: Vec = transformed_event.into(); - response_buffer.extend_from_slice(&bytes); + // Add transformed event to buffer (buffer may inject lifecycle events) + if let Some(buffer) = self.sse_buffer.as_mut() { + buffer.add_transformed_event(transformed_event); + } } - Ok(response_buffer) + // Get accumulated bytes from buffer and return + match self.sse_buffer.as_mut() { + Some(buffer) => { + let bytes = buffer.into_bytes(); + if !bytes.is_empty() { + let content = String::from_utf8_lossy(&bytes); + debug!( + "[PLANO_REQ_ID:{}] UPSTREAM_TRANSFORMED_CLIENT_RESPONSE: size={} content={}", + self.request_identifier(), + bytes.len(), + content + ); + } + Ok(bytes) + } + None => { + warn!("SSE buffer unexpectedly missing after initialization"); + Err(Action::Continue) + } + } } None => { warn!("Missing client_api for non-streaming response"); @@ -544,7 +531,7 @@ impl StreamContext { fn handle_bedrock_binary_stream( &mut self, body: &[u8], - client_api: &SupportedAPIs, + client_api: &SupportedAPIsFromClient, upstream_api: &SupportedUpstreamAPIs, ) -> Result, Action> { // Initialize decoder if not present @@ -552,87 +539,61 @@ impl StreamContext { self.binary_frame_decoder = Some(BedrockBinaryFrameDecoder::from_bytes(&[])); } - // Add incoming bytes to buffer + // Initialize SSE buffer if not present + if self.sse_buffer.is_none() { + self.sse_buffer = match SseStreamBuffer::try_from((client_api, upstream_api)) { + Ok(buffer) => Some(buffer), + Err(e) => { + warn!( + "[PLANO_REQ_ID:{}] BEDROCK_BUFFER_INIT_ERROR: {}", + self.request_identifier(), + e + ); + return Err(Action::Continue); + } + }; + } + + // Add incoming bytes to decoder buffer let decoder = self.binary_frame_decoder.as_mut().unwrap(); decoder.buffer_mut().extend_from_slice(body); - let mut response_buffer = Vec::new(); + // Process all complete frames loop { let decoded_frame = self.binary_frame_decoder.as_mut().unwrap().decode_frame(); match decoded_frame { Some(DecodedFrame::Complete(ref frame_ref)) => { let frame = DecodedFrame::Complete(frame_ref.clone()); + + // Convert frame to provider response type match ProviderStreamResponseType::try_from((&frame, client_api, upstream_api)) { Ok(provider_response) => { self.record_ttft_if_needed(); - // Handle ContentBlockStart and ContentBlockDelta events - match &provider_response { - ProviderStreamResponseType::MessagesStreamEvent(evt) => { - match evt { - MessagesStreamEvent::ContentBlockStart { - index, .. - } => { - // Mark that we've seen ContentBlockStart for this index - self.binary_frame_decoder - .as_mut() - .unwrap() - .set_content_block_start_sent(*index as i32); - debug!( - "[ARCHGW_REQ_ID:{}] BEDROCK_CONTENT_BLOCK_START_TRACKED: index={}", - self.request_identifier(), - *index - ); - } - MessagesStreamEvent::ContentBlockDelta { - index, .. - } => { - // Check if ContentBlockStart was sent for this index - let needs_start = !self - .binary_frame_decoder - .as_ref() - .unwrap() - .has_content_block_start_been_sent(*index as i32); - - if needs_start { - // Emit empty ContentBlockStart before delta - let content_block_start = - MessagesStreamEvent::ContentBlockStart { - index: *index, - content_block: MessagesContentBlock::Text { - text: String::new(), - cache_control: None, - }, - }; - let start_sse: String = content_block_start.into(); - response_buffer - .extend_from_slice(start_sse.as_bytes()); - - // Mark that we've now sent it - self.binary_frame_decoder - .as_mut() - .unwrap() - .set_content_block_start_sent(*index as i32); - - debug!( - "[ARCHGW_REQ_ID:{}] BEDROCK_INJECTED_CONTENT_BLOCK_START: index={}", - self.request_identifier(), - *index - ); - } - } - _ => {} - } - } - _ => {} + // Track token usage + if let Some(content) = provider_response.content_delta() { + let estimated_tokens = content.len() / 4; + self.response_tokens += estimated_tokens.max(1); + debug!( + "[PLANO_REQ_ID:{}] BEDROCK_TOKEN_UPDATE: delta_chars={} estimated_tokens={} total_tokens={}", + self.request_identifier(), + content.len(), + estimated_tokens.max(1), + self.response_tokens + ); } - let sse_string: String = provider_response.into(); - response_buffer.extend_from_slice(sse_string.as_bytes()); + // Create SseEvent from provider response + let event = SseEvent::from_provider_response(provider_response); + + // Add to buffer (buffer handles all shim logic including ContentBlockStart injection) + if let Some(buffer) = self.sse_buffer.as_mut() { + buffer.add_transformed_event(event); + } } Err(e) => { warn!( - "[ARCHGW_REQ_ID:{}] BEDROCK_FRAME_CONVERSION_ERROR: {}", + "[PLANO_REQ_ID:{}] BEDROCK_FRAME_CONVERSION_ERROR: {}", self.request_identifier(), e ); @@ -642,7 +603,7 @@ impl StreamContext { Some(DecodedFrame::Incomplete) => { // Incomplete frame - buffer retains partial data, wait for more bytes debug!( - "[ARCHGW_REQ_ID:{}] BEDROCK_INCOMPLETE_FRAME: waiting for more data", + "[PLANO_REQ_ID:{}] BEDROCK_INCOMPLETE_FRAME: waiting for more data", self.request_identifier() ); break; @@ -650,7 +611,7 @@ impl StreamContext { None => { // Decode error warn!( - "[ARCHGW_REQ_ID:{}] BEDROCK_DECODE_ERROR", + "[PLANO_REQ_ID:{}] BEDROCK_DECODE_ERROR", self.request_identifier() ); return Err(Action::Continue); @@ -658,8 +619,29 @@ impl StreamContext { } } - // Return accumulated complete frames (may be empty if all frames incomplete) - Ok(response_buffer) + // Get accumulated bytes from buffer and return + match self.sse_buffer.as_mut() { + Some(buffer) => { + let bytes = buffer.into_bytes(); + if !bytes.is_empty() { + let content = String::from_utf8_lossy(&bytes); + debug!( + "[PLANO_REQ_ID:{}] UPSTREAM_TRANSFORMED_CLIENT_RESPONSE: size={} content={}", + self.request_identifier(), + bytes.len(), + content + ); + } + Ok(bytes) + } + None => { + warn!( + "[PLANO_REQ_ID:{}] BEDROCK_BUFFER_MISSING", + self.request_identifier() + ); + Err(Action::Continue) + } + } } fn handle_non_streaming_response( @@ -668,7 +650,7 @@ impl StreamContext { provider_id: ProviderId, ) -> Result, Action> { debug!( - "[ARCHGW_REQ_ID:{}] NON_STREAMING_PROCESS: provider_id={:?} body_size={}", + "[PLANO_REQ_ID:{}] NON_STREAMING_PROCESS: provider_id={:?} body_size={}", self.request_identifier(), provider_id, body.len() @@ -680,7 +662,7 @@ impl StreamContext { Ok(response) => response, Err(e) => { warn!( - "[ARCHGW_REQ_ID:{}] UPSTREAM_RESPONSE_PARSE_ERROR: {} | body: {}", + "[PLANO_REQ_ID:{}] UPSTREAM_RESPONSE_PARSE_ERROR: {} | body: {}", self.request_identifier(), e, String::from_utf8_lossy(body) @@ -695,7 +677,7 @@ impl StreamContext { } None => { warn!( - "[ARCHGW_REQ_ID:{}] UPSTREAM_RESPONSE_ERROR: missing client_api", + "[PLANO_REQ_ID:{}] UPSTREAM_RESPONSE_ERROR: missing client_api", self.request_identifier() ); return Err(Action::Continue); @@ -707,7 +689,7 @@ impl StreamContext { response.extract_usage_counts() { debug!( - "[ARCHGW_REQ_ID:{}] RESPONSE_USAGE: prompt_tokens={} completion_tokens={} total_tokens={}", + "[PLANO_REQ_ID:{}] RESPONSE_USAGE: prompt_tokens={} completion_tokens={} total_tokens={}", self.request_identifier(), prompt_tokens, completion_tokens, @@ -716,7 +698,7 @@ impl StreamContext { self.response_tokens = completion_tokens; } else { warn!( - "[ARCHGW_REQ_ID:{}] RESPONSE_USAGE: no usage information found", + "[PLANO_REQ_ID:{}] RESPONSE_USAGE: no usage information found", self.request_identifier() ); } @@ -724,7 +706,7 @@ impl StreamContext { match serde_json::to_vec(&response) { Ok(bytes) => { debug!( - "[ARCHGW_REQ_ID:{}] CLIENT_RESPONSE_PAYLOAD: {}", + "[PLANO_REQ_ID:{}] CLIENT_RESPONSE_PAYLOAD: {}", self.request_identifier(), String::from_utf8_lossy(&bytes) ); @@ -782,13 +764,14 @@ impl HttpContext for StreamContext { self.select_llm_provider(); // Check if this is a supported API endpoint - if SupportedAPIs::from_endpoint(&request_path).is_none() { + if SupportedAPIsFromClient::from_endpoint(&request_path).is_none() { self.send_http_response(404, vec![], Some(b"Unsupported endpoint")); return Action::Continue; } // Get the SupportedApi for routing decisions - let supported_api: Option = SupportedAPIs::from_endpoint(&request_path); + let supported_api: Option = + SupportedAPIsFromClient::from_endpoint(&request_path); self.client_api = supported_api; // Debug: log provider, client API, resolved API, and request path @@ -800,7 +783,7 @@ impl HttpContext for StreamContext { Some(provider_id.compatible_api_for_client(api, self.streaming_response)); debug!( - "[ARCHGW_REQ_ID:{}] ROUTING_INFO: provider='{}' client_api={:?} resolved_api={:?} request_path='{}'", + "[PLANO_REQ_ID:{}] ROUTING_INFO: provider='{}' client_api={:?} resolved_api={:?} request_path='{}'", self.request_identifier(), provider.to_provider_id(), api, @@ -853,7 +836,7 @@ impl HttpContext for StreamContext { fn on_http_request_body(&mut self, body_size: usize, end_of_stream: bool) -> Action { debug!( - "[ARCHGW_REQ_ID:{}] REQUEST_BODY_CHUNK: bytes={} end_stream={}", + "[PLANO_REQ_ID:{}] REQUEST_BODY_CHUNK: bytes={} end_stream={}", self.request_identifier(), body_size, end_of_stream @@ -892,14 +875,14 @@ impl HttpContext for StreamContext { let mut deserialized_client_request: ProviderRequestType = match self.client_api.as_ref() { Some(the_client_api) => { info!( - "[ARCHGW_REQ_ID:{}] CLIENT_REQUEST_RECEIVED: api={:?} body_size={}", + "[PLANO_REQ_ID:{}] CLIENT_REQUEST_RECEIVED: api={:?} body_size={}", self.request_identifier(), the_client_api, body_bytes.len() ); debug!( - "[ARCHGW_REQ_ID:{}] CLIENT_REQUEST_PAYLOAD: {}", + "[PLANO_REQ_ID:{}] CLIENT_REQUEST_PAYLOAD: {}", self.request_identifier(), String::from_utf8_lossy(&body_bytes) ); @@ -908,7 +891,7 @@ impl HttpContext for StreamContext { Ok(deserialized) => deserialized, Err(e) => { warn!( - "[ARCHGW_REQ_ID:{}] CLIENT_REQUEST_PARSE_ERROR: {} | body: {}", + "[PLANO_REQ_ID:{}] CLIENT_REQUEST_PARSE_ERROR: {} | body: {}", self.request_identifier(), e, String::from_utf8_lossy(&body_bytes) @@ -951,7 +934,7 @@ impl HttpContext for StreamContext { "agent_orchestrator".to_string() } else { warn!( - "[ARCHGW_REQ_ID:{}] MODEL_RESOLUTION_ERROR: no model specified | req_model='{}' provider='{}' config_model={:?}", + "[PLANO_REQ_ID:{}] MODEL_RESOLUTION_ERROR: no model specified | req_model='{}' provider='{}' config_model={:?}", self.request_identifier(), model_requested, self.llm_provider().name, @@ -980,7 +963,7 @@ impl HttpContext for StreamContext { self.user_message = deserialized_client_request.get_recent_user_message(); info!( - "[ARCHGW_REQ_ID:{}] MODEL_RESOLUTION: req_model='{}' -> resolved_model='{}' provider='{}' streaming={}", + "[PLANO_REQ_ID:{}] MODEL_RESOLUTION: req_model='{}' -> resolved_model='{}' provider='{}' streaming={}", self.request_identifier(), model_requested, resolved_model, @@ -1011,14 +994,14 @@ impl HttpContext for StreamContext { match self.resolved_api.as_ref() { Some(upstream) => { info!( - "[ARCHGW_REQ_ID:{}] UPSTREAM_TRANSFORM: client_api={:?} -> upstream_api={:?}", + "[PLANO_REQ_ID:{}] UPSTREAM_TRANSFORM: client_api={:?} -> upstream_api={:?}", self.request_identifier(), self.client_api, upstream ); match ProviderRequestType::try_from((deserialized_client_request, upstream)) { Ok(request) => { debug!( - "[ARCHGW_REQ_ID:{}] UPSTREAM_REQUEST_PAYLOAD: {}", + "[PLANO_REQ_ID:{}] UPSTREAM_REQUEST_PAYLOAD: {}", self.request_identifier(), String::from_utf8_lossy(&request.to_bytes().unwrap_or_default()) ); @@ -1069,7 +1052,7 @@ impl HttpContext for StreamContext { self.upstream_status_code = StatusCode::from_u16(status_code).ok(); debug!( - "[ARCHGW_REQ_ID:{}] UPSTREAM_RESPONSE_STATUS: {}", + "[PLANO_REQ_ID:{}] UPSTREAM_RESPONSE_STATUS: {}", self.request_identifier(), status_code ); @@ -1096,7 +1079,7 @@ impl HttpContext for StreamContext { let current_time = get_current_time().unwrap(); if end_of_stream && body_size == 0 { debug!( - "[ARCHGW_REQ_ID:{}] RESPONSE_BODY_COMPLETE: total_bytes={}", + "[PLANO_REQ_ID:{}] RESPONSE_BODY_COMPLETE: total_bytes={}", self.request_identifier(), body_size ); @@ -1108,7 +1091,7 @@ impl HttpContext for StreamContext { if let Some(status_code) = &self.upstream_status_code { if status_code.is_client_error() || status_code.is_server_error() { info!( - "[ARCHGW_REQ_ID:{}] UPSTREAM_ERROR_RESPONSE: status={} body_size={}", + "[PLANO_REQ_ID:{}] UPSTREAM_ERROR_RESPONSE: status={} body_size={}", self.request_identifier(), status_code.as_u16(), body_size @@ -1118,7 +1101,7 @@ impl HttpContext for StreamContext { if body_size > 0 { if let Ok(body) = self.read_raw_response_body(body_size) { debug!( - "[ARCHGW_REQ_ID:{}] UPSTREAM_ERROR_BODY: {}", + "[PLANO_REQ_ID:{}] UPSTREAM_ERROR_BODY: {}", self.request_identifier(), String::from_utf8_lossy(&body) ); @@ -1131,15 +1114,16 @@ impl HttpContext for StreamContext { } match self.client_api { - Some(SupportedAPIs::OpenAIChatCompletions(_)) => {} - Some(SupportedAPIs::AnthropicMessagesAPI(_)) => {} + Some(SupportedAPIsFromClient::OpenAIChatCompletions(_)) => {} + Some(SupportedAPIsFromClient::AnthropicMessagesAPI(_)) => {} + Some(SupportedAPIsFromClient::OpenAIResponsesAPI(_)) => {} _ => { let api_info = match &self.client_api { Some(api) => format!("{}", api), None => "None".to_string(), }; info!( - "[ARCHGW_REQ_ID:{}], UNSUPPORTED API: {}", + "[PLANO_REQ_ID:{}], UNSUPPORTED API: {}", self.request_identifier(), api_info ); @@ -1153,7 +1137,7 @@ impl HttpContext for StreamContext { }; debug!( - "[ARCHGW_REQ_ID:{}] UPSTREAM_RAW_RESPONSE: body_size={} content={}", + "[PLANO_REQ_ID:{}] UPSTREAM_RAW_RESPONSE: body_size={} content={}", self.request_identifier(), body.len(), String::from_utf8_lossy(&body) diff --git a/crates/prompt_gateway/src/stream_context.rs b/crates/prompt_gateway/src/stream_context.rs index 96caa378..1e9d507b 100644 --- a/crates/prompt_gateway/src/stream_context.rs +++ b/crates/prompt_gateway/src/stream_context.rs @@ -264,13 +264,6 @@ impl StreamContext { .tool_calls .clone_into(&mut self.tool_calls); - if self.tool_calls.as_ref().unwrap().len() > 1 { - warn!( - "multiple tool calls not supported yet, tool_calls count found: {}", - self.tool_calls.as_ref().unwrap().len() - ); - } - if self.tool_calls.is_none() || self.tool_calls.as_ref().unwrap().is_empty() { // This means that Arch FC did not have enough information to resolve the function call // Arch FC probably responded with a message asking for more information. @@ -314,6 +307,14 @@ impl StreamContext { ); } + // At this point, we know tool_calls is not None and not empty + if self.tool_calls.as_ref().unwrap().len() > 1 { + warn!( + "multiple tool calls not supported yet, tool_calls count found: {}", + self.tool_calls.as_ref().unwrap().len() + ); + } + // update prompt target name from the tool call response callout_context.prompt_target_name = Some(self.tool_calls.as_ref().unwrap()[0].function.name.clone()); @@ -371,7 +372,26 @@ impl StreamContext { let tools_call_name = self.tool_calls.as_ref().unwrap()[0].function.name.clone(); let prompt_target = self.prompt_targets.get(&tools_call_name).unwrap().clone(); - let tool_params = &self.tool_calls.as_ref().unwrap()[0].function.arguments; + let tool_params_str = &self.tool_calls.as_ref().unwrap()[0].function.arguments; + + // Parse arguments JSON string into HashMap + // Note: convert from serde_json::Value to serde_yaml::Value for compatibility + let tool_params: Option> = match serde_json::from_str::>(tool_params_str) { + Ok(json_params) => { + let yaml_params: HashMap = json_params + .into_iter() + .filter_map(|(k, v)| { + serde_yaml::to_value(&v).ok().map(|yaml_v| (k, yaml_v)) + }) + .collect(); + Some(yaml_params) + }, + Err(e) => { + warn!("Failed to parse tool call arguments: {}", e); + None + } + }; + let endpoint_details = prompt_target.endpoint.as_ref().unwrap(); let endpoint_path: String = endpoint_details .path @@ -384,7 +404,7 @@ impl StreamContext { let (path, api_call_body) = match compute_request_path_body( &endpoint_path, - tool_params, + &tool_params, &prompt_target_params, &http_method, ) { @@ -870,7 +890,7 @@ mod test { id: "1".to_string(), function: common::api::open_ai::FunctionCallDetail { name: "test".to_string(), - arguments: None, + arguments: "{}".to_string(), }, tool_type: common::api::open_ai::ToolType::Function, }]), diff --git a/demos/samples_java/weather_forcecast_service/.classpath b/demos/samples_java/weather_forcecast_service/.classpath index 39abf1c5..e16ec965 100644 --- a/demos/samples_java/weather_forcecast_service/.classpath +++ b/demos/samples_java/weather_forcecast_service/.classpath @@ -9,6 +9,7 @@ + @@ -36,6 +37,13 @@ + + + + + + + diff --git a/demos/samples_python/currency_exchange/arch_config.yaml b/demos/samples_python/currency_exchange/arch_config.yaml index 1c399449..71c06d18 100644 --- a/demos/samples_python/currency_exchange/arch_config.yaml +++ b/demos/samples_python/currency_exchange/arch_config.yaml @@ -8,8 +8,15 @@ listeners: timeout: 30s llm_providers: - - access_key: $OPENAI_API_KEY - model: openai/gpt-4o + - model: openai/gpt-4o-mini + access_key: $OPENAI_API_KEY + default: true + + - model: openai/gpt-4o + access_key: $OPENAI_API_KEY + routing_preferences: + - name: code understanding + description: understand and explain existing code snippets, functions, or libraries endpoints: frankfurther_api: diff --git a/demos/samples_python/human_resources_agent/requirements.txt b/demos/samples_python/human_resources_agent/requirements.txt index 9a108c37..aaaff081 100644 --- a/demos/samples_python/human_resources_agent/requirements.txt +++ b/demos/samples_python/human_resources_agent/requirements.txt @@ -4,6 +4,7 @@ slack-sdk typing pandas gradio==5.3.0 +huggingface_hub<1.0.0 async_timeout==4.0.3 loguru==0.7.2 asyncio==3.4.3 diff --git a/demos/samples_python/multi_turn_rag_agent/requirements.txt b/demos/samples_python/multi_turn_rag_agent/requirements.txt index a555b460..d6a88e83 100644 --- a/demos/samples_python/multi_turn_rag_agent/requirements.txt +++ b/demos/samples_python/multi_turn_rag_agent/requirements.txt @@ -3,6 +3,7 @@ uvicorn typing pandas gradio==5.3.0 +huggingface_hub<1.0.0 async_timeout==4.0.3 loguru==0.7.2 asyncio==3.4.3 diff --git a/demos/samples_python/network_switch_operator_agent/requirements.txt b/demos/samples_python/network_switch_operator_agent/requirements.txt index 8aa2003a..52913a01 100644 --- a/demos/samples_python/network_switch_operator_agent/requirements.txt +++ b/demos/samples_python/network_switch_operator_agent/requirements.txt @@ -4,6 +4,7 @@ pydantic typing pandas gradio==5.3.0 +huggingface_hub<1.0.0 async_timeout==4.0.3 loguru==0.7.2 asyncio==3.4.3 diff --git a/demos/shared/chatbot_ui/requirements.txt b/demos/shared/chatbot_ui/requirements.txt index da4ac00b..7d94088a 100644 --- a/demos/shared/chatbot_ui/requirements.txt +++ b/demos/shared/chatbot_ui/requirements.txt @@ -1,4 +1,5 @@ gradio==5.3.0 +huggingface_hub<1.0.0 async_timeout==4.0.3 loguru==0.7.2 asyncio==3.4.3 diff --git a/demos/use_cases/mcp_filter/README.md b/demos/use_cases/mcp_filter/README.md new file mode 100644 index 00000000..a524c1b4 --- /dev/null +++ b/demos/use_cases/mcp_filter/README.md @@ -0,0 +1,106 @@ +# RAG Agent Demo + +A multi-agent RAG system demonstrating archgw's agent filter chain with MCP protocol. + +## Architecture + +This demo consists of three components: +1. **Query Rewriter** (MCP filter) - Rewrites user queries for better retrieval +2. **Context Builder** (MCP filter) - Retrieves relevant context from knowledge base +3. **RAG Agent** (REST) - Generates final responses based on augmented context + +## Components + +### Query Rewriter Filter (MCP) +- **Port**: 10501 +- **Tool**: `query_rewriter` +- Improves queries using LLM before retrieval + +### Context Builder Filter (MCP) +- **Port**: 10502 +- **Tool**: `context_builder` +- Augments queries with relevant passages from knowledge base + +### RAG Agent (REST/OpenAI) +- **Port**: 10505 +- **Endpoint**: `/v1/chat/completions` +- Generates responses using OpenAI-compatible API + +## Quick Start + +### 1. Start all agents +```bash +./start_agents.sh +``` + +This starts: +- Query Rewriter MCP server on port 10501 +- Context Builder MCP server on port 10502 +- RAG Agent REST server on port 10505 + +### 2. Start archgw +```bash +archgw up --foreground +``` + +### 3. Test the system +```bash +curl -X POST http://localhost:8001/v1/chat/completions \ + -H "Content-Type: application/json" \ + -d '{ + "model": "gpt-4o", + "messages": [{"role": "user", "content": "What is the guaranteed uptime for TechCorp?"}] + }' +``` + +## Configuration + +The `arch_config.yaml` defines how agents are connected: + +```yaml +filters: + - id: query_rewriter + url: mcp://host.docker.internal:10500 + tool: rewrite_query_with_archgw # MCP tool name + + - id: context_builder + url: mcp://host.docker.internal:10501 + tool: chat_completions +``` +How It Works + +1. User sends request to archgw listener on port 8001 +2. Request passes through MCP filter chain: + - **Query Rewriter** rewrites the query for better retrieval + - **Context Builder** augments query with relevant knowledge base passages +3. Augmented request is forwarded to **RAG Agent** REST endpoint +4. RAG Agent generates final response using LLM + +## Configuration + +See `arch_config.yaml` for the complete filter chain setup. The MCP filters use default settings: +- `type: mcp` (default) +- `transport: streamable-http` (default) +- Tool name defaults to filter ID `sample_queries.md` for example queries to test the RAG system. + +Example request: +```bash +curl -X POST http://localhost:8001/v1/chat/completions \ + -H "Content-Type: application/json" \ + -d '{ + "model": "gpt-4o", + "messages": [ + { + "role": "user", + "content": "What is the guaranteed uptime for TechCorp?" + } + ] + }' +``` +- `LLM_GATEWAY_ENDPOINT` - archgw endpoint (default: `http://localhost:12000/v1`) +- `OPENAI_API_KEY` - OpenAI API key for model providers + +## Additional Resources + +- See `sample_queries.md` for more example queries +- See `arch_config.yaml` for complete configuration details diff --git a/demos/use_cases/mcp_filter/arch_config.yaml b/demos/use_cases/mcp_filter/arch_config.yaml new file mode 100644 index 00000000..e5aacc03 --- /dev/null +++ b/demos/use_cases/mcp_filter/arch_config.yaml @@ -0,0 +1,41 @@ +version: v0.3.0 + +agents: + - id: rag_agent + url: http://host.docker.internal:10505 + +filters: + - id: query_rewriter + url: http://host.docker.internal:10501 + # type: mcp # default is mcp + # transport: streamable-http # default is streamable-http + # tool: query_rewriter # default name is the filter id + - id: context_builder + url: http://host.docker.internal:10502 + +model_providers: + - model: openai/gpt-4o-mini + access_key: $OPENAI_API_KEY + default: true + - model: openai/gpt-4o + access_key: $OPENAI_API_KEY + +model_aliases: + fast-llm: + target: gpt-4o-mini + smart-llm: + target: gpt-4o + +listeners: + - type: agent + name: agent_1 + port: 8001 + router: arch_agent_router + agents: + - id: rag_agent + description: virtual assistant for retrieval augmented generation tasks + filter_chain: + - query_rewriter + - context_builder +tracing: + random_sampling: 100 diff --git a/demos/use_cases/mcp_filter/docker-compose.yaml b/demos/use_cases/mcp_filter/docker-compose.yaml new file mode 100644 index 00000000..a5d45ed9 --- /dev/null +++ b/demos/use_cases/mcp_filter/docker-compose.yaml @@ -0,0 +1,17 @@ +services: + jaeger: + build: + context: ../../shared/jaeger + ports: + - "16686:16686" + - "4317:4317" + - "4318:4318" + open-web-ui: + image: dyrnq/open-webui:main + restart: always + ports: + - "8080:8080" + environment: + - DEFAULT_MODEL=gpt-4o-mini + - ENABLE_OPENAI_API=true + - OPENAI_API_BASE_URL=http://host.docker.internal:8001/v1 diff --git a/demos/use_cases/mcp_filter/mcp_query.rest b/demos/use_cases/mcp_filter/mcp_query.rest new file mode 100644 index 00000000..c08dd884 --- /dev/null +++ b/demos/use_cases/mcp_filter/mcp_query.rest @@ -0,0 +1,86 @@ +### Initialize MCP Session (SSE) +POST http://localhost:10501/mcp +Content-Type: application/json +Accept: application/json, text/event-stream + +{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"capabilities":{},"protocolVersion":"2024-11-05","clientInfo":{"name":"test","version":"1.0.0"}}} + +### Send Initialized Notification +POST http://localhost:10501/mcp +Content-Type: application/json +Accept: application/json, text/event-stream +mcp-session-id: 35d455dc07b8400887f86668590f12bb + +{ + "jsonrpc": "2.0", + "method": "notifications/initialized" +} + +### List Tools +POST http://localhost:10501/mcp +Content-Type: application/json +Accept: application/json, text/event-stream +mcp-session-id: eb10a691b36e4547b6c93c5dc5b47e11 + +{ + "jsonrpc": "2.0", + "id": "list-tools-1", + "method": "tools/list" +} + +### Call Query Rewriter Tool +POST http://localhost:10501/mcp +Content-Type: application/json +Accept: application/json, text/event-stream +mcp-session-id: 6b95ff75825a402b90eb3ea07e23fbce + +{ + "jsonrpc": "2.0", + "id": "3d3b886a-6216-4a26-a422-7a972529c0e7", + "method": "tools/call", + "params": { + "arguments": { + "messages": [ + { + "content": "What is the guaranteed uptime percentage for TechCorp's cloud services?", + "role": "user" + } + ] + }, + "name": "query_rewriter" + } +} + +### another test + +# Content-Type: application/json +# Accept: application/json, text/event-stream +# mcp-session-id: ed7a81a1d39549ecaadb867a6b2daf1e + +POST http://localhost:10501/mcp +content-type: application/json +mcp-session-id: e4ec1ae904e14e06b7d194da10e5f74c +accept: application/json, text/event-stream + +{"jsonrpc":"2.0","id":"4bb1043a-2953-4bcd-b801-f270b0ae8c39","method":"tools/call","params":{"arguments":{"messages":[{"content":"What is the guaranteed uptime percentage for TechCorp's cloud services?","role":"user"}]},"name":"query_rewriter"}} + + + +### stream test + +POST http://localhost:10501/mcp +content-type: application/json +mcp-session-id: 35d455dc07b8400887f86668590f12bb +accept: application/json, text/event-stream + +{ + "jsonrpc": "2.0", + "id": 1, + "method": "tools/call", + "params": { + "name": "long_job", + "arguments": { + "n": 3 + } + } +} diff --git a/demos/use_cases/mcp_filter/pyproject.toml b/demos/use_cases/mcp_filter/pyproject.toml new file mode 100644 index 00000000..7cd9411b --- /dev/null +++ b/demos/use_cases/mcp_filter/pyproject.toml @@ -0,0 +1,22 @@ +[project] +name = "rag_agent" +version = "0.1.0" +description = "RAG Agent" +readme = "README.md" +requires-python = ">=3.10" +dependencies = [ + "click>=8.2.1", + "mcp>=1.13.1", + "fastmcp>=2.14", + "pydantic>=2.11.7", + "fastapi>=0.104.1", + "uvicorn>=0.24.0", + "openai==2.13.0", +] + +[project.scripts] +rag_agent = "rag_agent:main" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" diff --git a/demos/use_cases/mcp_filter/sample_queries.md b/demos/use_cases/mcp_filter/sample_queries.md new file mode 100644 index 00000000..ba98cdfd --- /dev/null +++ b/demos/use_cases/mcp_filter/sample_queries.md @@ -0,0 +1,64 @@ +# Sample Queries for Knowledge Base RAG Agent + +## Service Level Agreement Queries +- What is the guaranteed uptime percentage for TechCorp's cloud services? +- What remedies are available if the API response time exceeds the agreed threshold? +- How quickly must TechCorp respond to critical support issues? +- What monitoring and reporting requirements are specified in the SLA? +- When was the TechCorp service agreement signed and by whom? + +## Privacy Policy Queries +- What encryption methods does DataSecure use to protect data? +- How long does DataSecure retain personal data after account deletion? +- What rights do users have regarding their personal information? +- Can DataSecure sell user data to third parties for marketing? +- Who should be contacted for privacy-related concerns at DataSecure? + +## Supply Chain Agreement Queries +- What types of automotive components does PrecisionParts supply? +- What are the payment terms and volume discount structure? +- What quality standards must the supplied components meet? +- What are the penalties for late delivery? +- What insurance coverage requirements apply to the supplier? + +## Student Data Management Queries +- What federal laws must EduTech comply with regarding student data? +- What security measures are in place to protect student information? +- How long are student records retained after graduation? +- What consent is required for students under 13 years old? +- Who can access student educational records? + +## Investment Advisory Queries +- What is FinanceFirst's management fee structure? +- What types of investments are included in the advisory services? +- What regulatory body oversees FinanceFirst Advisors? +- How often are portfolio reviews conducted? +- What are the client's responsibilities under this agreement? + +## Healthcare Standards Queries +- What is the target response time for emergency code teams? +- What hand hygiene compliance rate is required? +- How quickly must medical records be completed after patient encounters? +- What continuing education requirements apply to nursing staff? +- What patient safety protocols are mandatory upon admission? + +## Cross-Document Queries +- Which agreements include confidentiality or data protection provisions? +- What are the common termination notice periods across different contract types? +- Which documents specify insurance or liability coverage requirements? +- What compliance and regulatory requirements are mentioned across agreements? +- Which contracts include performance metrics or service level commitments? + +## Complex Analysis Queries +- Compare the data retention policies across the privacy policy and student data management documents. +- What are the different approaches to risk management across the supply chain and investment advisory agreements? +- How do the security measures in the healthcare standards compare to those in the privacy policy? +- Which agreements provide the most detailed compliance and regulatory frameworks? +- What common themes exist in the quality assurance requirements across different industries? + +## Document-Specific Detail Queries +- List all the specific percentages, timeframes, and numerical requirements mentioned in the SLA. +- What are all the contact persons and their roles mentioned across the documents? +- Identify all the compliance standards and certifications referenced in the supply chain agreement. +- What are the specific consequences or penalties mentioned for non-compliance across agreements? +- List all the third-party systems, tools, or services mentioned in the documents. diff --git a/demos/use_cases/mcp_filter/src/rag_agent/__init__.py b/demos/use_cases/mcp_filter/src/rag_agent/__init__.py new file mode 100644 index 00000000..08f8e21f --- /dev/null +++ b/demos/use_cases/mcp_filter/src/rag_agent/__init__.py @@ -0,0 +1,98 @@ +import click +from fastmcp import FastMCP + +mcp = None + + +@click.command() +@click.option( + "--transport", + "transport", + default="streamable-http", + help="Transport type: stdio or sse", +) +@click.option("--host", "host", default="localhost", help="Host to bind MCP server to") +@click.option("--port", "port", type=int, default=10500, help="Port for MCP server") +@click.option( + "--agent", + "agent", + required=True, + help="Agent name: query_rewriter, context_builder, or response_generator", +) +@click.option( + "--name", + "agent_name", + default=None, + help="Custom MCP server name (defaults to agent type)", +) +@click.option( + "--rest-server", + "rest_server", + is_flag=True, + help="Start REST server instead of MCP server", +) +@click.option("--rest-port", "rest_port", default=8000, help="Port for REST server") +def main(host, port, agent, transport, agent_name, rest_server, rest_port): + """Start a RAG agent as an MCP server or REST server.""" + + # Map friendly names to agent modules + agent_map = { + "query_rewriter": ("rag_agent.query_rewriter", "Query Rewriter Agent"), + "context_builder": ("rag_agent.context_builder", "Context Builder Agent"), + "response_generator": ( + "rag_agent.rag_agent", + "Response Generator Agent", + ), + } + + if agent not in agent_map: + print(f"Error: Unknown agent '{agent}'") + print(f"Available agents: {', '.join(agent_map.keys())}") + return + + module_name, default_name = agent_map[agent] + mcp_name = agent_name or default_name + + if rest_server: + # Only response_generator supports REST server mode + if agent != "response_generator": + print(f"Error: Agent '{agent}' does not support REST server mode.") + print(f"REST server is only supported for: response_generator") + print(f"Remove --rest-server flag to start {agent} as an MCP server.") + return + + print(f"Starting REST server on {host}:{rest_port} for agent: {agent}") + from rag_agent.rag_agent import start_server + + start_server(host=host, port=rest_port) + return + else: + # Only query_rewriter and context_builder support MCP + if agent not in ["query_rewriter", "context_builder"]: + print(f"Error: Agent '{agent}' does not support MCP mode.") + print(f"MCP is only supported for: query_rewriter, context_builder") + print(f"Use --rest-server flag to start {agent} as a REST server.") + return + + global mcp + mcp = FastMCP(mcp_name, host=host, port=port) + + print(f"Starting MCP server: {mcp_name}") + print(f" Agent: {agent}") + print(f" Transport: {transport}") + print(f" Host: {host}") + print(f" Port: {port}") + + # Import the agent module to register its tools + import importlib + + importlib.import_module(module_name) + + print(f"Agent '{agent}' loaded successfully") + print(f"MCP server ready on {transport}://{host}:{port}") + + mcp.run(transport=transport) + + +if __name__ == "__main__": + main() diff --git a/demos/use_cases/mcp_filter/src/rag_agent/__main__.py b/demos/use_cases/mcp_filter/src/rag_agent/__main__.py new file mode 100644 index 00000000..868d99ef --- /dev/null +++ b/demos/use_cases/mcp_filter/src/rag_agent/__main__.py @@ -0,0 +1,4 @@ +from . import main + +if __name__ == "__main__": + main() diff --git a/demos/use_cases/mcp_filter/src/rag_agent/api.py b/demos/use_cases/mcp_filter/src/rag_agent/api.py new file mode 100644 index 00000000..eb63ea99 --- /dev/null +++ b/demos/use_cases/mcp_filter/src/rag_agent/api.py @@ -0,0 +1,36 @@ +from pydantic import BaseModel +from typing import List, Optional, Dict, Any + + +class ChatMessage(BaseModel): + role: str + content: str + + +class ChatCompletionRequest(BaseModel): + model: str + messages: List[ChatMessage] + temperature: Optional[float] = 1.0 + max_tokens: Optional[int] = None + top_p: Optional[float] = 1.0 + frequency_penalty: Optional[float] = 0.0 + presence_penalty: Optional[float] = 0.0 + stream: Optional[bool] = False + stop: Optional[List[str]] = None + + +class ChatCompletionResponse(BaseModel): + id: str + object: str = "chat.completion" + created: int + model: str + choices: List[Dict[str, Any]] + usage: Dict[str, int] + + +class ChatCompletionStreamResponse(BaseModel): + id: str + object: str = "chat.completion.chunk" + created: int + model: str + choices: List[Dict[str, Any]] diff --git a/demos/use_cases/mcp_filter/src/rag_agent/context_builder.py b/demos/use_cases/mcp_filter/src/rag_agent/context_builder.py new file mode 100644 index 00000000..2fa6e307 --- /dev/null +++ b/demos/use_cases/mcp_filter/src/rag_agent/context_builder.py @@ -0,0 +1,205 @@ +import json +from typing import List, Optional, Dict, Any +from openai import AsyncOpenAI +import os +import logging +import csv +from pathlib import Path + +from .api import ChatMessage +from . import mcp +from fastmcp.server.dependencies import get_http_headers + +# Set up logging +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - [CONTEXT_BUILDER] - %(levelname)s - %(message)s", +) +logger = logging.getLogger(__name__) + + +# Configuration for archgw LLM gateway +LLM_GATEWAY_ENDPOINT = os.getenv("LLM_GATEWAY_ENDPOINT", "http://localhost:12000/v1") +RAG_MODEL = "gpt-4o-mini" + +# Initialize OpenAI client for archgw +archgw_client = AsyncOpenAI( + base_url=LLM_GATEWAY_ENDPOINT, + api_key="EMPTY", # archgw doesn't require a real API key +) + +# Global variable to store the knowledge base +knowledge_base = [] + + +def load_knowledge_base(): + """Load the sample_knowledge_base.csv file into memory on startup.""" + global knowledge_base + + # Get the path to the CSV file relative to this script + current_dir = Path(__file__).parent + csv_path = current_dir / "sample_knowledge_base.csv" + + print(f"Loading knowledge base from {csv_path}") + + try: + knowledge_base = [] + with open(csv_path, "r", encoding="utf-8-sig") as file: + csv_reader = csv.DictReader(file) + for row in csv_reader: + knowledge_base.append({"path": row["path"], "content": row["content"]}) + + logger.info(f"Loaded {len(knowledge_base)} documents from knowledge base") + + except Exception as e: + logger.error(f"Error loading knowledge base: {e}") + knowledge_base = [] + + +async def find_relevant_passages( + query: str, traceparent: Optional[str] = None, top_k: int = 3 +) -> List[Dict[str, str]]: + """Use the LLM to find the most relevant passages from the knowledge base.""" + + if not knowledge_base: + logger.warning("Knowledge base is empty") + return [] + + # Create a system prompt for passage selection + system_prompt = f"""You are a retrieval assistant that selects the most relevant document passages for a given query. + + Given a user query and a list of document passages, identify the {top_k} most relevant passages that would help answer the query. + + Query: {query} + + Available passages: + """ + + # Add all passages with indices + for i, doc in enumerate(knowledge_base): + system_prompt += ( + f"\n[{i}] Path: {doc['path']}\nContent: {doc['content'][:500]}...\n" + ) + + system_prompt += f""" + + Please respond with ONLY the indices of the {top_k} most relevant passages, separated by commas (e.g., "0,3,7"). + If fewer than {top_k} passages are relevant, return only the relevant ones. + If no passages are relevant, return "NONE".""" + + try: + # Call archgw to select relevant passages + logger.info(f"Calling archgw to find relevant passages for query: '{query}'") + + # Prepare extra headers if traceparent is provided + extra_headers = {"x-envoy-max-retries": "3"} + if traceparent: + extra_headers["traceparent"] = traceparent + + response = await archgw_client.chat.completions.create( + model=RAG_MODEL, + messages=[{"role": "system", "content": system_prompt}], + temperature=0.1, + max_tokens=50, + extra_headers=extra_headers, + ) + + result = response.choices[0].message.content.strip() + logger.info(f"LLM selected passages: {result}") + + # Parse the indices + if result.upper() == "NONE": + return [] + + selected_passages = [] + indices = [ + int(idx.strip()) for idx in result.split(",") if idx.strip().isdigit() + ] + + for idx in indices: + if 0 <= idx < len(knowledge_base): + selected_passages.append(knowledge_base[idx]) + + logger.info(f"Selected {len(selected_passages)} relevant passages") + return selected_passages + + except Exception as e: + logger.error(f"Error finding relevant passages: {e}") + return [] + + +async def augment_query_with_context( + messages: List[ChatMessage], traceparent: Optional[str] = None +) -> List[ChatMessage]: + """Extract user query, find relevant context, and augment the messages.""" + + # Find the last user message + last_user_message = None + last_user_index = -1 + + for i in range(len(messages) - 1, -1, -1): + if messages[i].role == "user": + last_user_message = messages[i].content + last_user_index = i + break + + if not last_user_message: + logger.warning("No user message found in conversation") + return messages + + logger.info(f"Processing user query: '{last_user_message}'") + + # Find relevant passages + relevant_passages = await find_relevant_passages(last_user_message, traceparent) + + if not relevant_passages: + logger.info("No relevant passages found, returning original messages") + return messages + + # Build context from relevant passages + context_parts = [] + for i, passage in enumerate(relevant_passages): + context_parts.append( + f"Document {i+1} ({passage['path']}):\n{passage['content']}" + ) + + context = "\n\n".join(context_parts) + + # Create augmented content with original query and context + augmented_content = f"""{last_user_message} RELEVANT CONTEXT: + {context}""" + + # Create updated messages with the augmented query + updated_messages = messages.copy() + updated_messages[last_user_index] = ChatMessage( + role="user", content=augmented_content + ) + + logger.info(f"Augmented user query with {len(relevant_passages)} relevant passages") + + return updated_messages + + +# Load knowledge base on module import +load_knowledge_base() + + +@mcp.tool() +async def context_builder(messages: List[ChatMessage]) -> List[ChatMessage]: + """MCP tool that augments user queries with relevant context from the knowledge base.""" + logger.info(f"Received chat completion request with {len(messages)} messages") + + # Get traceparent header from MCP request + headers = get_http_headers() + traceparent_header = headers.get("traceparent") + + if traceparent_header: + logger.info(f"Received traceparent header: {traceparent_header}") + else: + logger.info("No traceparent header found") + + # Augment the user query with relevant context + updated_messages = await augment_query_with_context(messages, traceparent_header) + + # Return as dict to minimize text serialization + return [{"role": msg.role, "content": msg.content} for msg in updated_messages] diff --git a/demos/use_cases/mcp_filter/src/rag_agent/query_rewriter.py b/demos/use_cases/mcp_filter/src/rag_agent/query_rewriter.py new file mode 100644 index 00000000..89e5b200 --- /dev/null +++ b/demos/use_cases/mcp_filter/src/rag_agent/query_rewriter.py @@ -0,0 +1,119 @@ +import asyncio +import json +from typing import List, Optional, Dict, Any +from openai import AsyncOpenAI +import os +import logging + +from .api import ChatMessage +from . import mcp +from fastmcp.server.dependencies import get_http_headers + +# Set up logging +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - [QUERY_REWRITER] - %(levelname)s - %(message)s", +) +logger = logging.getLogger(__name__) + + +# Configuration for archgw LLM gateway +LLM_GATEWAY_ENDPOINT = os.getenv("LLM_GATEWAY_ENDPOINT", "http://localhost:12000/v1") +QUERY_REWRITE_MODEL = "gpt-4o-mini" + +# Initialize OpenAI client for archgw +archgw_client = AsyncOpenAI( + base_url=LLM_GATEWAY_ENDPOINT, + api_key="EMPTY", # archgw doesn't require a real API key +) + + +async def rewrite_query_with_archgw( + messages: List[ChatMessage], traceparent_header: str +) -> str: + """Rewrite the user query using LLM for better retrieval.""" + system_prompt = """You are a query rewriter that improves user queries for better retrieval. + + Given a conversation history, rewrite the last user message to be more specific and context-aware. + The rewritten query should: + 1. Include relevant context from previous messages + 2. Be clear and specific for information retrieval + 3. Maintain the user's intent + 4. Be concise but comprehensive + + Return only the rewritten query, nothing else.""" + + # Prepare messages for the query rewriter - just add system prompt to existing messages + rewrite_messages = [{"role": "system", "content": system_prompt}] + + # Add conversation history + for msg in messages: + rewrite_messages.append({"role": msg.role, "content": msg.content}) + + try: + # Call archgw using OpenAI client + extra_headers = {"x-envoy-max-retries": "3"} + if traceparent_header: + extra_headers["traceparent"] = traceparent_header + logger.info(f"Calling archgw at {LLM_GATEWAY_ENDPOINT} to rewrite query") + response = await archgw_client.chat.completions.create( + model=QUERY_REWRITE_MODEL, + messages=rewrite_messages, + temperature=0.3, + max_tokens=200, + extra_headers=extra_headers, + ) + + rewritten_query = response.choices[0].message.content.strip() + logger.info(f"Query rewritten successfully: '{rewritten_query}'") + return rewritten_query + + except Exception as e: + logger.error(f"Error rewriting query: {e}") + + # If rewriting fails, return the original last user message + logger.info("Falling back to original user message") + for message in reversed(messages): + if message.role == "user": + return message.content + return "" + + +@mcp.tool() +async def query_rewriter(messages: List[ChatMessage]) -> List[ChatMessage]: + """Chat completions endpoint that rewrites the last user query using archgw. + + Returns a dict with a 'messages' key containing the updated message list. + """ + import time + import uuid + + logger.info(f"Received chat completion request with {len(messages)} messages") + + # Get traceparent header from HTTP request using FastMCP's dependency function + headers = get_http_headers() + traceparent_header = headers.get("traceparent") + + if traceparent_header: + logger.info(f"Received traceparent header: {traceparent_header}") + else: + logger.info("No traceparent header found") + + # Call archgw to rewrite the last user query + rewritten_query = await rewrite_query_with_archgw(messages, traceparent_header) + + # Create updated messages with the rewritten query + updated_messages = messages.copy() + + # Find and update the last user message with the rewritten query + for i in range(len(updated_messages) - 1, -1, -1): + if updated_messages[i].role == "user": + original_query = updated_messages[i].content + updated_messages[i] = ChatMessage(role="user", content=rewritten_query) + logger.info( + f"Updated user query from '{original_query}' to '{rewritten_query}'" + ) + break + + # Return as dict to minimize text serialization + return [{"role": msg.role, "content": msg.content} for msg in updated_messages] diff --git a/demos/use_cases/mcp_filter/src/rag_agent/rag_agent.py b/demos/use_cases/mcp_filter/src/rag_agent/rag_agent.py new file mode 100644 index 00000000..b590248a --- /dev/null +++ b/demos/use_cases/mcp_filter/src/rag_agent/rag_agent.py @@ -0,0 +1,303 @@ +import json +from fastapi import FastAPI, Request +from fastapi.responses import StreamingResponse +from openai import AsyncOpenAI +import os +import logging +import time +import uuid +import uvicorn +import asyncio + +from .api import ( + ChatCompletionRequest, + ChatCompletionResponse, + ChatCompletionStreamResponse, +) + +# Set up logging +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - [RESPONSE_GENERATOR] - %(levelname)s - %(message)s", +) +logger = logging.getLogger(__name__) + +# Configuration for archgw LLM gateway +LLM_GATEWAY_ENDPOINT = os.getenv("LLM_GATEWAY_ENDPOINT", "http://localhost:12000/v1") +RESPONSE_MODEL = "gpt-4o" + +# System prompt for response generation +SYSTEM_PROMPT = """You are a helpful assistant that generates coherent, contextual responses. + +Given a conversation history, generate a helpful and relevant response based on all the context available in the messages. +Your response should: +1. Be contextually aware of the entire conversation +2. Address the user's needs appropriately +3. Be helpful and informative +4. Maintain a natural conversational tone + +Generate a complete response to assist the user.""" + +# Initialize OpenAI client for archgw +archgw_client = AsyncOpenAI( + base_url=LLM_GATEWAY_ENDPOINT, + api_key="EMPTY", # archgw doesn't require a real API key +) + +# FastAPI app for REST server +app = FastAPI(title="RAG Agent Response Generator", version="1.0.0") + + +def prepare_response_messages(request_body: ChatCompletionRequest): + """Prepare messages for response generation by adding system prompt.""" + response_messages = [{"role": "system", "content": SYSTEM_PROMPT}] + + # Add conversation history + for msg in request_body.messages: + response_messages.append({"role": msg.role, "content": msg.content}) + + return response_messages + + +@app.post("/v1/chat/completions") +async def chat_completion_http(request: Request, request_body: ChatCompletionRequest): + """HTTP endpoint for chat completions with streaming support.""" + logger.info( + f"Received chat completion request with {len(request_body.messages)} messages" + ) + + # Get traceparent header from HTTP request + traceparent_header = request.headers.get("traceparent") + + if traceparent_header: + logger.info(f"Received traceparent header: {traceparent_header}") + else: + logger.info("No traceparent header found") + + # Check if streaming is requested + if request_body.stream: + return StreamingResponse( + stream_chat_completions(request_body, traceparent_header), + media_type="text/plain", + headers={ + "content-type": "text/event-stream", + }, + ) + else: + return await non_streaming_chat_completions(request_body, traceparent_header) + + +async def stream_chat_completions( + request_body: ChatCompletionRequest, traceparent_header: str = None +): + """Generate streaming chat completions.""" + # Prepare messages for response generation + response_messages = prepare_response_messages(request_body) + + try: + # Call archgw using OpenAI client for streaming + logger.info( + f"Calling archgw at {LLM_GATEWAY_ENDPOINT} to generate streaming response" + ) + + # Prepare extra headers if traceparent is provided + extra_headers = {"x-envoy-max-retries": "3"} + if traceparent_header: + extra_headers["traceparent"] = traceparent_header + + response_stream = await archgw_client.chat.completions.create( + model=RESPONSE_MODEL, + messages=response_messages, + temperature=request_body.temperature or 0.7, + max_tokens=request_body.max_tokens or 1000, + stream=True, + extra_headers=extra_headers, + ) + + completion_id = f"chatcmpl-{uuid.uuid4().hex[:8]}" + created_time = int(time.time()) + collected_content = [] + + async for chunk in response_stream: + if chunk.choices and chunk.choices[0].delta.content: + content = chunk.choices[0].delta.content + collected_content.append(content) + + # Create streaming response chunk + stream_chunk = ChatCompletionStreamResponse( + id=completion_id, + created=created_time, + model=request_body.model, + choices=[ + { + "index": 0, + "delta": {"content": content}, + "finish_reason": None, + } + ], + ) + + yield f"data: {stream_chunk.model_dump_json()}\n\n" + + # Send final chunk with complete response in expected format + full_response = "".join(collected_content) + updated_history = [{"role": "assistant", "content": full_response}] + + final_chunk = ChatCompletionStreamResponse( + id=completion_id, + created=created_time, + model=request_body.model, + choices=[ + { + "index": 0, + "delta": {}, + "finish_reason": "stop", + "message": { + "role": "assistant", + "content": json.dumps(updated_history), + }, + } + ], + ) + + yield f"data: {final_chunk.model_dump_json()}\n\n" + yield "data: [DONE]\n\n" + + except Exception as e: + logger.error(f"Error generating streaming response: {e}") + + # Send error as streaming response + error_chunk = ChatCompletionStreamResponse( + id=f"chatcmpl-{uuid.uuid4().hex[:8]}", + created=int(time.time()), + model=request_body.model, + choices=[ + { + "index": 0, + "delta": { + "content": "I apologize, but I'm having trouble generating a response right now. Please try again." + }, + "finish_reason": "stop", + } + ], + ) + + yield f"data: {error_chunk.model_dump_json()}\n\n" + yield "data: [DONE]\n\n" + + +async def non_streaming_chat_completions( + request_body: ChatCompletionRequest, traceparent_header: str = None +): + """Generate non-streaming chat completions.""" + # Prepare messages for response generation + response_messages = prepare_response_messages(request_body) + + try: + # Call archgw using OpenAI client + logger.info(f"Calling archgw at {LLM_GATEWAY_ENDPOINT} to generate response") + + # Prepare extra headers if traceparent is provided + extra_headers = {"x-envoy-max-retries": "3"} + if traceparent_header: + extra_headers["traceparent"] = traceparent_header + + response = await archgw_client.chat.completions.create( + model=RESPONSE_MODEL, + messages=response_messages, + temperature=request_body.temperature or 0.7, + max_tokens=request_body.max_tokens or 1000, + extra_headers=extra_headers, + ) + + generated_response = response.choices[0].message.content.strip() + logger.info(f"Response generated successfully") + + return ChatCompletionResponse( + id=f"chatcmpl-{uuid.uuid4().hex[:8]}", + created=int(time.time()), + model=request_body.model, + choices=[ + { + "index": 0, + "message": { + "role": "assistant", + "content": generated_response, + }, + "finish_reason": "stop", + } + ], + usage={ + "prompt_tokens": sum( + len(msg.content.split()) for msg in request_body.messages + ), + "completion_tokens": len(generated_response.split()), + "total_tokens": sum( + len(msg.content.split()) for msg in request_body.messages + ) + + len(generated_response.split()), + }, + ) + + except Exception as e: + logger.error(f"Error generating response: {e}") + + # Fallback response + fallback_message = "I apologize, but I'm having trouble generating a response right now. Please try again." + return ChatCompletionResponse( + id=f"chatcmpl-{uuid.uuid4().hex[:8]}", + created=int(time.time()), + model=request_body.model, + choices=[ + { + "index": 0, + "message": {"role": "assistant", "content": fallback_message}, + "finish_reason": "stop", + } + ], + usage={ + "prompt_tokens": sum( + len(msg.content.split()) for msg in request_body.messages + ), + "completion_tokens": len(fallback_message.split()), + "total_tokens": sum( + len(msg.content.split()) for msg in request_body.messages + ) + + len(fallback_message.split()), + }, + ) + + +@app.get("/health") +async def health_check(): + """Health check endpoint.""" + return {"status": "healthy"} + + +def start_server(host: str = "localhost", port: int = 8000): + """Start the REST server.""" + uvicorn.run( + app, + host=host, + port=port, + log_config={ + "version": 1, + "disable_existing_loggers": False, + "formatters": { + "default": { + "format": "%(asctime)s - [RESPONSE_GENERATOR] - %(levelname)s - %(message)s", + }, + }, + "handlers": { + "default": { + "formatter": "default", + "class": "logging.StreamHandler", + "stream": "ext://sys.stdout", + }, + }, + "root": { + "level": "INFO", + "handlers": ["default"], + }, + }, + ) diff --git a/demos/use_cases/mcp_filter/src/rag_agent/sample_knowledge_base.csv b/demos/use_cases/mcp_filter/src/rag_agent/sample_knowledge_base.csv new file mode 100644 index 00000000..b9e6e8f0 --- /dev/null +++ b/demos/use_cases/mcp_filter/src/rag_agent/sample_knowledge_base.csv @@ -0,0 +1,257 @@ +path,content +TechCorp_CloudServices_SLA_Agreement_2024,"SERVICE LEVEL AGREEMENT +This Service Level Agreement (""SLA"") is entered into on March 15, 2024, between TechCorp Solutions Inc., a Delaware corporation (""Provider""), and CloudFirst Enterprises LLC (""Customer""). + +DEFINITIONS +Service Availability: The percentage of time during which the cloud services are operational and accessible. +Downtime: Any period when the services are unavailable or inaccessible to Customer. +Response Time: The time between service request submission and initial response from Provider. + +SERVICE COMMITMENTS +Provider guarantees 99.9% uptime for all cloud infrastructure services during any calendar month. +Average response time for API calls shall not exceed 200 milliseconds under normal operating conditions. +Customer support response times: Critical issues within 1 hour, Standard issues within 4 hours. + +REMEDIES +For each full percentage point below 99.9% availability, Customer receives 10% credit on monthly fees. +If response times exceed 500ms for more than 5 minutes in any hour, Customer receives 5% monthly credit. + +MONITORING AND REPORTING +Provider will maintain real-time monitoring systems and provide monthly performance reports. +All metrics will be measured from Provider's monitoring systems located in primary data centers. + +This SLA remains in effect for the duration of the underlying service agreement. + +Executed by: +TechCorp Solutions Inc. +Sarah Mitchell, VP Operations +Date: March 15, 2024 + +CloudFirst Enterprises LLC +Robert Chen, CTO +Date: March 16, 2024" + +DataSecure_Privacy_Policy_v3.2,"PRIVACY POLICY +DataSecure Analytics, Inc. (""Company"") Privacy Policy +Effective Date: January 1, 2024 +Last Updated: February 28, 2024 + +INFORMATION COLLECTION +We collect information you provide directly, such as account details, usage preferences, and communication records. +Automatically collected data includes IP addresses, browser types, device information, and service interaction logs. +Third-party integrations may provide additional user behavior and demographic information with consent. + +DATA USAGE +Personal information is used to provide services, improve user experience, and communicate service updates. +Aggregated, non-identifiable data may be used for analytics, research, and service enhancement. +We do not sell personal information to third parties for marketing purposes. + +DATA PROTECTION +All data is encrypted in transit using TLS 1.3 and at rest using AES-256 encryption. +Access controls limit data access to authorized personnel only on a need-to-know basis. +Regular security audits and penetration testing ensure ongoing protection measures. + +DATA RETENTION +Personal data is retained for the duration of active service plus 24 months. +Logs and analytics data are retained for 12 months unless legally required otherwise. +Upon account deletion, personal data is permanently removed within 30 days. + +USER RIGHTS +Users may request access to, correction of, or deletion of their personal information. +Data portability requests will be fulfilled in standard formats within 30 days. +Marketing communications can be opted out of at any time. + +CONTACT +For privacy concerns, contact: privacy@datasecure.com +Data Protection Officer: Jennifer Walsh, jwalsh@datasecure.com" + +GlobalManufacturing_SupplyChain_Contract_Q2_2024,"SUPPLY CHAIN AGREEMENT +This Supply Chain Agreement is entered into between GlobalManufacturing Corp (""Buyer"") and PrecisionParts Ltd (""Supplier"") effective April 1, 2024. + +SCOPE OF SERVICES +Supplier will provide automotive components including brake assemblies, suspension parts, and electrical harnesses. +All products must meet ISO 9001 quality standards and automotive industry specifications. +Delivery schedule: Weekly shipments every Tuesday, with 48-hour advance shipping notifications. + +PRICING AND PAYMENT +Component pricing is fixed for initial 6-month term with quarterly price review thereafter. +Payment terms: Net 45 days from invoice date via electronic transfer. +Volume discounts apply: 5% for orders exceeding 10,000 units per month, 8% for orders exceeding 25,000 units. + +QUALITY REQUIREMENTS +All components must pass incoming inspection with less than 0.1% defect rate. +Supplier maintains quality certifications including IATF 16949 and environmental compliance. +Batch tracking and traceability required for all delivered components. + +LOGISTICS AND DELIVERY +Supplier responsible for packaging, labeling, and delivery to Buyer's distribution centers. +Delivery windows: 8 AM - 4 PM, Monday through Friday, with advance appointment scheduling. +Late delivery penalties: 2% of shipment value for each day beyond scheduled delivery. + +RISK MANAGEMENT +Supplier maintains business continuity plans and alternative sourcing strategies. +Force majeure events must be reported within 24 hours with mitigation plans. +Insurance requirements: $5M general liability, $2M product liability coverage. + +INTELLECTUAL PROPERTY +All custom tooling and specifications remain property of Buyer. +Supplier grants license to use necessary patents for component manufacturing. + +This agreement shall remain in effect for 24 months with automatic renewal unless terminated. + +GlobalManufacturing Corp +Michael Rodriguez, Supply Chain Director +Date: April 1, 2024 + +PrecisionParts Ltd +Amanda Foster, VP Sales +Date: April 2, 2024" + +EduTech_StudentData_Management_Policy_2024,"STUDENT DATA MANAGEMENT POLICY +EduTech Learning Platform - Data Management and Protection Policy +Document Version: 2.1 +Effective Date: August 15, 2024 + +SCOPE AND PURPOSE +This policy governs the collection, use, storage, and protection of student educational records and personal information. +Applies to all employees, contractors, and third-party service providers accessing student data. +Compliance with FERPA, COPPA, and state student privacy laws is mandatory. + +DATA CLASSIFICATION +Educational Records: Grades, attendance, assignments, and academic progress information. +Personal Information: Names, addresses, contact details, and demographic information. +Behavioral Data: Learning patterns, platform usage, and engagement metrics. + +COLLECTION PRINCIPLES +Data collection is limited to educational purposes and service improvement only. +Parental consent required for students under 13 years of age. +Students and parents have right to review and request corrections to educational records. + +ACCESS CONTROLS +Role-based access ensures personnel see only data necessary for their functions. +Multi-factor authentication required for all system access. +Access logs maintained and reviewed monthly for unauthorized activity. + +DATA SHARING +Educational records shared only with authorized school personnel and parents/students. +No data sharing with third parties for commercial purposes without explicit consent. +Research data must be de-identified and aggregated before external sharing. + +SECURITY MEASURES +Data encrypted using industry-standard protocols during transmission and storage. +Regular security assessments and vulnerability testing conducted quarterly. +Incident response plan includes notification procedures for data breaches. + +RETENTION AND DISPOSAL +Student records retained according to school district policies, typically 5-7 years post-graduation. +Inactive accounts and associated data purged after 2 years of non-use. +Secure data destruction protocols ensure complete removal of sensitive information. + +COMPLIANCE MONITORING +Annual privacy training required for all staff handling student data. +Regular audits ensure ongoing compliance with applicable privacy regulations. +Privacy impact assessments conducted for new features or data uses. + +Contact: Dr. Lisa Thompson, Chief Privacy Officer +Email: privacy@edutech-learning.com +Phone: (555) 123-4567" + +FinanceFirst_Investment_Advisory_Agreement_2024,"INVESTMENT ADVISORY AGREEMENT +This Investment Advisory Agreement is entered into between FinanceFirst Advisors LLC (""Advisor"") and Madison Investment Group (""Client"") on May 20, 2024. + +ADVISORY SERVICES +Advisor will provide comprehensive investment management and financial planning services. +Services include portfolio construction, asset allocation, risk assessment, and performance monitoring. +Regular portfolio reviews conducted quarterly with detailed performance reporting. + +INVESTMENT AUTHORITY +Client grants Advisor discretionary authority to make investment decisions within agreed parameters. +Investment universe includes stocks, bonds, ETFs, mutual funds, and alternative investments as appropriate. +All trades executed through qualified broker-dealers with best execution practices. + +FEE STRUCTURE +Management fee: 1.25% annually on assets under management, calculated and billed quarterly. +Performance fee: 15% of returns exceeding S&P 500 benchmark, calculated annually. +Additional fees may apply for specialized services such as tax planning or estate planning. + +CLIENT RESPONSIBILITIES +Client must provide accurate financial information and promptly communicate changes in circumstances. +Investment objectives and risk tolerance should be reviewed and updated annually. +Client responsible for reviewing and approving investment policy statement. + +RISK DISCLOSURE +All investments carry risk of loss, and past performance does not guarantee future results. +Diversification does not ensure profit or protect against loss in declining markets. +Alternative investments may have limited liquidity and higher volatility. + +REGULATORY COMPLIANCE +Advisor is registered with the Securities and Exchange Commission as an investment advisor. +All activities conducted in accordance with Investment Advisers Act of 1940 and applicable regulations. +Form ADV Part 2 brochure provided annually with material updates. + +CONFIDENTIALITY +All client information treated as confidential and shared only as necessary for service provision. +Third-party service providers bound by confidentiality agreements. +Client data protected through secure systems and access controls. + +TERMINATION +Either party may terminate agreement with 30 days written notice. +Upon termination, Advisor will assist with orderly transfer of assets to new custodian or advisor. +Final fee calculation prorated to date of termination. + +FinanceFirst Advisors LLC +Thomas Anderson, Managing Partner +Date: May 20, 2024 + +Madison Investment Group +Rebecca Martinez, Chief Investment Officer +Date: May 21, 2024" + +HealthSystem_PatientCare_Standards_2024,"PATIENT CARE STANDARDS AND PROTOCOLS +Metropolitan Health System - Clinical Care Standards +Document ID: MHS-PCS-2024-001 +Effective Date: June 1, 2024 + +PATIENT SAFETY PROTOCOLS +All patients must have proper identification verification using two unique identifiers. +Medication administration requires independent double-check for high-risk medications. +Fall risk assessments completed within 4 hours of admission with appropriate interventions. + +CLINICAL DOCUMENTATION +Medical records must be completed within 24 hours of patient encounter. +All entries require electronic signature with timestamp and provider identification. +Critical values and abnormal results must be communicated and documented immediately. + +INFECTION CONTROL +Hand hygiene compliance monitored with target rate of 95% or higher. +Personal protective equipment used according to transmission-based precautions. +Isolation procedures implemented within 2 hours of identification of infectious conditions. + +EMERGENCY RESPONSE +Code team response time target: 3 minutes from activation to arrival. +Crash cart and emergency equipment checks performed daily and documented. +All staff required to maintain current CPR and emergency response certifications. + +PATIENT COMMUNICATION +Patient rights and responsibilities communicated upon admission. +Informed consent obtained and documented prior to procedures and treatments. +Family involvement encouraged with respect for patient privacy preferences. + +QUALITY MEASURES +Patient satisfaction scores monitored monthly with target of 4.5/5.0 or higher. +Medication error rates tracked with goal of less than 1 per 1000 patient days. +Hospital-acquired infection rates measured and benchmarked against national standards. + +STAFF COMPETENCY +Annual competency assessments required for all clinical staff. +Continuing education requirements: 24 hours annually for nurses, 40 hours for physicians. +Specialty certifications maintained according to department and role requirements. + +TECHNOLOGY STANDARDS +Electronic health record system used for all patient documentation. +Telemedicine capabilities available for remote consultations and monitoring. +Clinical decision support tools integrated to assist with diagnosis and treatment decisions. + +Contact: Dr. Patricia Williams, Chief Medical Officer +Email: pwilliams@metrohealthsystem.org +Phone: (555) 987-6543" diff --git a/demos/use_cases/mcp_filter/start_agents.sh b/demos/use_cases/mcp_filter/start_agents.sh new file mode 100644 index 00000000..e044fdda --- /dev/null +++ b/demos/use_cases/mcp_filter/start_agents.sh @@ -0,0 +1,47 @@ +#!/bin/bash +set -e + +WAIT_FOR_PIDS=() + +log() { + timestamp=$(python3 -c 'from datetime import datetime; print(datetime.now().strftime("%Y-%m-%d %H:%M:%S,%f")[:23])') + message="$*" + echo "$timestamp - $message" +} + +cleanup() { + log "Caught signal, terminating all user processes ..." + for PID in "${WAIT_FOR_PIDS[@]}"; do + if kill $PID 2> /dev/null; then + log "killed process: $PID" + fi + done + exit 1 +} + +trap cleanup EXIT + +# log "Starting input guards filter on port 10500..." +# uv run python -m rag_agent --host 0.0.0.0 --port 10500 --agent input_guards & +# WAIT_FOR_PIDS+=($!) + + +log "Starting query_parser agent on port 10501..." +uv run python -m rag_agent --host 0.0.0.0 --port 10501 --agent query_rewriter & +WAIT_FOR_PIDS+=($!) + +log "Starting context_builder agent on port 10502..." +uv run python -m rag_agent --host 0.0.0.0 --port 10502 --agent context_builder & +WAIT_FOR_PIDS+=($!) + +# log "Starting response_generator agent on port 10400..." +# uv run python -m rag_agent --host 0.0.0.0 --port 10400 --agent response_generator & +# WAIT_FOR_PIDS+=($!) + +log "Starting response_generator agent on port 10505..." +uv run python -m rag_agent --rest-server --host 0.0.0.0 --rest-port 10505 --agent response_generator & +WAIT_FOR_PIDS+=($!) + +for PID in "${WAIT_FOR_PIDS[@]}"; do + wait "$PID" +done diff --git a/demos/use_cases/mcp_filter/test.rest b/demos/use_cases/mcp_filter/test.rest new file mode 100644 index 00000000..13d773b1 --- /dev/null +++ b/demos/use_cases/mcp_filter/test.rest @@ -0,0 +1,95 @@ +@baseUrl = http://0.0.0.0:10502 +@model = gpt-4o + +# Health Check +GET {{baseUrl}}/health + +### + +# Test 1: Simple Non-Streaming Chat Completion +POST {{baseUrl}}/v1/chat/completions +Content-Type: application/json + +{ + "model": "{{model}}", + "messages": [ + { + "role": "user", + "content": "Hello! Can you help me understand what machine learning is?" + } + ] +} + +### + +# Test 2: Simple Streaming Chat Completion +POST {{baseUrl}}/v1/chat/completions +Content-Type: application/json + +{ + "model": "{{model}}", + "messages": [ + { + "role": "user", + "content": "Explain the concept of artificial intelligence in simple terms." + } + ], + "stream": true +} + +### Test 3 +POST http://localhost:8001/v1/chat/completions +Content-Type: application/json + +{ + "model": "{{model}}", + "messages": [ + { + "role": "user", + "content": "What is the guaranteed uptime percentage for TechCorp's cloud services?" + } + ], + "stream": true +} + +### send request to context builder agent +POST http://localhost:10501/v1/chat/completions +Content-Type: application/json + +{ + "model": "gpt-4o-mini", + "messages": [ + { + "role": "user", + "content": "What is the guaranteed uptime percentage for TechCorp's cloud services?" + } + ] +} + +### test fast-llm +POST http://localhost:12000/v1/chat/completions +Content-Type: application/json + +{ + "model": "fast-llm", + "messages": [ + { + "role": "user", + "content": "hello" + } + ] +} + +### test smart-llm +POST http://localhost:12000/v1/chat/completions +Content-Type: application/json + +{ + "model": "smart-llm", + "messages": [ + { + "role": "user", + "content": "hello" + } + ] +} diff --git a/demos/use_cases/mcp_filter/uv.lock b/demos/use_cases/mcp_filter/uv.lock new file mode 100644 index 00000000..6a39c6ec --- /dev/null +++ b/demos/use_cases/mcp_filter/uv.lock @@ -0,0 +1,1830 @@ +version = 1 +requires-python = ">=3.10" + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, +] + +[[package]] +name = "anyio" +version = "4.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "idna" }, + { name = "sniffio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f1/b4/636b3b65173d3ce9a38ef5f0522789614e590dab6a8d505340a4efe4c567/anyio-4.10.0.tar.gz", hash = "sha256:3f3fae35c96039744587aa5b8371e7e8e603c0702999535961dd336026973ba6", size = 213252 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/12/e5e0282d673bb9746bacfb6e2dba8719989d3660cdb2ea79aee9a9651afb/anyio-4.10.0-py3-none-any.whl", hash = "sha256:60e474ac86736bbfd6f210f7a61218939c318f43f9972497381f1c5e930ed3d1", size = 107213 }, +] + +[[package]] +name = "async-timeout" +version = "5.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/ae/136395dfbfe00dfc94da3f3e136d0b13f394cba8f4841120e34226265780/async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3", size = 9274 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233 }, +] + +[[package]] +name = "attrs" +version = "25.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815 }, +] + +[[package]] +name = "authlib" +version = "1.6.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/9b/b1661026ff24bc641b76b78c5222d614776b0c085bcfdac9bd15a1cb4b35/authlib-1.6.6.tar.gz", hash = "sha256:45770e8e056d0f283451d9996fbb59b70d45722b45d854d58f32878d0a40c38e", size = 164894 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/51/321e821856452f7386c4e9df866f196720b1ad0c5ea1623ea7399969ae3b/authlib-1.6.6-py2.py3-none-any.whl", hash = "sha256:7d9e9bc535c13974313a87f53e8430eb6ea3d1cf6ae4f6efcd793f2e949143fd", size = 244005 }, +] + +[[package]] +name = "backports-tarfile" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/86/72/cd9b395f25e290e633655a100af28cb253e4393396264a98bd5f5951d50f/backports_tarfile-1.2.0.tar.gz", hash = "sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991", size = 86406 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl", hash = "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34", size = 30181 }, +] + +[[package]] +name = "beartype" +version = "0.22.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/94/1009e248bbfbab11397abca7193bea6626806be9a327d399810d523a07cb/beartype-0.22.9.tar.gz", hash = "sha256:8f82b54aa723a2848a56008d18875f91c1db02c32ef6a62319a002e3e25a975f", size = 1608866 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/cc/18245721fa7747065ab478316c7fea7c74777d07f37ae60db2e84f8172e8/beartype-0.22.9-py3-none-any.whl", hash = "sha256:d16c9bbc61ea14637596c5f6fbff2ee99cbe3573e46a716401734ef50c3060c2", size = 1333658 }, +] + +[[package]] +name = "cachetools" +version = "6.2.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bc/1d/ede8680603f6016887c062a2cf4fc8fdba905866a3ab8831aa8aa651320c/cachetools-6.2.4.tar.gz", hash = "sha256:82c5c05585e70b6ba2d3ae09ea60b79548872185d2f24ae1f2709d37299fd607", size = 31731 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/fc/1d7b80d0eb7b714984ce40efc78859c022cd930e402f599d8ca9e39c78a4/cachetools-6.2.4-py3-none-any.whl", hash = "sha256:69a7a52634fed8b8bf6e24a050fb60bff1c9bd8f6d24572b99c32d4e71e62a51", size = 11551 }, +] + +[[package]] +name = "certifi" +version = "2025.8.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216 }, +] + +[[package]] +name = "cffi" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/07/f44ca684db4e4f08a3fdc6eeb9a0d15dc6883efc7b8c90357fdbf74e186c/cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14", size = 182191 }, + { url = "https://files.pythonhosted.org/packages/08/fd/cc2fedbd887223f9f5d170c96e57cbf655df9831a6546c1727ae13fa977a/cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67", size = 178592 }, + { url = "https://files.pythonhosted.org/packages/de/cc/4635c320081c78d6ffc2cab0a76025b691a91204f4aa317d568ff9280a2d/cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382", size = 426024 }, + { url = "https://files.pythonhosted.org/packages/b6/7b/3b2b250f3aab91abe5f8a51ada1b717935fdaec53f790ad4100fe2ec64d1/cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702", size = 448188 }, + { url = "https://files.pythonhosted.org/packages/d3/48/1b9283ebbf0ec065148d8de05d647a986c5f22586b18120020452fff8f5d/cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3", size = 455571 }, + { url = "https://files.pythonhosted.org/packages/40/87/3b8452525437b40f39ca7ff70276679772ee7e8b394934ff60e63b7b090c/cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6", size = 436687 }, + { url = "https://files.pythonhosted.org/packages/8d/fb/4da72871d177d63649ac449aec2e8a29efe0274035880c7af59101ca2232/cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17", size = 446211 }, + { url = "https://files.pythonhosted.org/packages/ab/a0/62f00bcb411332106c02b663b26f3545a9ef136f80d5df746c05878f8c4b/cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8", size = 461325 }, + { url = "https://files.pythonhosted.org/packages/36/83/76127035ed2e7e27b0787604d99da630ac3123bfb02d8e80c633f218a11d/cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e", size = 438784 }, + { url = "https://files.pythonhosted.org/packages/21/81/a6cd025db2f08ac88b901b745c163d884641909641f9b826e8cb87645942/cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be", size = 461564 }, + { url = "https://files.pythonhosted.org/packages/f8/fe/4d41c2f200c4a457933dbd98d3cf4e911870877bd94d9656cc0fcb390681/cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c", size = 171804 }, + { url = "https://files.pythonhosted.org/packages/d1/b6/0b0f5ab93b0df4acc49cae758c81fe4e5ef26c3ae2e10cc69249dfd8b3ab/cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15", size = 181299 }, + { url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264 }, + { url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651 }, + { url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259 }, + { url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200 }, + { url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235 }, + { url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721 }, + { url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242 }, + { url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999 }, + { url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242 }, + { url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604 }, + { url = "https://files.pythonhosted.org/packages/34/33/e1b8a1ba29025adbdcda5fb3a36f94c03d771c1b7b12f726ff7fef2ebe36/cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", size = 171727 }, + { url = "https://files.pythonhosted.org/packages/3d/97/50228be003bb2802627d28ec0627837ac0bf35c90cf769812056f235b2d1/cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", size = 181400 }, + { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178 }, + { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840 }, + { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803 }, + { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850 }, + { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729 }, + { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256 }, + { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424 }, + { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568 }, + { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736 }, + { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448 }, + { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976 }, + { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989 }, + { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802 }, + { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792 }, + { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893 }, + { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810 }, + { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200 }, + { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447 }, + { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358 }, + { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469 }, + { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475 }, + { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009 }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/83/2d/5fd176ceb9b2fc619e63405525573493ca23441330fcdaee6bef9460e924/charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14", size = 122371 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d6/98/f3b8013223728a99b908c9344da3aa04ee6e3fa235f19409033eda92fb78/charset_normalizer-3.4.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fb7f67a1bfa6e40b438170ebdc8158b78dc465a5a67b6dde178a46987b244a72", size = 207695 }, + { url = "https://files.pythonhosted.org/packages/21/40/5188be1e3118c82dcb7c2a5ba101b783822cfb413a0268ed3be0468532de/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc9370a2da1ac13f0153780040f465839e6cccb4a1e44810124b4e22483c93fe", size = 147153 }, + { url = "https://files.pythonhosted.org/packages/37/60/5d0d74bc1e1380f0b72c327948d9c2aca14b46a9efd87604e724260f384c/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:07a0eae9e2787b586e129fdcbe1af6997f8d0e5abaa0bc98c0e20e124d67e601", size = 160428 }, + { url = "https://files.pythonhosted.org/packages/85/9a/d891f63722d9158688de58d050c59dc3da560ea7f04f4c53e769de5140f5/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:74d77e25adda8581ffc1c720f1c81ca082921329452eba58b16233ab1842141c", size = 157627 }, + { url = "https://files.pythonhosted.org/packages/65/1a/7425c952944a6521a9cfa7e675343f83fd82085b8af2b1373a2409c683dc/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0e909868420b7049dafd3a31d45125b31143eec59235311fc4c57ea26a4acd2", size = 152388 }, + { url = "https://files.pythonhosted.org/packages/f0/c9/a2c9c2a355a8594ce2446085e2ec97fd44d323c684ff32042e2a6b718e1d/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c6f162aabe9a91a309510d74eeb6507fab5fff92337a15acbe77753d88d9dcf0", size = 150077 }, + { url = "https://files.pythonhosted.org/packages/3b/38/20a1f44e4851aa1c9105d6e7110c9d020e093dfa5836d712a5f074a12bf7/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4ca4c094de7771a98d7fbd67d9e5dbf1eb73efa4f744a730437d8a3a5cf994f0", size = 161631 }, + { url = "https://files.pythonhosted.org/packages/a4/fa/384d2c0f57edad03d7bec3ebefb462090d8905b4ff5a2d2525f3bb711fac/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:02425242e96bcf29a49711b0ca9f37e451da7c70562bc10e8ed992a5a7a25cc0", size = 159210 }, + { url = "https://files.pythonhosted.org/packages/33/9e/eca49d35867ca2db336b6ca27617deed4653b97ebf45dfc21311ce473c37/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:78deba4d8f9590fe4dae384aeff04082510a709957e968753ff3c48399f6f92a", size = 153739 }, + { url = "https://files.pythonhosted.org/packages/2a/91/26c3036e62dfe8de8061182d33be5025e2424002125c9500faff74a6735e/charset_normalizer-3.4.3-cp310-cp310-win32.whl", hash = "sha256:d79c198e27580c8e958906f803e63cddb77653731be08851c7df0b1a14a8fc0f", size = 99825 }, + { url = "https://files.pythonhosted.org/packages/e2/c6/f05db471f81af1fa01839d44ae2a8bfeec8d2a8b4590f16c4e7393afd323/charset_normalizer-3.4.3-cp310-cp310-win_amd64.whl", hash = "sha256:c6e490913a46fa054e03699c70019ab869e990270597018cef1d8562132c2669", size = 107452 }, + { url = "https://files.pythonhosted.org/packages/7f/b5/991245018615474a60965a7c9cd2b4efbaabd16d582a5547c47ee1c7730b/charset_normalizer-3.4.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b256ee2e749283ef3ddcff51a675ff43798d92d746d1a6e4631bf8c707d22d0b", size = 204483 }, + { url = "https://files.pythonhosted.org/packages/c7/2a/ae245c41c06299ec18262825c1569c5d3298fc920e4ddf56ab011b417efd/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13faeacfe61784e2559e690fc53fa4c5ae97c6fcedb8eb6fb8d0a15b475d2c64", size = 145520 }, + { url = "https://files.pythonhosted.org/packages/3a/a4/b3b6c76e7a635748c4421d2b92c7b8f90a432f98bda5082049af37ffc8e3/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:00237675befef519d9af72169d8604a067d92755e84fe76492fef5441db05b91", size = 158876 }, + { url = "https://files.pythonhosted.org/packages/e2/e6/63bb0e10f90a8243c5def74b5b105b3bbbfb3e7bb753915fe333fb0c11ea/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:585f3b2a80fbd26b048a0be90c5aae8f06605d3c92615911c3a2b03a8a3b796f", size = 156083 }, + { url = "https://files.pythonhosted.org/packages/87/df/b7737ff046c974b183ea9aa111b74185ac8c3a326c6262d413bd5a1b8c69/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e78314bdc32fa80696f72fa16dc61168fda4d6a0c014e0380f9d02f0e5d8a07", size = 150295 }, + { url = "https://files.pythonhosted.org/packages/61/f1/190d9977e0084d3f1dc169acd060d479bbbc71b90bf3e7bf7b9927dec3eb/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:96b2b3d1a83ad55310de8c7b4a2d04d9277d5591f40761274856635acc5fcb30", size = 148379 }, + { url = "https://files.pythonhosted.org/packages/4c/92/27dbe365d34c68cfe0ca76f1edd70e8705d82b378cb54ebbaeabc2e3029d/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:939578d9d8fd4299220161fdd76e86c6a251987476f5243e8864a7844476ba14", size = 160018 }, + { url = "https://files.pythonhosted.org/packages/99/04/baae2a1ea1893a01635d475b9261c889a18fd48393634b6270827869fa34/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:fd10de089bcdcd1be95a2f73dbe6254798ec1bda9f450d5828c96f93e2536b9c", size = 157430 }, + { url = "https://files.pythonhosted.org/packages/2f/36/77da9c6a328c54d17b960c89eccacfab8271fdaaa228305330915b88afa9/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1e8ac75d72fa3775e0b7cb7e4629cec13b7514d928d15ef8ea06bca03ef01cae", size = 151600 }, + { url = "https://files.pythonhosted.org/packages/64/d4/9eb4ff2c167edbbf08cdd28e19078bf195762e9bd63371689cab5ecd3d0d/charset_normalizer-3.4.3-cp311-cp311-win32.whl", hash = "sha256:6cf8fd4c04756b6b60146d98cd8a77d0cdae0e1ca20329da2ac85eed779b6849", size = 99616 }, + { url = "https://files.pythonhosted.org/packages/f4/9c/996a4a028222e7761a96634d1820de8a744ff4327a00ada9c8942033089b/charset_normalizer-3.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:31a9a6f775f9bcd865d88ee350f0ffb0e25936a7f930ca98995c05abf1faf21c", size = 107108 }, + { url = "https://files.pythonhosted.org/packages/e9/5e/14c94999e418d9b87682734589404a25854d5f5d0408df68bc15b6ff54bb/charset_normalizer-3.4.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1", size = 205655 }, + { url = "https://files.pythonhosted.org/packages/7d/a8/c6ec5d389672521f644505a257f50544c074cf5fc292d5390331cd6fc9c3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884", size = 146223 }, + { url = "https://files.pythonhosted.org/packages/fc/eb/a2ffb08547f4e1e5415fb69eb7db25932c52a52bed371429648db4d84fb1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018", size = 159366 }, + { url = "https://files.pythonhosted.org/packages/82/10/0fd19f20c624b278dddaf83b8464dcddc2456cb4b02bb902a6da126b87a1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392", size = 157104 }, + { url = "https://files.pythonhosted.org/packages/16/ab/0233c3231af734f5dfcf0844aa9582d5a1466c985bbed6cedab85af9bfe3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f", size = 151830 }, + { url = "https://files.pythonhosted.org/packages/ae/02/e29e22b4e02839a0e4a06557b1999d0a47db3567e82989b5bb21f3fbbd9f/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154", size = 148854 }, + { url = "https://files.pythonhosted.org/packages/05/6b/e2539a0a4be302b481e8cafb5af8792da8093b486885a1ae4d15d452bcec/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491", size = 160670 }, + { url = "https://files.pythonhosted.org/packages/31/e7/883ee5676a2ef217a40ce0bffcc3d0dfbf9e64cbcfbdf822c52981c3304b/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93", size = 158501 }, + { url = "https://files.pythonhosted.org/packages/c1/35/6525b21aa0db614cf8b5792d232021dca3df7f90a1944db934efa5d20bb1/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f", size = 153173 }, + { url = "https://files.pythonhosted.org/packages/50/ee/f4704bad8201de513fdc8aac1cabc87e38c5818c93857140e06e772b5892/charset_normalizer-3.4.3-cp312-cp312-win32.whl", hash = "sha256:fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37", size = 99822 }, + { url = "https://files.pythonhosted.org/packages/39/f5/3b3836ca6064d0992c58c7561c6b6eee1b3892e9665d650c803bd5614522/charset_normalizer-3.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc", size = 107543 }, + { url = "https://files.pythonhosted.org/packages/65/ca/2135ac97709b400c7654b4b764daf5c5567c2da45a30cdd20f9eefe2d658/charset_normalizer-3.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe", size = 205326 }, + { url = "https://files.pythonhosted.org/packages/71/11/98a04c3c97dd34e49c7d247083af03645ca3730809a5509443f3c37f7c99/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8", size = 146008 }, + { url = "https://files.pythonhosted.org/packages/60/f5/4659a4cb3c4ec146bec80c32d8bb16033752574c20b1252ee842a95d1a1e/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9", size = 159196 }, + { url = "https://files.pythonhosted.org/packages/86/9e/f552f7a00611f168b9a5865a1414179b2c6de8235a4fa40189f6f79a1753/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31", size = 156819 }, + { url = "https://files.pythonhosted.org/packages/7e/95/42aa2156235cbc8fa61208aded06ef46111c4d3f0de233107b3f38631803/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f", size = 151350 }, + { url = "https://files.pythonhosted.org/packages/c2/a9/3865b02c56f300a6f94fc631ef54f0a8a29da74fb45a773dfd3dcd380af7/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927", size = 148644 }, + { url = "https://files.pythonhosted.org/packages/77/d9/cbcf1a2a5c7d7856f11e7ac2d782aec12bdfea60d104e60e0aa1c97849dc/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9", size = 160468 }, + { url = "https://files.pythonhosted.org/packages/f6/42/6f45efee8697b89fda4d50580f292b8f7f9306cb2971d4b53f8914e4d890/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5", size = 158187 }, + { url = "https://files.pythonhosted.org/packages/70/99/f1c3bdcfaa9c45b3ce96f70b14f070411366fa19549c1d4832c935d8e2c3/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc", size = 152699 }, + { url = "https://files.pythonhosted.org/packages/a3/ad/b0081f2f99a4b194bcbb1934ef3b12aa4d9702ced80a37026b7607c72e58/charset_normalizer-3.4.3-cp313-cp313-win32.whl", hash = "sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce", size = 99580 }, + { url = "https://files.pythonhosted.org/packages/9a/8f/ae790790c7b64f925e5c953b924aaa42a243fb778fed9e41f147b2a5715a/charset_normalizer-3.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef", size = 107366 }, + { url = "https://files.pythonhosted.org/packages/8e/91/b5a06ad970ddc7a0e513112d40113e834638f4ca1120eb727a249fb2715e/charset_normalizer-3.4.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15", size = 204342 }, + { url = "https://files.pythonhosted.org/packages/ce/ec/1edc30a377f0a02689342f214455c3f6c2fbedd896a1d2f856c002fc3062/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db", size = 145995 }, + { url = "https://files.pythonhosted.org/packages/17/e5/5e67ab85e6d22b04641acb5399c8684f4d37caf7558a53859f0283a650e9/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d", size = 158640 }, + { url = "https://files.pythonhosted.org/packages/f1/e5/38421987f6c697ee3722981289d554957c4be652f963d71c5e46a262e135/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096", size = 156636 }, + { url = "https://files.pythonhosted.org/packages/a0/e4/5a075de8daa3ec0745a9a3b54467e0c2967daaaf2cec04c845f73493e9a1/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa", size = 150939 }, + { url = "https://files.pythonhosted.org/packages/02/f7/3611b32318b30974131db62b4043f335861d4d9b49adc6d57c1149cc49d4/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049", size = 148580 }, + { url = "https://files.pythonhosted.org/packages/7e/61/19b36f4bd67f2793ab6a99b979b4e4f3d8fc754cbdffb805335df4337126/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0", size = 159870 }, + { url = "https://files.pythonhosted.org/packages/06/57/84722eefdd338c04cf3030ada66889298eaedf3e7a30a624201e0cbe424a/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92", size = 157797 }, + { url = "https://files.pythonhosted.org/packages/72/2a/aff5dd112b2f14bcc3462c312dce5445806bfc8ab3a7328555da95330e4b/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16", size = 152224 }, + { url = "https://files.pythonhosted.org/packages/b7/8c/9839225320046ed279c6e839d51f028342eb77c91c89b8ef2549f951f3ec/charset_normalizer-3.4.3-cp314-cp314-win32.whl", hash = "sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce", size = 100086 }, + { url = "https://files.pythonhosted.org/packages/ee/7a/36fbcf646e41f710ce0a563c1c9a343c6edf9be80786edeb15b6f62e17db/charset_normalizer-3.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c", size = 107400 }, + { url = "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", size = 53175 }, +] + +[[package]] +name = "click" +version = "8.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "platform_system == 'Windows'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215 }, +] + +[[package]] +name = "cloudpickle" +version = "3.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/27/fb/576f067976d320f5f0114a8d9fa1215425441bb35627b1993e5afd8111e5/cloudpickle-3.1.2.tar.gz", hash = "sha256:7fda9eb655c9c230dab534f1983763de5835249750e85fbcef43aaa30a9a2414", size = 22330 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl", hash = "sha256:9acb47f6afd73f60dc1df93bb801b472f05ff42fa6c84167d25cb206be1fbf4a", size = 22228 }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "cryptography" +version = "45.0.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a7/35/c495bffc2056f2dadb32434f1feedd79abde2a7f8363e1974afa9c33c7e2/cryptography-45.0.7.tar.gz", hash = "sha256:4b1654dfc64ea479c242508eb8c724044f1e964a47d1d1cacc5132292d851971", size = 744980 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/91/925c0ac74362172ae4516000fe877912e33b5983df735ff290c653de4913/cryptography-45.0.7-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:3be4f21c6245930688bd9e162829480de027f8bf962ede33d4f8ba7d67a00cee", size = 7041105 }, + { url = "https://files.pythonhosted.org/packages/fc/63/43641c5acce3a6105cf8bd5baeceeb1846bb63067d26dae3e5db59f1513a/cryptography-45.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:67285f8a611b0ebc0857ced2081e30302909f571a46bfa7a3cc0ad303fe015c6", size = 4205799 }, + { url = "https://files.pythonhosted.org/packages/bc/29/c238dd9107f10bfde09a4d1c52fd38828b1aa353ced11f358b5dd2507d24/cryptography-45.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:577470e39e60a6cd7780793202e63536026d9b8641de011ed9d8174da9ca5339", size = 4430504 }, + { url = "https://files.pythonhosted.org/packages/62/62/24203e7cbcc9bd7c94739428cd30680b18ae6b18377ae66075c8e4771b1b/cryptography-45.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:4bd3e5c4b9682bc112d634f2c6ccc6736ed3635fc3319ac2bb11d768cc5a00d8", size = 4209542 }, + { url = "https://files.pythonhosted.org/packages/cd/e3/e7de4771a08620eef2389b86cd87a2c50326827dea5528feb70595439ce4/cryptography-45.0.7-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:465ccac9d70115cd4de7186e60cfe989de73f7bb23e8a7aa45af18f7412e75bf", size = 3889244 }, + { url = "https://files.pythonhosted.org/packages/96/b8/bca71059e79a0bb2f8e4ec61d9c205fbe97876318566cde3b5092529faa9/cryptography-45.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:16ede8a4f7929b4b7ff3642eba2bf79aa1d71f24ab6ee443935c0d269b6bc513", size = 4461975 }, + { url = "https://files.pythonhosted.org/packages/58/67/3f5b26937fe1218c40e95ef4ff8d23c8dc05aa950d54200cc7ea5fb58d28/cryptography-45.0.7-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:8978132287a9d3ad6b54fcd1e08548033cc09dc6aacacb6c004c73c3eb5d3ac3", size = 4209082 }, + { url = "https://files.pythonhosted.org/packages/0e/e4/b3e68a4ac363406a56cf7b741eeb80d05284d8c60ee1a55cdc7587e2a553/cryptography-45.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:b6a0e535baec27b528cb07a119f321ac024592388c5681a5ced167ae98e9fff3", size = 4460397 }, + { url = "https://files.pythonhosted.org/packages/22/49/2c93f3cd4e3efc8cb22b02678c1fad691cff9dd71bb889e030d100acbfe0/cryptography-45.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:a24ee598d10befaec178efdff6054bc4d7e883f615bfbcd08126a0f4931c83a6", size = 4337244 }, + { url = "https://files.pythonhosted.org/packages/04/19/030f400de0bccccc09aa262706d90f2ec23d56bc4eb4f4e8268d0ddf3fb8/cryptography-45.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:fa26fa54c0a9384c27fcdc905a2fb7d60ac6e47d14bc2692145f2b3b1e2cfdbd", size = 4568862 }, + { url = "https://files.pythonhosted.org/packages/29/56/3034a3a353efa65116fa20eb3c990a8c9f0d3db4085429040a7eef9ada5f/cryptography-45.0.7-cp311-abi3-win32.whl", hash = "sha256:bef32a5e327bd8e5af915d3416ffefdbe65ed975b646b3805be81b23580b57b8", size = 2936578 }, + { url = "https://files.pythonhosted.org/packages/b3/61/0ab90f421c6194705a99d0fa9f6ee2045d916e4455fdbb095a9c2c9a520f/cryptography-45.0.7-cp311-abi3-win_amd64.whl", hash = "sha256:3808e6b2e5f0b46d981c24d79648e5c25c35e59902ea4391a0dcb3e667bf7443", size = 3405400 }, + { url = "https://files.pythonhosted.org/packages/63/e8/c436233ddf19c5f15b25ace33979a9dd2e7aa1a59209a0ee8554179f1cc0/cryptography-45.0.7-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bfb4c801f65dd61cedfc61a83732327fafbac55a47282e6f26f073ca7a41c3b2", size = 7021824 }, + { url = "https://files.pythonhosted.org/packages/bc/4c/8f57f2500d0ccd2675c5d0cc462095adf3faa8c52294ba085c036befb901/cryptography-45.0.7-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:81823935e2f8d476707e85a78a405953a03ef7b7b4f55f93f7c2d9680e5e0691", size = 4202233 }, + { url = "https://files.pythonhosted.org/packages/eb/ac/59b7790b4ccaed739fc44775ce4645c9b8ce54cbec53edf16c74fd80cb2b/cryptography-45.0.7-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3994c809c17fc570c2af12c9b840d7cea85a9fd3e5c0e0491f4fa3c029216d59", size = 4423075 }, + { url = "https://files.pythonhosted.org/packages/b8/56/d4f07ea21434bf891faa088a6ac15d6d98093a66e75e30ad08e88aa2b9ba/cryptography-45.0.7-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:dad43797959a74103cb59c5dac71409f9c27d34c8a05921341fb64ea8ccb1dd4", size = 4204517 }, + { url = "https://files.pythonhosted.org/packages/e8/ac/924a723299848b4c741c1059752c7cfe09473b6fd77d2920398fc26bfb53/cryptography-45.0.7-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ce7a453385e4c4693985b4a4a3533e041558851eae061a58a5405363b098fcd3", size = 3882893 }, + { url = "https://files.pythonhosted.org/packages/83/dc/4dab2ff0a871cc2d81d3ae6d780991c0192b259c35e4d83fe1de18b20c70/cryptography-45.0.7-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b04f85ac3a90c227b6e5890acb0edbaf3140938dbecf07bff618bf3638578cf1", size = 4450132 }, + { url = "https://files.pythonhosted.org/packages/12/dd/b2882b65db8fc944585d7fb00d67cf84a9cef4e77d9ba8f69082e911d0de/cryptography-45.0.7-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:48c41a44ef8b8c2e80ca4527ee81daa4c527df3ecbc9423c41a420a9559d0e27", size = 4204086 }, + { url = "https://files.pythonhosted.org/packages/5d/fa/1d5745d878048699b8eb87c984d4ccc5da4f5008dfd3ad7a94040caca23a/cryptography-45.0.7-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:f3df7b3d0f91b88b2106031fd995802a2e9ae13e02c36c1fc075b43f420f3a17", size = 4449383 }, + { url = "https://files.pythonhosted.org/packages/36/8b/fc61f87931bc030598e1876c45b936867bb72777eac693e905ab89832670/cryptography-45.0.7-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:dd342f085542f6eb894ca00ef70236ea46070c8a13824c6bde0dfdcd36065b9b", size = 4332186 }, + { url = "https://files.pythonhosted.org/packages/0b/11/09700ddad7443ccb11d674efdbe9a832b4455dc1f16566d9bd3834922ce5/cryptography-45.0.7-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1993a1bb7e4eccfb922b6cd414f072e08ff5816702a0bdb8941c247a6b1b287c", size = 4561639 }, + { url = "https://files.pythonhosted.org/packages/71/ed/8f4c1337e9d3b94d8e50ae0b08ad0304a5709d483bfcadfcc77a23dbcb52/cryptography-45.0.7-cp37-abi3-win32.whl", hash = "sha256:18fcf70f243fe07252dcb1b268a687f2358025ce32f9f88028ca5c364b123ef5", size = 2926552 }, + { url = "https://files.pythonhosted.org/packages/bc/ff/026513ecad58dacd45d1d24ebe52b852165a26e287177de1d545325c0c25/cryptography-45.0.7-cp37-abi3-win_amd64.whl", hash = "sha256:7285a89df4900ed3bfaad5679b1e668cb4b38a8de1ccbfc84b05f34512da0a90", size = 3392742 }, + { url = "https://files.pythonhosted.org/packages/13/3e/e42f1528ca1ea82256b835191eab1be014e0f9f934b60d98b0be8a38ed70/cryptography-45.0.7-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:de58755d723e86175756f463f2f0bddd45cc36fbd62601228a3f8761c9f58252", size = 3572442 }, + { url = "https://files.pythonhosted.org/packages/59/aa/e947693ab08674a2663ed2534cd8d345cf17bf6a1facf99273e8ec8986dc/cryptography-45.0.7-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a20e442e917889d1a6b3c570c9e3fa2fdc398c20868abcea268ea33c024c4083", size = 4142233 }, + { url = "https://files.pythonhosted.org/packages/24/06/09b6f6a2fc43474a32b8fe259038eef1500ee3d3c141599b57ac6c57612c/cryptography-45.0.7-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:258e0dff86d1d891169b5af222d362468a9570e2532923088658aa866eb11130", size = 4376202 }, + { url = "https://files.pythonhosted.org/packages/00/f2/c166af87e95ce6ae6d38471a7e039d3a0549c2d55d74e059680162052824/cryptography-45.0.7-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d97cf502abe2ab9eff8bd5e4aca274da8d06dd3ef08b759a8d6143f4ad65d4b4", size = 4141900 }, + { url = "https://files.pythonhosted.org/packages/16/b9/e96e0b6cb86eae27ea51fa8a3151535a18e66fe7c451fa90f7f89c85f541/cryptography-45.0.7-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:c987dad82e8c65ebc985f5dae5e74a3beda9d0a2a4daf8a1115f3772b59e5141", size = 4375562 }, + { url = "https://files.pythonhosted.org/packages/36/d0/36e8ee39274e9d77baf7d0dafda680cba6e52f3936b846f0d56d64fec915/cryptography-45.0.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c13b1e3afd29a5b3b2656257f14669ca8fa8d7956d509926f0b130b600b50ab7", size = 3322781 }, + { url = "https://files.pythonhosted.org/packages/99/4e/49199a4c82946938a3e05d2e8ad9482484ba48bbc1e809e3d506c686d051/cryptography-45.0.7-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:4a862753b36620af6fc54209264f92c716367f2f0ff4624952276a6bbd18cbde", size = 3584634 }, + { url = "https://files.pythonhosted.org/packages/16/ce/5f6ff59ea9c7779dba51b84871c19962529bdcc12e1a6ea172664916c550/cryptography-45.0.7-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:06ce84dc14df0bf6ea84666f958e6080cdb6fe1231be2a51f3fc1267d9f3fb34", size = 4149533 }, + { url = "https://files.pythonhosted.org/packages/ce/13/b3cfbd257ac96da4b88b46372e662009b7a16833bfc5da33bb97dd5631ae/cryptography-45.0.7-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d0c5c6bac22b177bf8da7435d9d27a6834ee130309749d162b26c3105c0795a9", size = 4385557 }, + { url = "https://files.pythonhosted.org/packages/1c/c5/8c59d6b7c7b439ba4fc8d0cab868027fd095f215031bc123c3a070962912/cryptography-45.0.7-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:2f641b64acc00811da98df63df7d59fd4706c0df449da71cb7ac39a0732b40ae", size = 4149023 }, + { url = "https://files.pythonhosted.org/packages/55/32/05385c86d6ca9ab0b4d5bb442d2e3d85e727939a11f3e163fc776ce5eb40/cryptography-45.0.7-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:f5414a788ecc6ee6bc58560e85ca624258a55ca434884445440a810796ea0e0b", size = 4385722 }, + { url = "https://files.pythonhosted.org/packages/23/87/7ce86f3fa14bc11a5a48c30d8103c26e09b6465f8d8e9d74cf7a0714f043/cryptography-45.0.7-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:1f3d56f73595376f4244646dd5c5870c14c196949807be39e79e7bd9bac3da63", size = 3332908 }, +] + +[[package]] +name = "cyclopts" +version = "4.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "docstring-parser" }, + { name = "rich" }, + { name = "rich-rst" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1b/0f/fe026df2ab8301e30a2b0bd425ff1462ad858fd4f991c1ac0389c2059c24/cyclopts-4.3.0.tar.gz", hash = "sha256:e95179cd0a959ce250ecfb2f0262a5996a92c1f9467bccad2f3d829e6833cef5", size = 151411 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/e8/77a231ae531cf38765b75ddf27dae28bb5f70b41d8bb4f15ce1650e93f57/cyclopts-4.3.0-py3-none-any.whl", hash = "sha256:91a30b69faf128ada7cfeaefd7d9649dc222e8b2a8697f1fc99e4ee7b7ca44f3", size = 187184 }, +] + +[[package]] +name = "diskcache" +version = "5.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3f/21/1c1ffc1a039ddcc459db43cc108658f32c57d271d7289a2794e401d0fdb6/diskcache-5.6.3.tar.gz", hash = "sha256:2c3a3fa2743d8535d832ec61c2054a1641f41775aa7c556758a109941e33e4fc", size = 67916 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/27/4570e78fc0bf5ea0ca45eb1de3818a23787af9b390c0b0a0033a1b8236f9/diskcache-5.6.3-py3-none-any.whl", hash = "sha256:5e31b2d5fbad117cc363ebaf6b689474db18a1f6438bc82358b024abd4c2ca19", size = 45550 }, +] + +[[package]] +name = "distro" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277 }, +] + +[[package]] +name = "dnspython" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/4a/263763cb2ba3816dd94b08ad3a33d5fdae34ecb856678773cc40a3605829/dnspython-2.7.0.tar.gz", hash = "sha256:ce9c432eda0dc91cf618a5cedf1a4e142651196bbcd2c80e89ed5a907e5cfaf1", size = 345197 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/1b/e0a87d256e40e8c888847551b20a017a6b98139178505dc7ffb96f04e954/dnspython-2.7.0-py3-none-any.whl", hash = "sha256:b4c34b7d10b51bcc3a5071e7b8dee77939f1e878477eeecc965e9835f63c6c86", size = 313632 }, +] + +[[package]] +name = "docstring-parser" +version = "0.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/9d/c3b43da9515bd270df0f80548d9944e389870713cc1fe2b8fb35fe2bcefd/docstring_parser-0.17.0.tar.gz", hash = "sha256:583de4a309722b3315439bb31d64ba3eebada841f2e2cee23b99df001434c912", size = 27442 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/e2/2537ebcff11c1ee1ff17d8d0b6f4db75873e3b0fb32c2d4a2ee31ecb310a/docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708", size = 36896 }, +] + +[[package]] +name = "docutils" +version = "0.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e9/86/5b41c32ecedcfdb4c77b28b6cb14234f252075f8cdb254531727a35547dd/docutils-0.22.tar.gz", hash = "sha256:ba9d57750e92331ebe7c08a1bbf7a7f8143b86c476acd51528b042216a6aad0f", size = 2277984 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/57/8db39bc5f98f042e0153b1de9fb88e1a409a33cda4dd7f723c2ed71e01f6/docutils-0.22-py3-none-any.whl", hash = "sha256:4ed966a0e96a0477d852f7af31bdcb3adc049fbb35ccba358c2ea8a03287615e", size = 630709 }, +] + +[[package]] +name = "email-validator" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dnspython" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/22/900cb125c76b7aaa450ce02fd727f452243f2e91a61af068b40adba60ea9/email_validator-2.3.0.tar.gz", hash = "sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426", size = 51238 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4", size = 35604 }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674 }, +] + +[[package]] +name = "fakeredis" +version = "2.32.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "redis" }, + { name = "sortedcontainers" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/56/14/b47b8471303af7deed7080290c14cff27a831fa47b38f45643e6bf889cee/fakeredis-2.32.1.tar.gz", hash = "sha256:dd8246db159f0b66a1ced7800c9d5ef07769e3d2fde44b389a57f2ce2834e444", size = 171582 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/d2/c28f6909864bfdb7411bb8f39fabedb5a50da1cbd7da5a1a3a46dfea2eab/fakeredis-2.32.1-py3-none-any.whl", hash = "sha256:e80c8886db2e47ba784f7dfe66aad6cd2eab76093c6bfda50041e5bc890d46cf", size = 118964 }, +] + +[package.optional-dependencies] +lua = [ + { name = "lupa" }, +] + +[[package]] +name = "fastapi" +version = "0.116.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/d7/6c8b3bfe33eeffa208183ec037fee0cce9f7f024089ab1c5d12ef04bd27c/fastapi-0.116.1.tar.gz", hash = "sha256:ed52cbf946abfd70c5a0dccb24673f0670deeb517a88b3544d03c2a6bf283143", size = 296485 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/47/d63c60f59a59467fda0f93f46335c9d18526d7071f025cb5b89d5353ea42/fastapi-0.116.1-py3-none-any.whl", hash = "sha256:c46ac7c312df840f0c9e220f7964bada936781bc4e2e6eb71f1c4d7553786565", size = 95631 }, +] + +[[package]] +name = "fastmcp" +version = "2.14.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "authlib" }, + { name = "cyclopts" }, + { name = "exceptiongroup" }, + { name = "httpx" }, + { name = "jsonschema-path" }, + { name = "mcp" }, + { name = "openapi-pydantic" }, + { name = "platformdirs" }, + { name = "py-key-value-aio", extra = ["disk", "keyring", "memory"] }, + { name = "pydantic", extra = ["email"] }, + { name = "pydocket" }, + { name = "pyperclip" }, + { name = "python-dotenv" }, + { name = "rich" }, + { name = "uvicorn" }, + { name = "websockets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/50/d38e4371bdc34e709f4731b1e882cb7bc50e51c1a224859d4cd381b3a79b/fastmcp-2.14.1.tar.gz", hash = "sha256:132725cbf77b68fa3c3d165eff0cfa47e40c1479457419e6a2cfda65bd84c8d6", size = 8263331 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/82/72401d09dc27c27fdf72ad6c2fe331e553e3c3646e01b5ff16473191033d/fastmcp-2.14.1-py3-none-any.whl", hash = "sha256:fb3e365cc1d52573ab89caeba9944dd4b056149097be169bce428e011f0a57e5", size = 412176 }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515 }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784 }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 }, +] + +[[package]] +name = "httpx-sse" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6e/fa/66bd985dd0b7c109a3bcb89272ee0bfb7e2b4d06309ad7b38ff866734b2a/httpx_sse-0.4.1.tar.gz", hash = "sha256:8f44d34414bc7b21bf3602713005c5df4917884f76072479b21f68befa4ea26e", size = 12998 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/0a/6269e3473b09aed2dab8aa1a600c70f31f00ae1349bee30658f7e358a159/httpx_sse-0.4.1-py3-none-any.whl", hash = "sha256:cba42174344c3a5b06f255ce65b350880f962d99ead85e776f23c6618a377a37", size = 8054 }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, +] + +[[package]] +name = "importlib-metadata" +version = "8.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656 }, +] + +[[package]] +name = "jaraco-classes" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", size = 11780 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790", size = 6777 }, +] + +[[package]] +name = "jaraco-context" +version = "6.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "backports-tarfile", marker = "python_full_version < '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/ad/f3777b81bf0b6e7bc7514a1656d3e637b2e8e15fab2ce3235730b3e7a4e6/jaraco_context-6.0.1.tar.gz", hash = "sha256:9bae4ea555cf0b14938dc0aee7c9f32ed303aa20a3b73e7dc80111628792d1b3", size = 13912 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/db/0c52c4cf5e4bd9f5d7135ec7669a3a767af21b3a308e1ed3674881e52b62/jaraco.context-6.0.1-py3-none-any.whl", hash = "sha256:f797fc481b490edb305122c9181830a3a5b76d84ef6d1aef2fb9b47ab956f9e4", size = 6825 }, +] + +[[package]] +name = "jaraco-functools" +version = "4.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f7/ed/1aa2d585304ec07262e1a83a9889880701079dde796ac7b1d1826f40c63d/jaraco_functools-4.3.0.tar.gz", hash = "sha256:cfd13ad0dd2c47a3600b439ef72d8615d482cedcff1632930d6f28924d92f294", size = 19755 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/09/726f168acad366b11e420df31bf1c702a54d373a83f968d94141a8c3fde0/jaraco_functools-4.3.0-py3-none-any.whl", hash = "sha256:227ff8ed6f7b8f62c56deff101545fa7543cf2c8e7b82a7c2116e672f29c26e8", size = 10408 }, +] + +[[package]] +name = "jeepney" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/6f/357efd7602486741aa73ffc0617fb310a29b588ed0fd69c2399acbb85b0c/jeepney-0.9.0.tar.gz", hash = "sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732", size = 106758 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683", size = 49010 }, +] + +[[package]] +name = "jiter" +version = "0.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/9d/ae7ddb4b8ab3fb1b51faf4deb36cb48a4fbbd7cb36bad6a5fca4741306f7/jiter-0.10.0.tar.gz", hash = "sha256:07a7142c38aacc85194391108dc91b5b57093c978a9932bd86a36862759d9500", size = 162759 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/7e/4011b5c77bec97cb2b572f566220364e3e21b51c48c5bd9c4a9c26b41b67/jiter-0.10.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:cd2fb72b02478f06a900a5782de2ef47e0396b3e1f7d5aba30daeb1fce66f303", size = 317215 }, + { url = "https://files.pythonhosted.org/packages/8a/4f/144c1b57c39692efc7ea7d8e247acf28e47d0912800b34d0ad815f6b2824/jiter-0.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:32bb468e3af278f095d3fa5b90314728a6916d89ba3d0ffb726dd9bf7367285e", size = 322814 }, + { url = "https://files.pythonhosted.org/packages/63/1f/db977336d332a9406c0b1f0b82be6f71f72526a806cbb2281baf201d38e3/jiter-0.10.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa8b3e0068c26ddedc7abc6fac37da2d0af16b921e288a5a613f4b86f050354f", size = 345237 }, + { url = "https://files.pythonhosted.org/packages/d7/1c/aa30a4a775e8a672ad7f21532bdbfb269f0706b39c6ff14e1f86bdd9e5ff/jiter-0.10.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:286299b74cc49e25cd42eea19b72aa82c515d2f2ee12d11392c56d8701f52224", size = 370999 }, + { url = "https://files.pythonhosted.org/packages/35/df/f8257abc4207830cb18880781b5f5b716bad5b2a22fb4330cfd357407c5b/jiter-0.10.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6ed5649ceeaeffc28d87fb012d25a4cd356dcd53eff5acff1f0466b831dda2a7", size = 491109 }, + { url = "https://files.pythonhosted.org/packages/06/76/9e1516fd7b4278aa13a2cc7f159e56befbea9aa65c71586305e7afa8b0b3/jiter-0.10.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2ab0051160cb758a70716448908ef14ad476c3774bd03ddce075f3c1f90a3d6", size = 388608 }, + { url = "https://files.pythonhosted.org/packages/6d/64/67750672b4354ca20ca18d3d1ccf2c62a072e8a2d452ac3cf8ced73571ef/jiter-0.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03997d2f37f6b67d2f5c475da4412be584e1cec273c1cfc03d642c46db43f8cf", size = 352454 }, + { url = "https://files.pythonhosted.org/packages/96/4d/5c4e36d48f169a54b53a305114be3efa2bbffd33b648cd1478a688f639c1/jiter-0.10.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c404a99352d839fed80d6afd6c1d66071f3bacaaa5c4268983fc10f769112e90", size = 391833 }, + { url = "https://files.pythonhosted.org/packages/0b/de/ce4a6166a78810bd83763d2fa13f85f73cbd3743a325469a4a9289af6dae/jiter-0.10.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:66e989410b6666d3ddb27a74c7e50d0829704ede652fd4c858e91f8d64b403d0", size = 523646 }, + { url = "https://files.pythonhosted.org/packages/a2/a6/3bc9acce53466972964cf4ad85efecb94f9244539ab6da1107f7aed82934/jiter-0.10.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b532d3af9ef4f6374609a3bcb5e05a1951d3bf6190dc6b176fdb277c9bbf15ee", size = 514735 }, + { url = "https://files.pythonhosted.org/packages/b4/d8/243c2ab8426a2a4dea85ba2a2ba43df379ccece2145320dfd4799b9633c5/jiter-0.10.0-cp310-cp310-win32.whl", hash = "sha256:da9be20b333970e28b72edc4dff63d4fec3398e05770fb3205f7fb460eb48dd4", size = 210747 }, + { url = "https://files.pythonhosted.org/packages/37/7a/8021bd615ef7788b98fc76ff533eaac846322c170e93cbffa01979197a45/jiter-0.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:f59e533afed0c5b0ac3eba20d2548c4a550336d8282ee69eb07b37ea526ee4e5", size = 207484 }, + { url = "https://files.pythonhosted.org/packages/1b/dd/6cefc6bd68b1c3c979cecfa7029ab582b57690a31cd2f346c4d0ce7951b6/jiter-0.10.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:3bebe0c558e19902c96e99217e0b8e8b17d570906e72ed8a87170bc290b1e978", size = 317473 }, + { url = "https://files.pythonhosted.org/packages/be/cf/fc33f5159ce132be1d8dd57251a1ec7a631c7df4bd11e1cd198308c6ae32/jiter-0.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:558cc7e44fd8e507a236bee6a02fa17199ba752874400a0ca6cd6e2196cdb7dc", size = 321971 }, + { url = "https://files.pythonhosted.org/packages/68/a4/da3f150cf1d51f6c472616fb7650429c7ce053e0c962b41b68557fdf6379/jiter-0.10.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d613e4b379a07d7c8453c5712ce7014e86c6ac93d990a0b8e7377e18505e98d", size = 345574 }, + { url = "https://files.pythonhosted.org/packages/84/34/6e8d412e60ff06b186040e77da5f83bc158e9735759fcae65b37d681f28b/jiter-0.10.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f62cf8ba0618eda841b9bf61797f21c5ebd15a7a1e19daab76e4e4b498d515b2", size = 371028 }, + { url = "https://files.pythonhosted.org/packages/fb/d9/9ee86173aae4576c35a2f50ae930d2ccb4c4c236f6cb9353267aa1d626b7/jiter-0.10.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:919d139cdfa8ae8945112398511cb7fca58a77382617d279556b344867a37e61", size = 491083 }, + { url = "https://files.pythonhosted.org/packages/d9/2c/f955de55e74771493ac9e188b0f731524c6a995dffdcb8c255b89c6fb74b/jiter-0.10.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:13ddbc6ae311175a3b03bd8994881bc4635c923754932918e18da841632349db", size = 388821 }, + { url = "https://files.pythonhosted.org/packages/81/5a/0e73541b6edd3f4aada586c24e50626c7815c561a7ba337d6a7eb0a915b4/jiter-0.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c440ea003ad10927a30521a9062ce10b5479592e8a70da27f21eeb457b4a9c5", size = 352174 }, + { url = "https://files.pythonhosted.org/packages/1c/c0/61eeec33b8c75b31cae42be14d44f9e6fe3ac15a4e58010256ac3abf3638/jiter-0.10.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:dc347c87944983481e138dea467c0551080c86b9d21de6ea9306efb12ca8f606", size = 391869 }, + { url = "https://files.pythonhosted.org/packages/41/22/5beb5ee4ad4ef7d86f5ea5b4509f680a20706c4a7659e74344777efb7739/jiter-0.10.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:13252b58c1f4d8c5b63ab103c03d909e8e1e7842d302473f482915d95fefd605", size = 523741 }, + { url = "https://files.pythonhosted.org/packages/ea/10/768e8818538e5817c637b0df52e54366ec4cebc3346108a4457ea7a98f32/jiter-0.10.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7d1bbf3c465de4a24ab12fb7766a0003f6f9bce48b8b6a886158c4d569452dc5", size = 514527 }, + { url = "https://files.pythonhosted.org/packages/73/6d/29b7c2dc76ce93cbedabfd842fc9096d01a0550c52692dfc33d3cc889815/jiter-0.10.0-cp311-cp311-win32.whl", hash = "sha256:db16e4848b7e826edca4ccdd5b145939758dadf0dc06e7007ad0e9cfb5928ae7", size = 210765 }, + { url = "https://files.pythonhosted.org/packages/c2/c9/d394706deb4c660137caf13e33d05a031d734eb99c051142e039d8ceb794/jiter-0.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:9c9c1d5f10e18909e993f9641f12fe1c77b3e9b533ee94ffa970acc14ded3812", size = 209234 }, + { url = "https://files.pythonhosted.org/packages/6d/b5/348b3313c58f5fbfb2194eb4d07e46a35748ba6e5b3b3046143f3040bafa/jiter-0.10.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:1e274728e4a5345a6dde2d343c8da018b9d4bd4350f5a472fa91f66fda44911b", size = 312262 }, + { url = "https://files.pythonhosted.org/packages/9c/4a/6a2397096162b21645162825f058d1709a02965606e537e3304b02742e9b/jiter-0.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7202ae396446c988cb2a5feb33a543ab2165b786ac97f53b59aafb803fef0744", size = 320124 }, + { url = "https://files.pythonhosted.org/packages/2a/85/1ce02cade7516b726dd88f59a4ee46914bf79d1676d1228ef2002ed2f1c9/jiter-0.10.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23ba7722d6748b6920ed02a8f1726fb4b33e0fd2f3f621816a8b486c66410ab2", size = 345330 }, + { url = "https://files.pythonhosted.org/packages/75/d0/bb6b4f209a77190ce10ea8d7e50bf3725fc16d3372d0a9f11985a2b23eff/jiter-0.10.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:371eab43c0a288537d30e1f0b193bc4eca90439fc08a022dd83e5e07500ed026", size = 369670 }, + { url = "https://files.pythonhosted.org/packages/a0/f5/a61787da9b8847a601e6827fbc42ecb12be2c925ced3252c8ffcb56afcaf/jiter-0.10.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6c675736059020365cebc845a820214765162728b51ab1e03a1b7b3abb70f74c", size = 489057 }, + { url = "https://files.pythonhosted.org/packages/12/e4/6f906272810a7b21406c760a53aadbe52e99ee070fc5c0cb191e316de30b/jiter-0.10.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0c5867d40ab716e4684858e4887489685968a47e3ba222e44cde6e4a2154f959", size = 389372 }, + { url = "https://files.pythonhosted.org/packages/e2/ba/77013b0b8ba904bf3762f11e0129b8928bff7f978a81838dfcc958ad5728/jiter-0.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:395bb9a26111b60141757d874d27fdea01b17e8fac958b91c20128ba8f4acc8a", size = 352038 }, + { url = "https://files.pythonhosted.org/packages/67/27/c62568e3ccb03368dbcc44a1ef3a423cb86778a4389e995125d3d1aaa0a4/jiter-0.10.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6842184aed5cdb07e0c7e20e5bdcfafe33515ee1741a6835353bb45fe5d1bd95", size = 391538 }, + { url = "https://files.pythonhosted.org/packages/c0/72/0d6b7e31fc17a8fdce76164884edef0698ba556b8eb0af9546ae1a06b91d/jiter-0.10.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:62755d1bcea9876770d4df713d82606c8c1a3dca88ff39046b85a048566d56ea", size = 523557 }, + { url = "https://files.pythonhosted.org/packages/2f/09/bc1661fbbcbeb6244bd2904ff3a06f340aa77a2b94e5a7373fd165960ea3/jiter-0.10.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:533efbce2cacec78d5ba73a41756beff8431dfa1694b6346ce7af3a12c42202b", size = 514202 }, + { url = "https://files.pythonhosted.org/packages/1b/84/5a5d5400e9d4d54b8004c9673bbe4403928a00d28529ff35b19e9d176b19/jiter-0.10.0-cp312-cp312-win32.whl", hash = "sha256:8be921f0cadd245e981b964dfbcd6fd4bc4e254cdc069490416dd7a2632ecc01", size = 211781 }, + { url = "https://files.pythonhosted.org/packages/9b/52/7ec47455e26f2d6e5f2ea4951a0652c06e5b995c291f723973ae9e724a65/jiter-0.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:a7c7d785ae9dda68c2678532a5a1581347e9c15362ae9f6e68f3fdbfb64f2e49", size = 206176 }, + { url = "https://files.pythonhosted.org/packages/2e/b0/279597e7a270e8d22623fea6c5d4eeac328e7d95c236ed51a2b884c54f70/jiter-0.10.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:e0588107ec8e11b6f5ef0e0d656fb2803ac6cf94a96b2b9fc675c0e3ab5e8644", size = 311617 }, + { url = "https://files.pythonhosted.org/packages/91/e3/0916334936f356d605f54cc164af4060e3e7094364add445a3bc79335d46/jiter-0.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cafc4628b616dc32530c20ee53d71589816cf385dd9449633e910d596b1f5c8a", size = 318947 }, + { url = "https://files.pythonhosted.org/packages/6a/8e/fd94e8c02d0e94539b7d669a7ebbd2776e51f329bb2c84d4385e8063a2ad/jiter-0.10.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:520ef6d981172693786a49ff5b09eda72a42e539f14788124a07530f785c3ad6", size = 344618 }, + { url = "https://files.pythonhosted.org/packages/6f/b0/f9f0a2ec42c6e9c2e61c327824687f1e2415b767e1089c1d9135f43816bd/jiter-0.10.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:554dedfd05937f8fc45d17ebdf298fe7e0c77458232bcb73d9fbbf4c6455f5b3", size = 368829 }, + { url = "https://files.pythonhosted.org/packages/e8/57/5bbcd5331910595ad53b9fd0c610392ac68692176f05ae48d6ce5c852967/jiter-0.10.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5bc299da7789deacf95f64052d97f75c16d4fc8c4c214a22bf8d859a4288a1c2", size = 491034 }, + { url = "https://files.pythonhosted.org/packages/9b/be/c393df00e6e6e9e623a73551774449f2f23b6ec6a502a3297aeeece2c65a/jiter-0.10.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5161e201172de298a8a1baad95eb85db4fb90e902353b1f6a41d64ea64644e25", size = 388529 }, + { url = "https://files.pythonhosted.org/packages/42/3e/df2235c54d365434c7f150b986a6e35f41ebdc2f95acea3036d99613025d/jiter-0.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e2227db6ba93cb3e2bf67c87e594adde0609f146344e8207e8730364db27041", size = 350671 }, + { url = "https://files.pythonhosted.org/packages/c6/77/71b0b24cbcc28f55ab4dbfe029f9a5b73aeadaba677843fc6dc9ed2b1d0a/jiter-0.10.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:15acb267ea5e2c64515574b06a8bf393fbfee6a50eb1673614aa45f4613c0cca", size = 390864 }, + { url = "https://files.pythonhosted.org/packages/6a/d3/ef774b6969b9b6178e1d1e7a89a3bd37d241f3d3ec5f8deb37bbd203714a/jiter-0.10.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:901b92f2e2947dc6dfcb52fd624453862e16665ea909a08398dde19c0731b7f4", size = 522989 }, + { url = "https://files.pythonhosted.org/packages/0c/41/9becdb1d8dd5d854142f45a9d71949ed7e87a8e312b0bede2de849388cb9/jiter-0.10.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d0cb9a125d5a3ec971a094a845eadde2db0de85b33c9f13eb94a0c63d463879e", size = 513495 }, + { url = "https://files.pythonhosted.org/packages/9c/36/3468e5a18238bdedae7c4d19461265b5e9b8e288d3f86cd89d00cbb48686/jiter-0.10.0-cp313-cp313-win32.whl", hash = "sha256:48a403277ad1ee208fb930bdf91745e4d2d6e47253eedc96e2559d1e6527006d", size = 211289 }, + { url = "https://files.pythonhosted.org/packages/7e/07/1c96b623128bcb913706e294adb5f768fb7baf8db5e1338ce7b4ee8c78ef/jiter-0.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:75f9eb72ecb640619c29bf714e78c9c46c9c4eaafd644bf78577ede459f330d4", size = 205074 }, + { url = "https://files.pythonhosted.org/packages/54/46/caa2c1342655f57d8f0f2519774c6d67132205909c65e9aa8255e1d7b4f4/jiter-0.10.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:28ed2a4c05a1f32ef0e1d24c2611330219fed727dae01789f4a335617634b1ca", size = 318225 }, + { url = "https://files.pythonhosted.org/packages/43/84/c7d44c75767e18946219ba2d703a5a32ab37b0bc21886a97bc6062e4da42/jiter-0.10.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14a4c418b1ec86a195f1ca69da8b23e8926c752b685af665ce30777233dfe070", size = 350235 }, + { url = "https://files.pythonhosted.org/packages/01/16/f5a0135ccd968b480daad0e6ab34b0c7c5ba3bc447e5088152696140dcb3/jiter-0.10.0-cp313-cp313t-win_amd64.whl", hash = "sha256:d7bfed2fe1fe0e4dda6ef682cee888ba444b21e7a6553e03252e4feb6cf0adca", size = 207278 }, + { url = "https://files.pythonhosted.org/packages/1c/9b/1d646da42c3de6c2188fdaa15bce8ecb22b635904fc68be025e21249ba44/jiter-0.10.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:5e9251a5e83fab8d87799d3e1a46cb4b7f2919b895c6f4483629ed2446f66522", size = 310866 }, + { url = "https://files.pythonhosted.org/packages/ad/0e/26538b158e8a7c7987e94e7aeb2999e2e82b1f9d2e1f6e9874ddf71ebda0/jiter-0.10.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:023aa0204126fe5b87ccbcd75c8a0d0261b9abdbbf46d55e7ae9f8e22424eeb8", size = 318772 }, + { url = "https://files.pythonhosted.org/packages/7b/fb/d302893151caa1c2636d6574d213e4b34e31fd077af6050a9c5cbb42f6fb/jiter-0.10.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c189c4f1779c05f75fc17c0c1267594ed918996a231593a21a5ca5438445216", size = 344534 }, + { url = "https://files.pythonhosted.org/packages/01/d8/5780b64a149d74e347c5128d82176eb1e3241b1391ac07935693466d6219/jiter-0.10.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:15720084d90d1098ca0229352607cd68256c76991f6b374af96f36920eae13c4", size = 369087 }, + { url = "https://files.pythonhosted.org/packages/e8/5b/f235a1437445160e777544f3ade57544daf96ba7e96c1a5b24a6f7ac7004/jiter-0.10.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e4f2fb68e5f1cfee30e2b2a09549a00683e0fde4c6a2ab88c94072fc33cb7426", size = 490694 }, + { url = "https://files.pythonhosted.org/packages/85/a9/9c3d4617caa2ff89cf61b41e83820c27ebb3f7b5fae8a72901e8cd6ff9be/jiter-0.10.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ce541693355fc6da424c08b7edf39a2895f58d6ea17d92cc2b168d20907dee12", size = 388992 }, + { url = "https://files.pythonhosted.org/packages/68/b1/344fd14049ba5c94526540af7eb661871f9c54d5f5601ff41a959b9a0bbd/jiter-0.10.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31c50c40272e189d50006ad5c73883caabb73d4e9748a688b216e85a9a9ca3b9", size = 351723 }, + { url = "https://files.pythonhosted.org/packages/41/89/4c0e345041186f82a31aee7b9d4219a910df672b9fef26f129f0cda07a29/jiter-0.10.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fa3402a2ff9815960e0372a47b75c76979d74402448509ccd49a275fa983ef8a", size = 392215 }, + { url = "https://files.pythonhosted.org/packages/55/58/ee607863e18d3f895feb802154a2177d7e823a7103f000df182e0f718b38/jiter-0.10.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:1956f934dca32d7bb647ea21d06d93ca40868b505c228556d3373cbd255ce853", size = 522762 }, + { url = "https://files.pythonhosted.org/packages/15/d0/9123fb41825490d16929e73c212de9a42913d68324a8ce3c8476cae7ac9d/jiter-0.10.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:fcedb049bdfc555e261d6f65a6abe1d5ad68825b7202ccb9692636c70fcced86", size = 513427 }, + { url = "https://files.pythonhosted.org/packages/d8/b3/2bd02071c5a2430d0b70403a34411fc519c2f227da7b03da9ba6a956f931/jiter-0.10.0-cp314-cp314-win32.whl", hash = "sha256:ac509f7eccca54b2a29daeb516fb95b6f0bd0d0d8084efaf8ed5dfc7b9f0b357", size = 210127 }, + { url = "https://files.pythonhosted.org/packages/03/0c/5fe86614ea050c3ecd728ab4035534387cd41e7c1855ef6c031f1ca93e3f/jiter-0.10.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5ed975b83a2b8639356151cef5c0d597c68376fc4922b45d0eb384ac058cfa00", size = 318527 }, + { url = "https://files.pythonhosted.org/packages/b3/4a/4175a563579e884192ba6e81725fc0448b042024419be8d83aa8a80a3f44/jiter-0.10.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3aa96f2abba33dc77f79b4cf791840230375f9534e5fac927ccceb58c5e604a5", size = 354213 }, +] + +[[package]] +name = "jsonschema" +version = "4.25.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/69/f7185de793a29082a9f3c7728268ffb31cb5095131a9c139a74078e27336/jsonschema-4.25.1.tar.gz", hash = "sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85", size = 357342 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl", hash = "sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63", size = 90040 }, +] + +[[package]] +name = "jsonschema-path" +version = "0.3.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pathable" }, + { name = "pyyaml" }, + { name = "referencing" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6e/45/41ebc679c2a4fced6a722f624c18d658dee42612b83ea24c1caf7c0eb3a8/jsonschema_path-0.3.4.tar.gz", hash = "sha256:8365356039f16cc65fddffafda5f58766e34bebab7d6d105616ab52bc4297001", size = 11159 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/58/3485da8cb93d2f393bce453adeef16896751f14ba3e2024bc21dc9597646/jsonschema_path-0.3.4-py3-none-any.whl", hash = "sha256:f502191fdc2b22050f9a81c9237be9d27145b9001c55842bece5e94e382e52f8", size = 14810 }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bf/ce/46fbd9c8119cfc3581ee5643ea49464d168028cfb5caff5fc0596d0cf914/jsonschema_specifications-2025.4.1.tar.gz", hash = "sha256:630159c9f4dbea161a6a2205c3011cc4f18ff381b189fff48bb39b9bf26ae608", size = 15513 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/0e/b27cdbaccf30b890c40ed1da9fd4a3593a5cf94dae54fb34f8a4b74fcd3f/jsonschema_specifications-2025.4.1-py3-none-any.whl", hash = "sha256:4653bffbd6584f7de83a67e0d620ef16900b390ddc7939d56684d6c81e33f1af", size = 18437 }, +] + +[[package]] +name = "keyring" +version = "25.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata", marker = "python_full_version < '3.12'" }, + { name = "jaraco-classes" }, + { name = "jaraco-context" }, + { name = "jaraco-functools" }, + { name = "jeepney", marker = "sys_platform == 'linux'" }, + { name = "pywin32-ctypes", marker = "sys_platform == 'win32'" }, + { name = "secretstorage", marker = "sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/4b/674af6ef2f97d56f0ab5153bf0bfa28ccb6c3ed4d1babf4305449668807b/keyring-25.7.0.tar.gz", hash = "sha256:fe01bd85eb3f8fb3dd0405defdeac9a5b4f6f0439edbb3149577f244a2e8245b", size = 63516 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl", hash = "sha256:be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f", size = 39160 }, +] + +[[package]] +name = "lupa" +version = "2.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b8/1c/191c3e6ec6502e3dbe25a53e27f69a5daeac3e56de1f73c0138224171ead/lupa-2.6.tar.gz", hash = "sha256:9a770a6e89576be3447668d7ced312cd6fd41d3c13c2462c9dc2c2ab570e45d9", size = 7240282 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a1/15/713cab5d0dfa4858f83b99b3e0329072df33dc14fc3ebbaa017e0f9755c4/lupa-2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6b3dabda836317e63c5ad052826e156610f356a04b3003dfa0dbe66b5d54d671", size = 954828 }, + { url = "https://files.pythonhosted.org/packages/2e/71/704740cbc6e587dd6cc8dabf2f04820ac6a671784e57cc3c29db795476db/lupa-2.6-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:8726d1c123bbe9fbb974ce29825e94121824e66003038ff4532c14cc2ed0c51c", size = 1919259 }, + { url = "https://files.pythonhosted.org/packages/eb/18/f248341c423c5d48837e35584c6c3eb4acab7e722b6057d7b3e28e42dae8/lupa-2.6-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:f4e159e7d814171199b246f9235ca8961f6461ea8c1165ab428afa13c9289a94", size = 984998 }, + { url = "https://files.pythonhosted.org/packages/44/1e/8a4bd471e018aad76bcb9455d298c2c96d82eced20f2ae8fcec8cd800948/lupa-2.6-cp310-cp310-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:202160e80dbfddfb79316692a563d843b767e0f6787bbd1c455f9d54052efa6c", size = 1174871 }, + { url = "https://files.pythonhosted.org/packages/2a/5c/3a3f23fd6a91b0986eea1ceaf82ad3f9b958fe3515a9981fb9c4eb046c8b/lupa-2.6-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5deede7c5b36ab64f869dae4831720428b67955b0bb186c8349cf6ea121c852b", size = 1057471 }, + { url = "https://files.pythonhosted.org/packages/45/ac/01be1fed778fb0c8f46ee8cbe344e4d782f6806fac12717f08af87aa4355/lupa-2.6-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:86f04901f920bbf7c0cac56807dc9597e42347123e6f1f3ca920f15f54188ce5", size = 2100592 }, + { url = "https://files.pythonhosted.org/packages/3f/6c/1a05bb873e30830f8574e10cd0b4cdbc72e9dbad2a09e25810b5e3b1f75d/lupa-2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6deef8f851d6afb965c84849aa5b8c38856942df54597a811ce0369ced678610", size = 1081396 }, + { url = "https://files.pythonhosted.org/packages/a2/c2/a19dd80d6dc98b39bbf8135b8198e38aa7ca3360b720eac68d1d7e9286b5/lupa-2.6-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:21f2b5549681c2a13b1170a26159d30875d367d28f0247b81ca347222c755038", size = 1192007 }, + { url = "https://files.pythonhosted.org/packages/4f/43/e1b297225c827f55752e46fdbfb021c8982081b0f24490e42776ea69ae3b/lupa-2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:66eea57630eab5e6f49fdc5d7811c0a2a41f2011be4ea56a087ea76112011eb7", size = 2196661 }, + { url = "https://files.pythonhosted.org/packages/2e/8f/2272d429a7fa9dc8dbd6e9c5c9073a03af6007eb22a4c78829fec6a34b80/lupa-2.6-cp310-cp310-win32.whl", hash = "sha256:60a403de8cab262a4fe813085dd77010effa6e2eb1886db2181df803140533b1", size = 1412738 }, + { url = "https://files.pythonhosted.org/packages/35/2a/1708911271dd49ad87b4b373b5a4b0e0a0516d3d2af7b76355946c7ee171/lupa-2.6-cp310-cp310-win_amd64.whl", hash = "sha256:e4656a39d93dfa947cf3db56dc16c7916cb0cc8024acd3a952071263f675df64", size = 1656898 }, + { url = "https://files.pythonhosted.org/packages/ca/29/1f66907c1ebf1881735afa695e646762c674f00738ebf66d795d59fc0665/lupa-2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6d988c0f9331b9f2a5a55186701a25444ab10a1432a1021ee58011499ecbbdd5", size = 962875 }, + { url = "https://files.pythonhosted.org/packages/e6/67/4a748604be360eb9c1c215f6a0da921cd1a2b44b2c5951aae6fb83019d3a/lupa-2.6-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:ebe1bbf48259382c72a6fe363dea61a0fd6fe19eab95e2ae881e20f3654587bf", size = 1935390 }, + { url = "https://files.pythonhosted.org/packages/ac/0c/8ef9ee933a350428b7bdb8335a37ef170ab0bb008bbf9ca8f4f4310116b6/lupa-2.6-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:a8fcee258487cf77cdd41560046843bb38c2e18989cd19671dd1e2596f798306", size = 992193 }, + { url = "https://files.pythonhosted.org/packages/65/46/e6c7facebdb438db8a65ed247e56908818389c1a5abbf6a36aab14f1057d/lupa-2.6-cp311-cp311-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:561a8e3be800827884e767a694727ed8482d066e0d6edfcbf423b05e63b05535", size = 1165844 }, + { url = "https://files.pythonhosted.org/packages/1c/26/9f1154c6c95f175ccbf96aa96c8f569c87f64f463b32473e839137601a8b/lupa-2.6-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:af880a62d47991cae78b8e9905c008cbfdc4a3a9723a66310c2634fc7644578c", size = 1048069 }, + { url = "https://files.pythonhosted.org/packages/68/67/2cc52ab73d6af81612b2ea24c870d3fa398443af8e2875e5befe142398b1/lupa-2.6-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:80b22923aa4023c86c0097b235615f89d469a0c4eee0489699c494d3367c4c85", size = 2079079 }, + { url = "https://files.pythonhosted.org/packages/2e/dc/f843f09bbf325f6e5ee61730cf6c3409fc78c010d968c7c78acba3019ca7/lupa-2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:153d2cc6b643f7efb9cfc0c6bb55ec784d5bac1a3660cfc5b958a7b8f38f4a75", size = 1071428 }, + { url = "https://files.pythonhosted.org/packages/2e/60/37533a8d85bf004697449acb97ecdacea851acad28f2ad3803662487dd2a/lupa-2.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:3fa8777e16f3ded50b72967dc17e23f5a08e4f1e2c9456aff2ebdb57f5b2869f", size = 1181756 }, + { url = "https://files.pythonhosted.org/packages/e4/f2/cf29b20dbb4927b6a3d27c339ac5d73e74306ecc28c8e2c900b2794142ba/lupa-2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8dbdcbe818c02a2f56f5ab5ce2de374dab03e84b25266cfbaef237829bc09b3f", size = 2175687 }, + { url = "https://files.pythonhosted.org/packages/94/7c/050e02f80c7131b63db1474bff511e63c545b5a8636a24cbef3fc4da20b6/lupa-2.6-cp311-cp311-win32.whl", hash = "sha256:defaf188fde8f7a1e5ce3a5e6d945e533b8b8d547c11e43b96c9b7fe527f56dc", size = 1412592 }, + { url = "https://files.pythonhosted.org/packages/6f/9a/6f2af98aa5d771cea661f66c8eb8f53772ec1ab1dfbce24126cfcd189436/lupa-2.6-cp311-cp311-win_amd64.whl", hash = "sha256:9505ae600b5c14f3e17e70f87f88d333717f60411faca1ddc6f3e61dce85fa9e", size = 1669194 }, + { url = "https://files.pythonhosted.org/packages/94/86/ce243390535c39d53ea17ccf0240815e6e457e413e40428a658ea4ee4b8d/lupa-2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:47ce718817ef1cc0c40d87c3d5ae56a800d61af00fbc0fad1ca9be12df2f3b56", size = 951707 }, + { url = "https://files.pythonhosted.org/packages/86/85/cedea5e6cbeb54396fdcc55f6b741696f3f036d23cfaf986d50d680446da/lupa-2.6-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:7aba985b15b101495aa4b07112cdc08baa0c545390d560ad5cfde2e9e34f4d58", size = 1916703 }, + { url = "https://files.pythonhosted.org/packages/24/be/3d6b5f9a8588c01a4d88129284c726017b2089f3a3fd3ba8bd977292fea0/lupa-2.6-cp312-cp312-macosx_11_0_x86_64.whl", hash = "sha256:b766f62f95b2739f2248977d29b0722e589dcf4f0ccfa827ccbd29f0148bd2e5", size = 985152 }, + { url = "https://files.pythonhosted.org/packages/eb/23/9f9a05beee5d5dce9deca4cb07c91c40a90541fc0a8e09db4ee670da550f/lupa-2.6-cp312-cp312-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:00a934c23331f94cb51760097ebfab14b005d55a6b30a2b480e3c53dd2fa290d", size = 1159599 }, + { url = "https://files.pythonhosted.org/packages/40/4e/e7c0583083db9d7f1fd023800a9767d8e4391e8330d56c2373d890ac971b/lupa-2.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21de9f38bd475303e34a042b7081aabdf50bd9bafd36ce4faea2f90fd9f15c31", size = 1038686 }, + { url = "https://files.pythonhosted.org/packages/1c/9f/5a4f7d959d4feba5e203ff0c31889e74d1ca3153122be4a46dca7d92bf7c/lupa-2.6-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf3bda96d3fc41237e964a69c23647d50d4e28421111360274d4799832c560e9", size = 2071956 }, + { url = "https://files.pythonhosted.org/packages/92/34/2f4f13ca65d01169b1720176aedc4af17bc19ee834598c7292db232cb6dc/lupa-2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5a76ead245da54801a81053794aa3975f213221f6542d14ec4b859ee2e7e0323", size = 1057199 }, + { url = "https://files.pythonhosted.org/packages/35/2a/5f7d2eebec6993b0dcd428e0184ad71afb06a45ba13e717f6501bfed1da3/lupa-2.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8dd0861741caa20886ddbda0a121d8e52fb9b5bb153d82fa9bba796962bf30e8", size = 1173693 }, + { url = "https://files.pythonhosted.org/packages/e4/29/089b4d2f8e34417349af3904bb40bec40b65c8731f45e3fd8d497ca573e5/lupa-2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:239e63948b0b23023f81d9a19a395e768ed3da6a299f84e7963b8f813f6e3f9c", size = 2164394 }, + { url = "https://files.pythonhosted.org/packages/f3/1b/79c17b23c921f81468a111cad843b076a17ef4b684c4a8dff32a7969c3f0/lupa-2.6-cp312-cp312-win32.whl", hash = "sha256:325894e1099499e7a6f9c351147661a2011887603c71086d36fe0f964d52d1ce", size = 1420647 }, + { url = "https://files.pythonhosted.org/packages/b8/15/5121e68aad3584e26e1425a5c9a79cd898f8a152292059e128c206ee817c/lupa-2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c735a1ce8ee60edb0fe71d665f1e6b7c55c6021f1d340eb8c865952c602cd36f", size = 1688529 }, + { url = "https://files.pythonhosted.org/packages/28/1d/21176b682ca5469001199d8b95fa1737e29957a3d185186e7a8b55345f2e/lupa-2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:663a6e58a0f60e7d212017d6678639ac8df0119bc13c2145029dcba084391310", size = 947232 }, + { url = "https://files.pythonhosted.org/packages/ce/4c/d327befb684660ca13cf79cd1f1d604331808f9f1b6fb6bf57832f8edf80/lupa-2.6-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:d1f5afda5c20b1f3217a80e9bc1b77037f8a6eb11612fd3ada19065303c8f380", size = 1908625 }, + { url = "https://files.pythonhosted.org/packages/66/8e/ad22b0a19454dfd08662237a84c792d6d420d36b061f239e084f29d1a4f3/lupa-2.6-cp313-cp313-macosx_11_0_x86_64.whl", hash = "sha256:26f2b3c085fe76e9119e48c1013c1cccdc1f51585d456858290475aa38e7089e", size = 981057 }, + { url = "https://files.pythonhosted.org/packages/5c/48/74859073ab276bd0566c719f9ca0108b0cfc1956ca0d68678d117d47d155/lupa-2.6-cp313-cp313-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:60d2f902c7b96fb8ab98493dcff315e7bb4d0b44dc9dd76eb37de575025d5685", size = 1156227 }, + { url = "https://files.pythonhosted.org/packages/09/6c/0e9ded061916877253c2266074060eb71ed99fb21d73c8c114a76725bce2/lupa-2.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a02d25dee3a3250967c36590128d9220ae02f2eda166a24279da0b481519cbff", size = 1035752 }, + { url = "https://files.pythonhosted.org/packages/dd/ef/f8c32e454ef9f3fe909f6c7d57a39f950996c37a3deb7b391fec7903dab7/lupa-2.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6eae1ee16b886b8914ff292dbefbf2f48abfbdee94b33a88d1d5475e02423203", size = 2069009 }, + { url = "https://files.pythonhosted.org/packages/53/dc/15b80c226a5225815a890ee1c11f07968e0aba7a852df41e8ae6fe285063/lupa-2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0edd5073a4ee74ab36f74fe61450148e6044f3952b8d21248581f3c5d1a58be", size = 1056301 }, + { url = "https://files.pythonhosted.org/packages/31/14/2086c1425c985acfb30997a67e90c39457122df41324d3c179d6ee2292c6/lupa-2.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0c53ee9f22a8a17e7d4266ad48e86f43771951797042dd51d1494aaa4f5f3f0a", size = 1170673 }, + { url = "https://files.pythonhosted.org/packages/10/e5/b216c054cf86576c0191bf9a9f05de6f7e8e07164897d95eea0078dca9b2/lupa-2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:de7c0f157a9064a400d828789191a96da7f4ce889969a588b87ec80de9b14772", size = 2162227 }, + { url = "https://files.pythonhosted.org/packages/59/2f/33ecb5bedf4f3bc297ceacb7f016ff951331d352f58e7e791589609ea306/lupa-2.6-cp313-cp313-win32.whl", hash = "sha256:ee9523941ae0a87b5b703417720c5d78f72d2f5bc23883a2ea80a949a3ed9e75", size = 1419558 }, + { url = "https://files.pythonhosted.org/packages/f9/b4/55e885834c847ea610e111d87b9ed4768f0afdaeebc00cd46810f25029f6/lupa-2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b1335a5835b0a25ebdbc75cf0bda195e54d133e4d994877ef025e218c2e59db9", size = 1683424 }, + { url = "https://files.pythonhosted.org/packages/66/9d/d9427394e54d22a35d1139ef12e845fd700d4872a67a34db32516170b746/lupa-2.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:dcb6d0a3264873e1653bc188499f48c1fb4b41a779e315eba45256cfe7bc33c1", size = 953818 }, + { url = "https://files.pythonhosted.org/packages/10/41/27bbe81953fb2f9ecfced5d9c99f85b37964cfaf6aa8453bb11283983721/lupa-2.6-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:a37e01f2128f8c36106726cb9d360bac087d58c54b4522b033cc5691c584db18", size = 1915850 }, + { url = "https://files.pythonhosted.org/packages/a3/98/f9ff60db84a75ba8725506bbf448fb085bc77868a021998ed2a66d920568/lupa-2.6-cp314-cp314-macosx_11_0_x86_64.whl", hash = "sha256:458bd7e9ff3c150b245b0fcfbb9bd2593d1152ea7f0a7b91c1d185846da033fe", size = 982344 }, + { url = "https://files.pythonhosted.org/packages/41/f7/f39e0f1c055c3b887d86b404aaf0ca197b5edfd235a8b81b45b25bac7fc3/lupa-2.6-cp314-cp314-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:052ee82cac5206a02df77119c325339acbc09f5ce66967f66a2e12a0f3211cad", size = 1156543 }, + { url = "https://files.pythonhosted.org/packages/9e/9c/59e6cffa0d672d662ae17bd7ac8ecd2c89c9449dee499e3eb13ca9cd10d9/lupa-2.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96594eca3c87dd07938009e95e591e43d554c1dbd0385be03c100367141db5a8", size = 1047974 }, + { url = "https://files.pythonhosted.org/packages/23/c6/a04e9cef7c052717fcb28fb63b3824802488f688391895b618e39be0f684/lupa-2.6-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8faddd9d198688c8884091173a088a8e920ecc96cda2ffed576a23574c4b3f6", size = 2073458 }, + { url = "https://files.pythonhosted.org/packages/e6/10/824173d10f38b51fc77785228f01411b6ca28826ce27404c7c912e0e442c/lupa-2.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:daebb3a6b58095c917e76ba727ab37b27477fb926957c825205fbda431552134", size = 1067683 }, + { url = "https://files.pythonhosted.org/packages/b6/dc/9692fbcf3c924d9c4ece2d8d2f724451ac2e09af0bd2a782db1cef34e799/lupa-2.6-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f3154e68972befe0f81564e37d8142b5d5d79931a18309226a04ec92487d4ea3", size = 1171892 }, + { url = "https://files.pythonhosted.org/packages/84/ff/e318b628d4643c278c96ab3ddea07fc36b075a57383c837f5b11e537ba9d/lupa-2.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e4dadf77b9fedc0bfa53417cc28dc2278a26d4cbd95c29f8927ad4d8fe0a7ef9", size = 2166641 }, + { url = "https://files.pythonhosted.org/packages/12/f7/a6f9ec2806cf2d50826980cdb4b3cffc7691dc6f95e13cc728846d5cb793/lupa-2.6-cp314-cp314-win32.whl", hash = "sha256:cb34169c6fa3bab3e8ac58ca21b8a7102f6a94b6a5d08d3636312f3f02fafd8f", size = 1456857 }, + { url = "https://files.pythonhosted.org/packages/c5/de/df71896f25bdc18360fdfa3b802cd7d57d7fede41a0e9724a4625b412c85/lupa-2.6-cp314-cp314-win_amd64.whl", hash = "sha256:b74f944fe46c421e25d0f8692aef1e842192f6f7f68034201382ac440ef9ea67", size = 1731191 }, + { url = "https://files.pythonhosted.org/packages/47/3c/a1f23b01c54669465f5f4c4083107d496fbe6fb45998771420e9aadcf145/lupa-2.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0e21b716408a21ab65723f8841cf7f2f37a844b7a965eeabb785e27fca4099cf", size = 999343 }, + { url = "https://files.pythonhosted.org/packages/c5/6d/501994291cb640bfa2ccf7f554be4e6914afa21c4026bd01bff9ca8aac57/lupa-2.6-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:589db872a141bfff828340079bbdf3e9a31f2689f4ca0d88f97d9e8c2eae6142", size = 2000730 }, + { url = "https://files.pythonhosted.org/packages/53/a5/457ffb4f3f20469956c2d4c4842a7675e884efc895b2f23d126d23e126cc/lupa-2.6-cp314-cp314t-macosx_11_0_x86_64.whl", hash = "sha256:cd852a91a4a9d4dcbb9a58100f820a75a425703ec3e3f049055f60b8533b7953", size = 1021553 }, + { url = "https://files.pythonhosted.org/packages/51/6b/36bb5a5d0960f2a5c7c700e0819abb76fd9bf9c1d8a66e5106416d6e9b14/lupa-2.6-cp314-cp314t-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:0334753be028358922415ca97a64a3048e4ed155413fc4eaf87dd0a7e2752983", size = 1133275 }, + { url = "https://files.pythonhosted.org/packages/19/86/202ff4429f663013f37d2229f6176ca9f83678a50257d70f61a0a97281bf/lupa-2.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:661d895cd38c87658a34780fac54a690ec036ead743e41b74c3fb81a9e65a6aa", size = 1038441 }, + { url = "https://files.pythonhosted.org/packages/a7/42/d8125f8e420714e5b52e9c08d88b5329dfb02dcca731b4f21faaee6cc5b5/lupa-2.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aa58454ccc13878cc177c62529a2056be734da16369e451987ff92784994ca7", size = 2058324 }, + { url = "https://files.pythonhosted.org/packages/2b/2c/47bf8b84059876e877a339717ddb595a4a7b0e8740bacae78ba527562e1c/lupa-2.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1425017264e470c98022bba8cff5bd46d054a827f5df6b80274f9cc71dafd24f", size = 1060250 }, + { url = "https://files.pythonhosted.org/packages/c2/06/d88add2b6406ca1bdec99d11a429222837ca6d03bea42ca75afa169a78cb/lupa-2.6-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:224af0532d216e3105f0a127410f12320f7c5f1aa0300bdf9646b8d9afb0048c", size = 1151126 }, + { url = "https://files.pythonhosted.org/packages/b4/a0/89e6a024c3b4485b89ef86881c9d55e097e7cb0bdb74efb746f2fa6a9a76/lupa-2.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9abb98d5a8fd27c8285302e82199f0e56e463066f88f619d6594a450bf269d80", size = 2153693 }, + { url = "https://files.pythonhosted.org/packages/b6/36/a0f007dc58fc1bbf51fb85dcc82fcb1f21b8c4261361de7dab0e3d8521ef/lupa-2.6-cp314-cp314t-win32.whl", hash = "sha256:1849efeba7a8f6fb8aa2c13790bee988fd242ae404bd459509640eeea3d1e291", size = 1590104 }, + { url = "https://files.pythonhosted.org/packages/7d/5e/db903ce9cf82c48d6b91bf6d63ae4c8d0d17958939a4e04ba6b9f38b8643/lupa-2.6-cp314-cp314t-win_amd64.whl", hash = "sha256:fc1498d1a4fc028bc521c26d0fad4ca00ed63b952e32fb95949bda76a04bad52", size = 1913818 }, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321 }, +] + +[[package]] +name = "mcp" +version = "1.24.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "httpx" }, + { name = "httpx-sse" }, + { name = "jsonschema" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "pyjwt", extra = ["crypto"] }, + { name = "python-multipart" }, + { name = "pywin32", marker = "sys_platform == 'win32'" }, + { name = "sse-starlette" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, + { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d6/2c/db9ae5ab1fcdd9cd2bcc7ca3b7361b712e30590b64d5151a31563af8f82d/mcp-1.24.0.tar.gz", hash = "sha256:aeaad134664ce56f2721d1abf300666a1e8348563f4d3baff361c3b652448efc", size = 604375 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/0d/5cf14e177c8ae655a2fd9324a6ef657ca4cafd3fc2201c87716055e29641/mcp-1.24.0-py3-none-any.whl", hash = "sha256:db130e103cc50ddc3dffc928382f33ba3eaef0b711f7a87c05e7ded65b1ca062", size = 232896 }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 } +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 = "more-itertools" +version = "10.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ea/5d/38b681d3fce7a266dd9ab73c66959406d565b3e85f21d5e66e1181d93721/more_itertools-10.8.0.tar.gz", hash = "sha256:f638ddf8a1a0d134181275fb5d58b086ead7c6a72429ad725c67503f13ba30bd", size = 137431 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl", hash = "sha256:52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b", size = 69667 }, +] + +[[package]] +name = "openai" +version = "2.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0f/39/8e347e9fda125324d253084bb1b82407e5e3c7777a03dc398f79b2d95626/openai-2.13.0.tar.gz", hash = "sha256:9ff633b07a19469ec476b1e2b5b26c5ef700886524a7a72f65e6f0b5203142d5", size = 626583 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/d5/eb52edff49d3d5ea116e225538c118699ddeb7c29fa17ec28af14bc10033/openai-2.13.0-py3-none-any.whl", hash = "sha256:746521065fed68df2f9c2d85613bb50844343ea81f60009b60e6a600c9352c79", size = 1066837 }, +] + +[[package]] +name = "openapi-pydantic" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/2e/58d83848dd1a79cb92ed8e63f6ba901ca282c5f09d04af9423ec26c56fd7/openapi_pydantic-0.5.1.tar.gz", hash = "sha256:ff6835af6bde7a459fb93eb93bb92b8749b754fc6e51b2f1590a19dc3005ee0d", size = 60892 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/cf/03675d8bd8ecbf4445504d8071adab19f5f993676795708e36402ab38263/openapi_pydantic-0.5.1-py3-none-any.whl", hash = "sha256:a3a09ef4586f5bd760a8df7f43028b60cafb6d9f61de2acba9574766255ab146", size = 96381 }, +] + +[[package]] +name = "opentelemetry-api" +version = "1.39.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/97/b9/3161be15bb8e3ad01be8be5a968a9237c3027c5be504362ff800fca3e442/opentelemetry_api-1.39.1.tar.gz", hash = "sha256:fbde8c80e1b937a2c61f20347e91c0c18a1940cecf012d62e65a7caf08967c9c", size = 65767 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/df/d3f1ddf4bb4cb50ed9b1139cc7b1c54c34a1e7ce8fd1b9a37c0d1551a6bd/opentelemetry_api-1.39.1-py3-none-any.whl", hash = "sha256:2edd8463432a7f8443edce90972169b195e7d6a05500cd29e6d13898187c9950", size = 66356 }, +] + +[[package]] +name = "opentelemetry-exporter-prometheus" +version = "0.60b1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-sdk" }, + { name = "prometheus-client" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/14/39/7dafa6fff210737267bed35a8855b6ac7399b9e582b8cf1f25f842517012/opentelemetry_exporter_prometheus-0.60b1.tar.gz", hash = "sha256:a4011b46906323f71724649d301b4dc188aaa068852e814f4df38cc76eac616b", size = 14976 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/0d/4be6bf5477a3eb3d917d2f17d3c0b6720cd6cb97898444a61d43cc983f5c/opentelemetry_exporter_prometheus-0.60b1-py3-none-any.whl", hash = "sha256:49f59178de4f4590e3cef0b8b95cf6e071aae70e1f060566df5546fad773b8fd", size = 13019 }, +] + +[[package]] +name = "opentelemetry-instrumentation" +version = "0.60b1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "packaging" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/41/0f/7e6b713ac117c1f5e4e3300748af699b9902a2e5e34c9cf443dde25a01fa/opentelemetry_instrumentation-0.60b1.tar.gz", hash = "sha256:57ddc7974c6eb35865af0426d1a17132b88b2ed8586897fee187fd5b8944bd6a", size = 31706 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/d2/6788e83c5c86a2690101681aeef27eeb2a6bf22df52d3f263a22cee20915/opentelemetry_instrumentation-0.60b1-py3-none-any.whl", hash = "sha256:04480db952b48fb1ed0073f822f0ee26012b7be7c3eac1a3793122737c78632d", size = 33096 }, +] + +[[package]] +name = "opentelemetry-sdk" +version = "1.39.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/fb/c76080c9ba07e1e8235d24cdcc4d125ef7aa3edf23eb4e497c2e50889adc/opentelemetry_sdk-1.39.1.tar.gz", hash = "sha256:cf4d4563caf7bff906c9f7967e2be22d0d6b349b908be0d90fb21c8e9c995cc6", size = 171460 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/98/e91cf858f203d86f4eccdf763dcf01cf03f1dae80c3750f7e635bfa206b6/opentelemetry_sdk-1.39.1-py3-none-any.whl", hash = "sha256:4d5482c478513ecb0a5d938dcc61394e647066e0cc2676bee9f3af3f3f45f01c", size = 132565 }, +] + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.60b1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/91/df/553f93ed38bf22f4b999d9be9c185adb558982214f33eae539d3b5cd0858/opentelemetry_semantic_conventions-0.60b1.tar.gz", hash = "sha256:87c228b5a0669b748c76d76df6c364c369c28f1c465e50f661e39737e84bc953", size = 137935 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/5e/5958555e09635d09b75de3c4f8b9cae7335ca545d77392ffe7331534c402/opentelemetry_semantic_conventions-0.60b1-py3-none-any.whl", hash = "sha256:9fa8c8b0c110da289809292b0591220d3a7b53c1526a23021e977d68597893fb", size = 219982 }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469 }, +] + +[[package]] +name = "pathable" +version = "0.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/67/93/8f2c2075b180c12c1e9f6a09d1a985bc2036906b13dff1d8917e395f2048/pathable-0.4.4.tar.gz", hash = "sha256:6905a3cd17804edfac7875b5f6c9142a218c7caef78693c2dbbbfbac186d88b2", size = 8124 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/eb/b6260b31b1a96386c0a880edebe26f89669098acea8e0318bff6adb378fd/pathable-0.4.4-py3-none-any.whl", hash = "sha256:5ae9e94793b6ef5a4cbe0a7ce9dbbefc1eec38df253763fd0aeeacf2762dbbc2", size = 9592 }, +] + +[[package]] +name = "pathvalidate" +version = "3.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fa/2a/52a8da6fe965dea6192eb716b357558e103aea0a1e9a8352ad575a8406ca/pathvalidate-3.3.1.tar.gz", hash = "sha256:b18c07212bfead624345bb8e1d6141cdcf15a39736994ea0b94035ad2b1ba177", size = 63262 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/70/875f4a23bfc4731703a5835487d0d2fb999031bd415e7d17c0ae615c18b7/pathvalidate-3.3.1-py3-none-any.whl", hash = "sha256:5263baab691f8e1af96092fa5137ee17df5bdfbd6cff1fcac4d6ef4bc2e1735f", size = 24305 }, +] + +[[package]] +name = "platformdirs" +version = "4.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", size = 21715 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731 }, +] + +[[package]] +name = "prometheus-client" +version = "0.23.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/23/53/3edb5d68ecf6b38fcbcc1ad28391117d2a322d9a1a3eff04bfdb184d8c3b/prometheus_client-0.23.1.tar.gz", hash = "sha256:6ae8f9081eaaaf153a2e959d2e6c4f4fb57b12ef76c8c7980202f1e57b48b2ce", size = 80481 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/db/14bafcb4af2139e046d03fd00dea7873e48eafe18b7d2797e73d6681f210/prometheus_client-0.23.1-py3-none-any.whl", hash = "sha256:dd1913e6e76b59cfe44e7a4b83e01afc9873c1bdfd2ed8739f1e76aeca115f99", size = 61145 }, +] + +[[package]] +name = "py-key-value-aio" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beartype" }, + { name = "py-key-value-shared" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/93/ce/3136b771dddf5ac905cc193b461eb67967cf3979688c6696e1f2cdcde7ea/py_key_value_aio-0.3.0.tar.gz", hash = "sha256:858e852fcf6d696d231266da66042d3355a7f9871650415feef9fca7a6cd4155", size = 50801 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/10/72f6f213b8f0bce36eff21fda0a13271834e9eeff7f9609b01afdc253c79/py_key_value_aio-0.3.0-py3-none-any.whl", hash = "sha256:1c781915766078bfd608daa769fefb97e65d1d73746a3dfb640460e322071b64", size = 96342 }, +] + +[package.optional-dependencies] +disk = [ + { name = "diskcache" }, + { name = "pathvalidate" }, +] +keyring = [ + { name = "keyring" }, +] +memory = [ + { name = "cachetools" }, +] +redis = [ + { name = "redis" }, +] + +[[package]] +name = "py-key-value-shared" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beartype" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7b/e4/1971dfc4620a3a15b4579fe99e024f5edd6e0967a71154771a059daff4db/py_key_value_shared-0.3.0.tar.gz", hash = "sha256:8fdd786cf96c3e900102945f92aa1473138ebe960ef49da1c833790160c28a4b", size = 11666 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/e4/b8b0a03ece72f47dce2307d36e1c34725b7223d209fc679315ffe6a4e2c3/py_key_value_shared-0.3.0-py3-none-any.whl", hash = "sha256:5b0efba7ebca08bb158b1e93afc2f07d30b8f40c2fc12ce24a4c0d84f42f9298", size = 19560 }, +] + +[[package]] +name = "pycparser" +version = "2.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 }, +] + +[[package]] +name = "pydantic" +version = "2.11.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/00/dd/4325abf92c39ba8623b5af936ddb36ffcfe0beae70405d456ab1fb2f5b8c/pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db", size = 788350 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/c0/ec2b1c8712ca690e5d61979dee872603e92b8a32f94cc1b72d53beab008a/pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b", size = 444782 }, +] + +[package.optional-dependencies] +email = [ + { name = "email-validator" }, +] + +[[package]] +name = "pydantic-core" +version = "2.33.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/92/b31726561b5dae176c2d2c2dc43a9c5bfba5d32f96f8b4c0a600dd492447/pydantic_core-2.33.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2b3d326aaef0c0399d9afffeb6367d5e26ddc24d351dbc9c636840ac355dc5d8", size = 2028817 }, + { url = "https://files.pythonhosted.org/packages/a3/44/3f0b95fafdaca04a483c4e685fe437c6891001bf3ce8b2fded82b9ea3aa1/pydantic_core-2.33.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e5b2671f05ba48b94cb90ce55d8bdcaaedb8ba00cc5359f6810fc918713983d", size = 1861357 }, + { url = "https://files.pythonhosted.org/packages/30/97/e8f13b55766234caae05372826e8e4b3b96e7b248be3157f53237682e43c/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0069c9acc3f3981b9ff4cdfaf088e98d83440a4c7ea1bc07460af3d4dc22e72d", size = 1898011 }, + { url = "https://files.pythonhosted.org/packages/9b/a3/99c48cf7bafc991cc3ee66fd544c0aae8dc907b752f1dad2d79b1b5a471f/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d53b22f2032c42eaaf025f7c40c2e3b94568ae077a606f006d206a463bc69572", size = 1982730 }, + { url = "https://files.pythonhosted.org/packages/de/8e/a5b882ec4307010a840fb8b58bd9bf65d1840c92eae7534c7441709bf54b/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0405262705a123b7ce9f0b92f123334d67b70fd1f20a9372b907ce1080c7ba02", size = 2136178 }, + { url = "https://files.pythonhosted.org/packages/e4/bb/71e35fc3ed05af6834e890edb75968e2802fe98778971ab5cba20a162315/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4b25d91e288e2c4e0662b8038a28c6a07eaac3e196cfc4ff69de4ea3db992a1b", size = 2736462 }, + { url = "https://files.pythonhosted.org/packages/31/0d/c8f7593e6bc7066289bbc366f2235701dcbebcd1ff0ef8e64f6f239fb47d/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bdfe4b3789761f3bcb4b1ddf33355a71079858958e3a552f16d5af19768fef2", size = 2005652 }, + { url = "https://files.pythonhosted.org/packages/d2/7a/996d8bd75f3eda405e3dd219ff5ff0a283cd8e34add39d8ef9157e722867/pydantic_core-2.33.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:efec8db3266b76ef9607c2c4c419bdb06bf335ae433b80816089ea7585816f6a", size = 2113306 }, + { url = "https://files.pythonhosted.org/packages/ff/84/daf2a6fb2db40ffda6578a7e8c5a6e9c8affb251a05c233ae37098118788/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:031c57d67ca86902726e0fae2214ce6770bbe2f710dc33063187a68744a5ecac", size = 2073720 }, + { url = "https://files.pythonhosted.org/packages/77/fb/2258da019f4825128445ae79456a5499c032b55849dbd5bed78c95ccf163/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:f8de619080e944347f5f20de29a975c2d815d9ddd8be9b9b7268e2e3ef68605a", size = 2244915 }, + { url = "https://files.pythonhosted.org/packages/d8/7a/925ff73756031289468326e355b6fa8316960d0d65f8b5d6b3a3e7866de7/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:73662edf539e72a9440129f231ed3757faab89630d291b784ca99237fb94db2b", size = 2241884 }, + { url = "https://files.pythonhosted.org/packages/0b/b0/249ee6d2646f1cdadcb813805fe76265745c4010cf20a8eba7b0e639d9b2/pydantic_core-2.33.2-cp310-cp310-win32.whl", hash = "sha256:0a39979dcbb70998b0e505fb1556a1d550a0781463ce84ebf915ba293ccb7e22", size = 1910496 }, + { url = "https://files.pythonhosted.org/packages/66/ff/172ba8f12a42d4b552917aa65d1f2328990d3ccfc01d5b7c943ec084299f/pydantic_core-2.33.2-cp310-cp310-win_amd64.whl", hash = "sha256:b0379a2b24882fef529ec3b4987cb5d003b9cda32256024e6fe1586ac45fc640", size = 1955019 }, + { url = "https://files.pythonhosted.org/packages/3f/8d/71db63483d518cbbf290261a1fc2839d17ff89fce7089e08cad07ccfce67/pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7", size = 2028584 }, + { url = "https://files.pythonhosted.org/packages/24/2f/3cfa7244ae292dd850989f328722d2aef313f74ffc471184dc509e1e4e5a/pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246", size = 1855071 }, + { url = "https://files.pythonhosted.org/packages/b3/d3/4ae42d33f5e3f50dd467761304be2fa0a9417fbf09735bc2cce003480f2a/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f", size = 1897823 }, + { url = "https://files.pythonhosted.org/packages/f4/f3/aa5976e8352b7695ff808599794b1fba2a9ae2ee954a3426855935799488/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc", size = 1983792 }, + { url = "https://files.pythonhosted.org/packages/d5/7a/cda9b5a23c552037717f2b2a5257e9b2bfe45e687386df9591eff7b46d28/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de", size = 2136338 }, + { url = "https://files.pythonhosted.org/packages/2b/9f/b8f9ec8dd1417eb9da784e91e1667d58a2a4a7b7b34cf4af765ef663a7e5/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a", size = 2730998 }, + { url = "https://files.pythonhosted.org/packages/47/bc/cd720e078576bdb8255d5032c5d63ee5c0bf4b7173dd955185a1d658c456/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef", size = 2003200 }, + { url = "https://files.pythonhosted.org/packages/ca/22/3602b895ee2cd29d11a2b349372446ae9727c32e78a94b3d588a40fdf187/pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e", size = 2113890 }, + { url = "https://files.pythonhosted.org/packages/ff/e6/e3c5908c03cf00d629eb38393a98fccc38ee0ce8ecce32f69fc7d7b558a7/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d", size = 2073359 }, + { url = "https://files.pythonhosted.org/packages/12/e7/6a36a07c59ebefc8777d1ffdaf5ae71b06b21952582e4b07eba88a421c79/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30", size = 2245883 }, + { url = "https://files.pythonhosted.org/packages/16/3f/59b3187aaa6cc0c1e6616e8045b284de2b6a87b027cce2ffcea073adf1d2/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf", size = 2241074 }, + { url = "https://files.pythonhosted.org/packages/e0/ed/55532bb88f674d5d8f67ab121a2a13c385df382de2a1677f30ad385f7438/pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51", size = 1910538 }, + { url = "https://files.pythonhosted.org/packages/fe/1b/25b7cccd4519c0b23c2dd636ad39d381abf113085ce4f7bec2b0dc755eb1/pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab", size = 1952909 }, + { url = "https://files.pythonhosted.org/packages/49/a9/d809358e49126438055884c4366a1f6227f0f84f635a9014e2deb9b9de54/pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65", size = 1897786 }, + { url = "https://files.pythonhosted.org/packages/18/8a/2b41c97f554ec8c71f2a8a5f85cb56a8b0956addfe8b0efb5b3d77e8bdc3/pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc", size = 2009000 }, + { url = "https://files.pythonhosted.org/packages/a1/02/6224312aacb3c8ecbaa959897af57181fb6cf3a3d7917fd44d0f2917e6f2/pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7", size = 1847996 }, + { url = "https://files.pythonhosted.org/packages/d6/46/6dcdf084a523dbe0a0be59d054734b86a981726f221f4562aed313dbcb49/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025", size = 1880957 }, + { url = "https://files.pythonhosted.org/packages/ec/6b/1ec2c03837ac00886ba8160ce041ce4e325b41d06a034adbef11339ae422/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011", size = 1964199 }, + { url = "https://files.pythonhosted.org/packages/2d/1d/6bf34d6adb9debd9136bd197ca72642203ce9aaaa85cfcbfcf20f9696e83/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f", size = 2120296 }, + { url = "https://files.pythonhosted.org/packages/e0/94/2bd0aaf5a591e974b32a9f7123f16637776c304471a0ab33cf263cf5591a/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88", size = 2676109 }, + { url = "https://files.pythonhosted.org/packages/f9/41/4b043778cf9c4285d59742281a769eac371b9e47e35f98ad321349cc5d61/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1", size = 2002028 }, + { url = "https://files.pythonhosted.org/packages/cb/d5/7bb781bf2748ce3d03af04d5c969fa1308880e1dca35a9bd94e1a96a922e/pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b", size = 2100044 }, + { url = "https://files.pythonhosted.org/packages/fe/36/def5e53e1eb0ad896785702a5bbfd25eed546cdcf4087ad285021a90ed53/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1", size = 2058881 }, + { url = "https://files.pythonhosted.org/packages/01/6c/57f8d70b2ee57fc3dc8b9610315949837fa8c11d86927b9bb044f8705419/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6", size = 2227034 }, + { url = "https://files.pythonhosted.org/packages/27/b9/9c17f0396a82b3d5cbea4c24d742083422639e7bb1d5bf600e12cb176a13/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea", size = 2234187 }, + { url = "https://files.pythonhosted.org/packages/b0/6a/adf5734ffd52bf86d865093ad70b2ce543415e0e356f6cacabbc0d9ad910/pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290", size = 1892628 }, + { url = "https://files.pythonhosted.org/packages/43/e4/5479fecb3606c1368d496a825d8411e126133c41224c1e7238be58b87d7e/pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2", size = 1955866 }, + { url = "https://files.pythonhosted.org/packages/0d/24/8b11e8b3e2be9dd82df4b11408a67c61bb4dc4f8e11b5b0fc888b38118b5/pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab", size = 1888894 }, + { url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688 }, + { url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808 }, + { url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580 }, + { url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859 }, + { url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810 }, + { url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498 }, + { url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611 }, + { url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924 }, + { url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196 }, + { url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389 }, + { url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223 }, + { url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473 }, + { url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269 }, + { url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921 }, + { url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162 }, + { url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560 }, + { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777 }, + { url = "https://files.pythonhosted.org/packages/30/68/373d55e58b7e83ce371691f6eaa7175e3a24b956c44628eb25d7da007917/pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c4aa4e82353f65e548c476b37e64189783aa5384903bfea4f41580f255fddfa", size = 2023982 }, + { url = "https://files.pythonhosted.org/packages/a4/16/145f54ac08c96a63d8ed6442f9dec17b2773d19920b627b18d4f10a061ea/pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d946c8bf0d5c24bf4fe333af284c59a19358aa3ec18cb3dc4370080da1e8ad29", size = 1858412 }, + { url = "https://files.pythonhosted.org/packages/41/b1/c6dc6c3e2de4516c0bb2c46f6a373b91b5660312342a0cf5826e38ad82fa/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87b31b6846e361ef83fedb187bb5b4372d0da3f7e28d85415efa92d6125d6e6d", size = 1892749 }, + { url = "https://files.pythonhosted.org/packages/12/73/8cd57e20afba760b21b742106f9dbdfa6697f1570b189c7457a1af4cd8a0/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa9d91b338f2df0508606f7009fde642391425189bba6d8c653afd80fd6bb64e", size = 2067527 }, + { url = "https://files.pythonhosted.org/packages/e3/d5/0bb5d988cc019b3cba4a78f2d4b3854427fc47ee8ec8e9eaabf787da239c/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2058a32994f1fde4ca0480ab9d1e75a0e8c87c22b53a3ae66554f9af78f2fe8c", size = 2108225 }, + { url = "https://files.pythonhosted.org/packages/f1/c5/00c02d1571913d496aabf146106ad8239dc132485ee22efe08085084ff7c/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:0e03262ab796d986f978f79c943fc5f620381be7287148b8010b4097f79a39ec", size = 2069490 }, + { url = "https://files.pythonhosted.org/packages/22/a8/dccc38768274d3ed3a59b5d06f59ccb845778687652daa71df0cab4040d7/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1a8695a8d00c73e50bff9dfda4d540b7dee29ff9b8053e38380426a85ef10052", size = 2237525 }, + { url = "https://files.pythonhosted.org/packages/d4/e7/4f98c0b125dda7cf7ccd14ba936218397b44f50a56dd8c16a3091df116c3/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:fa754d1850735a0b0e03bcffd9d4b4343eb417e47196e4485d9cca326073a42c", size = 2238446 }, + { url = "https://files.pythonhosted.org/packages/ce/91/2ec36480fdb0b783cd9ef6795753c1dea13882f2e68e73bce76ae8c21e6a/pydantic_core-2.33.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a11c8d26a50bfab49002947d3d237abe4d9e4b5bdc8846a63537b6488e197808", size = 2066678 }, + { url = "https://files.pythonhosted.org/packages/7b/27/d4ae6487d73948d6f20dddcd94be4ea43e74349b56eba82e9bdee2d7494c/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8", size = 2025200 }, + { url = "https://files.pythonhosted.org/packages/f1/b8/b3cb95375f05d33801024079b9392a5ab45267a63400bf1866e7ce0f0de4/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593", size = 1859123 }, + { url = "https://files.pythonhosted.org/packages/05/bc/0d0b5adeda59a261cd30a1235a445bf55c7e46ae44aea28f7bd6ed46e091/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612", size = 1892852 }, + { url = "https://files.pythonhosted.org/packages/3e/11/d37bdebbda2e449cb3f519f6ce950927b56d62f0b84fd9cb9e372a26a3d5/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7", size = 2067484 }, + { url = "https://files.pythonhosted.org/packages/8c/55/1f95f0a05ce72ecb02a8a8a1c3be0579bbc29b1d5ab68f1378b7bebc5057/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e", size = 2108896 }, + { url = "https://files.pythonhosted.org/packages/53/89/2b2de6c81fa131f423246a9109d7b2a375e83968ad0800d6e57d0574629b/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8", size = 2069475 }, + { url = "https://files.pythonhosted.org/packages/b8/e9/1f7efbe20d0b2b10f6718944b5d8ece9152390904f29a78e68d4e7961159/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf", size = 2239013 }, + { url = "https://files.pythonhosted.org/packages/3c/b2/5309c905a93811524a49b4e031e9851a6b00ff0fb668794472ea7746b448/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb", size = 2238715 }, + { url = "https://files.pythonhosted.org/packages/32/56/8a7ca5d2cd2cda1d245d34b1c9a942920a718082ae8e54e5f3e5a58b7add/pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1", size = 2066757 }, +] + +[[package]] +name = "pydantic-settings" +version = "2.10.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/68/85/1ea668bbab3c50071ca613c6ab30047fb36ab0da1b92fa8f17bbc38fd36c/pydantic_settings-2.10.1.tar.gz", hash = "sha256:06f0062169818d0f5524420a360d632d5857b83cffd4d42fe29597807a1614ee", size = 172583 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/f0/427018098906416f580e3cf1366d3b1abfb408a0652e9f31600c24a1903c/pydantic_settings-2.10.1-py3-none-any.whl", hash = "sha256:a60952460b99cf661dc25c29c0ef171721f98bfcb52ef8d9ea4c943d7c8cc796", size = 45235 }, +] + +[[package]] +name = "pydocket" +version = "0.15.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cloudpickle" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "fakeredis", extra = ["lua"] }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-prometheus" }, + { name = "opentelemetry-instrumentation" }, + { name = "prometheus-client" }, + { name = "py-key-value-aio", extra = ["memory", "redis"] }, + { name = "python-json-logger" }, + { name = "redis" }, + { name = "rich" }, + { name = "typer" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/86/77/842e41be3cf3592b971cf42b24cae76e282294f474dc2dbf7cd6808d1b09/pydocket-0.15.5.tar.gz", hash = "sha256:b3af47702a293dd1da2e5e0f8f73f27fd3b3c95e36de72a2f71026d16908d5ba", size = 277245 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/c0/fdbc6e04e3369b90c6bf6567bc62871cf59e88550b94529821500dc807c1/pydocket-0.15.5-py3-none-any.whl", hash = "sha256:ad0d86c9a1bea394e875bcf8c793be2d0a7ebd1891bfe99e2e9eaf99ef0cb42e", size = 58517 }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217 }, +] + +[[package]] +name = "pyjwt" +version = "2.10.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/46/bd74733ff231675599650d3e47f361794b22ef3e3770998dda30d3b63726/pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", size = 87785 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997 }, +] + +[package.optional-dependencies] +crypto = [ + { name = "cryptography" }, +] + +[[package]] +name = "pyperclip" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/30/23/2f0a3efc4d6a32f3b63cdff36cd398d9701d26cda58e3ab97ac79fb5e60d/pyperclip-1.9.0.tar.gz", hash = "sha256:b7de0142ddc81bfc5c7507eea19da920b92252b548b96186caf94a5e2527d310", size = 20961 } + +[[package]] +name = "python-dotenv" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/b0/4bc07ccd3572a2f9df7e6782f52b0c6c90dcbb803ac4a167702d7d0dfe1e/python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab", size = 41978 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556 }, +] + +[[package]] +name = "python-json-logger" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/29/bf/eca6a3d43db1dae7070f70e160ab20b807627ba953663ba07928cdd3dc58/python_json_logger-4.0.0.tar.gz", hash = "sha256:f58e68eb46e1faed27e0f574a55a0455eecd7b8a5b88b85a784519ba3cff047f", size = 17683 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/e5/fecf13f06e5e5f67e8837d777d1bc43fac0ed2b77a676804df5c34744727/python_json_logger-4.0.0-py3-none-any.whl", hash = "sha256:af09c9daf6a813aa4cc7180395f50f2a9e5fa056034c9953aec92e381c5ba1e2", size = 15548 }, +] + +[[package]] +name = "python-multipart" +version = "0.0.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546 }, +] + +[[package]] +name = "pywin32" +version = "311" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/40/44efbb0dfbd33aca6a6483191dae0716070ed99e2ecb0c53683f400a0b4f/pywin32-311-cp310-cp310-win32.whl", hash = "sha256:d03ff496d2a0cd4a5893504789d4a15399133fe82517455e78bad62efbb7f0a3", size = 8760432 }, + { url = "https://files.pythonhosted.org/packages/5e/bf/360243b1e953bd254a82f12653974be395ba880e7ec23e3731d9f73921cc/pywin32-311-cp310-cp310-win_amd64.whl", hash = "sha256:797c2772017851984b97180b0bebe4b620bb86328e8a884bb626156295a63b3b", size = 9590103 }, + { url = "https://files.pythonhosted.org/packages/57/38/d290720e6f138086fb3d5ffe0b6caa019a791dd57866940c82e4eeaf2012/pywin32-311-cp310-cp310-win_arm64.whl", hash = "sha256:0502d1facf1fed4839a9a51ccbcc63d952cf318f78ffc00a7e78528ac27d7a2b", size = 8778557 }, + { url = "https://files.pythonhosted.org/packages/7c/af/449a6a91e5d6db51420875c54f6aff7c97a86a3b13a0b4f1a5c13b988de3/pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151", size = 8697031 }, + { url = "https://files.pythonhosted.org/packages/51/8f/9bb81dd5bb77d22243d33c8397f09377056d5c687aa6d4042bea7fbf8364/pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503", size = 9508308 }, + { url = "https://files.pythonhosted.org/packages/44/7b/9c2ab54f74a138c491aba1b1cd0795ba61f144c711daea84a88b63dc0f6c/pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2", size = 8703930 }, + { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543 }, + { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040 }, + { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102 }, + { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700 }, + { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700 }, + { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318 }, + { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714 }, + { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800 }, + { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540 }, +] + +[[package]] +name = "pywin32-ctypes" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756 }, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199 }, + { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758 }, + { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463 }, + { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280 }, + { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239 }, + { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802 }, + { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527 }, + { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052 }, + { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774 }, + { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612 }, + { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040 }, + { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829 }, + { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167 }, + { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952 }, + { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301 }, + { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638 }, + { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850 }, + { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980 }, + { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873 }, + { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302 }, + { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154 }, + { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223 }, + { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542 }, + { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164 }, + { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611 }, + { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591 }, + { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338 }, + { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 }, + { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 }, + { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 }, + { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361 }, + { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523 }, + { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660 }, + { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 }, + { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 }, + { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 }, +] + +[[package]] +name = "rag-agent" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "click" }, + { name = "fastapi" }, + { name = "fastmcp" }, + { name = "mcp" }, + { name = "openai" }, + { name = "pydantic" }, + { name = "uvicorn" }, +] + +[package.metadata] +requires-dist = [ + { name = "click", specifier = ">=8.2.1" }, + { name = "fastapi", specifier = ">=0.104.1" }, + { name = "fastmcp", specifier = ">=2.14" }, + { name = "mcp", specifier = ">=1.13.1" }, + { name = "openai", specifier = "==2.13.0" }, + { name = "pydantic", specifier = ">=2.11.7" }, + { name = "uvicorn", specifier = ">=0.24.0" }, +] + +[[package]] +name = "redis" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "async-timeout", marker = "python_full_version < '3.11.3'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/c8/983d5c6579a411d8a99bc5823cc5712768859b5ce2c8afe1a65b37832c81/redis-7.1.0.tar.gz", hash = "sha256:b1cc3cfa5a2cb9c2ab3ba700864fb0ad75617b41f01352ce5779dabf6d5f9c3c", size = 4796669 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/f0/8956f8a86b20d7bb9d6ac0187cf4cd54d8065bc9a1a09eb8011d4d326596/redis-7.1.0-py3-none-any.whl", hash = "sha256:23c52b208f92b56103e17c5d06bdc1a6c2c0b3106583985a76a18f83b265de2b", size = 354159 }, +] + +[[package]] +name = "referencing" +version = "0.36.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2f/db/98b5c277be99dd18bfd91dd04e1b759cad18d1a338188c936e92f921c7e2/referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", size = 74744 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0", size = 26775 }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738 }, +] + +[[package]] +name = "rich" +version = "14.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fe/75/af448d8e52bf1d8fa6a9d089ca6c07ff4453d86c65c145d0a300bb073b9b/rich-14.1.0.tar.gz", hash = "sha256:e497a48b844b0320d45007cdebfeaeed8db2a4f4bcf49f15e455cfc4af11eaa8", size = 224441 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/30/3c4d035596d3cf444529e0b2953ad0466f6049528a879d27534700580395/rich-14.1.0-py3-none-any.whl", hash = "sha256:536f5f1785986d6dbdea3c75205c473f970777b4a0d6c6dd1b696aa05a3fa04f", size = 243368 }, +] + +[[package]] +name = "rich-rst" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils" }, + { name = "rich" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b0/69/5514c3a87b5f10f09a34bb011bc0927bc12c596c8dae5915604e71abc386/rich_rst-1.3.1.tar.gz", hash = "sha256:fad46e3ba42785ea8c1785e2ceaa56e0ffa32dbe5410dec432f37e4107c4f383", size = 13839 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/bc/cc4e3dbc5e7992398dcb7a8eda0cbcf4fb792a0cdb93f857b478bf3cf884/rich_rst-1.3.1-py3-none-any.whl", hash = "sha256:498a74e3896507ab04492d326e794c3ef76e7cda078703aa592d1853d91098c1", size = 11621 }, +] + +[[package]] +name = "rpds-py" +version = "0.27.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e9/dd/2c0cbe774744272b0ae725f44032c77bdcab6e8bcf544bffa3b6e70c8dba/rpds_py-0.27.1.tar.gz", hash = "sha256:26a1c73171d10b7acccbded82bf6a586ab8203601e565badc74bbbf8bc5a10f8", size = 27479 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/ed/3aef893e2dd30e77e35d20d4ddb45ca459db59cead748cad9796ad479411/rpds_py-0.27.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:68afeec26d42ab3b47e541b272166a0b4400313946871cba3ed3a4fc0cab1cef", size = 371606 }, + { url = "https://files.pythonhosted.org/packages/6d/82/9818b443e5d3eb4c83c3994561387f116aae9833b35c484474769c4a8faf/rpds_py-0.27.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:74e5b2f7bb6fa38b1b10546d27acbacf2a022a8b5543efb06cfebc72a59c85be", size = 353452 }, + { url = "https://files.pythonhosted.org/packages/99/c7/d2a110ffaaa397fc6793a83c7bd3545d9ab22658b7cdff05a24a4535cc45/rpds_py-0.27.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9024de74731df54546fab0bfbcdb49fae19159ecaecfc8f37c18d2c7e2c0bd61", size = 381519 }, + { url = "https://files.pythonhosted.org/packages/5a/bc/e89581d1f9d1be7d0247eaef602566869fdc0d084008ba139e27e775366c/rpds_py-0.27.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:31d3ebadefcd73b73928ed0b2fd696f7fefda8629229f81929ac9c1854d0cffb", size = 394424 }, + { url = "https://files.pythonhosted.org/packages/ac/2e/36a6861f797530e74bb6ed53495f8741f1ef95939eed01d761e73d559067/rpds_py-0.27.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b2e7f8f169d775dd9092a1743768d771f1d1300453ddfe6325ae3ab5332b4657", size = 523467 }, + { url = "https://files.pythonhosted.org/packages/c4/59/c1bc2be32564fa499f988f0a5c6505c2f4746ef96e58e4d7de5cf923d77e/rpds_py-0.27.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d905d16f77eb6ab2e324e09bfa277b4c8e5e6b8a78a3e7ff8f3cdf773b4c013", size = 402660 }, + { url = "https://files.pythonhosted.org/packages/0a/ec/ef8bf895f0628dd0a59e54d81caed6891663cb9c54a0f4bb7da918cb88cf/rpds_py-0.27.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50c946f048209e6362e22576baea09193809f87687a95a8db24e5fbdb307b93a", size = 384062 }, + { url = "https://files.pythonhosted.org/packages/69/f7/f47ff154be8d9a5e691c083a920bba89cef88d5247c241c10b9898f595a1/rpds_py-0.27.1-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:3deab27804d65cd8289eb814c2c0e807c4b9d9916c9225e363cb0cf875eb67c1", size = 401289 }, + { url = "https://files.pythonhosted.org/packages/3b/d9/ca410363efd0615814ae579f6829cafb39225cd63e5ea5ed1404cb345293/rpds_py-0.27.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8b61097f7488de4be8244c89915da8ed212832ccf1e7c7753a25a394bf9b1f10", size = 417718 }, + { url = "https://files.pythonhosted.org/packages/e3/a0/8cb5c2ff38340f221cc067cc093d1270e10658ba4e8d263df923daa18e86/rpds_py-0.27.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8a3f29aba6e2d7d90528d3c792555a93497fe6538aa65eb675b44505be747808", size = 558333 }, + { url = "https://files.pythonhosted.org/packages/6f/8c/1b0de79177c5d5103843774ce12b84caa7164dfc6cd66378768d37db11bf/rpds_py-0.27.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:dd6cd0485b7d347304067153a6dc1d73f7d4fd995a396ef32a24d24b8ac63ac8", size = 589127 }, + { url = "https://files.pythonhosted.org/packages/c8/5e/26abb098d5e01266b0f3a2488d299d19ccc26849735d9d2b95c39397e945/rpds_py-0.27.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6f4461bf931108c9fa226ffb0e257c1b18dc2d44cd72b125bec50ee0ab1248a9", size = 554899 }, + { url = "https://files.pythonhosted.org/packages/de/41/905cc90ced13550db017f8f20c6d8e8470066c5738ba480d7ba63e3d136b/rpds_py-0.27.1-cp310-cp310-win32.whl", hash = "sha256:ee5422d7fb21f6a00c1901bf6559c49fee13a5159d0288320737bbf6585bd3e4", size = 217450 }, + { url = "https://files.pythonhosted.org/packages/75/3d/6bef47b0e253616ccdf67c283e25f2d16e18ccddd38f92af81d5a3420206/rpds_py-0.27.1-cp310-cp310-win_amd64.whl", hash = "sha256:3e039aabf6d5f83c745d5f9a0a381d031e9ed871967c0a5c38d201aca41f3ba1", size = 228447 }, + { url = "https://files.pythonhosted.org/packages/b5/c1/7907329fbef97cbd49db6f7303893bd1dd5a4a3eae415839ffdfb0762cae/rpds_py-0.27.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:be898f271f851f68b318872ce6ebebbc62f303b654e43bf72683dbdc25b7c881", size = 371063 }, + { url = "https://files.pythonhosted.org/packages/11/94/2aab4bc86228bcf7c48760990273653a4900de89c7537ffe1b0d6097ed39/rpds_py-0.27.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:62ac3d4e3e07b58ee0ddecd71d6ce3b1637de2d373501412df395a0ec5f9beb5", size = 353210 }, + { url = "https://files.pythonhosted.org/packages/3a/57/f5eb3ecf434342f4f1a46009530e93fd201a0b5b83379034ebdb1d7c1a58/rpds_py-0.27.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4708c5c0ceb2d034f9991623631d3d23cb16e65c83736ea020cdbe28d57c0a0e", size = 381636 }, + { url = "https://files.pythonhosted.org/packages/ae/f4/ef95c5945e2ceb5119571b184dd5a1cc4b8541bbdf67461998cfeac9cb1e/rpds_py-0.27.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:abfa1171a9952d2e0002aba2ad3780820b00cc3d9c98c6630f2e93271501f66c", size = 394341 }, + { url = "https://files.pythonhosted.org/packages/5a/7e/4bd610754bf492d398b61725eb9598ddd5eb86b07d7d9483dbcd810e20bc/rpds_py-0.27.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4b507d19f817ebaca79574b16eb2ae412e5c0835542c93fe9983f1e432aca195", size = 523428 }, + { url = "https://files.pythonhosted.org/packages/9f/e5/059b9f65a8c9149361a8b75094864ab83b94718344db511fd6117936ed2a/rpds_py-0.27.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:168b025f8fd8d8d10957405f3fdcef3dc20f5982d398f90851f4abc58c566c52", size = 402923 }, + { url = "https://files.pythonhosted.org/packages/f5/48/64cabb7daced2968dd08e8a1b7988bf358d7bd5bcd5dc89a652f4668543c/rpds_py-0.27.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb56c6210ef77caa58e16e8c17d35c63fe3f5b60fd9ba9d424470c3400bcf9ed", size = 384094 }, + { url = "https://files.pythonhosted.org/packages/ae/e1/dc9094d6ff566bff87add8a510c89b9e158ad2ecd97ee26e677da29a9e1b/rpds_py-0.27.1-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:d252f2d8ca0195faa707f8eb9368955760880b2b42a8ee16d382bf5dd807f89a", size = 401093 }, + { url = "https://files.pythonhosted.org/packages/37/8e/ac8577e3ecdd5593e283d46907d7011618994e1d7ab992711ae0f78b9937/rpds_py-0.27.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6e5e54da1e74b91dbc7996b56640f79b195d5925c2b78efaa8c5d53e1d88edde", size = 417969 }, + { url = "https://files.pythonhosted.org/packages/66/6d/87507430a8f74a93556fe55c6485ba9c259949a853ce407b1e23fea5ba31/rpds_py-0.27.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ffce0481cc6e95e5b3f0a47ee17ffbd234399e6d532f394c8dce320c3b089c21", size = 558302 }, + { url = "https://files.pythonhosted.org/packages/3a/bb/1db4781ce1dda3eecc735e3152659a27b90a02ca62bfeea17aee45cc0fbc/rpds_py-0.27.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:a205fdfe55c90c2cd8e540ca9ceba65cbe6629b443bc05db1f590a3db8189ff9", size = 589259 }, + { url = "https://files.pythonhosted.org/packages/7b/0e/ae1c8943d11a814d01b482e1f8da903f88047a962dff9bbdadf3bd6e6fd1/rpds_py-0.27.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:689fb5200a749db0415b092972e8eba85847c23885c8543a8b0f5c009b1a5948", size = 554983 }, + { url = "https://files.pythonhosted.org/packages/b2/d5/0b2a55415931db4f112bdab072443ff76131b5ac4f4dc98d10d2d357eb03/rpds_py-0.27.1-cp311-cp311-win32.whl", hash = "sha256:3182af66048c00a075010bc7f4860f33913528a4b6fc09094a6e7598e462fe39", size = 217154 }, + { url = "https://files.pythonhosted.org/packages/24/75/3b7ffe0d50dc86a6a964af0d1cc3a4a2cdf437cb7b099a4747bbb96d1819/rpds_py-0.27.1-cp311-cp311-win_amd64.whl", hash = "sha256:b4938466c6b257b2f5c4ff98acd8128ec36b5059e5c8f8372d79316b1c36bb15", size = 228627 }, + { url = "https://files.pythonhosted.org/packages/8d/3f/4fd04c32abc02c710f09a72a30c9a55ea3cc154ef8099078fd50a0596f8e/rpds_py-0.27.1-cp311-cp311-win_arm64.whl", hash = "sha256:2f57af9b4d0793e53266ee4325535a31ba48e2f875da81a9177c9926dfa60746", size = 220998 }, + { url = "https://files.pythonhosted.org/packages/bd/fe/38de28dee5df58b8198c743fe2bea0c785c6d40941b9950bac4cdb71a014/rpds_py-0.27.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:ae2775c1973e3c30316892737b91f9283f9908e3cc7625b9331271eaaed7dc90", size = 361887 }, + { url = "https://files.pythonhosted.org/packages/7c/9a/4b6c7eedc7dd90986bf0fab6ea2a091ec11c01b15f8ba0a14d3f80450468/rpds_py-0.27.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2643400120f55c8a96f7c9d858f7be0c88d383cd4653ae2cf0d0c88f668073e5", size = 345795 }, + { url = "https://files.pythonhosted.org/packages/6f/0e/e650e1b81922847a09cca820237b0edee69416a01268b7754d506ade11ad/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16323f674c089b0360674a4abd28d5042947d54ba620f72514d69be4ff64845e", size = 385121 }, + { url = "https://files.pythonhosted.org/packages/1b/ea/b306067a712988e2bff00dcc7c8f31d26c29b6d5931b461aa4b60a013e33/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9a1f4814b65eacac94a00fc9a526e3fdafd78e439469644032032d0d63de4881", size = 398976 }, + { url = "https://files.pythonhosted.org/packages/2c/0a/26dc43c8840cb8fe239fe12dbc8d8de40f2365e838f3d395835dde72f0e5/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ba32c16b064267b22f1850a34051121d423b6f7338a12b9459550eb2096e7ec", size = 525953 }, + { url = "https://files.pythonhosted.org/packages/22/14/c85e8127b573aaf3a0cbd7fbb8c9c99e735a4a02180c84da2a463b766e9e/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5c20f33fd10485b80f65e800bbe5f6785af510b9f4056c5a3c612ebc83ba6cb", size = 407915 }, + { url = "https://files.pythonhosted.org/packages/ed/7b/8f4fee9ba1fb5ec856eb22d725a4efa3deb47f769597c809e03578b0f9d9/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:466bfe65bd932da36ff279ddd92de56b042f2266d752719beb97b08526268ec5", size = 386883 }, + { url = "https://files.pythonhosted.org/packages/86/47/28fa6d60f8b74fcdceba81b272f8d9836ac0340570f68f5df6b41838547b/rpds_py-0.27.1-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:41e532bbdcb57c92ba3be62c42e9f096431b4cf478da9bc3bc6ce5c38ab7ba7a", size = 405699 }, + { url = "https://files.pythonhosted.org/packages/d0/fd/c5987b5e054548df56953a21fe2ebed51fc1ec7c8f24fd41c067b68c4a0a/rpds_py-0.27.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f149826d742b406579466283769a8ea448eed82a789af0ed17b0cd5770433444", size = 423713 }, + { url = "https://files.pythonhosted.org/packages/ac/ba/3c4978b54a73ed19a7d74531be37a8bcc542d917c770e14d372b8daea186/rpds_py-0.27.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:80c60cfb5310677bd67cb1e85a1e8eb52e12529545441b43e6f14d90b878775a", size = 562324 }, + { url = "https://files.pythonhosted.org/packages/b5/6c/6943a91768fec16db09a42b08644b960cff540c66aab89b74be6d4a144ba/rpds_py-0.27.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:7ee6521b9baf06085f62ba9c7a3e5becffbc32480d2f1b351559c001c38ce4c1", size = 593646 }, + { url = "https://files.pythonhosted.org/packages/11/73/9d7a8f4be5f4396f011a6bb7a19fe26303a0dac9064462f5651ced2f572f/rpds_py-0.27.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a512c8263249a9d68cac08b05dd59d2b3f2061d99b322813cbcc14c3c7421998", size = 558137 }, + { url = "https://files.pythonhosted.org/packages/6e/96/6772cbfa0e2485bcceef8071de7821f81aeac8bb45fbfd5542a3e8108165/rpds_py-0.27.1-cp312-cp312-win32.whl", hash = "sha256:819064fa048ba01b6dadc5116f3ac48610435ac9a0058bbde98e569f9e785c39", size = 221343 }, + { url = "https://files.pythonhosted.org/packages/67/b6/c82f0faa9af1c6a64669f73a17ee0eeef25aff30bb9a1c318509efe45d84/rpds_py-0.27.1-cp312-cp312-win_amd64.whl", hash = "sha256:d9199717881f13c32c4046a15f024971a3b78ad4ea029e8da6b86e5aa9cf4594", size = 232497 }, + { url = "https://files.pythonhosted.org/packages/e1/96/2817b44bd2ed11aebacc9251da03689d56109b9aba5e311297b6902136e2/rpds_py-0.27.1-cp312-cp312-win_arm64.whl", hash = "sha256:33aa65b97826a0e885ef6e278fbd934e98cdcfed80b63946025f01e2f5b29502", size = 222790 }, + { url = "https://files.pythonhosted.org/packages/cc/77/610aeee8d41e39080c7e14afa5387138e3c9fa9756ab893d09d99e7d8e98/rpds_py-0.27.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:e4b9fcfbc021633863a37e92571d6f91851fa656f0180246e84cbd8b3f6b329b", size = 361741 }, + { url = "https://files.pythonhosted.org/packages/3a/fc/c43765f201c6a1c60be2043cbdb664013def52460a4c7adace89d6682bf4/rpds_py-0.27.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1441811a96eadca93c517d08df75de45e5ffe68aa3089924f963c782c4b898cf", size = 345574 }, + { url = "https://files.pythonhosted.org/packages/20/42/ee2b2ca114294cd9847d0ef9c26d2b0851b2e7e00bf14cc4c0b581df0fc3/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55266dafa22e672f5a4f65019015f90336ed31c6383bd53f5e7826d21a0e0b83", size = 385051 }, + { url = "https://files.pythonhosted.org/packages/fd/e8/1e430fe311e4799e02e2d1af7c765f024e95e17d651612425b226705f910/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d78827d7ac08627ea2c8e02c9e5b41180ea5ea1f747e9db0915e3adf36b62dcf", size = 398395 }, + { url = "https://files.pythonhosted.org/packages/82/95/9dc227d441ff2670651c27a739acb2535ccaf8b351a88d78c088965e5996/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae92443798a40a92dc5f0b01d8a7c93adde0c4dc965310a29ae7c64d72b9fad2", size = 524334 }, + { url = "https://files.pythonhosted.org/packages/87/01/a670c232f401d9ad461d9a332aa4080cd3cb1d1df18213dbd0d2a6a7ab51/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c46c9dd2403b66a2a3b9720ec4b74d4ab49d4fabf9f03dfdce2d42af913fe8d0", size = 407691 }, + { url = "https://files.pythonhosted.org/packages/03/36/0a14aebbaa26fe7fab4780c76f2239e76cc95a0090bdb25e31d95c492fcd/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2efe4eb1d01b7f5f1939f4ef30ecea6c6b3521eec451fb93191bf84b2a522418", size = 386868 }, + { url = "https://files.pythonhosted.org/packages/3b/03/8c897fb8b5347ff6c1cc31239b9611c5bf79d78c984430887a353e1409a1/rpds_py-0.27.1-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:15d3b4d83582d10c601f481eca29c3f138d44c92187d197aff663a269197c02d", size = 405469 }, + { url = "https://files.pythonhosted.org/packages/da/07/88c60edc2df74850d496d78a1fdcdc7b54360a7f610a4d50008309d41b94/rpds_py-0.27.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4ed2e16abbc982a169d30d1a420274a709949e2cbdef119fe2ec9d870b42f274", size = 422125 }, + { url = "https://files.pythonhosted.org/packages/6b/86/5f4c707603e41b05f191a749984f390dabcbc467cf833769b47bf14ba04f/rpds_py-0.27.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a75f305c9b013289121ec0f1181931975df78738cdf650093e6b86d74aa7d8dd", size = 562341 }, + { url = "https://files.pythonhosted.org/packages/b2/92/3c0cb2492094e3cd9baf9e49bbb7befeceb584ea0c1a8b5939dca4da12e5/rpds_py-0.27.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:67ce7620704745881a3d4b0ada80ab4d99df390838839921f99e63c474f82cf2", size = 592511 }, + { url = "https://files.pythonhosted.org/packages/10/bb/82e64fbb0047c46a168faa28d0d45a7851cd0582f850b966811d30f67ad8/rpds_py-0.27.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9d992ac10eb86d9b6f369647b6a3f412fc0075cfd5d799530e84d335e440a002", size = 557736 }, + { url = "https://files.pythonhosted.org/packages/00/95/3c863973d409210da7fb41958172c6b7dbe7fc34e04d3cc1f10bb85e979f/rpds_py-0.27.1-cp313-cp313-win32.whl", hash = "sha256:4f75e4bd8ab8db624e02c8e2fc4063021b58becdbe6df793a8111d9343aec1e3", size = 221462 }, + { url = "https://files.pythonhosted.org/packages/ce/2c/5867b14a81dc217b56d95a9f2a40fdbc56a1ab0181b80132beeecbd4b2d6/rpds_py-0.27.1-cp313-cp313-win_amd64.whl", hash = "sha256:f9025faafc62ed0b75a53e541895ca272815bec18abe2249ff6501c8f2e12b83", size = 232034 }, + { url = "https://files.pythonhosted.org/packages/c7/78/3958f3f018c01923823f1e47f1cc338e398814b92d83cd278364446fac66/rpds_py-0.27.1-cp313-cp313-win_arm64.whl", hash = "sha256:ed10dc32829e7d222b7d3b93136d25a406ba9788f6a7ebf6809092da1f4d279d", size = 222392 }, + { url = "https://files.pythonhosted.org/packages/01/76/1cdf1f91aed5c3a7bf2eba1f1c4e4d6f57832d73003919a20118870ea659/rpds_py-0.27.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:92022bbbad0d4426e616815b16bc4127f83c9a74940e1ccf3cfe0b387aba0228", size = 358355 }, + { url = "https://files.pythonhosted.org/packages/c3/6f/bf142541229374287604caf3bb2a4ae17f0a580798fd72d3b009b532db4e/rpds_py-0.27.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:47162fdab9407ec3f160805ac3e154df042e577dd53341745fc7fb3f625e6d92", size = 342138 }, + { url = "https://files.pythonhosted.org/packages/1a/77/355b1c041d6be40886c44ff5e798b4e2769e497b790f0f7fd1e78d17e9a8/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb89bec23fddc489e5d78b550a7b773557c9ab58b7946154a10a6f7a214a48b2", size = 380247 }, + { url = "https://files.pythonhosted.org/packages/d6/a4/d9cef5c3946ea271ce2243c51481971cd6e34f21925af2783dd17b26e815/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e48af21883ded2b3e9eb48cb7880ad8598b31ab752ff3be6457001d78f416723", size = 390699 }, + { url = "https://files.pythonhosted.org/packages/3a/06/005106a7b8c6c1a7e91b73169e49870f4af5256119d34a361ae5240a0c1d/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6f5b7bd8e219ed50299e58551a410b64daafb5017d54bbe822e003856f06a802", size = 521852 }, + { url = "https://files.pythonhosted.org/packages/e5/3e/50fb1dac0948e17a02eb05c24510a8fe12d5ce8561c6b7b7d1339ab7ab9c/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08f1e20bccf73b08d12d804d6e1c22ca5530e71659e6673bce31a6bb71c1e73f", size = 402582 }, + { url = "https://files.pythonhosted.org/packages/cb/b0/f4e224090dc5b0ec15f31a02d746ab24101dd430847c4d99123798661bfc/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dc5dceeaefcc96dc192e3a80bbe1d6c410c469e97bdd47494a7d930987f18b2", size = 384126 }, + { url = "https://files.pythonhosted.org/packages/54/77/ac339d5f82b6afff1df8f0fe0d2145cc827992cb5f8eeb90fc9f31ef7a63/rpds_py-0.27.1-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:d76f9cc8665acdc0c9177043746775aa7babbf479b5520b78ae4002d889f5c21", size = 399486 }, + { url = "https://files.pythonhosted.org/packages/d6/29/3e1c255eee6ac358c056a57d6d6869baa00a62fa32eea5ee0632039c50a3/rpds_py-0.27.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:134fae0e36022edad8290a6661edf40c023562964efea0cc0ec7f5d392d2aaef", size = 414832 }, + { url = "https://files.pythonhosted.org/packages/3f/db/6d498b844342deb3fa1d030598db93937a9964fcf5cb4da4feb5f17be34b/rpds_py-0.27.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb11a4f1b2b63337cfd3b4d110af778a59aae51c81d195768e353d8b52f88081", size = 557249 }, + { url = "https://files.pythonhosted.org/packages/60/f3/690dd38e2310b6f68858a331399b4d6dbb9132c3e8ef8b4333b96caf403d/rpds_py-0.27.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:13e608ac9f50a0ed4faec0e90ece76ae33b34c0e8656e3dceb9a7db994c692cd", size = 587356 }, + { url = "https://files.pythonhosted.org/packages/86/e3/84507781cccd0145f35b1dc32c72675200c5ce8d5b30f813e49424ef68fc/rpds_py-0.27.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dd2135527aa40f061350c3f8f89da2644de26cd73e4de458e79606384f4f68e7", size = 555300 }, + { url = "https://files.pythonhosted.org/packages/e5/ee/375469849e6b429b3516206b4580a79e9ef3eb12920ddbd4492b56eaacbe/rpds_py-0.27.1-cp313-cp313t-win32.whl", hash = "sha256:3020724ade63fe320a972e2ffd93b5623227e684315adce194941167fee02688", size = 216714 }, + { url = "https://files.pythonhosted.org/packages/21/87/3fc94e47c9bd0742660e84706c311a860dcae4374cf4a03c477e23ce605a/rpds_py-0.27.1-cp313-cp313t-win_amd64.whl", hash = "sha256:8ee50c3e41739886606388ba3ab3ee2aae9f35fb23f833091833255a31740797", size = 228943 }, + { url = "https://files.pythonhosted.org/packages/70/36/b6e6066520a07cf029d385de869729a895917b411e777ab1cde878100a1d/rpds_py-0.27.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:acb9aafccaae278f449d9c713b64a9e68662e7799dbd5859e2c6b3c67b56d334", size = 362472 }, + { url = "https://files.pythonhosted.org/packages/af/07/b4646032e0dcec0df9c73a3bd52f63bc6c5f9cda992f06bd0e73fe3fbebd/rpds_py-0.27.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b7fb801aa7f845ddf601c49630deeeccde7ce10065561d92729bfe81bd21fb33", size = 345676 }, + { url = "https://files.pythonhosted.org/packages/b0/16/2f1003ee5d0af4bcb13c0cf894957984c32a6751ed7206db2aee7379a55e/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fe0dd05afb46597b9a2e11c351e5e4283c741237e7f617ffb3252780cca9336a", size = 385313 }, + { url = "https://files.pythonhosted.org/packages/05/cd/7eb6dd7b232e7f2654d03fa07f1414d7dfc980e82ba71e40a7c46fd95484/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b6dfb0e058adb12d8b1d1b25f686e94ffa65d9995a5157afe99743bf7369d62b", size = 399080 }, + { url = "https://files.pythonhosted.org/packages/20/51/5829afd5000ec1cb60f304711f02572d619040aa3ec033d8226817d1e571/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed090ccd235f6fa8bb5861684567f0a83e04f52dfc2e5c05f2e4b1309fcf85e7", size = 523868 }, + { url = "https://files.pythonhosted.org/packages/05/2c/30eebca20d5db95720ab4d2faec1b5e4c1025c473f703738c371241476a2/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bf876e79763eecf3e7356f157540d6a093cef395b65514f17a356f62af6cc136", size = 408750 }, + { url = "https://files.pythonhosted.org/packages/90/1a/cdb5083f043597c4d4276eae4e4c70c55ab5accec078da8611f24575a367/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:12ed005216a51b1d6e2b02a7bd31885fe317e45897de81d86dcce7d74618ffff", size = 387688 }, + { url = "https://files.pythonhosted.org/packages/7c/92/cf786a15320e173f945d205ab31585cc43969743bb1a48b6888f7a2b0a2d/rpds_py-0.27.1-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:ee4308f409a40e50593c7e3bb8cbe0b4d4c66d1674a316324f0c2f5383b486f9", size = 407225 }, + { url = "https://files.pythonhosted.org/packages/33/5c/85ee16df5b65063ef26017bef33096557a4c83fbe56218ac7cd8c235f16d/rpds_py-0.27.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0b08d152555acf1f455154d498ca855618c1378ec810646fcd7c76416ac6dc60", size = 423361 }, + { url = "https://files.pythonhosted.org/packages/4b/8e/1c2741307fcabd1a334ecf008e92c4f47bb6f848712cf15c923becfe82bb/rpds_py-0.27.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:dce51c828941973a5684d458214d3a36fcd28da3e1875d659388f4f9f12cc33e", size = 562493 }, + { url = "https://files.pythonhosted.org/packages/04/03/5159321baae9b2222442a70c1f988cbbd66b9be0675dd3936461269be360/rpds_py-0.27.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:c1476d6f29eb81aa4151c9a31219b03f1f798dc43d8af1250a870735516a1212", size = 592623 }, + { url = "https://files.pythonhosted.org/packages/ff/39/c09fd1ad28b85bc1d4554a8710233c9f4cefd03d7717a1b8fbfd171d1167/rpds_py-0.27.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3ce0cac322b0d69b63c9cdb895ee1b65805ec9ffad37639f291dd79467bee675", size = 558800 }, + { url = "https://files.pythonhosted.org/packages/c5/d6/99228e6bbcf4baa764b18258f519a9035131d91b538d4e0e294313462a98/rpds_py-0.27.1-cp314-cp314-win32.whl", hash = "sha256:dfbfac137d2a3d0725758cd141f878bf4329ba25e34979797c89474a89a8a3a3", size = 221943 }, + { url = "https://files.pythonhosted.org/packages/be/07/c802bc6b8e95be83b79bdf23d1aa61d68324cb1006e245d6c58e959e314d/rpds_py-0.27.1-cp314-cp314-win_amd64.whl", hash = "sha256:a6e57b0abfe7cc513450fcf529eb486b6e4d3f8aee83e92eb5f1ef848218d456", size = 233739 }, + { url = "https://files.pythonhosted.org/packages/c8/89/3e1b1c16d4c2d547c5717377a8df99aee8099ff050f87c45cb4d5fa70891/rpds_py-0.27.1-cp314-cp314-win_arm64.whl", hash = "sha256:faf8d146f3d476abfee026c4ae3bdd9ca14236ae4e4c310cbd1cf75ba33d24a3", size = 223120 }, + { url = "https://files.pythonhosted.org/packages/62/7e/dc7931dc2fa4a6e46b2a4fa744a9fe5c548efd70e0ba74f40b39fa4a8c10/rpds_py-0.27.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:ba81d2b56b6d4911ce735aad0a1d4495e808b8ee4dc58715998741a26874e7c2", size = 358944 }, + { url = "https://files.pythonhosted.org/packages/e6/22/4af76ac4e9f336bfb1a5f240d18a33c6b2fcaadb7472ac7680576512b49a/rpds_py-0.27.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:84f7d509870098de0e864cad0102711c1e24e9b1a50ee713b65928adb22269e4", size = 342283 }, + { url = "https://files.pythonhosted.org/packages/1c/15/2a7c619b3c2272ea9feb9ade67a45c40b3eeb500d503ad4c28c395dc51b4/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9e960fc78fecd1100539f14132425e1d5fe44ecb9239f8f27f079962021523e", size = 380320 }, + { url = "https://files.pythonhosted.org/packages/a2/7d/4c6d243ba4a3057e994bb5bedd01b5c963c12fe38dde707a52acdb3849e7/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:62f85b665cedab1a503747617393573995dac4600ff51869d69ad2f39eb5e817", size = 391760 }, + { url = "https://files.pythonhosted.org/packages/b4/71/b19401a909b83bcd67f90221330bc1ef11bc486fe4e04c24388d28a618ae/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fed467af29776f6556250c9ed85ea5a4dd121ab56a5f8b206e3e7a4c551e48ec", size = 522476 }, + { url = "https://files.pythonhosted.org/packages/e4/44/1a3b9715c0455d2e2f0f6df5ee6d6f5afdc423d0773a8a682ed2b43c566c/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2729615f9d430af0ae6b36cf042cb55c0936408d543fb691e1a9e36648fd35a", size = 403418 }, + { url = "https://files.pythonhosted.org/packages/1c/4b/fb6c4f14984eb56673bc868a66536f53417ddb13ed44b391998100a06a96/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1b207d881a9aef7ba753d69c123a35d96ca7cb808056998f6b9e8747321f03b8", size = 384771 }, + { url = "https://files.pythonhosted.org/packages/c0/56/d5265d2d28b7420d7b4d4d85cad8ef891760f5135102e60d5c970b976e41/rpds_py-0.27.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:639fd5efec029f99b79ae47e5d7e00ad8a773da899b6309f6786ecaf22948c48", size = 400022 }, + { url = "https://files.pythonhosted.org/packages/8f/e9/9f5fc70164a569bdd6ed9046486c3568d6926e3a49bdefeeccfb18655875/rpds_py-0.27.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fecc80cb2a90e28af8a9b366edacf33d7a91cbfe4c2c4544ea1246e949cfebeb", size = 416787 }, + { url = "https://files.pythonhosted.org/packages/d4/64/56dd03430ba491db943a81dcdef115a985aac5f44f565cd39a00c766d45c/rpds_py-0.27.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:42a89282d711711d0a62d6f57d81aa43a1368686c45bc1c46b7f079d55692734", size = 557538 }, + { url = "https://files.pythonhosted.org/packages/3f/36/92cc885a3129993b1d963a2a42ecf64e6a8e129d2c7cc980dbeba84e55fb/rpds_py-0.27.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:cf9931f14223de59551ab9d38ed18d92f14f055a5f78c1d8ad6493f735021bbb", size = 588512 }, + { url = "https://files.pythonhosted.org/packages/dd/10/6b283707780a81919f71625351182b4f98932ac89a09023cb61865136244/rpds_py-0.27.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f39f58a27cc6e59f432b568ed8429c7e1641324fbe38131de852cd77b2d534b0", size = 555813 }, + { url = "https://files.pythonhosted.org/packages/04/2e/30b5ea18c01379da6272a92825dd7e53dc9d15c88a19e97932d35d430ef7/rpds_py-0.27.1-cp314-cp314t-win32.whl", hash = "sha256:d5fa0ee122dc09e23607a28e6d7b150da16c662e66409bbe85230e4c85bb528a", size = 217385 }, + { url = "https://files.pythonhosted.org/packages/32/7d/97119da51cb1dd3f2f3c0805f155a3aa4a95fa44fe7d78ae15e69edf4f34/rpds_py-0.27.1-cp314-cp314t-win_amd64.whl", hash = "sha256:6567d2bb951e21232c2f660c24cf3470bb96de56cdcb3f071a83feeaff8a2772", size = 230097 }, + { url = "https://files.pythonhosted.org/packages/d5/63/b7cc415c345625d5e62f694ea356c58fb964861409008118f1245f8c3347/rpds_py-0.27.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7ba22cb9693df986033b91ae1d7a979bc399237d45fccf875b76f62bb9e52ddf", size = 371360 }, + { url = "https://files.pythonhosted.org/packages/e5/8c/12e1b24b560cf378b8ffbdb9dc73abd529e1adcfcf82727dfd29c4a7b88d/rpds_py-0.27.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5b640501be9288c77738b5492b3fd3abc4ba95c50c2e41273c8a1459f08298d3", size = 353933 }, + { url = "https://files.pythonhosted.org/packages/9b/85/1bb2210c1f7a1b99e91fea486b9f0f894aa5da3a5ec7097cbad7dec6d40f/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb08b65b93e0c6dd70aac7f7890a9c0938d5ec71d5cb32d45cf844fb8ae47636", size = 382962 }, + { url = "https://files.pythonhosted.org/packages/cc/c9/a839b9f219cf80ed65f27a7f5ddbb2809c1b85c966020ae2dff490e0b18e/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d7ff07d696a7a38152ebdb8212ca9e5baab56656749f3d6004b34ab726b550b8", size = 394412 }, + { url = "https://files.pythonhosted.org/packages/02/2d/b1d7f928b0b1f4fc2e0133e8051d199b01d7384875adc63b6ddadf3de7e5/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fb7c72262deae25366e3b6c0c0ba46007967aea15d1eea746e44ddba8ec58dcc", size = 523972 }, + { url = "https://files.pythonhosted.org/packages/a9/af/2cbf56edd2d07716df1aec8a726b3159deb47cb5c27e1e42b71d705a7c2f/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7b002cab05d6339716b03a4a3a2ce26737f6231d7b523f339fa061d53368c9d8", size = 403273 }, + { url = "https://files.pythonhosted.org/packages/c0/93/425e32200158d44ff01da5d9612c3b6711fe69f606f06e3895511f17473b/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23f6b69d1c26c4704fec01311963a41d7de3ee0570a84ebde4d544e5a1859ffc", size = 385278 }, + { url = "https://files.pythonhosted.org/packages/eb/1a/1a04a915ecd0551bfa9e77b7672d1937b4b72a0fc204a17deef76001cfb2/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:530064db9146b247351f2a0250b8f00b289accea4596a033e94be2389977de71", size = 402084 }, + { url = "https://files.pythonhosted.org/packages/51/f7/66585c0fe5714368b62951d2513b684e5215beaceab2c6629549ddb15036/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7b90b0496570bd6b0321724a330d8b545827c4df2034b6ddfc5f5275f55da2ad", size = 419041 }, + { url = "https://files.pythonhosted.org/packages/8e/7e/83a508f6b8e219bba2d4af077c35ba0e0cdd35a751a3be6a7cba5a55ad71/rpds_py-0.27.1-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:879b0e14a2da6a1102a3fc8af580fc1ead37e6d6692a781bd8c83da37429b5ab", size = 560084 }, + { url = "https://files.pythonhosted.org/packages/66/66/bb945683b958a1b19eb0fe715594630d0f36396ebdef4d9b89c2fa09aa56/rpds_py-0.27.1-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:0d807710df3b5faa66c731afa162ea29717ab3be17bdc15f90f2d9f183da4059", size = 590115 }, + { url = "https://files.pythonhosted.org/packages/12/00/ccfaafaf7db7e7adace915e5c2f2c2410e16402561801e9c7f96683002d3/rpds_py-0.27.1-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:3adc388fc3afb6540aec081fa59e6e0d3908722771aa1e37ffe22b220a436f0b", size = 556561 }, + { url = "https://files.pythonhosted.org/packages/e1/b7/92b6ed9aad103bfe1c45df98453dfae40969eef2cb6c6239c58d7e96f1b3/rpds_py-0.27.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c796c0c1cc68cb08b0284db4229f5af76168172670c74908fdbd4b7d7f515819", size = 229125 }, + { url = "https://files.pythonhosted.org/packages/0c/ed/e1fba02de17f4f76318b834425257c8ea297e415e12c68b4361f63e8ae92/rpds_py-0.27.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:cdfe4bb2f9fe7458b7453ad3c33e726d6d1c7c0a72960bcc23800d77384e42df", size = 371402 }, + { url = "https://files.pythonhosted.org/packages/af/7c/e16b959b316048b55585a697e94add55a4ae0d984434d279ea83442e460d/rpds_py-0.27.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:8fabb8fd848a5f75a2324e4a84501ee3a5e3c78d8603f83475441866e60b94a3", size = 354084 }, + { url = "https://files.pythonhosted.org/packages/de/c1/ade645f55de76799fdd08682d51ae6724cb46f318573f18be49b1e040428/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eda8719d598f2f7f3e0f885cba8646644b55a187762bec091fa14a2b819746a9", size = 383090 }, + { url = "https://files.pythonhosted.org/packages/1f/27/89070ca9b856e52960da1472efcb6c20ba27cfe902f4f23ed095b9cfc61d/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3c64d07e95606ec402a0a1c511fe003873fa6af630bda59bac77fac8b4318ebc", size = 394519 }, + { url = "https://files.pythonhosted.org/packages/b3/28/be120586874ef906aa5aeeae95ae8df4184bc757e5b6bd1c729ccff45ed5/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:93a2ed40de81bcff59aabebb626562d48332f3d028ca2036f1d23cbb52750be4", size = 523817 }, + { url = "https://files.pythonhosted.org/packages/a8/ef/70cc197bc11cfcde02a86f36ac1eed15c56667c2ebddbdb76a47e90306da/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:387ce8c44ae94e0ec50532d9cb0edce17311024c9794eb196b90e1058aadeb66", size = 403240 }, + { url = "https://files.pythonhosted.org/packages/cf/35/46936cca449f7f518f2f4996e0e8344db4b57e2081e752441154089d2a5f/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aaf94f812c95b5e60ebaf8bfb1898a7d7cb9c1af5744d4a67fa47796e0465d4e", size = 385194 }, + { url = "https://files.pythonhosted.org/packages/e1/62/29c0d3e5125c3270b51415af7cbff1ec587379c84f55a5761cc9efa8cd06/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:4848ca84d6ded9b58e474dfdbad4b8bfb450344c0551ddc8d958bf4b36aa837c", size = 402086 }, + { url = "https://files.pythonhosted.org/packages/8f/66/03e1087679227785474466fdd04157fb793b3b76e3fcf01cbf4c693c1949/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2bde09cbcf2248b73c7c323be49b280180ff39fadcfe04e7b6f54a678d02a7cf", size = 419272 }, + { url = "https://files.pythonhosted.org/packages/6a/24/e3e72d265121e00b063aef3e3501e5b2473cf1b23511d56e529531acf01e/rpds_py-0.27.1-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:94c44ee01fd21c9058f124d2d4f0c9dc7634bec93cd4b38eefc385dabe71acbf", size = 560003 }, + { url = "https://files.pythonhosted.org/packages/26/ca/f5a344c534214cc2d41118c0699fffbdc2c1bc7046f2a2b9609765ab9c92/rpds_py-0.27.1-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:df8b74962e35c9249425d90144e721eed198e6555a0e22a563d29fe4486b51f6", size = 590482 }, + { url = "https://files.pythonhosted.org/packages/ce/08/4349bdd5c64d9d193c360aa9db89adeee6f6682ab8825dca0a3f535f434f/rpds_py-0.27.1-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:dc23e6820e3b40847e2f4a7726462ba0cf53089512abe9ee16318c366494c17a", size = 556523 }, +] + +[[package]] +name = "secretstorage" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "jeepney" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1c/03/e834bcd866f2f8a49a85eaff47340affa3bfa391ee9912a952a1faa68c7b/secretstorage-3.5.0.tar.gz", hash = "sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be", size = 19884 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl", hash = "sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137", size = 15554 }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755 }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, +] + +[[package]] +name = "sortedcontainers" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575 }, +] + +[[package]] +name = "sse-starlette" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/6f/22ed6e33f8a9e76ca0a412405f31abb844b779d52c5f96660766edcd737c/sse_starlette-3.0.2.tar.gz", hash = "sha256:ccd60b5765ebb3584d0de2d7a6e4f745672581de4f5005ab31c3a25d10b52b3a", size = 20985 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/10/c78f463b4ef22eef8491f218f692be838282cd65480f6e423d7730dfd1fb/sse_starlette-3.0.2-py3-none-any.whl", hash = "sha256:16b7cbfddbcd4eaca11f7b586f3b8a080f1afe952c15813455b162edea619e5a", size = 11297 }, +] + +[[package]] +name = "starlette" +version = "0.47.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/15/b9/cc3017f9a9c9b6e27c5106cc10cc7904653c3eec0729793aec10479dd669/starlette-0.47.3.tar.gz", hash = "sha256:6bc94f839cc176c4858894f1f8908f0ab79dfec1a6b8402f6da9be26ebea52e9", size = 2584144 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/fd/901cfa59aaa5b30a99e16876f11abe38b59a1a2c51ffb3d7142bb6089069/starlette-0.47.3-py3-none-any.whl", hash = "sha256:89c0778ca62a76b826101e7c709e70680a1699ca7da6b44d38eb0a7e61fe4b51", size = 72991 }, +] + +[[package]] +name = "tomli" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/ed/3f73f72945444548f33eba9a87fc7a6e969915e7b1acc8260b30e1f76a2f/tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549", size = 17392 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/2e/299f62b401438d5fe1624119c723f5d877acc86a4c2492da405626665f12/tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45", size = 153236 }, + { url = "https://files.pythonhosted.org/packages/86/7f/d8fffe6a7aefdb61bced88fcb5e280cfd71e08939da5894161bd71bea022/tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba", size = 148084 }, + { url = "https://files.pythonhosted.org/packages/47/5c/24935fb6a2ee63e86d80e4d3b58b222dafaf438c416752c8b58537c8b89a/tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf", size = 234832 }, + { url = "https://files.pythonhosted.org/packages/89/da/75dfd804fc11e6612846758a23f13271b76d577e299592b4371a4ca4cd09/tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441", size = 242052 }, + { url = "https://files.pythonhosted.org/packages/70/8c/f48ac899f7b3ca7eb13af73bacbc93aec37f9c954df3c08ad96991c8c373/tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845", size = 239555 }, + { url = "https://files.pythonhosted.org/packages/ba/28/72f8afd73f1d0e7829bfc093f4cb98ce0a40ffc0cc997009ee1ed94ba705/tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c", size = 245128 }, + { url = "https://files.pythonhosted.org/packages/b6/eb/a7679c8ac85208706d27436e8d421dfa39d4c914dcf5fa8083a9305f58d9/tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456", size = 96445 }, + { url = "https://files.pythonhosted.org/packages/0a/fe/3d3420c4cb1ad9cb462fb52967080575f15898da97e21cb6f1361d505383/tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be", size = 107165 }, + { url = "https://files.pythonhosted.org/packages/ff/b7/40f36368fcabc518bb11c8f06379a0fd631985046c038aca08c6d6a43c6e/tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac", size = 154891 }, + { url = "https://files.pythonhosted.org/packages/f9/3f/d9dd692199e3b3aab2e4e4dd948abd0f790d9ded8cd10cbaae276a898434/tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22", size = 148796 }, + { url = "https://files.pythonhosted.org/packages/60/83/59bff4996c2cf9f9387a0f5a3394629c7efa5ef16142076a23a90f1955fa/tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f", size = 242121 }, + { url = "https://files.pythonhosted.org/packages/45/e5/7c5119ff39de8693d6baab6c0b6dcb556d192c165596e9fc231ea1052041/tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52", size = 250070 }, + { url = "https://files.pythonhosted.org/packages/45/12/ad5126d3a278f27e6701abde51d342aa78d06e27ce2bb596a01f7709a5a2/tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8", size = 245859 }, + { url = "https://files.pythonhosted.org/packages/fb/a1/4d6865da6a71c603cfe6ad0e6556c73c76548557a8d658f9e3b142df245f/tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6", size = 250296 }, + { url = "https://files.pythonhosted.org/packages/a0/b7/a7a7042715d55c9ba6e8b196d65d2cb662578b4d8cd17d882d45322b0d78/tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876", size = 97124 }, + { url = "https://files.pythonhosted.org/packages/06/1e/f22f100db15a68b520664eb3328fb0ae4e90530887928558112c8d1f4515/tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878", size = 107698 }, + { url = "https://files.pythonhosted.org/packages/89/48/06ee6eabe4fdd9ecd48bf488f4ac783844fd777f547b8d1b61c11939974e/tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b", size = 154819 }, + { url = "https://files.pythonhosted.org/packages/f1/01/88793757d54d8937015c75dcdfb673c65471945f6be98e6a0410fba167ed/tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae", size = 148766 }, + { url = "https://files.pythonhosted.org/packages/42/17/5e2c956f0144b812e7e107f94f1cc54af734eb17b5191c0bbfb72de5e93e/tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b", size = 240771 }, + { url = "https://files.pythonhosted.org/packages/d5/f4/0fbd014909748706c01d16824eadb0307115f9562a15cbb012cd9b3512c5/tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf", size = 248586 }, + { url = "https://files.pythonhosted.org/packages/30/77/fed85e114bde5e81ecf9bc5da0cc69f2914b38f4708c80ae67d0c10180c5/tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f", size = 244792 }, + { url = "https://files.pythonhosted.org/packages/55/92/afed3d497f7c186dc71e6ee6d4fcb0acfa5f7d0a1a2878f8beae379ae0cc/tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05", size = 248909 }, + { url = "https://files.pythonhosted.org/packages/f8/84/ef50c51b5a9472e7265ce1ffc7f24cd4023d289e109f669bdb1553f6a7c2/tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606", size = 96946 }, + { url = "https://files.pythonhosted.org/packages/b2/b7/718cd1da0884f281f95ccfa3a6cc572d30053cba64603f79d431d3c9b61b/tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999", size = 107705 }, + { url = "https://files.pythonhosted.org/packages/19/94/aeafa14a52e16163008060506fcb6aa1949d13548d13752171a755c65611/tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e", size = 154244 }, + { url = "https://files.pythonhosted.org/packages/db/e4/1e58409aa78eefa47ccd19779fc6f36787edbe7d4cd330eeeedb33a4515b/tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3", size = 148637 }, + { url = "https://files.pythonhosted.org/packages/26/b6/d1eccb62f665e44359226811064596dd6a366ea1f985839c566cd61525ae/tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc", size = 241925 }, + { url = "https://files.pythonhosted.org/packages/70/91/7cdab9a03e6d3d2bb11beae108da5bdc1c34bdeb06e21163482544ddcc90/tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0", size = 249045 }, + { url = "https://files.pythonhosted.org/packages/15/1b/8c26874ed1f6e4f1fcfeb868db8a794cbe9f227299402db58cfcc858766c/tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879", size = 245835 }, + { url = "https://files.pythonhosted.org/packages/fd/42/8e3c6a9a4b1a1360c1a2a39f0b972cef2cc9ebd56025168c4137192a9321/tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005", size = 253109 }, + { url = "https://files.pythonhosted.org/packages/22/0c/b4da635000a71b5f80130937eeac12e686eefb376b8dee113b4a582bba42/tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463", size = 97930 }, + { url = "https://files.pythonhosted.org/packages/b9/74/cb1abc870a418ae99cd5c9547d6bce30701a954e0e721821df483ef7223c/tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8", size = 107964 }, + { url = "https://files.pythonhosted.org/packages/54/78/5c46fff6432a712af9f792944f4fcd7067d8823157949f4e40c56b8b3c83/tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77", size = 163065 }, + { url = "https://files.pythonhosted.org/packages/39/67/f85d9bd23182f45eca8939cd2bc7050e1f90c41f4a2ecbbd5963a1d1c486/tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf", size = 159088 }, + { url = "https://files.pythonhosted.org/packages/26/5a/4b546a0405b9cc0659b399f12b6adb750757baf04250b148d3c5059fc4eb/tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530", size = 268193 }, + { url = "https://files.pythonhosted.org/packages/42/4f/2c12a72ae22cf7b59a7fe75b3465b7aba40ea9145d026ba41cb382075b0e/tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b", size = 275488 }, + { url = "https://files.pythonhosted.org/packages/92/04/a038d65dbe160c3aa5a624e93ad98111090f6804027d474ba9c37c8ae186/tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67", size = 272669 }, + { url = "https://files.pythonhosted.org/packages/be/2f/8b7c60a9d1612a7cbc39ffcca4f21a73bf368a80fc25bccf8253e2563267/tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f", size = 279709 }, + { url = "https://files.pythonhosted.org/packages/7e/46/cc36c679f09f27ded940281c38607716c86cf8ba4a518d524e349c8b4874/tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0", size = 107563 }, + { url = "https://files.pythonhosted.org/packages/84/ff/426ca8683cf7b753614480484f6437f568fd2fda2edbdf57a2d3d8b27a0b/tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba", size = 119756 }, + { url = "https://files.pythonhosted.org/packages/77/b8/0135fadc89e73be292b473cb820b4f5a08197779206b33191e801feeae40/tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", size = 14408 }, +] + +[[package]] +name = "tqdm" +version = "4.67.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "platform_system == 'Windows'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540 }, +] + +[[package]] +name = "typer" +version = "0.20.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8f/28/7c85c8032b91dbe79725b6f17d2fffc595dff06a35c7a30a37bef73a1ab4/typer-0.20.0.tar.gz", hash = "sha256:1aaf6494031793e4876fb0bacfa6a912b551cf43c1e63c800df8b1a866720c37", size = 106492 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/64/7713ffe4b5983314e9d436a90d5bd4f63b6054e2aca783a3cfc44cb95bbf/typer-0.20.0-py3-none-any.whl", hash = "sha256:5b463df6793ec1dca6213a3cf4c0f03bc6e322ac5e16e13ddd622a889489784a", size = 47028 }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614 }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552 }, +] + +[[package]] +name = "urllib3" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795 }, +] + +[[package]] +name = "uvicorn" +version = "0.35.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/42/e0e305207bb88c6b8d3061399c6a961ffe5fbb7e2aa63c9234df7259e9cd/uvicorn-0.35.0.tar.gz", hash = "sha256:bc662f087f7cf2ce11a1d7fd70b90c9f98ef2e2831556dd078d131b96cc94a01", size = 78473 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/e2/dc81b1bd1dcfe91735810265e9d26bc8ec5da45b4c0f6237e286819194c3/uvicorn-0.35.0-py3-none-any.whl", hash = "sha256:197535216b25ff9b785e29a0b79199f55222193d47f820816e7da751e9bc8d4a", size = 66406 }, +] + +[[package]] +name = "websockets" +version = "15.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/da/6462a9f510c0c49837bbc9345aca92d767a56c1fb2939e1579df1e1cdcf7/websockets-15.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b", size = 175423 }, + { url = "https://files.pythonhosted.org/packages/1c/9f/9d11c1a4eb046a9e106483b9ff69bce7ac880443f00e5ce64261b47b07e7/websockets-15.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205", size = 173080 }, + { url = "https://files.pythonhosted.org/packages/d5/4f/b462242432d93ea45f297b6179c7333dd0402b855a912a04e7fc61c0d71f/websockets-15.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5756779642579d902eed757b21b0164cd6fe338506a8083eb58af5c372e39d9a", size = 173329 }, + { url = "https://files.pythonhosted.org/packages/6e/0c/6afa1f4644d7ed50284ac59cc70ef8abd44ccf7d45850d989ea7310538d0/websockets-15.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdfe3e2a29e4db3659dbd5bbf04560cea53dd9610273917799f1cde46aa725e", size = 182312 }, + { url = "https://files.pythonhosted.org/packages/dd/d4/ffc8bd1350b229ca7a4db2a3e1c482cf87cea1baccd0ef3e72bc720caeec/websockets-15.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c2529b320eb9e35af0fa3016c187dffb84a3ecc572bcee7c3ce302bfeba52bf", size = 181319 }, + { url = "https://files.pythonhosted.org/packages/97/3a/5323a6bb94917af13bbb34009fac01e55c51dfde354f63692bf2533ffbc2/websockets-15.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac1e5c9054fe23226fb11e05a6e630837f074174c4c2f0fe442996112a6de4fb", size = 181631 }, + { url = "https://files.pythonhosted.org/packages/a6/cc/1aeb0f7cee59ef065724041bb7ed667b6ab1eeffe5141696cccec2687b66/websockets-15.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5df592cd503496351d6dc14f7cdad49f268d8e618f80dce0cd5a36b93c3fc08d", size = 182016 }, + { url = "https://files.pythonhosted.org/packages/79/f9/c86f8f7af208e4161a7f7e02774e9d0a81c632ae76db2ff22549e1718a51/websockets-15.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0a34631031a8f05657e8e90903e656959234f3a04552259458aac0b0f9ae6fd9", size = 181426 }, + { url = "https://files.pythonhosted.org/packages/c7/b9/828b0bc6753db905b91df6ae477c0b14a141090df64fb17f8a9d7e3516cf/websockets-15.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d00075aa65772e7ce9e990cab3ff1de702aa09be3940d1dc88d5abf1ab8a09c", size = 181360 }, + { url = "https://files.pythonhosted.org/packages/89/fb/250f5533ec468ba6327055b7d98b9df056fb1ce623b8b6aaafb30b55d02e/websockets-15.0.1-cp310-cp310-win32.whl", hash = "sha256:1234d4ef35db82f5446dca8e35a7da7964d02c127b095e172e54397fb6a6c256", size = 176388 }, + { url = "https://files.pythonhosted.org/packages/1c/46/aca7082012768bb98e5608f01658ff3ac8437e563eca41cf068bd5849a5e/websockets-15.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:39c1fec2c11dc8d89bba6b2bf1556af381611a173ac2b511cf7231622058af41", size = 176830 }, + { url = "https://files.pythonhosted.org/packages/9f/32/18fcd5919c293a398db67443acd33fde142f283853076049824fc58e6f75/websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431", size = 175423 }, + { url = "https://files.pythonhosted.org/packages/76/70/ba1ad96b07869275ef42e2ce21f07a5b0148936688c2baf7e4a1f60d5058/websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57", size = 173082 }, + { url = "https://files.pythonhosted.org/packages/86/f2/10b55821dd40eb696ce4704a87d57774696f9451108cff0d2824c97e0f97/websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905", size = 173330 }, + { url = "https://files.pythonhosted.org/packages/a5/90/1c37ae8b8a113d3daf1065222b6af61cc44102da95388ac0018fcb7d93d9/websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562", size = 182878 }, + { url = "https://files.pythonhosted.org/packages/8e/8d/96e8e288b2a41dffafb78e8904ea7367ee4f891dafc2ab8d87e2124cb3d3/websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792", size = 181883 }, + { url = "https://files.pythonhosted.org/packages/93/1f/5d6dbf551766308f6f50f8baf8e9860be6182911e8106da7a7f73785f4c4/websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413", size = 182252 }, + { url = "https://files.pythonhosted.org/packages/d4/78/2d4fed9123e6620cbf1706c0de8a1632e1a28e7774d94346d7de1bba2ca3/websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8", size = 182521 }, + { url = "https://files.pythonhosted.org/packages/e7/3b/66d4c1b444dd1a9823c4a81f50231b921bab54eee2f69e70319b4e21f1ca/websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3", size = 181958 }, + { url = "https://files.pythonhosted.org/packages/08/ff/e9eed2ee5fed6f76fdd6032ca5cd38c57ca9661430bb3d5fb2872dc8703c/websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf", size = 181918 }, + { url = "https://files.pythonhosted.org/packages/d8/75/994634a49b7e12532be6a42103597b71098fd25900f7437d6055ed39930a/websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85", size = 176388 }, + { url = "https://files.pythonhosted.org/packages/98/93/e36c73f78400a65f5e236cd376713c34182e6663f6889cd45a4a04d8f203/websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065", size = 176828 }, + { url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437 }, + { url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096 }, + { url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332 }, + { url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152 }, + { url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096 }, + { url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523 }, + { url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790 }, + { url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165 }, + { url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160 }, + { url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395 }, + { url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841 }, + { url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440 }, + { url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098 }, + { url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329 }, + { url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111 }, + { url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054 }, + { url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496 }, + { url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829 }, + { url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217 }, + { url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195 }, + { url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393 }, + { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837 }, + { url = "https://files.pythonhosted.org/packages/02/9e/d40f779fa16f74d3468357197af8d6ad07e7c5a27ea1ca74ceb38986f77a/websockets-15.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0c9e74d766f2818bb95f84c25be4dea09841ac0f734d1966f415e4edfc4ef1c3", size = 173109 }, + { url = "https://files.pythonhosted.org/packages/bc/cd/5b887b8585a593073fd92f7c23ecd3985cd2c3175025a91b0d69b0551372/websockets-15.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1009ee0c7739c08a0cd59de430d6de452a55e42d6b522de7aa15e6f67db0b8e1", size = 173343 }, + { url = "https://files.pythonhosted.org/packages/fe/ae/d34f7556890341e900a95acf4886833646306269f899d58ad62f588bf410/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76d1f20b1c7a2fa82367e04982e708723ba0e7b8d43aa643d3dcd404d74f1475", size = 174599 }, + { url = "https://files.pythonhosted.org/packages/71/e6/5fd43993a87db364ec60fc1d608273a1a465c0caba69176dd160e197ce42/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f29d80eb9a9263b8d109135351caf568cc3f80b9928bccde535c235de55c22d9", size = 174207 }, + { url = "https://files.pythonhosted.org/packages/2b/fb/c492d6daa5ec067c2988ac80c61359ace5c4c674c532985ac5a123436cec/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04", size = 174155 }, + { url = "https://files.pythonhosted.org/packages/68/a1/dcb68430b1d00b698ae7a7e0194433bce4f07ded185f0ee5fb21e2a2e91e/websockets-15.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122", size = 176884 }, + { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743 }, +] + +[[package]] +name = "wrapt" +version = "1.17.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/8f/aeb76c5b46e273670962298c23e7ddde79916cb74db802131d49a85e4b7d/wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0", size = 55547 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/23/bb82321b86411eb51e5a5db3fb8f8032fd30bd7c2d74bfe936136b2fa1d6/wrapt-1.17.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88bbae4d40d5a46142e70d58bf664a89b6b4befaea7b2ecc14e03cedb8e06c04", size = 53482 }, + { url = "https://files.pythonhosted.org/packages/45/69/f3c47642b79485a30a59c63f6d739ed779fb4cc8323205d047d741d55220/wrapt-1.17.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6b13af258d6a9ad602d57d889f83b9d5543acd471eee12eb51f5b01f8eb1bc2", size = 38676 }, + { url = "https://files.pythonhosted.org/packages/d1/71/e7e7f5670c1eafd9e990438e69d8fb46fa91a50785332e06b560c869454f/wrapt-1.17.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd341868a4b6714a5962c1af0bd44f7c404ef78720c7de4892901e540417111c", size = 38957 }, + { url = "https://files.pythonhosted.org/packages/de/17/9f8f86755c191d6779d7ddead1a53c7a8aa18bccb7cea8e7e72dfa6a8a09/wrapt-1.17.3-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f9b2601381be482f70e5d1051a5965c25fb3625455a2bf520b5a077b22afb775", size = 81975 }, + { url = "https://files.pythonhosted.org/packages/f2/15/dd576273491f9f43dd09fce517f6c2ce6eb4fe21681726068db0d0467096/wrapt-1.17.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:343e44b2a8e60e06a7e0d29c1671a0d9951f59174f3709962b5143f60a2a98bd", size = 83149 }, + { url = "https://files.pythonhosted.org/packages/0c/c4/5eb4ce0d4814521fee7aa806264bf7a114e748ad05110441cd5b8a5c744b/wrapt-1.17.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:33486899acd2d7d3066156b03465b949da3fd41a5da6e394ec49d271baefcf05", size = 82209 }, + { url = "https://files.pythonhosted.org/packages/31/4b/819e9e0eb5c8dc86f60dfc42aa4e2c0d6c3db8732bce93cc752e604bb5f5/wrapt-1.17.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e6f40a8aa5a92f150bdb3e1c44b7e98fb7113955b2e5394122fa5532fec4b418", size = 81551 }, + { url = "https://files.pythonhosted.org/packages/f8/83/ed6baf89ba3a56694700139698cf703aac9f0f9eb03dab92f57551bd5385/wrapt-1.17.3-cp310-cp310-win32.whl", hash = "sha256:a36692b8491d30a8c75f1dfee65bef119d6f39ea84ee04d9f9311f83c5ad9390", size = 36464 }, + { url = "https://files.pythonhosted.org/packages/2f/90/ee61d36862340ad7e9d15a02529df6b948676b9a5829fd5e16640156627d/wrapt-1.17.3-cp310-cp310-win_amd64.whl", hash = "sha256:afd964fd43b10c12213574db492cb8f73b2f0826c8df07a68288f8f19af2ebe6", size = 38748 }, + { url = "https://files.pythonhosted.org/packages/bd/c3/cefe0bd330d389c9983ced15d326f45373f4073c9f4a8c2f99b50bfea329/wrapt-1.17.3-cp310-cp310-win_arm64.whl", hash = "sha256:af338aa93554be859173c39c85243970dc6a289fa907402289eeae7543e1ae18", size = 36810 }, + { url = "https://files.pythonhosted.org/packages/52/db/00e2a219213856074a213503fdac0511203dceefff26e1daa15250cc01a0/wrapt-1.17.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:273a736c4645e63ac582c60a56b0acb529ef07f78e08dc6bfadf6a46b19c0da7", size = 53482 }, + { url = "https://files.pythonhosted.org/packages/5e/30/ca3c4a5eba478408572096fe9ce36e6e915994dd26a4e9e98b4f729c06d9/wrapt-1.17.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5531d911795e3f935a9c23eb1c8c03c211661a5060aab167065896bbf62a5f85", size = 38674 }, + { url = "https://files.pythonhosted.org/packages/31/25/3e8cc2c46b5329c5957cec959cb76a10718e1a513309c31399a4dad07eb3/wrapt-1.17.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0610b46293c59a3adbae3dee552b648b984176f8562ee0dba099a56cfbe4df1f", size = 38959 }, + { url = "https://files.pythonhosted.org/packages/5d/8f/a32a99fc03e4b37e31b57cb9cefc65050ea08147a8ce12f288616b05ef54/wrapt-1.17.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b32888aad8b6e68f83a8fdccbf3165f5469702a7544472bdf41f582970ed3311", size = 82376 }, + { url = "https://files.pythonhosted.org/packages/31/57/4930cb8d9d70d59c27ee1332a318c20291749b4fba31f113c2f8ac49a72e/wrapt-1.17.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cccf4f81371f257440c88faed6b74f1053eef90807b77e31ca057b2db74edb1", size = 83604 }, + { url = "https://files.pythonhosted.org/packages/a8/f3/1afd48de81d63dd66e01b263a6fbb86e1b5053b419b9b33d13e1f6d0f7d0/wrapt-1.17.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8a210b158a34164de8bb68b0e7780041a903d7b00c87e906fb69928bf7890d5", size = 82782 }, + { url = "https://files.pythonhosted.org/packages/1e/d7/4ad5327612173b144998232f98a85bb24b60c352afb73bc48e3e0d2bdc4e/wrapt-1.17.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:79573c24a46ce11aab457b472efd8d125e5a51da2d1d24387666cd85f54c05b2", size = 82076 }, + { url = "https://files.pythonhosted.org/packages/bb/59/e0adfc831674a65694f18ea6dc821f9fcb9ec82c2ce7e3d73a88ba2e8718/wrapt-1.17.3-cp311-cp311-win32.whl", hash = "sha256:c31eebe420a9a5d2887b13000b043ff6ca27c452a9a22fa71f35f118e8d4bf89", size = 36457 }, + { url = "https://files.pythonhosted.org/packages/83/88/16b7231ba49861b6f75fc309b11012ede4d6b0a9c90969d9e0db8d991aeb/wrapt-1.17.3-cp311-cp311-win_amd64.whl", hash = "sha256:0b1831115c97f0663cb77aa27d381237e73ad4f721391a9bfb2fe8bc25fa6e77", size = 38745 }, + { url = "https://files.pythonhosted.org/packages/9a/1e/c4d4f3398ec073012c51d1c8d87f715f56765444e1a4b11e5180577b7e6e/wrapt-1.17.3-cp311-cp311-win_arm64.whl", hash = "sha256:5a7b3c1ee8265eb4c8f1b7d29943f195c00673f5ab60c192eba2d4a7eae5f46a", size = 36806 }, + { url = "https://files.pythonhosted.org/packages/9f/41/cad1aba93e752f1f9268c77270da3c469883d56e2798e7df6240dcb2287b/wrapt-1.17.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ab232e7fdb44cdfbf55fc3afa31bcdb0d8980b9b95c38b6405df2acb672af0e0", size = 53998 }, + { url = "https://files.pythonhosted.org/packages/60/f8/096a7cc13097a1869fe44efe68dace40d2a16ecb853141394047f0780b96/wrapt-1.17.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9baa544e6acc91130e926e8c802a17f3b16fbea0fd441b5a60f5cf2cc5c3deba", size = 39020 }, + { url = "https://files.pythonhosted.org/packages/33/df/bdf864b8997aab4febb96a9ae5c124f700a5abd9b5e13d2a3214ec4be705/wrapt-1.17.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b538e31eca1a7ea4605e44f81a48aa24c4632a277431a6ed3f328835901f4fd", size = 39098 }, + { url = "https://files.pythonhosted.org/packages/9f/81/5d931d78d0eb732b95dc3ddaeeb71c8bb572fb01356e9133916cd729ecdd/wrapt-1.17.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:042ec3bb8f319c147b1301f2393bc19dba6e176b7da446853406d041c36c7828", size = 88036 }, + { url = "https://files.pythonhosted.org/packages/ca/38/2e1785df03b3d72d34fc6252d91d9d12dc27a5c89caef3335a1bbb8908ca/wrapt-1.17.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3af60380ba0b7b5aeb329bc4e402acd25bd877e98b3727b0135cb5c2efdaefe9", size = 88156 }, + { url = "https://files.pythonhosted.org/packages/b3/8b/48cdb60fe0603e34e05cffda0b2a4adab81fd43718e11111a4b0100fd7c1/wrapt-1.17.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b02e424deef65c9f7326d8c19220a2c9040c51dc165cddb732f16198c168396", size = 87102 }, + { url = "https://files.pythonhosted.org/packages/3c/51/d81abca783b58f40a154f1b2c56db1d2d9e0d04fa2d4224e357529f57a57/wrapt-1.17.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:74afa28374a3c3a11b3b5e5fca0ae03bef8450d6aa3ab3a1e2c30e3a75d023dc", size = 87732 }, + { url = "https://files.pythonhosted.org/packages/9e/b1/43b286ca1392a006d5336412d41663eeef1ad57485f3e52c767376ba7e5a/wrapt-1.17.3-cp312-cp312-win32.whl", hash = "sha256:4da9f45279fff3543c371d5ababc57a0384f70be244de7759c85a7f989cb4ebe", size = 36705 }, + { url = "https://files.pythonhosted.org/packages/28/de/49493f962bd3c586ab4b88066e967aa2e0703d6ef2c43aa28cb83bf7b507/wrapt-1.17.3-cp312-cp312-win_amd64.whl", hash = "sha256:e71d5c6ebac14875668a1e90baf2ea0ef5b7ac7918355850c0908ae82bcb297c", size = 38877 }, + { url = "https://files.pythonhosted.org/packages/f1/48/0f7102fe9cb1e8a5a77f80d4f0956d62d97034bbe88d33e94699f99d181d/wrapt-1.17.3-cp312-cp312-win_arm64.whl", hash = "sha256:604d076c55e2fdd4c1c03d06dc1a31b95130010517b5019db15365ec4a405fc6", size = 36885 }, + { url = "https://files.pythonhosted.org/packages/fc/f6/759ece88472157acb55fc195e5b116e06730f1b651b5b314c66291729193/wrapt-1.17.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0", size = 54003 }, + { url = "https://files.pythonhosted.org/packages/4f/a9/49940b9dc6d47027dc850c116d79b4155f15c08547d04db0f07121499347/wrapt-1.17.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77", size = 39025 }, + { url = "https://files.pythonhosted.org/packages/45/35/6a08de0f2c96dcdd7fe464d7420ddb9a7655a6561150e5fc4da9356aeaab/wrapt-1.17.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7", size = 39108 }, + { url = "https://files.pythonhosted.org/packages/0c/37/6faf15cfa41bf1f3dba80cd3f5ccc6622dfccb660ab26ed79f0178c7497f/wrapt-1.17.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6fd1ad24dc235e4ab88cda009e19bf347aabb975e44fd5c2fb22a3f6e4141277", size = 88072 }, + { url = "https://files.pythonhosted.org/packages/78/f2/efe19ada4a38e4e15b6dff39c3e3f3f73f5decf901f66e6f72fe79623a06/wrapt-1.17.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ed61b7c2d49cee3c027372df5809a59d60cf1b6c2f81ee980a091f3afed6a2d", size = 88214 }, + { url = "https://files.pythonhosted.org/packages/40/90/ca86701e9de1622b16e09689fc24b76f69b06bb0150990f6f4e8b0eeb576/wrapt-1.17.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:423ed5420ad5f5529db9ce89eac09c8a2f97da18eb1c870237e84c5a5c2d60aa", size = 87105 }, + { url = "https://files.pythonhosted.org/packages/fd/e0/d10bd257c9a3e15cbf5523025252cc14d77468e8ed644aafb2d6f54cb95d/wrapt-1.17.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e01375f275f010fcbf7f643b4279896d04e571889b8a5b3f848423d91bf07050", size = 87766 }, + { url = "https://files.pythonhosted.org/packages/e8/cf/7d848740203c7b4b27eb55dbfede11aca974a51c3d894f6cc4b865f42f58/wrapt-1.17.3-cp313-cp313-win32.whl", hash = "sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8", size = 36711 }, + { url = "https://files.pythonhosted.org/packages/57/54/35a84d0a4d23ea675994104e667ceff49227ce473ba6a59ba2c84f250b74/wrapt-1.17.3-cp313-cp313-win_amd64.whl", hash = "sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb", size = 38885 }, + { url = "https://files.pythonhosted.org/packages/01/77/66e54407c59d7b02a3c4e0af3783168fff8e5d61def52cda8728439d86bc/wrapt-1.17.3-cp313-cp313-win_arm64.whl", hash = "sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16", size = 36896 }, + { url = "https://files.pythonhosted.org/packages/02/a2/cd864b2a14f20d14f4c496fab97802001560f9f41554eef6df201cd7f76c/wrapt-1.17.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39", size = 54132 }, + { url = "https://files.pythonhosted.org/packages/d5/46/d011725b0c89e853dc44cceb738a307cde5d240d023d6d40a82d1b4e1182/wrapt-1.17.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235", size = 39091 }, + { url = "https://files.pythonhosted.org/packages/2e/9e/3ad852d77c35aae7ddebdbc3b6d35ec8013af7d7dddad0ad911f3d891dae/wrapt-1.17.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c", size = 39172 }, + { url = "https://files.pythonhosted.org/packages/c3/f7/c983d2762bcce2326c317c26a6a1e7016f7eb039c27cdf5c4e30f4160f31/wrapt-1.17.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b", size = 87163 }, + { url = "https://files.pythonhosted.org/packages/e4/0f/f673f75d489c7f22d17fe0193e84b41540d962f75fce579cf6873167c29b/wrapt-1.17.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa", size = 87963 }, + { url = "https://files.pythonhosted.org/packages/df/61/515ad6caca68995da2fac7a6af97faab8f78ebe3bf4f761e1b77efbc47b5/wrapt-1.17.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7", size = 86945 }, + { url = "https://files.pythonhosted.org/packages/d3/bd/4e70162ce398462a467bc09e768bee112f1412e563620adc353de9055d33/wrapt-1.17.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4", size = 86857 }, + { url = "https://files.pythonhosted.org/packages/2b/b8/da8560695e9284810b8d3df8a19396a6e40e7518059584a1a394a2b35e0a/wrapt-1.17.3-cp314-cp314-win32.whl", hash = "sha256:fbd3c8319de8e1dc79d346929cd71d523622da527cca14e0c1d257e31c2b8b10", size = 37178 }, + { url = "https://files.pythonhosted.org/packages/db/c8/b71eeb192c440d67a5a0449aaee2310a1a1e8eca41676046f99ed2487e9f/wrapt-1.17.3-cp314-cp314-win_amd64.whl", hash = "sha256:e1a4120ae5705f673727d3253de3ed0e016f7cd78dc463db1b31e2463e1f3cf6", size = 39310 }, + { url = "https://files.pythonhosted.org/packages/45/20/2cda20fd4865fa40f86f6c46ed37a2a8356a7a2fde0773269311f2af56c7/wrapt-1.17.3-cp314-cp314-win_arm64.whl", hash = "sha256:507553480670cab08a800b9463bdb881b2edeed77dc677b0a5915e6106e91a58", size = 37266 }, + { url = "https://files.pythonhosted.org/packages/77/ed/dd5cf21aec36c80443c6f900449260b80e2a65cf963668eaef3b9accce36/wrapt-1.17.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a", size = 56544 }, + { url = "https://files.pythonhosted.org/packages/8d/96/450c651cc753877ad100c7949ab4d2e2ecc4d97157e00fa8f45df682456a/wrapt-1.17.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067", size = 40283 }, + { url = "https://files.pythonhosted.org/packages/d1/86/2fcad95994d9b572db57632acb6f900695a648c3e063f2cd344b3f5c5a37/wrapt-1.17.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454", size = 40366 }, + { url = "https://files.pythonhosted.org/packages/64/0e/f4472f2fdde2d4617975144311f8800ef73677a159be7fe61fa50997d6c0/wrapt-1.17.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e", size = 108571 }, + { url = "https://files.pythonhosted.org/packages/cc/01/9b85a99996b0a97c8a17484684f206cbb6ba73c1ce6890ac668bcf3838fb/wrapt-1.17.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f", size = 113094 }, + { url = "https://files.pythonhosted.org/packages/25/02/78926c1efddcc7b3aa0bc3d6b33a822f7d898059f7cd9ace8c8318e559ef/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056", size = 110659 }, + { url = "https://files.pythonhosted.org/packages/dc/ee/c414501ad518ac3e6fe184753632fe5e5ecacdcf0effc23f31c1e4f7bfcf/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804", size = 106946 }, + { url = "https://files.pythonhosted.org/packages/be/44/a1bd64b723d13bb151d6cc91b986146a1952385e0392a78567e12149c7b4/wrapt-1.17.3-cp314-cp314t-win32.whl", hash = "sha256:41b1d2bc74c2cac6f9074df52b2efbef2b30bdfe5f40cb78f8ca22963bc62977", size = 38717 }, + { url = "https://files.pythonhosted.org/packages/79/d9/7cfd5a312760ac4dd8bf0184a6ee9e43c33e47f3dadc303032ce012b8fa3/wrapt-1.17.3-cp314-cp314t-win_amd64.whl", hash = "sha256:73d496de46cd2cdbdbcce4ae4bcdb4afb6a11234a1df9c085249d55166b95116", size = 41334 }, + { url = "https://files.pythonhosted.org/packages/46/78/10ad9781128ed2f99dbc474f43283b13fea8ba58723e98844367531c18e9/wrapt-1.17.3-cp314-cp314t-win_arm64.whl", hash = "sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6", size = 38471 }, + { url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591 }, +] + +[[package]] +name = "zipp" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276 }, +] diff --git a/demos/use_cases/model_alias_routing/arch_config_with_aliases.yaml b/demos/use_cases/model_alias_routing/arch_config_with_aliases.yaml index b9dcab81..00b72a6d 100644 --- a/demos/use_cases/model_alias_routing/arch_config_with_aliases.yaml +++ b/demos/use_cases/model_alias_routing/arch_config_with_aliases.yaml @@ -47,6 +47,9 @@ llm_providers: - model: ollama/llama3.1 base_url: http://host.docker.internal:11434 + # Grok (xAI) Models + - model: xai/grok-4-0709 + access_key: $GROK_API_KEY # Model aliases - friendly names that map to actual provider names model_aliases: @@ -83,5 +86,6 @@ model_aliases: coding-model: target: us.amazon.nova-premier-v1:0 -tracing: - random_sampling: 100 + # Alias for grok testing + arch.grok.v1: + target: grok-4-0709 diff --git a/demos/use_cases/model_choice_with_test_harness/poetry.lock b/demos/use_cases/model_choice_with_test_harness/poetry.lock index 8f7a71b7..779c5b11 100644 --- a/demos/use_cases/model_choice_with_test_harness/poetry.lock +++ b/demos/use_cases/model_choice_with_test_harness/poetry.lock @@ -79,7 +79,6 @@ files = [ ] [package.dependencies] -archgw_modelserver = ">=0.3.12,<0.4.0" click = ">=8.1.7,<9.0.0" jinja2 = ">=3.1.4,<4.0.0" jsonschema = ">=4.23.0,<5.0.0" @@ -93,11 +92,6 @@ description = "A model server for serving models" optional = false python-versions = "<4.0,>=3.10" groups = ["main"] -files = [ - {file = "archgw_modelserver-0.3.12-py3-none-any.whl", hash = "sha256:ddc4536108e74bee637b41e35f80ad5cdf1708a32f670f33b2eef3526eb6d4a1"}, - {file = "archgw_modelserver-0.3.12.tar.gz", hash = "sha256:b749edd0b3d4e6f01e94bcf6b363bf52b8ca515e8191a0951b99152f45745212"}, -] - [package.dependencies] accelerate = ">=1.0.0,<2.0.0" dateparser = "*" diff --git a/demos/use_cases/model_choice_with_test_harness/pyproject.toml b/demos/use_cases/model_choice_with_test_harness/pyproject.toml index d2a97be1..4147942e 100644 --- a/demos/use_cases/model_choice_with_test_harness/pyproject.toml +++ b/demos/use_cases/model_choice_with_test_harness/pyproject.toml @@ -12,7 +12,7 @@ python = ">=3.10,<3.13.3" pydantic = "^2.0" openai = "^1.0" pyyaml = "^6.0" -archgw ="^0.3.18" +archgw ="^0.3.22" [tool.poetry.group.dev.dependencies] pytest = "^8.3" diff --git a/demos/use_cases/orchestrating_agents/Dockerfile b/demos/use_cases/orchestrating_agents/Dockerfile deleted file mode 100644 index b53cb719..00000000 --- a/demos/use_cases/orchestrating_agents/Dockerfile +++ /dev/null @@ -1,41 +0,0 @@ -# took inspiration from https://medium.com/@albertazzir/blazing-fast-python-docker-builds-with-poetry-a78a66f5aed0 - -# The builder image, used to build the virtual environment -FROM python:3.10 as builder - -RUN pip install poetry==1.8.3 - -ENV POETRY_NO_INTERACTION=1 \ - POETRY_VIRTUALENVS_IN_PROJECT=1 \ - POETRY_VIRTUALENVS_CREATE=1 \ - POETRY_CACHE_DIR=/tmp/poetry_cache - -WORKDIR /code - -COPY pyproject.toml poetry.lock ./ -RUN touch README.md - -RUN poetry install --no-root && rm -rf $POETRY_CACHE_DIR - -# The runtime image, used to just run the code provided its virtual environment -FROM python:3.10-slim as runtime - -RUN apt-get update && apt-get install -y curl - -WORKDIR /code - -ENV VIRTUAL_ENV=/code/.venv \ - PATH="/code/.venv/bin:$PATH" - -COPY --from=builder ${VIRTUAL_ENV} ${VIRTUAL_ENV} - -COPY main.py ./ - -HEALTHCHECK \ - --interval=5s \ - --timeout=1s \ - --start-period=1s \ - --retries=3 \ - CMD curl http://localhost:80/healthz - -CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "80", "--log-level", "debug"] diff --git a/demos/use_cases/orchestrating_agents/arch_config.yaml b/demos/use_cases/orchestrating_agents/arch_config.yaml deleted file mode 100644 index b52ceb22..00000000 --- a/demos/use_cases/orchestrating_agents/arch_config.yaml +++ /dev/null @@ -1,44 +0,0 @@ -version: v0.1.0 - -listeners: - ingress_traffic: - address: 0.0.0.0 - port: 10000 - message_format: openai - timeout: 30s - - egress_traffic: - address: 0.0.0.0 - port: 12000 - message_format: openai - timeout: 30s - -overrides: - use_agent_orchestrator: true - -endpoints: - agent_gateway: - endpoint: host.docker.internal:18083 - connect_timeout: 0.005s - -llm_providers: - - access_key: $OPENAI_API_KEY - model: openai/gpt-4o-mini - default: true - -system_prompt: | - You are a helpful assistant. - -prompt_targets: - - name: sales_agent - description: handles queries related to sales and purchases - - - name: issues_and_repairs - description: handles issues, repairs, or refunds - - - name: escalate_to_human - description: escalates to human agent - -tracing: - random_sampling: 100 - trace_arch_internal: true diff --git a/demos/use_cases/orchestrating_agents/docker-compose.yaml b/demos/use_cases/orchestrating_agents/docker-compose.yaml deleted file mode 100644 index 288ecf30..00000000 --- a/demos/use_cases/orchestrating_agents/docker-compose.yaml +++ /dev/null @@ -1,29 +0,0 @@ -services: - triage_service: - build: - context: ./ - environment: - - OLTP_HOST=http://jaeger:4317 - extra_hosts: - - "host.docker.internal:host-gateway" - ports: - - "18083:80" - - chatbot_ui: - build: - context: ../../shared/chatbot_ui - ports: - - "18080:8080" - environment: - # this is only because we are running the sample app in the same docker container environemtn as archgw - - CHAT_COMPLETION_ENDPOINT=http://host.docker.internal:10000/v1 - extra_hosts: - - "host.docker.internal:host-gateway" - - jaeger: - build: - context: ../../shared/jaeger - ports: - - "16686:16686" - - "4317:4317" - - "4318:4318" diff --git a/demos/use_cases/orchestrating_agents/hurl_tests/simple_issues_repairs.hurl b/demos/use_cases/orchestrating_agents/hurl_tests/simple_issues_repairs.hurl deleted file mode 100644 index 3820de57..00000000 --- a/demos/use_cases/orchestrating_agents/hurl_tests/simple_issues_repairs.hurl +++ /dev/null @@ -1,19 +0,0 @@ -POST http://localhost:10000/v1/chat/completions -Content-Type: application/json - -{ - "messages": [ - { - "role": "user", - "content": "I bought a package recently and it not working properly" - } - ] -} -HTTP 200 -[Asserts] -header "content-type" == "application/json" -jsonpath "$.model" matches /^gpt-4o-2/ -jsonpath "$.metadata.x-arch-state" != null -jsonpath "$.usage" != null -jsonpath "$.choices[0].message.content" != null -jsonpath "$.choices[0].message.role" == "assistant" diff --git a/demos/use_cases/orchestrating_agents/hurl_tests/simple_sale_agent.hurl b/demos/use_cases/orchestrating_agents/hurl_tests/simple_sale_agent.hurl deleted file mode 100644 index 4db2c67c..00000000 --- a/demos/use_cases/orchestrating_agents/hurl_tests/simple_sale_agent.hurl +++ /dev/null @@ -1,19 +0,0 @@ -POST http://localhost:10000/v1/chat/completions -Content-Type: application/json - -{ - "messages": [ - { - "role": "user", - "content": "I want to sell red shoes" - } - ] -} -HTTP 200 -[Asserts] -header "content-type" == "application/json" -jsonpath "$.model" matches /^gpt-4o-mini/ -jsonpath "$.metadata.x-arch-state" != null -jsonpath "$.usage" != null -jsonpath "$.choices[0].message.content" != null -jsonpath "$.choices[0].message.role" == "assistant" diff --git a/demos/use_cases/orchestrating_agents/hurl_tests/simple_stream.hurl b/demos/use_cases/orchestrating_agents/hurl_tests/simple_stream.hurl deleted file mode 100644 index f060fed0..00000000 --- a/demos/use_cases/orchestrating_agents/hurl_tests/simple_stream.hurl +++ /dev/null @@ -1,16 +0,0 @@ -POST http://localhost:10000/v1/chat/completions -Content-Type: application/json - -{ - "messages": [ - { - "role": "user", - "content": "I want to sell red shoes" - } - ], - "stream": true -} -HTTP 200 -[Asserts] -header "content-type" matches /text\/event-stream/ -body matches /^data: .*?sales_agent.*?\n/ diff --git a/demos/use_cases/orchestrating_agents/main.py b/demos/use_cases/orchestrating_agents/main.py deleted file mode 100644 index 4f288e50..00000000 --- a/demos/use_cases/orchestrating_agents/main.py +++ /dev/null @@ -1,115 +0,0 @@ -import logging -import json -from typing import List, Dict, Any -from fastapi import FastAPI, Request -from fastapi.responses import StreamingResponse -from pydantic import BaseModel -import openai - -# Setup logging -logging.basicConfig(level=logging.INFO) -logger = logging.getLogger("uvicorn.error") - -app = FastAPI() - - -class Message(BaseModel): - role: str - content: str - - -class ChatCompletionsRequest(BaseModel): - messages: List[Message] - model: str - metadata: Dict[str, Any] = {} - stream: bool = False - - -openai_client = openai.OpenAI( - api_key="None", # archgw picks the API key from the config file - base_url="http://host.docker.internal:12000/v1", -) - - -def call_openai(messages: List[Dict[str, str]], stream: bool, model: str): - logger.info(f"llm agent model: {model}") - completion = openai_client.chat.completions.create( - model=model, - messages=messages, - stream=stream, - ) - - if stream: - - def stream(): - for line in completion: - if line.choices and len(line.choices) > 0 and line.choices[0].delta: - chunk_response_str = json.dumps(line.model_dump()) - yield "data: " + chunk_response_str + "\n\n" - yield "data: [DONE]" + "\n\n" - - return StreamingResponse(stream(), media_type="text/event-stream") - else: - return completion - - -class Agent: - def __init__(self, role: str, instructions: str, model: str = ""): - self.model = model - self.system_prompt = f"You are a {role}.\n{instructions}" - - def handle(self, req: ChatCompletionsRequest): - messages = [{"role": "system", "content": self.get_system_prompt()}] + [ - message.model_dump() for message in req.messages - ] - - model = req.model - if self.model: - model = self.model - return call_openai(messages, req.stream, model) - - def get_system_prompt(self) -> str: - return self.system_prompt - - -# Define your agents -AGENTS = { - "sales_agent": Agent( - role="sales agent", - instructions=( - "Always answer in a sentence or less.\n" - "Follow the following routine with the user:\n" - "1. Engage\n" - "2. Quote ridiculous price\n" - "3. Reveal caveat if user agrees." - ), - model="gpt-4o-mini", - ), - "issues_and_repairs": Agent( - role="issues and repairs agent", - instructions="Propose a solution, offer refund if necessary.", - model="gpt-4o", - ), - "escalate_to_human": Agent( - role="human escalation agent", - instructions="Escalate issues to a human.", - # skipping model name here as arch gateway will pick the default model from the config file - ), - "unknown_agent": Agent( - role="general assistant", instructions="Assist the user in general queries." - ), -} - - -@app.post("/v1/chat/completions") -def completion_api(req: ChatCompletionsRequest, request: Request): - agent_name = req.metadata.get("agent-name", "unknown_agent") - agent = AGENTS.get(agent_name) - logger.info(f"Routing to agent: {agent_name}") - - return agent.handle(req) - - -@app.get("/healthz") -async def healthz(): - return {"status": "ok"} diff --git a/demos/use_cases/orchestrating_agents/poetry.lock b/demos/use_cases/orchestrating_agents/poetry.lock deleted file mode 100644 index 929a5c86..00000000 --- a/demos/use_cases/orchestrating_agents/poetry.lock +++ /dev/null @@ -1,573 +0,0 @@ -# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. - -[[package]] -name = "annotated-types" -version = "0.7.0" -description = "Reusable constraint types to use with typing.Annotated" -optional = false -python-versions = ">=3.8" -files = [ - {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, - {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, -] - -[[package]] -name = "anyio" -version = "4.9.0" -description = "High level compatibility layer for multiple asynchronous event loop implementations" -optional = false -python-versions = ">=3.9" -files = [ - {file = "anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c"}, - {file = "anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028"}, -] - -[package.dependencies] -exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} -idna = ">=2.8" -sniffio = ">=1.1" -typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} - -[package.extras] -doc = ["Sphinx (>=8.2,<9.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx_rtd_theme"] -test = ["anyio[trio]", "blockbuster (>=1.5.23)", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21)"] -trio = ["trio (>=0.26.1)"] - -[[package]] -name = "certifi" -version = "2025.1.31" -description = "Python package for providing Mozilla's CA Bundle." -optional = false -python-versions = ">=3.6" -files = [ - {file = "certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe"}, - {file = "certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651"}, -] - -[[package]] -name = "click" -version = "8.1.8" -description = "Composable command line interface toolkit" -optional = false -python-versions = ">=3.7" -files = [ - {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, - {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "platform_system == \"Windows\""} - -[[package]] -name = "colorama" -version = "0.4.6" -description = "Cross-platform colored terminal text." -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -files = [ - {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, - {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, -] - -[[package]] -name = "distro" -version = "1.9.0" -description = "Distro - an OS platform information API" -optional = false -python-versions = ">=3.6" -files = [ - {file = "distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2"}, - {file = "distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed"}, -] - -[[package]] -name = "exceptiongroup" -version = "1.2.2" -description = "Backport of PEP 654 (exception groups)" -optional = false -python-versions = ">=3.7" -files = [ - {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, - {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, -] - -[package.extras] -test = ["pytest (>=6)"] - -[[package]] -name = "fastapi" -version = "0.115.11" -description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" -optional = false -python-versions = ">=3.8" -files = [ - {file = "fastapi-0.115.11-py3-none-any.whl", hash = "sha256:32e1541b7b74602e4ef4a0260ecaf3aadf9d4f19590bba3e1bf2ac4666aa2c64"}, - {file = "fastapi-0.115.11.tar.gz", hash = "sha256:cc81f03f688678b92600a65a5e618b93592c65005db37157147204d8924bf94f"}, -] - -[package.dependencies] -pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0" -starlette = ">=0.40.0,<0.47.0" -typing-extensions = ">=4.8.0" - -[package.extras] -all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=3.1.5)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] -standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "jinja2 (>=3.1.5)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"] - -[[package]] -name = "h11" -version = "0.14.0" -description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" -optional = false -python-versions = ">=3.7" -files = [ - {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, - {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, -] - -[[package]] -name = "httpcore" -version = "1.0.7" -description = "A minimal low-level HTTP client." -optional = false -python-versions = ">=3.8" -files = [ - {file = "httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd"}, - {file = "httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c"}, -] - -[package.dependencies] -certifi = "*" -h11 = ">=0.13,<0.15" - -[package.extras] -asyncio = ["anyio (>=4.0,<5.0)"] -http2 = ["h2 (>=3,<5)"] -socks = ["socksio (==1.*)"] -trio = ["trio (>=0.22.0,<1.0)"] - -[[package]] -name = "httpx" -version = "0.28.1" -description = "The next generation HTTP client." -optional = false -python-versions = ">=3.8" -files = [ - {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}, - {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"}, -] - -[package.dependencies] -anyio = "*" -certifi = "*" -httpcore = "==1.*" -idna = "*" - -[package.extras] -brotli = ["brotli", "brotlicffi"] -cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] -http2 = ["h2 (>=3,<5)"] -socks = ["socksio (==1.*)"] -zstd = ["zstandard (>=0.18.0)"] - -[[package]] -name = "idna" -version = "3.10" -description = "Internationalized Domain Names in Applications (IDNA)" -optional = false -python-versions = ">=3.6" -files = [ - {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, - {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, -] - -[package.extras] -all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] - -[[package]] -name = "jiter" -version = "0.9.0" -description = "Fast iterable JSON parser." -optional = false -python-versions = ">=3.8" -files = [ - {file = "jiter-0.9.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:816ec9b60fdfd1fec87da1d7ed46c66c44ffec37ab2ef7de5b147b2fce3fd5ad"}, - {file = "jiter-0.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9b1d3086f8a3ee0194ecf2008cf81286a5c3e540d977fa038ff23576c023c0ea"}, - {file = "jiter-0.9.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1339f839b91ae30b37c409bf16ccd3dc453e8b8c3ed4bd1d6a567193651a4a51"}, - {file = "jiter-0.9.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ffba79584b3b670fefae66ceb3a28822365d25b7bf811e030609a3d5b876f538"}, - {file = "jiter-0.9.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cfc7d0a8e899089d11f065e289cb5b2daf3d82fbe028f49b20d7b809193958d"}, - {file = "jiter-0.9.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e00a1a2bbfaaf237e13c3d1592356eab3e9015d7efd59359ac8b51eb56390a12"}, - {file = "jiter-0.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1d9870561eb26b11448854dce0ff27a9a27cb616b632468cafc938de25e9e51"}, - {file = "jiter-0.9.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9872aeff3f21e437651df378cb75aeb7043e5297261222b6441a620218b58708"}, - {file = "jiter-0.9.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:1fd19112d1049bdd47f17bfbb44a2c0001061312dcf0e72765bfa8abd4aa30e5"}, - {file = "jiter-0.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6ef5da104664e526836070e4a23b5f68dec1cc673b60bf1edb1bfbe8a55d0678"}, - {file = "jiter-0.9.0-cp310-cp310-win32.whl", hash = "sha256:cb12e6d65ebbefe5518de819f3eda53b73187b7089040b2d17f5b39001ff31c4"}, - {file = "jiter-0.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:c43ca669493626d8672be3b645dbb406ef25af3f4b6384cfd306da7eb2e70322"}, - {file = "jiter-0.9.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6c4d99c71508912a7e556d631768dcdef43648a93660670986916b297f1c54af"}, - {file = "jiter-0.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8f60fb8ce7df529812bf6c625635a19d27f30806885139e367af93f6e734ef58"}, - {file = "jiter-0.9.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:51c4e1a4f8ea84d98b7b98912aa4290ac3d1eabfde8e3c34541fae30e9d1f08b"}, - {file = "jiter-0.9.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f4c677c424dc76684fea3e7285a7a2a7493424bea89ac441045e6a1fb1d7b3b"}, - {file = "jiter-0.9.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2221176dfec87f3470b21e6abca056e6b04ce9bff72315cb0b243ca9e835a4b5"}, - {file = "jiter-0.9.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3c7adb66f899ffa25e3c92bfcb593391ee1947dbdd6a9a970e0d7e713237d572"}, - {file = "jiter-0.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c98d27330fdfb77913c1097a7aab07f38ff2259048949f499c9901700789ac15"}, - {file = "jiter-0.9.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:eda3f8cc74df66892b1d06b5d41a71670c22d95a1ca2cbab73654745ce9d0419"}, - {file = "jiter-0.9.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:dd5ab5ddc11418dce28343123644a100f487eaccf1de27a459ab36d6cca31043"}, - {file = "jiter-0.9.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:42f8a68a69f047b310319ef8e2f52fdb2e7976fb3313ef27df495cf77bcad965"}, - {file = "jiter-0.9.0-cp311-cp311-win32.whl", hash = "sha256:a25519efb78a42254d59326ee417d6f5161b06f5da827d94cf521fed961b1ff2"}, - {file = "jiter-0.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:923b54afdd697dfd00d368b7ccad008cccfeb1efb4e621f32860c75e9f25edbd"}, - {file = "jiter-0.9.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:7b46249cfd6c48da28f89eb0be3f52d6fdb40ab88e2c66804f546674e539ec11"}, - {file = "jiter-0.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:609cf3c78852f1189894383cf0b0b977665f54cb38788e3e6b941fa6d982c00e"}, - {file = "jiter-0.9.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d726a3890a54561e55a9c5faea1f7655eda7f105bd165067575ace6e65f80bb2"}, - {file = "jiter-0.9.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2e89dc075c1fef8fa9be219e249f14040270dbc507df4215c324a1839522ea75"}, - {file = "jiter-0.9.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:04e8ffa3c353b1bc4134f96f167a2082494351e42888dfcf06e944f2729cbe1d"}, - {file = "jiter-0.9.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:203f28a72a05ae0e129b3ed1f75f56bc419d5f91dfacd057519a8bd137b00c42"}, - {file = "jiter-0.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fca1a02ad60ec30bb230f65bc01f611c8608b02d269f998bc29cca8619a919dc"}, - {file = "jiter-0.9.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:237e5cee4d5d2659aaf91bbf8ec45052cc217d9446070699441a91b386ae27dc"}, - {file = "jiter-0.9.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:528b6b71745e7326eed73c53d4aa57e2a522242320b6f7d65b9c5af83cf49b6e"}, - {file = "jiter-0.9.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9f48e86b57bc711eb5acdfd12b6cb580a59cc9a993f6e7dcb6d8b50522dcd50d"}, - {file = "jiter-0.9.0-cp312-cp312-win32.whl", hash = "sha256:699edfde481e191d81f9cf6d2211debbfe4bd92f06410e7637dffb8dd5dfde06"}, - {file = "jiter-0.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:099500d07b43f61d8bd780466d429c45a7b25411b334c60ca875fa775f68ccb0"}, - {file = "jiter-0.9.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:2764891d3f3e8b18dce2cff24949153ee30c9239da7c00f032511091ba688ff7"}, - {file = "jiter-0.9.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:387b22fbfd7a62418d5212b4638026d01723761c75c1c8232a8b8c37c2f1003b"}, - {file = "jiter-0.9.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d8da8629ccae3606c61d9184970423655fb4e33d03330bcdfe52d234d32f69"}, - {file = "jiter-0.9.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1be73d8982bdc278b7b9377426a4b44ceb5c7952073dd7488e4ae96b88e1103"}, - {file = "jiter-0.9.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2228eaaaa111ec54b9e89f7481bffb3972e9059301a878d085b2b449fbbde635"}, - {file = "jiter-0.9.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:11509bfecbc319459647d4ac3fd391d26fdf530dad00c13c4dadabf5b81f01a4"}, - {file = "jiter-0.9.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f22238da568be8bbd8e0650e12feeb2cfea15eda4f9fc271d3b362a4fa0604d"}, - {file = "jiter-0.9.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:17f5d55eb856597607562257c8e36c42bc87f16bef52ef7129b7da11afc779f3"}, - {file = "jiter-0.9.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:6a99bed9fbb02f5bed416d137944419a69aa4c423e44189bc49718859ea83bc5"}, - {file = "jiter-0.9.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:e057adb0cd1bd39606100be0eafe742de2de88c79df632955b9ab53a086b3c8d"}, - {file = "jiter-0.9.0-cp313-cp313-win32.whl", hash = "sha256:f7e6850991f3940f62d387ccfa54d1a92bd4bb9f89690b53aea36b4364bcab53"}, - {file = "jiter-0.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:c8ae3bf27cd1ac5e6e8b7a27487bf3ab5f82318211ec2e1346a5b058756361f7"}, - {file = "jiter-0.9.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f0b2827fb88dda2cbecbbc3e596ef08d69bda06c6f57930aec8e79505dc17001"}, - {file = "jiter-0.9.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:062b756ceb1d40b0b28f326cba26cfd575a4918415b036464a52f08632731e5a"}, - {file = "jiter-0.9.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6f7838bc467ab7e8ef9f387bd6de195c43bad82a569c1699cb822f6609dd4cdf"}, - {file = "jiter-0.9.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:4a2d16360d0642cd68236f931b85fe50288834c383492e4279d9f1792e309571"}, - {file = "jiter-0.9.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e84ed1c9c9ec10bbb8c37f450077cbe3c0d4e8c2b19f0a49a60ac7ace73c7452"}, - {file = "jiter-0.9.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9f3c848209ccd1bfa344a1240763975ca917de753c7875c77ec3034f4151d06c"}, - {file = "jiter-0.9.0-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7825f46e50646bee937e0f849d14ef3a417910966136f59cd1eb848b8b5bb3e4"}, - {file = "jiter-0.9.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d82a811928b26d1a6311a886b2566f68ccf2b23cf3bfed042e18686f1f22c2d7"}, - {file = "jiter-0.9.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0c058ecb51763a67f019ae423b1cbe3fa90f7ee6280c31a1baa6ccc0c0e2d06e"}, - {file = "jiter-0.9.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9897115ad716c48f0120c1f0c4efae348ec47037319a6c63b2d7838bb53aaef4"}, - {file = "jiter-0.9.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:351f4c90a24c4fb8c87c6a73af2944c440494ed2bea2094feecacb75c50398ae"}, - {file = "jiter-0.9.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:d45807b0f236c485e1e525e2ce3a854807dfe28ccf0d013dd4a563395e28008a"}, - {file = "jiter-0.9.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:1537a890724ba00fdba21787010ac6f24dad47f763410e9e1093277913592784"}, - {file = "jiter-0.9.0-cp38-cp38-win32.whl", hash = "sha256:e3630ec20cbeaddd4b65513fa3857e1b7c4190d4481ef07fb63d0fad59033321"}, - {file = "jiter-0.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:2685f44bf80e95f8910553bf2d33b9c87bf25fceae6e9f0c1355f75d2922b0ee"}, - {file = "jiter-0.9.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:9ef340fae98065071ccd5805fe81c99c8f80484e820e40043689cf97fb66b3e2"}, - {file = "jiter-0.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:efb767d92c63b2cd9ec9f24feeb48f49574a713870ec87e9ba0c2c6e9329c3e2"}, - {file = "jiter-0.9.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:113f30f87fb1f412510c6d7ed13e91422cfd329436364a690c34c8b8bd880c42"}, - {file = "jiter-0.9.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8793b6df019b988526f5a633fdc7456ea75e4a79bd8396a3373c371fc59f5c9b"}, - {file = "jiter-0.9.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7a9aaa5102dba4e079bb728076fadd5a2dca94c05c04ce68004cfd96f128ea34"}, - {file = "jiter-0.9.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d838650f6ebaf4ccadfb04522463e74a4c378d7e667e0eb1865cfe3990bfac49"}, - {file = "jiter-0.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0194f813efdf4b8865ad5f5c5f50f8566df7d770a82c51ef593d09e0b347020"}, - {file = "jiter-0.9.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a7954a401d0a8a0b8bc669199db78af435aae1e3569187c2939c477c53cb6a0a"}, - {file = "jiter-0.9.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4feafe787eb8a8d98168ab15637ca2577f6ddf77ac6c8c66242c2d028aa5420e"}, - {file = "jiter-0.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:27cd1f2e8bb377f31d3190b34e4328d280325ad7ef55c6ac9abde72f79e84d2e"}, - {file = "jiter-0.9.0-cp39-cp39-win32.whl", hash = "sha256:161d461dcbe658cf0bd0aa375b30a968b087cdddc624fc585f3867c63c6eca95"}, - {file = "jiter-0.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:e8b36d8a16a61993be33e75126ad3d8aa29cf450b09576f3c427d27647fcb4aa"}, - {file = "jiter-0.9.0.tar.gz", hash = "sha256:aadba0964deb424daa24492abc3d229c60c4a31bfee205aedbf1acc7639d7893"}, -] - -[[package]] -name = "openai" -version = "1.66.5" -description = "The official Python library for the openai API" -optional = false -python-versions = ">=3.8" -files = [ - {file = "openai-1.66.5-py3-none-any.whl", hash = "sha256:74be528175f8389f67675830c51a15bd51e874425c86d3de6153bf70ed6c2884"}, - {file = "openai-1.66.5.tar.gz", hash = "sha256:f61b8fac29490ca8fdc6d996aa6926c18dbe5639536f8c40219c40db05511b11"}, -] - -[package.dependencies] -anyio = ">=3.5.0,<5" -distro = ">=1.7.0,<2" -httpx = ">=0.23.0,<1" -jiter = ">=0.4.0,<1" -pydantic = ">=1.9.0,<3" -sniffio = "*" -tqdm = ">4" -typing-extensions = ">=4.11,<5" - -[package.extras] -datalib = ["numpy (>=1)", "pandas (>=1.2.3)", "pandas-stubs (>=1.1.0.11)"] -realtime = ["websockets (>=13,<15)"] - -[[package]] -name = "pydantic" -version = "2.10.6" -description = "Data validation using Python type hints" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584"}, - {file = "pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236"}, -] - -[package.dependencies] -annotated-types = ">=0.6.0" -pydantic-core = "2.27.2" -typing-extensions = ">=4.12.2" - -[package.extras] -email = ["email-validator (>=2.0.0)"] -timezone = ["tzdata"] - -[[package]] -name = "pydantic-core" -version = "2.27.2" -description = "Core functionality for Pydantic validation and serialization" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pydantic_core-2.27.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa"}, - {file = "pydantic_core-2.27.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c"}, - {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7969e133a6f183be60e9f6f56bfae753585680f3b7307a8e555a948d443cc05a"}, - {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3de9961f2a346257caf0aa508a4da705467f53778e9ef6fe744c038119737ef5"}, - {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2bb4d3e5873c37bb3dd58714d4cd0b0e6238cebc4177ac8fe878f8b3aa8e74c"}, - {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:280d219beebb0752699480fe8f1dc61ab6615c2046d76b7ab7ee38858de0a4e7"}, - {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47956ae78b6422cbd46f772f1746799cbb862de838fd8d1fbd34a82e05b0983a"}, - {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:14d4a5c49d2f009d62a2a7140d3064f686d17a5d1a268bc641954ba181880236"}, - {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:337b443af21d488716f8d0b6164de833e788aa6bd7e3a39c005febc1284f4962"}, - {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:03d0f86ea3184a12f41a2d23f7ccb79cdb5a18e06993f8a45baa8dfec746f0e9"}, - {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7041c36f5680c6e0f08d922aed302e98b3745d97fe1589db0a3eebf6624523af"}, - {file = "pydantic_core-2.27.2-cp310-cp310-win32.whl", hash = "sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4"}, - {file = "pydantic_core-2.27.2-cp310-cp310-win_amd64.whl", hash = "sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31"}, - {file = "pydantic_core-2.27.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc"}, - {file = "pydantic_core-2.27.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7"}, - {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15"}, - {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306"}, - {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99"}, - {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459"}, - {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048"}, - {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d"}, - {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b"}, - {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474"}, - {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6"}, - {file = "pydantic_core-2.27.2-cp311-cp311-win32.whl", hash = "sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c"}, - {file = "pydantic_core-2.27.2-cp311-cp311-win_amd64.whl", hash = "sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc"}, - {file = "pydantic_core-2.27.2-cp311-cp311-win_arm64.whl", hash = "sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4"}, - {file = "pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0"}, - {file = "pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef"}, - {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7"}, - {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934"}, - {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6"}, - {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c"}, - {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2"}, - {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4"}, - {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3"}, - {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4"}, - {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57"}, - {file = "pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc"}, - {file = "pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9"}, - {file = "pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b"}, - {file = "pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b"}, - {file = "pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154"}, - {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9"}, - {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9"}, - {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1"}, - {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a"}, - {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e"}, - {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4"}, - {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27"}, - {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee"}, - {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1"}, - {file = "pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130"}, - {file = "pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee"}, - {file = "pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b"}, - {file = "pydantic_core-2.27.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d3e8d504bdd3f10835468f29008d72fc8359d95c9c415ce6e767203db6127506"}, - {file = "pydantic_core-2.27.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:521eb9b7f036c9b6187f0b47318ab0d7ca14bd87f776240b90b21c1f4f149320"}, - {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85210c4d99a0114f5a9481b44560d7d1e35e32cc5634c656bc48e590b669b145"}, - {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d716e2e30c6f140d7560ef1538953a5cd1a87264c737643d481f2779fc247fe1"}, - {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f66d89ba397d92f840f8654756196d93804278457b5fbede59598a1f9f90b228"}, - {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:669e193c1c576a58f132e3158f9dfa9662969edb1a250c54d8fa52590045f046"}, - {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdbe7629b996647b99c01b37f11170a57ae675375b14b8c13b8518b8320ced5"}, - {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d262606bf386a5ba0b0af3b97f37c83d7011439e3dc1a9298f21efb292e42f1a"}, - {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:cabb9bcb7e0d97f74df8646f34fc76fbf793b7f6dc2438517d7a9e50eee4f14d"}, - {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_armv7l.whl", hash = "sha256:d2d63f1215638d28221f664596b1ccb3944f6e25dd18cd3b86b0a4c408d5ebb9"}, - {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bca101c00bff0adb45a833f8451b9105d9df18accb8743b08107d7ada14bd7da"}, - {file = "pydantic_core-2.27.2-cp38-cp38-win32.whl", hash = "sha256:f6f8e111843bbb0dee4cb6594cdc73e79b3329b526037ec242a3e49012495b3b"}, - {file = "pydantic_core-2.27.2-cp38-cp38-win_amd64.whl", hash = "sha256:fd1aea04935a508f62e0d0ef1f5ae968774a32afc306fb8545e06f5ff5cdf3ad"}, - {file = "pydantic_core-2.27.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c10eb4f1659290b523af58fa7cffb452a61ad6ae5613404519aee4bfbf1df993"}, - {file = "pydantic_core-2.27.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ef592d4bad47296fb11f96cd7dc898b92e795032b4894dfb4076cfccd43a9308"}, - {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c61709a844acc6bf0b7dce7daae75195a10aac96a596ea1b776996414791ede4"}, - {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c5f762659e47fdb7b16956c71598292f60a03aa92f8b6351504359dbdba6cf"}, - {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c9775e339e42e79ec99c441d9730fccf07414af63eac2f0e48e08fd38a64d76"}, - {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57762139821c31847cfb2df63c12f725788bd9f04bc2fb392790959b8f70f118"}, - {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d1e85068e818c73e048fe28cfc769040bb1f475524f4745a5dc621f75ac7630"}, - {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:097830ed52fd9e427942ff3b9bc17fab52913b2f50f2880dc4a5611446606a54"}, - {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:044a50963a614ecfae59bb1eaf7ea7efc4bc62f49ed594e18fa1e5d953c40e9f"}, - {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:4e0b4220ba5b40d727c7f879eac379b822eee5d8fff418e9d3381ee45b3b0362"}, - {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5e4f4bb20d75e9325cc9696c6802657b58bc1dbbe3022f32cc2b2b632c3fbb96"}, - {file = "pydantic_core-2.27.2-cp39-cp39-win32.whl", hash = "sha256:cca63613e90d001b9f2f9a9ceb276c308bfa2a43fafb75c8031c4f66039e8c6e"}, - {file = "pydantic_core-2.27.2-cp39-cp39-win_amd64.whl", hash = "sha256:77d1bca19b0f7021b3a982e6f903dcd5b2b06076def36a652e3907f596e29f67"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d591580c34f4d731592f0e9fe40f9cc1b430d297eecc70b962e93c5c668f15f"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:82f986faf4e644ffc189a7f1aafc86e46ef70372bb153e7001e8afccc6e54133"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:bec317a27290e2537f922639cafd54990551725fc844249e64c523301d0822fc"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:0296abcb83a797db256b773f45773da397da75a08f5fcaef41f2044adec05f50"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0d75070718e369e452075a6017fbf187f788e17ed67a3abd47fa934d001863d9"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c33939a82924da9ed65dab5a65d427205a73181d8098e79b6b426bdf8ad4e656"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:00bad2484fa6bda1e216e7345a798bd37c68fb2d97558edd584942aa41b7d278"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c817e2b40aba42bac6f457498dacabc568c3b7a986fc9ba7c8d9d260b71485fb"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:251136cdad0cb722e93732cb45ca5299fb56e1344a833640bf93b2803f8d1bfd"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d2088237af596f0a524d3afc39ab3b036e8adb054ee57cbb1dcf8e09da5b29cc"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d4041c0b966a84b4ae7a09832eb691a35aec90910cd2dbe7a208de59be77965b"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:8083d4e875ebe0b864ffef72a4304827015cff328a1be6e22cc850753bfb122b"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f141ee28a0ad2123b6611b6ceff018039df17f32ada8b534e6aa039545a3efb2"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7d0c8399fcc1848491f00e0314bd59fb34a9c008761bcb422a057670c3f65e35"}, - {file = "pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39"}, -] - -[package.dependencies] -typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" - -[[package]] -name = "pyyaml" -version = "6.0.2" -description = "YAML parser and emitter for Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, - {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, - {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, - {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, - {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, - {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, - {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, - {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, - {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, - {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, - {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, - {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, - {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, - {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, - {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, - {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, - {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, - {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, - {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, - {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, - {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, - {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, - {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, - {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, - {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, - {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, - {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, - {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, - {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, - {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, - {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, - {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, - {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, - {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, - {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, - {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, - {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, - {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, - {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, - {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, - {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, - {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, - {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, - {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, - {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, - {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, - {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, - {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, - {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, - {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, - {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, - {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, - {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, -] - -[[package]] -name = "sniffio" -version = "1.3.1" -description = "Sniff out which async library your code is running under" -optional = false -python-versions = ">=3.7" -files = [ - {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, - {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, -] - -[[package]] -name = "starlette" -version = "0.46.1" -description = "The little ASGI library that shines." -optional = false -python-versions = ">=3.9" -files = [ - {file = "starlette-0.46.1-py3-none-any.whl", hash = "sha256:77c74ed9d2720138b25875133f3a2dae6d854af2ec37dceb56aef370c1d8a227"}, - {file = "starlette-0.46.1.tar.gz", hash = "sha256:3c88d58ee4bd1bb807c0d1acb381838afc7752f9ddaec81bbe4383611d833230"}, -] - -[package.dependencies] -anyio = ">=3.6.2,<5" - -[package.extras] -full = ["httpx (>=0.27.0,<0.29.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.18)", "pyyaml"] - -[[package]] -name = "tqdm" -version = "4.67.1" -description = "Fast, Extensible Progress Meter" -optional = false -python-versions = ">=3.7" -files = [ - {file = "tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2"}, - {file = "tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "platform_system == \"Windows\""} - -[package.extras] -dev = ["nbval", "pytest (>=6)", "pytest-asyncio (>=0.24)", "pytest-cov", "pytest-timeout"] -discord = ["requests"] -notebook = ["ipywidgets (>=6)"] -slack = ["slack-sdk"] -telegram = ["requests"] - -[[package]] -name = "typing-extensions" -version = "4.12.2" -description = "Backported and Experimental Type Hints for Python 3.8+" -optional = false -python-versions = ">=3.8" -files = [ - {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, - {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, -] - -[[package]] -name = "uvicorn" -version = "0.34.0" -description = "The lightning-fast ASGI server." -optional = false -python-versions = ">=3.9" -files = [ - {file = "uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4"}, - {file = "uvicorn-0.34.0.tar.gz", hash = "sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9"}, -] - -[package.dependencies] -click = ">=7.0" -h11 = ">=0.8" -typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""} - -[package.extras] -standard = ["colorama (>=0.4)", "httptools (>=0.6.3)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"] - -[metadata] -lock-version = "2.0" -python-versions = "^3.10" -content-hash = "d005d82268b6f8c2a68b26c454bced5c34bf3c971c0cbfefde3fc0c45c675f55" diff --git a/demos/use_cases/orchestrating_agents/pyproject.toml b/demos/use_cases/orchestrating_agents/pyproject.toml deleted file mode 100644 index 7b422438..00000000 --- a/demos/use_cases/orchestrating_agents/pyproject.toml +++ /dev/null @@ -1,20 +0,0 @@ -[tool.poetry] -name = "api-server" -version = "0.1.0" -description = "" -authors = ["Adil Hafeez "] -readme = "README.md" - -[tool.poetry.dependencies] -python = "^3.10" -fastapi = "^0.115.4" -pyyaml = "^6.0.2" -uvicorn = "^0.34.0" -openai = "^1.66.5" - -[build-system] -requires = ["poetry-core"] -build-backend = "poetry.core.masonry.api" - -[tool.poetry.scripts] -api-server = "api_server.main:app" diff --git a/demos/use_cases/orchestrating_agents/run_demo.sh b/demos/use_cases/orchestrating_agents/run_demo.sh deleted file mode 100644 index eb47dce6..00000000 --- a/demos/use_cases/orchestrating_agents/run_demo.sh +++ /dev/null @@ -1,47 +0,0 @@ -#!/bin/bash -set -e - -# Function to start the demo -start_demo() { - # Step 1: Check if .env file exists - if [ -f ".env" ]; then - echo ".env file already exists. Skipping creation." - else - # Step 2: Create `.env` file and set OpenAI key - if [ -z "$OPENAI_API_KEY" ]; then - echo "Error: OPENAI_API_KEY environment variable is not set for the demo." - exit 1 - fi - - echo "Creating .env file..." - echo "OPENAI_API_KEY=$OPENAI_API_KEY" > .env - echo ".env file created with OPENAI_API_KEY." - fi - - # Step 3: Start Arch - echo "Starting Arch with arch_config.yaml..." - archgw up arch_config.yaml - - # Step 4: Start developer services - echo "Starting Network Agent using Docker Compose..." - docker compose up -d # Run in detached mode -} - -# Function to stop the demo -stop_demo() { - # Step 1: Stop Docker Compose services - echo "Stopping Network Agent using Docker Compose..." - docker compose down - - # Step 2: Stop Arch - echo "Stopping Arch..." - archgw down -} - -# Main script logic -if [ "$1" == "down" ]; then - stop_demo -else - # Default action is to bring the demo up - start_demo -fi diff --git a/demos/use_cases/preference_based_routing/README.md b/demos/use_cases/preference_based_routing/README.md index 9446f85a..139d6252 100644 --- a/demos/use_cases/preference_based_routing/README.md +++ b/demos/use_cases/preference_based_routing/README.md @@ -14,9 +14,9 @@ Make sure your machine is up to date with [latest version of archgw]([url](https 2. start archgw in the foreground ```bash (venv) $ archgw up --service archgw --foreground -2025-05-30 18:00:09,953 - cli.main - INFO - Starting archgw cli version: 0.3.18 +2025-05-30 18:00:09,953 - cli.main - INFO - Starting archgw cli version: 0.3.22 2025-05-30 18:00:09,953 - cli.main - INFO - Validating /Users/adilhafeez/src/intelligent-prompt-gateway/demos/use_cases/preference_based_routing/arch_config.yaml -2025-05-30 18:00:10,422 - cli.core - INFO - Starting arch gateway, image name: archgw, tag: katanemo/archgw:0.3.18 +2025-05-30 18:00:10,422 - cli.core - INFO - Starting arch gateway, image name: archgw, tag: katanemo/archgw:0.3.22 2025-05-30 18:00:10,662 - cli.core - INFO - archgw status: running, health status: starting 2025-05-30 18:00:11,712 - cli.core - INFO - archgw status: running, health status: starting 2025-05-30 18:00:12,761 - cli.core - INFO - archgw is running and is healthy! diff --git a/docs/db_setup/README.md b/docs/db_setup/README.md new file mode 100644 index 00000000..34aff973 --- /dev/null +++ b/docs/db_setup/README.md @@ -0,0 +1,109 @@ +# Database Setup for Conversation State Storage + +This directory contains SQL scripts needed to set up database tables for storing conversation state when using the OpenAI Responses API. + +## Prerequisites + +- PostgreSQL database (Supabase or self-hosted) +- Database connection credentials +- `psql` CLI tool or database admin access + +## Setup Instructions + +### Option 1: Using psql + +```bash +psql $DATABASE_URL -f docs/db_setup/conversation_states.sql +``` + +### Option 2: Using Supabase Dashboard + +1. Log in to your Supabase project dashboard +2. Navigate to the SQL Editor +3. Copy and paste the contents of `conversation_states.sql` +4. Run the query + +### Option 3: Direct Database Connection + +Connect to your PostgreSQL database using your preferred client and execute the SQL from `conversation_states.sql`. + +## Verification + +After running the setup, verify the table was created: + +```sql +SELECT tablename FROM pg_tables WHERE tablename = 'conversation_states'; +``` + +You should see `conversation_states` in the results. + +## Configuration + +After setting up the database table, configure your application to use Supabase storage by setting the appropriate environment variable or configuration parameter with your database connection string. + +### Supabase Connection String + +**Important:** Supabase requires different connection strings depending on your network: + +- **IPv4 Networks (Most Common)**: Use the **Session Pooler** connection string (port 5432): + ``` + postgresql://postgres.[PROJECT-REF]:[PASSWORD]@aws-0-[REGION].pooler.supabase.com:5432/postgres + ``` + +- **IPv6 Networks**: Use the direct connection (port 5432): + ``` + postgresql://postgres:[PASSWORD]@db.[PROJECT-REF].supabase.co:5432/postgres + ``` + +**How to get your connection string:** +1. Go to your Supabase project dashboard +2. Settings → Database → Connection Pooling +3. Copy the **Session mode** connection string +4. Replace `[YOUR-PASSWORD]` with your actual database password +5. URL-encode special characters in the password (e.g., `#` becomes `%23`) + +**Example:** +```bash +# If your password is "MyPass#123", encode it as "MyPass%23123" +export DATABASE_URL="postgresql://postgres.myproject:MyPass%23123@aws-0-us-west-2.pooler.supabase.com:5432/postgres" +``` + +### Testing the Connection + +To test your connection string works: +```bash +export TEST_DATABASE_URL="your-connection-string-here" +cd crates/brightstaff +cargo test supabase -- --nocapture +``` + +## Table Schema + +The `conversation_states` table stores: +- `response_id` (TEXT, PRIMARY KEY): Unique identifier for each conversation +- `input_items` (JSONB): Array of conversation messages and context +- `created_at` (BIGINT): Unix timestamp when conversation started +- `model` (TEXT): Model name used for the conversation +- `provider` (TEXT): LLM provider name +- `updated_at` (TIMESTAMP): Last update time (auto-managed) + +## Maintenance + +### Cleanup Old Conversations + +To prevent unbounded growth, consider periodically cleaning up old conversation states: + +```sql +-- Delete conversations older than 7 days +DELETE FROM conversation_states +WHERE updated_at < NOW() - INTERVAL '7 days'; +``` + +You can automate this with a cron job or database trigger. + +## Troubleshooting + +If you encounter errors on first use: +- **"Table 'conversation_states' does not exist"**: Run the setup SQL +- **Connection errors**: Verify your DATABASE_URL is correct +- **Permission errors**: Ensure your database user has CREATE TABLE privileges diff --git a/docs/db_setup/conversation_states.sql b/docs/db_setup/conversation_states.sql new file mode 100644 index 00000000..26272423 --- /dev/null +++ b/docs/db_setup/conversation_states.sql @@ -0,0 +1,31 @@ +-- Conversation State Storage Table +-- This table stores conversational context for the OpenAI Responses API +-- Run this SQL against your PostgreSQL/Supabase database before enabling conversation state storage + +CREATE TABLE IF NOT EXISTS conversation_states ( + response_id TEXT PRIMARY KEY, + input_items JSONB NOT NULL, + created_at BIGINT NOT NULL, + model TEXT NOT NULL, + provider TEXT NOT NULL, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- Indexes for common query patterns +CREATE INDEX IF NOT EXISTS idx_conversation_states_created_at + ON conversation_states(created_at); + +CREATE INDEX IF NOT EXISTS idx_conversation_states_provider + ON conversation_states(provider); + +-- Optional: Add a policy for automatic cleanup of old conversations +-- Uncomment and adjust the retention period as needed +-- CREATE INDEX IF NOT EXISTS idx_conversation_states_updated_at +-- ON conversation_states(updated_at); + +COMMENT ON TABLE conversation_states IS 'Stores conversation history for OpenAI Responses API continuity'; +COMMENT ON COLUMN conversation_states.response_id IS 'Unique identifier for the conversation state'; +COMMENT ON COLUMN conversation_states.input_items IS 'JSONB array of conversation messages and context'; +COMMENT ON COLUMN conversation_states.created_at IS 'Unix timestamp (seconds) when the conversation started'; +COMMENT ON COLUMN conversation_states.model IS 'Model name used for this conversation'; +COMMENT ON COLUMN conversation_states.provider IS 'LLM provider (e.g., openai, anthropic, bedrock)'; diff --git a/docs/source/conf.py b/docs/source/conf.py index 81d62da7..11048b61 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -15,7 +15,7 @@ from sphinxawesome_theme.postprocess import Icons project = "Arch Docs" copyright = "2025, Katanemo Labs, Inc" author = "Katanemo Labs, Inc" -release = " v0.3.18" +release = " v0.3.22" # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration diff --git a/docs/source/get_started/quickstart.rst b/docs/source/get_started/quickstart.rst index 1f5f82ee..546285c2 100644 --- a/docs/source/get_started/quickstart.rst +++ b/docs/source/get_started/quickstart.rst @@ -25,7 +25,7 @@ Arch's CLI allows you to manage and interact with the Arch gateway efficiently. $ python -m venv venv $ source venv/bin/activate # On Windows, use: venv\Scripts\activate - $ pip install archgw==0.3.18 + $ pip install archgw==0.3.22 Build AI Agent with Arch Gateway diff --git a/docs/source/resources/deployment.rst b/docs/source/resources/deployment.rst index 29006672..c23e0adc 100644 --- a/docs/source/resources/deployment.rst +++ b/docs/source/resources/deployment.rst @@ -25,7 +25,7 @@ Create a ``docker-compose.yml`` file with the following configuration: # docker-compose.yml services: archgw: - image: katanemo/archgw:0.3.18 + image: katanemo/archgw:0.3.22 container_name: archgw ports: - "10000:10000" # ingress (client -> arch) @@ -35,7 +35,6 @@ Create a ``docker-compose.yml`` file with the following configuration: environment: - OPENAI_API_KEY=${OPENAI_API_KEY:?error} - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY:?error} - - MODEL_SERVER_PORT=51000 Starting the Stack ~~~~~~~~~~~~~~~~~~ diff --git a/docs/source/resources/includes/arch_config_full_reference_rendered.yaml b/docs/source/resources/includes/arch_config_full_reference_rendered.yaml index 0594bde2..cc0d70f3 100644 --- a/docs/source/resources/includes/arch_config_full_reference_rendered.yaml +++ b/docs/source/resources/includes/arch_config_full_reference_rendered.yaml @@ -61,6 +61,9 @@ model_providers: port: 80 protocol: http provider_interface: mistral +- model: Arch-Function + name: arch-function + provider_interface: arch overrides: prompt_target_intent_matching_threshold: 0.6 prompt_guards: diff --git a/docs/source/resources/includes/arch_config_state_storage_example.yaml b/docs/source/resources/includes/arch_config_state_storage_example.yaml new file mode 100644 index 00000000..27a417c0 --- /dev/null +++ b/docs/source/resources/includes/arch_config_state_storage_example.yaml @@ -0,0 +1,32 @@ +version: v0.1 + +listeners: + egress_traffic: + address: 0.0.0.0 + port: 12000 + message_format: openai + timeout: 30s + +llm_providers: + + # OpenAI Models + - model: openai/gpt-5-mini-2025-08-07 + access_key: $OPENAI_API_KEY + default: true + + # Anthropic Models + - model: anthropic/claude-sonnet-4-20250514 + access_key: $ANTHROPIC_API_KEY + +# State storage configuration for v1/responses API +# Manages conversation state for multi-turn conversations +state_storage: + # Type: memory | postgres + type: postgres + + # Connection string for postgres type + # Environment variables are supported using $VAR_NAME or ${VAR_NAME} syntax + # Replace [USER] and [HOST] with your actual database credentials + # Variables like $DB_PASSWORD MUST be set before running config validation/rendering + # Example: Replace [USER] with 'myuser' and [HOST] with 'db.example.com:5432' + connection_string: "postgresql://[USER]:$DB_PASSWORD@[HOST]:5432/postgres" diff --git a/model_server/.vscode/launch.json b/model_server/.vscode/launch.json deleted file mode 100644 index 19ed7342..00000000 --- a/model_server/.vscode/launch.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - - { - "name": "model server", - "type": "debugpy", - "request": "launch", - "module": "uvicorn", - "args": ["src.main:app","--reload", "--port", "51000"] - } - ] -} diff --git a/model_server/.vscode/settings.json b/model_server/.vscode/settings.json deleted file mode 100644 index 98ba633e..00000000 --- a/model_server/.vscode/settings.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "python.testing.pytestArgs": [ - "." - ], - "python.testing.unittestEnabled": false, - "python.testing.pytestEnabled": true -} diff --git a/model_server/Dockerfile b/model_server/Dockerfile deleted file mode 100644 index 1c9d5ff4..00000000 --- a/model_server/Dockerfile +++ /dev/null @@ -1,31 +0,0 @@ -FROM python:3.12 AS builder - -COPY requirements.txt . -RUN pip install --prefix=/runtime -r requirements.txt - -FROM python:3.12-slim AS output - -# curl is needed for health check in docker-compose -RUN apt-get update && apt-get install -y curl && apt-get clean && rm -rf /var/lib/apt/lists/* - -COPY --from=builder /runtime /usr/local - -WORKDIR /src - -# specify list of models that will go into the image as a comma separated list -# following models have been tested to work with this image -# "sentence-transformers/all-MiniLM-L6-v2,sentence-transformers/all-mpnet-base-v2,thenlper/gte-base,thenlper/gte-large,thenlper/gte-small" -ENV MODELS="" - -COPY ./app ./app -COPY ./app/guard_model_config.yaml . -COPY ./app/openai_params.yaml . - -# comment it out for now as we don't want to download the model every time we build the image -# we will mount host cache to docker image to avoid downloading the model every time -# see docker-compose file for more details - -# RUN python install.py && \ -# find /root/.cache/torch/sentence_transformers/ -name onnx -exec rm -rf {} + - -CMD ["uvicorn", "src.app.main:app", "--host", "0.0.0.0", "--port", "80"] diff --git a/model_server/Dockerfile.gpu b/model_server/Dockerfile.gpu deleted file mode 100644 index b1b29c06..00000000 --- a/model_server/Dockerfile.gpu +++ /dev/null @@ -1,64 +0,0 @@ -# Use NVIDIA CUDA base image to enable GPU support -FROM nvidia/cuda:12.1.0-cudnn8-runtime-ubuntu22.04 as base -ENV DEBIAN_FRONTEND=noninteractive - -# Install Python 3.10 -RUN apt-get update && \ - apt-get install -y python3.10 python3-pip python3-dev python-is-python3 && \ - rm -rf /var/lib/apt/lists/* - - - -# -# builder -# -FROM base AS builder - -WORKDIR /src - -# Upgrade pip -RUN pip install --upgrade pip - -# Install git for cloning repositories -RUN apt-get update && apt-get install -y git && apt-get clean - -# Copy requirements.txt -COPY requirements.txt /src/ - -# Install Python dependencies -RUN pip install --force-reinstall -r requirements.txt - -RUN apt-get update && \ - apt-get install -y cuda-toolkit-12-2 - -# Check for NVIDIA GPU and CUDA support and install EETQ if detected -RUN if command -v nvcc >/dev/null 2>&1; then \ - echo "CUDA and NVIDIA GPU detected, installing EETQ..." && \ - git clone https://github.com/NetEase-FuXi/EETQ.git && \ - cd EETQ && \ - git submodule update --init --recursive && \ - pip install .; \ - else \ - echo "CUDA or NVIDIA GPU not detected, skipping EETQ installation."; \ - fi - -COPY . /src - -# Specify list of models that will go into the image as a comma separated list -ENV MODELS="" -ENV DEBIAN_FRONTEND=noninteractive - -COPY /app /app -WORKDIR /app - -# Install required tools -RUN apt-get update && apt-get install -y \ - curl \ - && rm -rf /var/lib/apt/lists/* - -# Uncomment if you want to install the model during the image build -# RUN python install.py && \ -# find /root/.cache/torch/sentence_transformers/ -name onnx -exec rm -rf {} + - -# Set the default command to run the application -CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "80"] diff --git a/model_server/README.md b/model_server/README.md deleted file mode 100644 index 93248579..00000000 --- a/model_server/README.md +++ /dev/null @@ -1,2 +0,0 @@ -# Model Server Package # -This model server package is a dependency of the Arch intelligent prompt gateway. It should not be used alone. Please refer to the [quickstart-guide](https://github.com/katanemo/arch?tab=readme-ov-file#quickstart) for more details on how to get start with Arch. diff --git a/model_server/__init__.py b/model_server/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/model_server/poetry.lock b/model_server/poetry.lock deleted file mode 100644 index 9710e47b..00000000 --- a/model_server/poetry.lock +++ /dev/null @@ -1,3102 +0,0 @@ -# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. - -[[package]] -name = "accelerate" -version = "1.11.0" -description = "Accelerate" -optional = false -python-versions = ">=3.10.0" -groups = ["main"] -files = [ - {file = "accelerate-1.11.0-py3-none-any.whl", hash = "sha256:a628fa6beb069b8e549460fc449135d5bd8d73e7a11fd09f0bc9fc4ace7f06f1"}, - {file = "accelerate-1.11.0.tar.gz", hash = "sha256:bb1caf2597b4cd632b917b5000c591d10730bb024a79746f1ee205bba80bd229"}, -] - -[package.dependencies] -huggingface_hub = ">=0.21.0" -numpy = ">=1.17" -packaging = ">=20.0" -psutil = "*" -pyyaml = "*" -safetensors = ">=0.4.3" -torch = ">=2.0.0" - -[package.extras] -deepspeed = ["deepspeed"] -dev = ["bitsandbytes", "datasets", "diffusers", "evaluate", "parameterized", "pytest (>=7.2.0)", "pytest-order", "pytest-subtests", "pytest-xdist", "rich", "ruff (==0.13.1)", "scikit-learn", "scipy", "timm", "torchdata (>=0.8.0)", "torchpippy (>=0.2.0)", "tqdm", "transformers"] -quality = ["ruff (==0.13.1)"] -rich = ["rich"] -sagemaker = ["sagemaker"] -test-dev = ["bitsandbytes", "datasets", "diffusers", "evaluate", "scikit-learn", "scipy", "timm", "torchdata (>=0.8.0)", "torchpippy (>=0.2.0)", "tqdm", "transformers"] -test-fp8 = ["torchao"] -test-prod = ["parameterized", "pytest (>=7.2.0)", "pytest-order", "pytest-subtests", "pytest-xdist"] -test-trackers = ["comet-ml", "dvclive", "matplotlib", "swanlab[dashboard]", "tensorboard", "trackio", "wandb"] -testing = ["bitsandbytes", "datasets", "diffusers", "evaluate", "parameterized", "pytest (>=7.2.0)", "pytest-order", "pytest-subtests", "pytest-xdist", "scikit-learn", "scipy", "timm", "torchdata (>=0.8.0)", "torchpippy (>=0.2.0)", "tqdm", "transformers"] - -[[package]] -name = "annotated-types" -version = "0.7.0" -description = "Reusable constraint types to use with typing.Annotated" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, - {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, -] - -[[package]] -name = "anyio" -version = "4.11.0" -description = "High-level concurrency and networking framework on top of asyncio or Trio" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc"}, - {file = "anyio-4.11.0.tar.gz", hash = "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4"}, -] - -[package.dependencies] -exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} -idna = ">=2.8" -sniffio = ">=1.1" -typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} - -[package.extras] -trio = ["trio (>=0.31.0)"] - -[[package]] -name = "asgiref" -version = "3.10.0" -description = "ASGI specs, helper code, and adapters" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "asgiref-3.10.0-py3-none-any.whl", hash = "sha256:aef8a81283a34d0ab31630c9b7dfe70c812c95eba78171367ca8745e88124734"}, - {file = "asgiref-3.10.0.tar.gz", hash = "sha256:d89f2d8cd8b56dada7d52fa7dc8075baa08fb836560710d38c292a7a3f78c04e"}, -] - -[package.dependencies] -typing_extensions = {version = ">=4", markers = "python_version < \"3.11\""} - -[package.extras] -tests = ["mypy (>=1.14.0)", "pytest", "pytest-asyncio"] - -[[package]] -name = "backports-asyncio-runner" -version = "1.2.0" -description = "Backport of asyncio.Runner, a context manager that controls event loop life cycle." -optional = false -python-versions = "<3.11,>=3.8" -groups = ["dev"] -markers = "python_version == \"3.10\"" -files = [ - {file = "backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5"}, - {file = "backports_asyncio_runner-1.2.0.tar.gz", hash = "sha256:a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162"}, -] - -[[package]] -name = "certifi" -version = "2025.10.5" -description = "Python package for providing Mozilla's CA Bundle." -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "certifi-2025.10.5-py3-none-any.whl", hash = "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de"}, - {file = "certifi-2025.10.5.tar.gz", hash = "sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43"}, -] - -[[package]] -name = "charset-normalizer" -version = "3.4.4" -description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ce8a0633f41a967713a59c4139d29110c07e826d131a316b50ce11b1d79b4f84"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaabd426fe94daf8fd157c32e571c85cb12e66692f15516a83a03264b08d06c3"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c4ef880e27901b6cc782f1b95f82da9313c0eb95c3af699103088fa0ac3ce9ac"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2aaba3b0819274cc41757a1da876f810a3e4d7b6eb25699253a4effef9e8e4af"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:778d2e08eda00f4256d7f672ca9fef386071c9202f5e4607920b86d7803387f2"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f155a433c2ec037d4e8df17d18922c3a0d9b3232a396690f17175d2946f0218d"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a8bf8d0f749c5757af2142fe7903a9df1d2e8aa3841559b2bad34b08d0e2bcf3"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:194f08cbb32dc406d6e1aea671a68be0823673db2832b38405deba2fb0d88f63"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:6aee717dcfead04c6eb1ce3bd29ac1e22663cdea57f943c87d1eab9a025438d7"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:cd4b7ca9984e5e7985c12bc60a6f173f3c958eae74f3ef6624bb6b26e2abbae4"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_riscv64.whl", hash = "sha256:b7cf1017d601aa35e6bb650b6ad28652c9cd78ee6caff19f3c28d03e1c80acbf"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:e912091979546adf63357d7e2ccff9b44f026c075aeaf25a52d0e95ad2281074"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:5cb4d72eea50c8868f5288b7f7f33ed276118325c1dfd3957089f6b519e1382a"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-win32.whl", hash = "sha256:837c2ce8c5a65a2035be9b3569c684358dfbf109fd3b6969630a87535495ceaa"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:44c2a8734b333e0578090c4cd6b16f275e07aa6614ca8715e6c038e865e70576"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-win32.whl", hash = "sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-win_arm64.whl", hash = "sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50"}, - {file = "charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f"}, - {file = "charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a"}, -] - -[[package]] -name = "click" -version = "8.3.0" -description = "Composable command line interface toolkit" -optional = false -python-versions = ">=3.10" -groups = ["main"] -files = [ - {file = "click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc"}, - {file = "click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "platform_system == \"Windows\""} - -[[package]] -name = "colorama" -version = "0.4.6" -description = "Cross-platform colored terminal text." -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -groups = ["main", "dev"] -files = [ - {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, - {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, -] -markers = {main = "platform_system == \"Windows\"", dev = "sys_platform == \"win32\""} - -[[package]] -name = "dateparser" -version = "1.2.2" -description = "Date parsing library designed to parse dates from HTML pages" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "dateparser-1.2.2-py3-none-any.whl", hash = "sha256:5a5d7211a09013499867547023a2a0c91d5a27d15dd4dbcea676ea9fe66f2482"}, - {file = "dateparser-1.2.2.tar.gz", hash = "sha256:986316f17cb8cdc23ea8ce563027c5ef12fc725b6fb1d137c14ca08777c5ecf7"}, -] - -[package.dependencies] -python-dateutil = ">=2.7.0" -pytz = ">=2024.2" -regex = ">=2024.9.11" -tzlocal = ">=0.2" - -[package.extras] -calendars = ["convertdate (>=2.2.1)", "hijridate"] -fasttext = ["fasttext (>=0.9.1)", "numpy (>=1.19.3,<2)"] -langdetect = ["langdetect (>=1.0.0)"] - -[[package]] -name = "distro" -version = "1.9.0" -description = "Distro - an OS platform information API" -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2"}, - {file = "distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed"}, -] - -[[package]] -name = "exceptiongroup" -version = "1.3.0" -description = "Backport of PEP 654 (exception groups)" -optional = false -python-versions = ">=3.7" -groups = ["main", "dev"] -markers = "python_version == \"3.10\"" -files = [ - {file = "exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10"}, - {file = "exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88"}, -] - -[package.dependencies] -typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""} - -[package.extras] -test = ["pytest (>=6)"] - -[[package]] -name = "fastapi" -version = "0.115.0" -description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "fastapi-0.115.0-py3-none-any.whl", hash = "sha256:17ea427674467486e997206a5ab25760f6b09e069f099b96f5b55a32fb6f1631"}, - {file = "fastapi-0.115.0.tar.gz", hash = "sha256:f93b4ca3529a8ebc6fc3fcf710e5efa8de3df9b41570958abf1d97d843138004"}, -] - -[package.dependencies] -pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0" -starlette = ">=0.37.2,<0.39.0" -typing-extensions = ">=4.8.0" - -[package.extras] -all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.7)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] -standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "jinja2 (>=2.11.2)", "python-multipart (>=0.0.7)", "uvicorn[standard] (>=0.12.0)"] - -[[package]] -name = "filelock" -version = "3.20.0" -description = "A platform independent file lock." -optional = false -python-versions = ">=3.10" -groups = ["main"] -files = [ - {file = "filelock-3.20.0-py3-none-any.whl", hash = "sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2"}, - {file = "filelock-3.20.0.tar.gz", hash = "sha256:711e943b4ec6be42e1d4e6690b48dc175c822967466bb31c0c293f34334c13f4"}, -] - -[[package]] -name = "fsspec" -version = "2025.9.0" -description = "File-system specification" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "fsspec-2025.9.0-py3-none-any.whl", hash = "sha256:530dc2a2af60a414a832059574df4a6e10cce927f6f4a78209390fe38955cfb7"}, - {file = "fsspec-2025.9.0.tar.gz", hash = "sha256:19fd429483d25d28b65ec68f9f4adc16c17ea2c7c7bf54ec61360d478fb19c19"}, -] - -[package.extras] -abfs = ["adlfs"] -adl = ["adlfs"] -arrow = ["pyarrow (>=1)"] -dask = ["dask", "distributed"] -dev = ["pre-commit", "ruff (>=0.5)"] -doc = ["numpydoc", "sphinx", "sphinx-design", "sphinx-rtd-theme", "yarl"] -dropbox = ["dropbox", "dropboxdrivefs", "requests"] -full = ["adlfs", "aiohttp (!=4.0.0a0,!=4.0.0a1)", "dask", "distributed", "dropbox", "dropboxdrivefs", "fusepy", "gcsfs", "libarchive-c", "ocifs", "panel", "paramiko", "pyarrow (>=1)", "pygit2", "requests", "s3fs", "smbprotocol", "tqdm"] -fuse = ["fusepy"] -gcs = ["gcsfs"] -git = ["pygit2"] -github = ["requests"] -gs = ["gcsfs"] -gui = ["panel"] -hdfs = ["pyarrow (>=1)"] -http = ["aiohttp (!=4.0.0a0,!=4.0.0a1)"] -libarchive = ["libarchive-c"] -oci = ["ocifs"] -s3 = ["s3fs"] -sftp = ["paramiko"] -smb = ["smbprotocol"] -ssh = ["paramiko"] -test = ["aiohttp (!=4.0.0a0,!=4.0.0a1)", "numpy", "pytest", "pytest-asyncio (!=0.22.0)", "pytest-benchmark", "pytest-cov", "pytest-mock", "pytest-recording", "pytest-rerunfailures", "requests"] -test-downstream = ["aiobotocore (>=2.5.4,<3.0.0)", "dask[dataframe,test]", "moto[server] (>4,<5)", "pytest-timeout", "xarray"] -test-full = ["adlfs", "aiohttp (!=4.0.0a0,!=4.0.0a1)", "cloudpickle", "dask", "distributed", "dropbox", "dropboxdrivefs", "fastparquet", "fusepy", "gcsfs", "jinja2", "kerchunk", "libarchive-c", "lz4", "notebook", "numpy", "ocifs", "pandas", "panel", "paramiko", "pyarrow", "pyarrow (>=1)", "pyftpdlib", "pygit2", "pytest", "pytest-asyncio (!=0.22.0)", "pytest-benchmark", "pytest-cov", "pytest-mock", "pytest-recording", "pytest-rerunfailures", "python-snappy", "requests", "smbprotocol", "tqdm", "urllib3", "zarr", "zstandard ; python_version < \"3.14\""] -tqdm = ["tqdm"] - -[[package]] -name = "googleapis-common-protos" -version = "1.71.0" -description = "Common protobufs used in Google APIs" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "googleapis_common_protos-1.71.0-py3-none-any.whl", hash = "sha256:59034a1d849dc4d18971997a72ac56246570afdd17f9369a0ff68218d50ab78c"}, - {file = "googleapis_common_protos-1.71.0.tar.gz", hash = "sha256:1aec01e574e29da63c80ba9f7bbf1ccfaacf1da877f23609fe236ca7c72a2e2e"}, -] - -[package.dependencies] -protobuf = ">=3.20.2,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<7.0.0" - -[package.extras] -grpc = ["grpcio (>=1.44.0,<2.0.0)"] - -[[package]] -name = "grpcio" -version = "1.76.0" -description = "HTTP/2-based RPC framework" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "grpcio-1.76.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:65a20de41e85648e00305c1bb09a3598f840422e522277641145a32d42dcefcc"}, - {file = "grpcio-1.76.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:40ad3afe81676fd9ec6d9d406eda00933f218038433980aa19d401490e46ecde"}, - {file = "grpcio-1.76.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:035d90bc79eaa4bed83f524331d55e35820725c9fbb00ffa1904d5550ed7ede3"}, - {file = "grpcio-1.76.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:4215d3a102bd95e2e11b5395c78562967959824156af11fa93d18fdd18050990"}, - {file = "grpcio-1.76.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:49ce47231818806067aea3324d4bf13825b658ad662d3b25fada0bdad9b8a6af"}, - {file = "grpcio-1.76.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8cc3309d8e08fd79089e13ed4819d0af72aa935dd8f435a195fd152796752ff2"}, - {file = "grpcio-1.76.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:971fd5a1d6e62e00d945423a567e42eb1fa678ba89072832185ca836a94daaa6"}, - {file = "grpcio-1.76.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9d9adda641db7207e800a7f089068f6f645959f2df27e870ee81d44701dd9db3"}, - {file = "grpcio-1.76.0-cp310-cp310-win32.whl", hash = "sha256:063065249d9e7e0782d03d2bca50787f53bd0fb89a67de9a7b521c4a01f1989b"}, - {file = "grpcio-1.76.0-cp310-cp310-win_amd64.whl", hash = "sha256:a6ae758eb08088d36812dd5d9af7a9859c05b1e0f714470ea243694b49278e7b"}, - {file = "grpcio-1.76.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:2e1743fbd7f5fa713a1b0a8ac8ebabf0ec980b5d8809ec358d488e273b9cf02a"}, - {file = "grpcio-1.76.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:a8c2cf1209497cf659a667d7dea88985e834c24b7c3b605e6254cbb5076d985c"}, - {file = "grpcio-1.76.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:08caea849a9d3c71a542827d6df9d5a69067b0a1efbea8a855633ff5d9571465"}, - {file = "grpcio-1.76.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:f0e34c2079d47ae9f6188211db9e777c619a21d4faba6977774e8fa43b085e48"}, - {file = "grpcio-1.76.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8843114c0cfce61b40ad48df65abcfc00d4dba82eae8718fab5352390848c5da"}, - {file = "grpcio-1.76.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8eddfb4d203a237da6f3cc8a540dad0517d274b5a1e9e636fd8d2c79b5c1d397"}, - {file = "grpcio-1.76.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:32483fe2aab2c3794101c2a159070584e5db11d0aa091b2c0ea9c4fc43d0d749"}, - {file = "grpcio-1.76.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dcfe41187da8992c5f40aa8c5ec086fa3672834d2be57a32384c08d5a05b4c00"}, - {file = "grpcio-1.76.0-cp311-cp311-win32.whl", hash = "sha256:2107b0c024d1b35f4083f11245c0e23846ae64d02f40b2b226684840260ed054"}, - {file = "grpcio-1.76.0-cp311-cp311-win_amd64.whl", hash = "sha256:522175aba7af9113c48ec10cc471b9b9bd4f6ceb36aeb4544a8e2c80ed9d252d"}, - {file = "grpcio-1.76.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:81fd9652b37b36f16138611c7e884eb82e0cec137c40d3ef7c3f9b3ed00f6ed8"}, - {file = "grpcio-1.76.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:04bbe1bfe3a68bbfd4e52402ab7d4eb59d72d02647ae2042204326cf4bbad280"}, - {file = "grpcio-1.76.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d388087771c837cdb6515539f43b9d4bf0b0f23593a24054ac16f7a960be16f4"}, - {file = "grpcio-1.76.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:9f8f757bebaaea112c00dba718fc0d3260052ce714e25804a03f93f5d1c6cc11"}, - {file = "grpcio-1.76.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:980a846182ce88c4f2f7e2c22c56aefd515daeb36149d1c897f83cf57999e0b6"}, - {file = "grpcio-1.76.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f92f88e6c033db65a5ae3d97905c8fea9c725b63e28d5a75cb73b49bda5024d8"}, - {file = "grpcio-1.76.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4baf3cbe2f0be3289eb68ac8ae771156971848bb8aaff60bad42005539431980"}, - {file = "grpcio-1.76.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:615ba64c208aaceb5ec83bfdce7728b80bfeb8be97562944836a7a0a9647d882"}, - {file = "grpcio-1.76.0-cp312-cp312-win32.whl", hash = "sha256:45d59a649a82df5718fd9527ce775fd66d1af35e6d31abdcdc906a49c6822958"}, - {file = "grpcio-1.76.0-cp312-cp312-win_amd64.whl", hash = "sha256:c088e7a90b6017307f423efbb9d1ba97a22aa2170876223f9709e9d1de0b5347"}, - {file = "grpcio-1.76.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:26ef06c73eb53267c2b319f43e6634c7556ea37672029241a056629af27c10e2"}, - {file = "grpcio-1.76.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:45e0111e73f43f735d70786557dc38141185072d7ff8dc1829d6a77ac1471468"}, - {file = "grpcio-1.76.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:83d57312a58dcfe2a3a0f9d1389b299438909a02db60e2f2ea2ae2d8034909d3"}, - {file = "grpcio-1.76.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:3e2a27c89eb9ac3d81ec8835e12414d73536c6e620355d65102503064a4ed6eb"}, - {file = "grpcio-1.76.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61f69297cba3950a524f61c7c8ee12e55c486cb5f7db47ff9dcee33da6f0d3ae"}, - {file = "grpcio-1.76.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6a15c17af8839b6801d554263c546c69c4d7718ad4321e3166175b37eaacca77"}, - {file = "grpcio-1.76.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:25a18e9810fbc7e7f03ec2516addc116a957f8cbb8cbc95ccc80faa072743d03"}, - {file = "grpcio-1.76.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:931091142fd8cc14edccc0845a79248bc155425eee9a98b2db2ea4f00a235a42"}, - {file = "grpcio-1.76.0-cp313-cp313-win32.whl", hash = "sha256:5e8571632780e08526f118f74170ad8d50fb0a48c23a746bef2a6ebade3abd6f"}, - {file = "grpcio-1.76.0-cp313-cp313-win_amd64.whl", hash = "sha256:f9f7bd5faab55f47231ad8dba7787866b69f5e93bc306e3915606779bbfb4ba8"}, - {file = "grpcio-1.76.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:ff8a59ea85a1f2191a0ffcc61298c571bc566332f82e5f5be1b83c9d8e668a62"}, - {file = "grpcio-1.76.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:06c3d6b076e7b593905d04fdba6a0525711b3466f43b3400266f04ff735de0cd"}, - {file = "grpcio-1.76.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fd5ef5932f6475c436c4a55e4336ebbe47bd3272be04964a03d316bbf4afbcbc"}, - {file = "grpcio-1.76.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b331680e46239e090f5b3cead313cc772f6caa7d0fc8de349337563125361a4a"}, - {file = "grpcio-1.76.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2229ae655ec4e8999599469559e97630185fdd53ae1e8997d147b7c9b2b72cba"}, - {file = "grpcio-1.76.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:490fa6d203992c47c7b9e4a9d39003a0c2bcc1c9aa3c058730884bbbb0ee9f09"}, - {file = "grpcio-1.76.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:479496325ce554792dba6548fae3df31a72cef7bad71ca2e12b0e58f9b336bfc"}, - {file = "grpcio-1.76.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1c9b93f79f48b03ada57ea24725d83a30284a012ec27eab2cf7e50a550cbbbcc"}, - {file = "grpcio-1.76.0-cp314-cp314-win32.whl", hash = "sha256:747fa73efa9b8b1488a95d0ba1039c8e2dca0f741612d80415b1e1c560febf4e"}, - {file = "grpcio-1.76.0-cp314-cp314-win_amd64.whl", hash = "sha256:922fa70ba549fce362d2e2871ab542082d66e2aaf0c19480ea453905b01f384e"}, - {file = "grpcio-1.76.0-cp39-cp39-linux_armv7l.whl", hash = "sha256:8ebe63ee5f8fa4296b1b8cfc743f870d10e902ca18afc65c68cf46fd39bb0783"}, - {file = "grpcio-1.76.0-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:3bf0f392c0b806905ed174dcd8bdd5e418a40d5567a05615a030a5aeddea692d"}, - {file = "grpcio-1.76.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0b7604868b38c1bfd5cf72d768aedd7db41d78cb6a4a18585e33fb0f9f2363fd"}, - {file = "grpcio-1.76.0-cp39-cp39-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:e6d1db20594d9daba22f90da738b1a0441a7427552cc6e2e3d1297aeddc00378"}, - {file = "grpcio-1.76.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d099566accf23d21037f18a2a63d323075bebace807742e4b0ac210971d4dd70"}, - {file = "grpcio-1.76.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:ebea5cc3aa8ea72e04df9913492f9a96d9348db876f9dda3ad729cfedf7ac416"}, - {file = "grpcio-1.76.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:0c37db8606c258e2ee0c56b78c62fc9dee0e901b5dbdcf816c2dd4ad652b8b0c"}, - {file = "grpcio-1.76.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:ebebf83299b0cb1721a8859ea98f3a77811e35dce7609c5c963b9ad90728f886"}, - {file = "grpcio-1.76.0-cp39-cp39-win32.whl", hash = "sha256:0aaa82d0813fd4c8e589fac9b65d7dd88702555f702fb10417f96e2a2a6d4c0f"}, - {file = "grpcio-1.76.0-cp39-cp39-win_amd64.whl", hash = "sha256:acab0277c40eff7143c2323190ea57b9ee5fd353d8190ee9652369fae735668a"}, - {file = "grpcio-1.76.0.tar.gz", hash = "sha256:7be78388d6da1a25c0d5ec506523db58b18be22d9c37d8d3a32c08be4987bd73"}, -] - -[package.dependencies] -typing-extensions = ">=4.12,<5.0" - -[package.extras] -protobuf = ["grpcio-tools (>=1.76.0)"] - -[[package]] -name = "h11" -version = "0.16.0" -description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"}, - {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, -] - -[[package]] -name = "hf-xet" -version = "1.2.0" -description = "Fast transfer of large files with the Hugging Face Hub." -optional = false -python-versions = ">=3.8" -groups = ["main"] -markers = "platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"arm64\" or platform_machine == \"aarch64\"" -files = [ - {file = "hf_xet-1.2.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:ceeefcd1b7aed4956ae8499e2199607765fbd1c60510752003b6cc0b8413b649"}, - {file = "hf_xet-1.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b70218dd548e9840224df5638fdc94bd033552963cfa97f9170829381179c813"}, - {file = "hf_xet-1.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d40b18769bb9a8bc82a9ede575ce1a44c75eb80e7375a01d76259089529b5dc"}, - {file = "hf_xet-1.2.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:cd3a6027d59cfb60177c12d6424e31f4b5ff13d8e3a1247b3a584bf8977e6df5"}, - {file = "hf_xet-1.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6de1fc44f58f6dd937956c8d304d8c2dea264c80680bcfa61ca4a15e7b76780f"}, - {file = "hf_xet-1.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f182f264ed2acd566c514e45da9f2119110e48a87a327ca271027904c70c5832"}, - {file = "hf_xet-1.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:293a7a3787e5c95d7be1857358a9130694a9c6021de3f27fa233f37267174382"}, - {file = "hf_xet-1.2.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:10bfab528b968c70e062607f663e21e34e2bba349e8038db546646875495179e"}, - {file = "hf_xet-1.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2a212e842647b02eb6a911187dc878e79c4aa0aa397e88dd3b26761676e8c1f8"}, - {file = "hf_xet-1.2.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:30e06daccb3a7d4c065f34fc26c14c74f4653069bb2b194e7f18f17cbe9939c0"}, - {file = "hf_xet-1.2.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:29c8fc913a529ec0a91867ce3d119ac1aac966e098cf49501800c870328cc090"}, - {file = "hf_xet-1.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e159cbfcfbb29f920db2c09ed8b660eb894640d284f102ada929b6e3dc410a"}, - {file = "hf_xet-1.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9c91d5ae931510107f148874e9e2de8a16052b6f1b3ca3c1b12f15ccb491390f"}, - {file = "hf_xet-1.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:210d577732b519ac6ede149d2f2f34049d44e8622bf14eb3d63bbcd2d4b332dc"}, - {file = "hf_xet-1.2.0-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:46740d4ac024a7ca9b22bebf77460ff43332868b661186a8e46c227fdae01848"}, - {file = "hf_xet-1.2.0-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:27df617a076420d8845bea087f59303da8be17ed7ec0cd7ee3b9b9f579dff0e4"}, - {file = "hf_xet-1.2.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3651fd5bfe0281951b988c0facbe726aa5e347b103a675f49a3fa8144c7968fd"}, - {file = "hf_xet-1.2.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d06fa97c8562fb3ee7a378dd9b51e343bc5bc8190254202c9771029152f5e08c"}, - {file = "hf_xet-1.2.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4c1428c9ae73ec0939410ec73023c4f842927f39db09b063b9482dac5a3bb737"}, - {file = "hf_xet-1.2.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a55558084c16b09b5ed32ab9ed38421e2d87cf3f1f89815764d1177081b99865"}, - {file = "hf_xet-1.2.0-cp37-abi3-win_amd64.whl", hash = "sha256:e6584a52253f72c9f52f9e549d5895ca7a471608495c4ecaa6cc73dba2b24d69"}, - {file = "hf_xet-1.2.0.tar.gz", hash = "sha256:a8c27070ca547293b6890c4bf389f713f80e8c478631432962bb7f4bc0bd7d7f"}, -] - -[package.extras] -tests = ["pytest"] - -[[package]] -name = "httpcore" -version = "1.0.9" -description = "A minimal low-level HTTP client." -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"}, - {file = "httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"}, -] - -[package.dependencies] -certifi = "*" -h11 = ">=0.16" - -[package.extras] -asyncio = ["anyio (>=4.0,<5.0)"] -http2 = ["h2 (>=3,<5)"] -socks = ["socksio (==1.*)"] -trio = ["trio (>=0.22.0,<1.0)"] - -[[package]] -name = "httpx" -version = "0.27.2" -description = "The next generation HTTP client." -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0"}, - {file = "httpx-0.27.2.tar.gz", hash = "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2"}, -] - -[package.dependencies] -anyio = "*" -certifi = "*" -httpcore = "==1.*" -idna = "*" -sniffio = "*" - -[package.extras] -brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi ; platform_python_implementation != \"CPython\""] -cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] -http2 = ["h2 (>=3,<5)"] -socks = ["socksio (==1.*)"] -zstd = ["zstandard (>=0.18.0)"] - -[[package]] -name = "huggingface-hub" -version = "0.36.0" -description = "Client library to download and publish models, datasets and other repos on the huggingface.co hub" -optional = false -python-versions = ">=3.8.0" -groups = ["main"] -files = [ - {file = "huggingface_hub-0.36.0-py3-none-any.whl", hash = "sha256:7bcc9ad17d5b3f07b57c78e79d527102d08313caa278a641993acddcb894548d"}, - {file = "huggingface_hub-0.36.0.tar.gz", hash = "sha256:47b3f0e2539c39bf5cde015d63b72ec49baff67b6931c3d97f3f84532e2b8d25"}, -] - -[package.dependencies] -filelock = "*" -fsspec = ">=2023.5.0" -hf-xet = {version = ">=1.1.3,<2.0.0", markers = "platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"arm64\" or platform_machine == \"aarch64\""} -packaging = ">=20.9" -pyyaml = ">=5.1" -requests = "*" -tqdm = ">=4.42.1" -typing-extensions = ">=3.7.4.3" - -[package.extras] -all = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "authlib (>=1.3.2)", "fastapi", "gradio (>=4.0.0)", "httpx", "itsdangerous", "jedi", "libcst (>=1.4.0)", "mypy (==1.15.0) ; python_version >= \"3.9\"", "mypy (>=1.14.1,<1.15.0) ; python_version == \"3.8\"", "numpy", "pytest (>=8.1.1,<8.2.2)", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-mock", "pytest-rerunfailures (<16.0)", "pytest-vcr", "pytest-xdist", "ruff (>=0.9.0)", "soundfile", "ty", "types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)", "urllib3 (<2.0)"] -cli = ["InquirerPy (==0.3.4)"] -dev = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "authlib (>=1.3.2)", "fastapi", "gradio (>=4.0.0)", "httpx", "itsdangerous", "jedi", "libcst (>=1.4.0)", "mypy (==1.15.0) ; python_version >= \"3.9\"", "mypy (>=1.14.1,<1.15.0) ; python_version == \"3.8\"", "numpy", "pytest (>=8.1.1,<8.2.2)", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-mock", "pytest-rerunfailures (<16.0)", "pytest-vcr", "pytest-xdist", "ruff (>=0.9.0)", "soundfile", "ty", "types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)", "urllib3 (<2.0)"] -fastai = ["fastai (>=2.4)", "fastcore (>=1.3.27)", "toml"] -hf-transfer = ["hf-transfer (>=0.1.4)"] -hf-xet = ["hf-xet (>=1.1.2,<2.0.0)"] -inference = ["aiohttp"] -mcp = ["aiohttp", "mcp (>=1.8.0)", "typer"] -oauth = ["authlib (>=1.3.2)", "fastapi", "httpx", "itsdangerous"] -quality = ["libcst (>=1.4.0)", "mypy (==1.15.0) ; python_version >= \"3.9\"", "mypy (>=1.14.1,<1.15.0) ; python_version == \"3.8\"", "ruff (>=0.9.0)", "ty"] -tensorflow = ["graphviz", "pydot", "tensorflow"] -tensorflow-testing = ["keras (<3.0)", "tensorflow"] -testing = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "authlib (>=1.3.2)", "fastapi", "gradio (>=4.0.0)", "httpx", "itsdangerous", "jedi", "numpy", "pytest (>=8.1.1,<8.2.2)", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-mock", "pytest-rerunfailures (<16.0)", "pytest-vcr", "pytest-xdist", "soundfile", "urllib3 (<2.0)"] -torch = ["safetensors[torch]", "torch"] -typing = ["types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)"] - -[[package]] -name = "idna" -version = "3.11" -description = "Internationalized Domain Names in Applications (IDNA)" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea"}, - {file = "idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902"}, -] - -[package.extras] -all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] - -[[package]] -name = "importlib-metadata" -version = "8.7.0" -description = "Read metadata from Python packages" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd"}, - {file = "importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000"}, -] - -[package.dependencies] -zipp = ">=3.20" - -[package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -enabler = ["pytest-enabler (>=2.2)"] -perf = ["ipython"] -test = ["flufl.flake8", "importlib_resources (>=1.3) ; python_version < \"3.9\"", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] -type = ["pytest-mypy"] - -[[package]] -name = "iniconfig" -version = "2.3.0" -description = "brain-dead simple config-ini parsing" -optional = false -python-versions = ">=3.10" -groups = ["dev"] -files = [ - {file = "iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12"}, - {file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"}, -] - -[[package]] -name = "jinja2" -version = "3.1.6" -description = "A very fast and expressive template engine." -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, - {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, -] - -[package.dependencies] -MarkupSafe = ">=2.0" - -[package.extras] -i18n = ["Babel (>=2.7)"] - -[[package]] -name = "jiter" -version = "0.11.1" -description = "Fast iterable JSON parser." -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "jiter-0.11.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:ed58841a491bbbf3f7c55a6b68fff568439ab73b2cce27ace0e169057b5851df"}, - {file = "jiter-0.11.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:499beb9b2d7e51d61095a8de39ebcab1d1778f2a74085f8305a969f6cee9f3e4"}, - {file = "jiter-0.11.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b87b2821795e28cc990939b68ce7a038edea680a24910bd68a79d54ff3f03c02"}, - {file = "jiter-0.11.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:83f6fa494d8bba14ab100417c80e70d32d737e805cb85be2052d771c76fcd1f8"}, - {file = "jiter-0.11.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5fbc6aea1daa2ec6f5ed465f0c5e7b0607175062ceebbea5ca70dd5ddab58083"}, - {file = "jiter-0.11.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:302288e2edc43174bb2db838e94688d724f9aad26c5fb9a74f7a5fb427452a6a"}, - {file = "jiter-0.11.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85db563fe3b367bb568af5d29dea4d4066d923b8e01f3417d25ebecd958de815"}, - {file = "jiter-0.11.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f1c1ba2b6b22f775444ef53bc2d5778396d3520abc7b2e1da8eb0c27cb3ffb10"}, - {file = "jiter-0.11.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:523be464b14f8fd0cc78da6964b87b5515a056427a2579f9085ce30197a1b54a"}, - {file = "jiter-0.11.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:25b99b3f04cd2a38fefb22e822e35eb203a2cd37d680dbbc0c0ba966918af336"}, - {file = "jiter-0.11.1-cp310-cp310-win32.whl", hash = "sha256:47a79e90545a596bb9104109777894033347b11180d4751a216afef14072dbe7"}, - {file = "jiter-0.11.1-cp310-cp310-win_amd64.whl", hash = "sha256:cace75621ae9bd66878bf69fbd4dfc1a28ef8661e0c2d0eb72d3d6f1268eddf5"}, - {file = "jiter-0.11.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:9b0088ff3c374ce8ce0168523ec8e97122ebb788f950cf7bb8e39c7dc6a876a2"}, - {file = "jiter-0.11.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:74433962dd3c3090655e02e461267095d6c84f0741c7827de11022ef8d7ff661"}, - {file = "jiter-0.11.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6d98030e345e6546df2cc2c08309c502466c66c4747b043f1a0d415fada862b8"}, - {file = "jiter-0.11.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1d6db0b2e788db46bec2cf729a88b6dd36959af2abd9fa2312dfba5acdd96dcb"}, - {file = "jiter-0.11.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55678fbbda261eafe7289165dd2ddd0e922df5f9a1ae46d7c79a5a15242bd7d1"}, - {file = "jiter-0.11.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6a6b74fae8e40497653b52ce6ca0f1b13457af769af6fb9c1113efc8b5b4d9be"}, - {file = "jiter-0.11.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a55a453f8b035eb4f7852a79a065d616b7971a17f5e37a9296b4b38d3b619e4"}, - {file = "jiter-0.11.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2638148099022e6bdb3f42904289cd2e403609356fb06eb36ddec2d50958bc29"}, - {file = "jiter-0.11.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:252490567a5d990986f83b95a5f1ca1bf205ebd27b3e9e93bb7c2592380e29b9"}, - {file = "jiter-0.11.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d431d52b0ca2436eea6195f0f48528202100c7deda354cb7aac0a302167594d5"}, - {file = "jiter-0.11.1-cp311-cp311-win32.whl", hash = "sha256:db6f41e40f8bae20c86cb574b48c4fd9f28ee1c71cb044e9ec12e78ab757ba3a"}, - {file = "jiter-0.11.1-cp311-cp311-win_amd64.whl", hash = "sha256:0cc407b8e6cdff01b06bb80f61225c8b090c3df108ebade5e0c3c10993735b19"}, - {file = "jiter-0.11.1-cp311-cp311-win_arm64.whl", hash = "sha256:fe04ea475392a91896d1936367854d346724a1045a247e5d1c196410473b8869"}, - {file = "jiter-0.11.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:c92148eec91052538ce6823dfca9525f5cfc8b622d7f07e9891a280f61b8c96c"}, - {file = "jiter-0.11.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ecd4da91b5415f183a6be8f7158d127bdd9e6a3174138293c0d48d6ea2f2009d"}, - {file = "jiter-0.11.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7e3ac25c00b9275684d47aa42febaa90a9958e19fd1726c4ecf755fbe5e553b"}, - {file = "jiter-0.11.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:57d7305c0a841858f866cd459cd9303f73883fb5e097257f3d4a3920722c69d4"}, - {file = "jiter-0.11.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e86fa10e117dce22c547f31dd6d2a9a222707d54853d8de4e9a2279d2c97f239"}, - {file = "jiter-0.11.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ae5ef1d48aec7e01ee8420155d901bb1d192998fa811a65ebb82c043ee186711"}, - {file = "jiter-0.11.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb68e7bf65c990531ad8715e57d50195daf7c8e6f1509e617b4e692af1108939"}, - {file = "jiter-0.11.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:43b30c8154ded5845fa454ef954ee67bfccce629b2dea7d01f795b42bc2bda54"}, - {file = "jiter-0.11.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:586cafbd9dd1f3ce6a22b4a085eaa6be578e47ba9b18e198d4333e598a91db2d"}, - {file = "jiter-0.11.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:677cc2517d437a83bb30019fd4cf7cad74b465914c56ecac3440d597ac135250"}, - {file = "jiter-0.11.1-cp312-cp312-win32.whl", hash = "sha256:fa992af648fcee2b850a3286a35f62bbbaeddbb6dbda19a00d8fbc846a947b6e"}, - {file = "jiter-0.11.1-cp312-cp312-win_amd64.whl", hash = "sha256:88b5cae9fa51efeb3d4bd4e52bfd4c85ccc9cac44282e2a9640893a042ba4d87"}, - {file = "jiter-0.11.1-cp312-cp312-win_arm64.whl", hash = "sha256:9a6cae1ab335551917f882f2c3c1efe7617b71b4c02381e4382a8fc80a02588c"}, - {file = "jiter-0.11.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:71b6a920a5550f057d49d0e8bcc60945a8da998019e83f01adf110e226267663"}, - {file = "jiter-0.11.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0b3de72e925388453a5171be83379549300db01284f04d2a6f244d1d8de36f94"}, - {file = "jiter-0.11.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc19dd65a2bd3d9c044c5b4ebf657ca1e6003a97c0fc10f555aa4f7fb9821c00"}, - {file = "jiter-0.11.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d58faaa936743cd1464540562f60b7ce4fd927e695e8bc31b3da5b914baa9abd"}, - {file = "jiter-0.11.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:902640c3103625317291cb73773413b4d71847cdf9383ba65528745ff89f1d14"}, - {file = "jiter-0.11.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:30405f726e4c2ed487b176c09f8b877a957f535d60c1bf194abb8dadedb5836f"}, - {file = "jiter-0.11.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3217f61728b0baadd2551844870f65219ac4a1285d5e1a4abddff3d51fdabe96"}, - {file = "jiter-0.11.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b1364cc90c03a8196f35f396f84029f12abe925415049204446db86598c8b72c"}, - {file = "jiter-0.11.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:53a54bf8e873820ab186b2dca9f6c3303f00d65ae5e7b7d6bda1b95aa472d646"}, - {file = "jiter-0.11.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:7e29aca023627b0e0c2392d4248f6414d566ff3974fa08ff2ac8dbb96dfee92a"}, - {file = "jiter-0.11.1-cp313-cp313-win32.whl", hash = "sha256:f153e31d8bca11363751e875c0a70b3d25160ecbaee7b51e457f14498fb39d8b"}, - {file = "jiter-0.11.1-cp313-cp313-win_amd64.whl", hash = "sha256:f773f84080b667c69c4ea0403fc67bb08b07e2b7ce1ef335dea5868451e60fed"}, - {file = "jiter-0.11.1-cp313-cp313-win_arm64.whl", hash = "sha256:635ecd45c04e4c340d2187bcb1cea204c7cc9d32c1364d251564bf42e0e39c2d"}, - {file = "jiter-0.11.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d892b184da4d94d94ddb4031296931c74ec8b325513a541ebfd6dfb9ae89904b"}, - {file = "jiter-0.11.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa22c223a3041dacb2fcd37c70dfd648b44662b4a48e242592f95bda5ab09d58"}, - {file = "jiter-0.11.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:330e8e6a11ad4980cd66a0f4a3e0e2e0f646c911ce047014f984841924729789"}, - {file = "jiter-0.11.1-cp313-cp313t-win_amd64.whl", hash = "sha256:09e2e386ebf298547ca3a3704b729471f7ec666c2906c5c26c1a915ea24741ec"}, - {file = "jiter-0.11.1-cp313-cp313t-win_arm64.whl", hash = "sha256:fe4a431c291157e11cee7c34627990ea75e8d153894365a3bc84b7a959d23ca8"}, - {file = "jiter-0.11.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:0fa1f70da7a8a9713ff8e5f75ec3f90c0c870be6d526aa95e7c906f6a1c8c676"}, - {file = "jiter-0.11.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:569ee559e5046a42feb6828c55307cf20fe43308e3ae0d8e9e4f8d8634d99944"}, - {file = "jiter-0.11.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f69955fa1d92e81987f092b233f0be49d4c937da107b7f7dcf56306f1d3fcce9"}, - {file = "jiter-0.11.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:090f4c9d4a825e0fcbd0a2647c9a88a0f366b75654d982d95a9590745ff0c48d"}, - {file = "jiter-0.11.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bbf3d8cedf9e9d825233e0dcac28ff15c47b7c5512fdfe2e25fd5bbb6e6b0cee"}, - {file = "jiter-0.11.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2aa9b1958f9c30d3d1a558b75f0626733c60eb9b7774a86b34d88060be1e67fe"}, - {file = "jiter-0.11.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e42d1ca16590b768c5e7d723055acd2633908baacb3628dd430842e2e035aa90"}, - {file = "jiter-0.11.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5db4c2486a023820b701a17aec9c5a6173c5ba4393f26662f032f2de9c848b0f"}, - {file = "jiter-0.11.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:4573b78777ccfac954859a6eff45cbd9d281d80c8af049d0f1a3d9fc323d5c3a"}, - {file = "jiter-0.11.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:7593ac6f40831d7961cb67633c39b9fef6689a211d7919e958f45710504f52d3"}, - {file = "jiter-0.11.1-cp314-cp314-win32.whl", hash = "sha256:87202ec6ff9626ff5f9351507def98fcf0df60e9a146308e8ab221432228f4ea"}, - {file = "jiter-0.11.1-cp314-cp314-win_amd64.whl", hash = "sha256:a5dd268f6531a182c89d0dd9a3f8848e86e92dfff4201b77a18e6b98aa59798c"}, - {file = "jiter-0.11.1-cp314-cp314-win_arm64.whl", hash = "sha256:5d761f863f912a44748a21b5c4979c04252588ded8d1d2760976d2e42cd8d991"}, - {file = "jiter-0.11.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2cc5a3965285ddc33e0cab933e96b640bc9ba5940cea27ebbbf6695e72d6511c"}, - {file = "jiter-0.11.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b572b3636a784c2768b2342f36a23078c8d3aa6d8a30745398b1bab58a6f1a8"}, - {file = "jiter-0.11.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ad93e3d67a981f96596d65d2298fe8d1aa649deb5374a2fb6a434410ee11915e"}, - {file = "jiter-0.11.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a83097ce379e202dcc3fe3fc71a16d523d1ee9192c8e4e854158f96b3efe3f2f"}, - {file = "jiter-0.11.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7042c51e7fbeca65631eb0c332f90c0c082eab04334e7ccc28a8588e8e2804d9"}, - {file = "jiter-0.11.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a68d679c0e47649a61df591660507608adc2652442de7ec8276538ac46abe08"}, - {file = "jiter-0.11.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a1b0da75dbf4b6ec0b3c9e604d1ee8beaf15bc046fff7180f7d89e3cdbd3bb51"}, - {file = "jiter-0.11.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:69dd514bf0fa31c62147d6002e5ca2b3e7ef5894f5ac6f0a19752385f4e89437"}, - {file = "jiter-0.11.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:bb31ac0b339efa24c0ca606febd8b77ef11c58d09af1b5f2be4c99e907b11111"}, - {file = "jiter-0.11.1-cp314-cp314t-win32.whl", hash = "sha256:b2ce0d6156a1d3ad41da3eec63b17e03e296b78b0e0da660876fccfada86d2f7"}, - {file = "jiter-0.11.1-cp314-cp314t-win_amd64.whl", hash = "sha256:f4db07d127b54c4a2d43b4cf05ff0193e4f73e0dd90c74037e16df0b29f666e1"}, - {file = "jiter-0.11.1-cp314-cp314t-win_arm64.whl", hash = "sha256:28e4fdf2d7ebfc935523e50d1efa3970043cfaa161674fe66f9642409d001dfe"}, - {file = "jiter-0.11.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:baa99c8db49467527658bb479857344daf0a14dff909b7f6714579ac439d1253"}, - {file = "jiter-0.11.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:860fe55fa3b01ad0edf2adde1098247ff5c303d0121f9ce028c03d4f88c69502"}, - {file = "jiter-0.11.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:173dd349d99b6feaf5a25a6fbcaf3489a6f947708d808240587a23df711c67db"}, - {file = "jiter-0.11.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:14ac1dca837514cc946a6ac2c4995d9695303ecc754af70a3163d057d1a444ab"}, - {file = "jiter-0.11.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:69af47de5f93a231d5b85f7372d3284a5be8edb4cc758f006ec5a1406965ac5e"}, - {file = "jiter-0.11.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:685f8b3abd3bbd3e06e4dfe2429ff87fd5d7a782701151af99b1fcbd80e31b2b"}, - {file = "jiter-0.11.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d04afa2d4e5526e54ae8a58feea953b1844bf6e3526bc589f9de68e86d0ea01"}, - {file = "jiter-0.11.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1e92b927259035b50d8e11a8fdfe0ebd014d883e4552d37881643fa289a4bcf1"}, - {file = "jiter-0.11.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e7bd8be4fad8d4c5558b7801770cd2da6c072919c6f247cc5336edb143f25304"}, - {file = "jiter-0.11.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:121381a77a3c85987f3eba0d30ceaca9116f7463bedeec2fa79b2e7286b89b60"}, - {file = "jiter-0.11.1-cp39-cp39-win32.whl", hash = "sha256:160225407f6dfabdf9be1b44e22f06bc293a78a28ffa4347054698bd712dad06"}, - {file = "jiter-0.11.1-cp39-cp39-win_amd64.whl", hash = "sha256:028e0d59bcdfa1079f8df886cdaefc6f515c27a5288dec956999260c7e4a7cfd"}, - {file = "jiter-0.11.1-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:e642b5270e61dd02265866398707f90e365b5db2eb65a4f30c789d826682e1f6"}, - {file = "jiter-0.11.1-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:464ba6d000585e4e2fd1e891f31f1231f497273414f5019e27c00a4b8f7a24ad"}, - {file = "jiter-0.11.1-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:055568693ab35e0bf3a171b03bb40b2dcb10352359e0ab9b5ed0da2bf1eb6f6f"}, - {file = "jiter-0.11.1-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e0c69ea798d08a915ba4478113efa9e694971e410056392f4526d796f136d3fa"}, - {file = "jiter-0.11.1-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:0d4d6993edc83cf75e8c6828a8d6ce40a09ee87e38c7bfba6924f39e1337e21d"}, - {file = "jiter-0.11.1-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:f78d151c83a87a6cf5461d5ee55bc730dd9ae227377ac6f115b922989b95f838"}, - {file = "jiter-0.11.1-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9022974781155cd5521d5cb10997a03ee5e31e8454c9d999dcdccd253f2353f"}, - {file = "jiter-0.11.1-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18c77aaa9117510d5bdc6a946baf21b1f0cfa58ef04d31c8d016f206f2118960"}, - {file = "jiter-0.11.1.tar.gz", hash = "sha256:849dcfc76481c0ea0099391235b7ca97d7279e0fa4c86005457ac7c88e8b76dc"}, -] - -[[package]] -name = "markupsafe" -version = "3.0.3" -description = "Safely add untrusted strings to HTML/XML markup." -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -files = [ - {file = "markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559"}, - {file = "markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419"}, - {file = "markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695"}, - {file = "markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591"}, - {file = "markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c"}, - {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f"}, - {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6"}, - {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1"}, - {file = "markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa"}, - {file = "markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8"}, - {file = "markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1"}, - {file = "markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad"}, - {file = "markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a"}, - {file = "markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50"}, - {file = "markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf"}, - {file = "markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f"}, - {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a"}, - {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115"}, - {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a"}, - {file = "markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19"}, - {file = "markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01"}, - {file = "markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c"}, - {file = "markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e"}, - {file = "markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce"}, - {file = "markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d"}, - {file = "markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d"}, - {file = "markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a"}, - {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b"}, - {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f"}, - {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b"}, - {file = "markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d"}, - {file = "markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c"}, - {file = "markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f"}, - {file = "markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795"}, - {file = "markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219"}, - {file = "markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6"}, - {file = "markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676"}, - {file = "markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9"}, - {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1"}, - {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc"}, - {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12"}, - {file = "markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed"}, - {file = "markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5"}, - {file = "markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485"}, - {file = "markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73"}, - {file = "markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37"}, - {file = "markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19"}, - {file = "markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025"}, - {file = "markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6"}, - {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f"}, - {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb"}, - {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009"}, - {file = "markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354"}, - {file = "markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218"}, - {file = "markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287"}, - {file = "markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe"}, - {file = "markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026"}, - {file = "markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737"}, - {file = "markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97"}, - {file = "markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d"}, - {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda"}, - {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf"}, - {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe"}, - {file = "markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9"}, - {file = "markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581"}, - {file = "markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4"}, - {file = "markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab"}, - {file = "markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175"}, - {file = "markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634"}, - {file = "markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50"}, - {file = "markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e"}, - {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5"}, - {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523"}, - {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc"}, - {file = "markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d"}, - {file = "markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9"}, - {file = "markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa"}, - {file = "markupsafe-3.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26"}, - {file = "markupsafe-3.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc"}, - {file = "markupsafe-3.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c"}, - {file = "markupsafe-3.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42"}, - {file = "markupsafe-3.0.3-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b"}, - {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758"}, - {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2"}, - {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d"}, - {file = "markupsafe-3.0.3-cp39-cp39-win32.whl", hash = "sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7"}, - {file = "markupsafe-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e"}, - {file = "markupsafe-3.0.3-cp39-cp39-win_arm64.whl", hash = "sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8"}, - {file = "markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698"}, -] - -[[package]] -name = "mpmath" -version = "1.3.0" -description = "Python library for arbitrary-precision floating-point arithmetic" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c"}, - {file = "mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f"}, -] - -[package.extras] -develop = ["codecov", "pycodestyle", "pytest (>=4.6)", "pytest-cov", "wheel"] -docs = ["sphinx"] -gmpy = ["gmpy2 (>=2.1.0a4) ; platform_python_implementation != \"PyPy\""] -tests = ["pytest (>=4.6)"] - -[[package]] -name = "networkx" -version = "3.4.2" -description = "Python package for creating and manipulating graphs and networks" -optional = false -python-versions = ">=3.10" -groups = ["main"] -markers = "python_version < \"3.13\"" -files = [ - {file = "networkx-3.4.2-py3-none-any.whl", hash = "sha256:df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f"}, - {file = "networkx-3.4.2.tar.gz", hash = "sha256:307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1"}, -] - -[package.extras] -default = ["matplotlib (>=3.7)", "numpy (>=1.24)", "pandas (>=2.0)", "scipy (>=1.10,!=1.11.0,!=1.11.1)"] -developer = ["changelist (==0.5)", "mypy (>=1.1)", "pre-commit (>=3.2)", "rtoml"] -doc = ["intersphinx-registry", "myst-nb (>=1.1)", "numpydoc (>=1.8.0)", "pillow (>=9.4)", "pydata-sphinx-theme (>=0.15)", "sphinx (>=7.3)", "sphinx-gallery (>=0.16)", "texext (>=0.6.7)"] -example = ["cairocffi (>=1.7)", "contextily (>=1.6)", "igraph (>=0.11)", "momepy (>=0.7.2)", "osmnx (>=1.9)", "scikit-learn (>=1.5)", "seaborn (>=0.13)"] -extra = ["lxml (>=4.6)", "pydot (>=3.0.1)", "pygraphviz (>=1.14)", "sympy (>=1.10)"] -test = ["pytest (>=7.2)", "pytest-cov (>=4.0)"] - -[[package]] -name = "networkx" -version = "3.5" -description = "Python package for creating and manipulating graphs and networks" -optional = false -python-versions = ">=3.11" -groups = ["main"] -markers = "python_version >= \"3.13\"" -files = [ - {file = "networkx-3.5-py3-none-any.whl", hash = "sha256:0030d386a9a06dee3565298b4a734b68589749a544acbb6c412dc9e2489ec6ec"}, - {file = "networkx-3.5.tar.gz", hash = "sha256:d4c6f9cf81f52d69230866796b82afbccdec3db7ae4fbd1b65ea750feed50037"}, -] - -[package.extras] -default = ["matplotlib (>=3.8)", "numpy (>=1.25)", "pandas (>=2.0)", "scipy (>=1.11.2)"] -developer = ["mypy (>=1.15)", "pre-commit (>=4.1)"] -doc = ["intersphinx-registry", "myst-nb (>=1.1)", "numpydoc (>=1.8.0)", "pillow (>=10)", "pydata-sphinx-theme (>=0.16)", "sphinx (>=8.0)", "sphinx-gallery (>=0.18)", "texext (>=0.6.7)"] -example = ["cairocffi (>=1.7)", "contextily (>=1.6)", "igraph (>=0.11)", "momepy (>=0.7.2)", "osmnx (>=2.0.0)", "scikit-learn (>=1.5)", "seaborn (>=0.13)"] -extra = ["lxml (>=4.6)", "pydot (>=3.0.1)", "pygraphviz (>=1.14)", "sympy (>=1.10)"] -test = ["pytest (>=7.2)", "pytest-cov (>=4.0)", "pytest-xdist (>=3.0)"] -test-extras = ["pytest-mpl", "pytest-randomly"] - -[[package]] -name = "numpy" -version = "2.2.6" -description = "Fundamental package for array computing in Python" -optional = false -python-versions = ">=3.10" -groups = ["main"] -markers = "python_version < \"3.13\"" -files = [ - {file = "numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb"}, - {file = "numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90"}, - {file = "numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163"}, - {file = "numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf"}, - {file = "numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83"}, - {file = "numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915"}, - {file = "numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680"}, - {file = "numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289"}, - {file = "numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d"}, - {file = "numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3"}, - {file = "numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae"}, - {file = "numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a"}, - {file = "numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42"}, - {file = "numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491"}, - {file = "numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a"}, - {file = "numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf"}, - {file = "numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1"}, - {file = "numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab"}, - {file = "numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47"}, - {file = "numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303"}, - {file = "numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff"}, - {file = "numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c"}, - {file = "numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3"}, - {file = "numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282"}, - {file = "numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87"}, - {file = "numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249"}, - {file = "numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49"}, - {file = "numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de"}, - {file = "numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4"}, - {file = "numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2"}, - {file = "numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84"}, - {file = "numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b"}, - {file = "numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d"}, - {file = "numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566"}, - {file = "numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f"}, - {file = "numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f"}, - {file = "numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868"}, - {file = "numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d"}, - {file = "numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd"}, - {file = "numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c"}, - {file = "numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6"}, - {file = "numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda"}, - {file = "numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40"}, - {file = "numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8"}, - {file = "numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f"}, - {file = "numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa"}, - {file = "numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571"}, - {file = "numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1"}, - {file = "numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff"}, - {file = "numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06"}, - {file = "numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d"}, - {file = "numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db"}, - {file = "numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543"}, - {file = "numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00"}, - {file = "numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd"}, -] - -[[package]] -name = "numpy" -version = "2.3.4" -description = "Fundamental package for array computing in Python" -optional = false -python-versions = ">=3.11" -groups = ["main"] -markers = "python_version >= \"3.13\"" -files = [ - {file = "numpy-2.3.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e78aecd2800b32e8347ce49316d3eaf04aed849cd5b38e0af39f829a4e59f5eb"}, - {file = "numpy-2.3.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7fd09cc5d65bda1e79432859c40978010622112e9194e581e3415a3eccc7f43f"}, - {file = "numpy-2.3.4-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:1b219560ae2c1de48ead517d085bc2d05b9433f8e49d0955c82e8cd37bd7bf36"}, - {file = "numpy-2.3.4-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:bafa7d87d4c99752d07815ed7a2c0964f8ab311eb8168f41b910bd01d15b6032"}, - {file = "numpy-2.3.4-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36dc13af226aeab72b7abad501d370d606326a0029b9f435eacb3b8c94b8a8b7"}, - {file = "numpy-2.3.4-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7b2f9a18b5ff9824a6af80de4f37f4ec3c2aab05ef08f51c77a093f5b89adda"}, - {file = "numpy-2.3.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9984bd645a8db6ca15d850ff996856d8762c51a2239225288f08f9050ca240a0"}, - {file = "numpy-2.3.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:64c5825affc76942973a70acf438a8ab618dbd692b84cd5ec40a0a0509edc09a"}, - {file = "numpy-2.3.4-cp311-cp311-win32.whl", hash = "sha256:ed759bf7a70342f7817d88376eb7142fab9fef8320d6019ef87fae05a99874e1"}, - {file = "numpy-2.3.4-cp311-cp311-win_amd64.whl", hash = "sha256:faba246fb30ea2a526c2e9645f61612341de1a83fb1e0c5edf4ddda5a9c10996"}, - {file = "numpy-2.3.4-cp311-cp311-win_arm64.whl", hash = "sha256:4c01835e718bcebe80394fd0ac66c07cbb90147ebbdad3dcecd3f25de2ae7e2c"}, - {file = "numpy-2.3.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ef1b5a3e808bc40827b5fa2c8196151a4c5abe110e1726949d7abddfe5c7ae11"}, - {file = "numpy-2.3.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c2f91f496a87235c6aaf6d3f3d89b17dba64996abadccb289f48456cff931ca9"}, - {file = "numpy-2.3.4-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:f77e5b3d3da652b474cc80a14084927a5e86a5eccf54ca8ca5cbd697bf7f2667"}, - {file = "numpy-2.3.4-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:8ab1c5f5ee40d6e01cbe96de5863e39b215a4d24e7d007cad56c7184fdf4aeef"}, - {file = "numpy-2.3.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77b84453f3adcb994ddbd0d1c5d11db2d6bda1a2b7fd5ac5bd4649d6f5dc682e"}, - {file = "numpy-2.3.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4121c5beb58a7f9e6dfdee612cb24f4df5cd4db6e8261d7f4d7450a997a65d6a"}, - {file = "numpy-2.3.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:65611ecbb00ac9846efe04db15cbe6186f562f6bb7e5e05f077e53a599225d16"}, - {file = "numpy-2.3.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dabc42f9c6577bcc13001b8810d300fe814b4cfbe8a92c873f269484594f9786"}, - {file = "numpy-2.3.4-cp312-cp312-win32.whl", hash = "sha256:a49d797192a8d950ca59ee2d0337a4d804f713bb5c3c50e8db26d49666e351dc"}, - {file = "numpy-2.3.4-cp312-cp312-win_amd64.whl", hash = "sha256:985f1e46358f06c2a09921e8921e2c98168ed4ae12ccd6e5e87a4f1857923f32"}, - {file = "numpy-2.3.4-cp312-cp312-win_arm64.whl", hash = "sha256:4635239814149e06e2cb9db3dd584b2fa64316c96f10656983b8026a82e6e4db"}, - {file = "numpy-2.3.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c090d4860032b857d94144d1a9976b8e36709e40386db289aaf6672de2a81966"}, - {file = "numpy-2.3.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a13fc473b6db0be619e45f11f9e81260f7302f8d180c49a22b6e6120022596b3"}, - {file = "numpy-2.3.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:3634093d0b428e6c32c3a69b78e554f0cd20ee420dcad5a9f3b2a63762ce4197"}, - {file = "numpy-2.3.4-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:043885b4f7e6e232d7df4f51ffdef8c36320ee9d5f227b380ea636722c7ed12e"}, - {file = "numpy-2.3.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4ee6a571d1e4f0ea6d5f22d6e5fbd6ed1dc2b18542848e1e7301bd190500c9d7"}, - {file = "numpy-2.3.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fc8a63918b04b8571789688b2780ab2b4a33ab44bfe8ccea36d3eba51228c953"}, - {file = "numpy-2.3.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:40cc556d5abbc54aabe2b1ae287042d7bdb80c08edede19f0c0afb36ae586f37"}, - {file = "numpy-2.3.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ecb63014bb7f4ce653f8be7f1df8cbc6093a5a2811211770f6606cc92b5a78fd"}, - {file = "numpy-2.3.4-cp313-cp313-win32.whl", hash = "sha256:e8370eb6925bb8c1c4264fec52b0384b44f675f191df91cbe0140ec9f0955646"}, - {file = "numpy-2.3.4-cp313-cp313-win_amd64.whl", hash = "sha256:56209416e81a7893036eea03abcb91c130643eb14233b2515c90dcac963fe99d"}, - {file = "numpy-2.3.4-cp313-cp313-win_arm64.whl", hash = "sha256:a700a4031bc0fd6936e78a752eefb79092cecad2599ea9c8039c548bc097f9bc"}, - {file = "numpy-2.3.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:86966db35c4040fdca64f0816a1c1dd8dbd027d90fca5a57e00e1ca4cd41b879"}, - {file = "numpy-2.3.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:838f045478638b26c375ee96ea89464d38428c69170360b23a1a50fa4baa3562"}, - {file = "numpy-2.3.4-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d7315ed1dab0286adca467377c8381cd748f3dc92235f22a7dfc42745644a96a"}, - {file = "numpy-2.3.4-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:84f01a4d18b2cc4ade1814a08e5f3c907b079c847051d720fad15ce37aa930b6"}, - {file = "numpy-2.3.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:817e719a868f0dacde4abdfc5c1910b301877970195db9ab6a5e2c4bd5b121f7"}, - {file = "numpy-2.3.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85e071da78d92a214212cacea81c6da557cab307f2c34b5f85b628e94803f9c0"}, - {file = "numpy-2.3.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2ec646892819370cf3558f518797f16597b4e4669894a2ba712caccc9da53f1f"}, - {file = "numpy-2.3.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:035796aaaddfe2f9664b9a9372f089cfc88bd795a67bd1bfe15e6e770934cf64"}, - {file = "numpy-2.3.4-cp313-cp313t-win32.whl", hash = "sha256:fea80f4f4cf83b54c3a051f2f727870ee51e22f0248d3114b8e755d160b38cfb"}, - {file = "numpy-2.3.4-cp313-cp313t-win_amd64.whl", hash = "sha256:15eea9f306b98e0be91eb344a94c0e630689ef302e10c2ce5f7e11905c704f9c"}, - {file = "numpy-2.3.4-cp313-cp313t-win_arm64.whl", hash = "sha256:b6c231c9c2fadbae4011ca5e7e83e12dc4a5072f1a1d85a0a7b3ed754d145a40"}, - {file = "numpy-2.3.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:81c3e6d8c97295a7360d367f9f8553973651b76907988bb6066376bc2252f24e"}, - {file = "numpy-2.3.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7c26b0b2bf58009ed1f38a641f3db4be8d960a417ca96d14e5b06df1506d41ff"}, - {file = "numpy-2.3.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:62b2198c438058a20b6704351b35a1d7db881812d8512d67a69c9de1f18ca05f"}, - {file = "numpy-2.3.4-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:9d729d60f8d53a7361707f4b68a9663c968882dd4f09e0d58c044c8bf5faee7b"}, - {file = "numpy-2.3.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd0c630cf256b0a7fd9d0a11c9413b42fef5101219ce6ed5a09624f5a65392c7"}, - {file = "numpy-2.3.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5e081bc082825f8b139f9e9fe42942cb4054524598aaeb177ff476cc76d09d2"}, - {file = "numpy-2.3.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:15fb27364ed84114438fff8aaf998c9e19adbeba08c0b75409f8c452a8692c52"}, - {file = "numpy-2.3.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:85d9fb2d8cd998c84d13a79a09cc0c1091648e848e4e6249b0ccd7f6b487fa26"}, - {file = "numpy-2.3.4-cp314-cp314-win32.whl", hash = "sha256:e73d63fd04e3a9d6bc187f5455d81abfad05660b212c8804bf3b407e984cd2bc"}, - {file = "numpy-2.3.4-cp314-cp314-win_amd64.whl", hash = "sha256:3da3491cee49cf16157e70f607c03a217ea6647b1cea4819c4f48e53d49139b9"}, - {file = "numpy-2.3.4-cp314-cp314-win_arm64.whl", hash = "sha256:6d9cd732068e8288dbe2717177320723ccec4fb064123f0caf9bbd90ab5be868"}, - {file = "numpy-2.3.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:22758999b256b595cf0b1d102b133bb61866ba5ceecf15f759623b64c020c9ec"}, - {file = "numpy-2.3.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9cb177bc55b010b19798dc5497d540dea67fd13a8d9e882b2dae71de0cf09eb3"}, - {file = "numpy-2.3.4-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:0f2bcc76f1e05e5ab58893407c63d90b2029908fa41f9f1cc51eecce936c3365"}, - {file = "numpy-2.3.4-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:8dc20bde86802df2ed8397a08d793da0ad7a5fd4ea3ac85d757bf5dd4ad7c252"}, - {file = "numpy-2.3.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e199c087e2aa71c8f9ce1cb7a8e10677dc12457e7cc1be4798632da37c3e86e"}, - {file = "numpy-2.3.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85597b2d25ddf655495e2363fe044b0ae999b75bc4d630dc0d886484b03a5eb0"}, - {file = "numpy-2.3.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:04a69abe45b49c5955923cf2c407843d1c85013b424ae8a560bba16c92fe44a0"}, - {file = "numpy-2.3.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e1708fac43ef8b419c975926ce1eaf793b0c13b7356cfab6ab0dc34c0a02ac0f"}, - {file = "numpy-2.3.4-cp314-cp314t-win32.whl", hash = "sha256:863e3b5f4d9915aaf1b8ec79ae560ad21f0b8d5e3adc31e73126491bb86dee1d"}, - {file = "numpy-2.3.4-cp314-cp314t-win_amd64.whl", hash = "sha256:962064de37b9aef801d33bc579690f8bfe6c5e70e29b61783f60bcba838a14d6"}, - {file = "numpy-2.3.4-cp314-cp314t-win_arm64.whl", hash = "sha256:8b5a9a39c45d852b62693d9b3f3e0fe052541f804296ff401a72a1b60edafb29"}, - {file = "numpy-2.3.4-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:6e274603039f924c0fe5cb73438fa9246699c78a6df1bd3decef9ae592ae1c05"}, - {file = "numpy-2.3.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d149aee5c72176d9ddbc6803aef9c0f6d2ceeea7626574fc68518da5476fa346"}, - {file = "numpy-2.3.4-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:6d34ed9db9e6395bb6cd33286035f73a59b058169733a9db9f85e650b88df37e"}, - {file = "numpy-2.3.4-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:fdebe771ca06bb8d6abce84e51dca9f7921fe6ad34a0c914541b063e9a68928b"}, - {file = "numpy-2.3.4-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:957e92defe6c08211eb77902253b14fe5b480ebc5112bc741fd5e9cd0608f847"}, - {file = "numpy-2.3.4-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13b9062e4f5c7ee5c7e5be96f29ba71bc5a37fed3d1d77c37390ae00724d296d"}, - {file = "numpy-2.3.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:81b3a59793523e552c4a96109dde028aa4448ae06ccac5a76ff6532a85558a7f"}, - {file = "numpy-2.3.4.tar.gz", hash = "sha256:a7d018bfedb375a8d979ac758b120ba846a7fe764911a64465fd87b8729f4a6a"}, -] - -[[package]] -name = "nvidia-cublas-cu12" -version = "12.6.4.1" -description = "CUBLAS native runtime libraries" -optional = false -python-versions = ">=3" -groups = ["main"] -markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and python_version >= \"3.13\"" -files = [ - {file = "nvidia_cublas_cu12-12.6.4.1-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:08ed2686e9875d01b58e3cb379c6896df8e76c75e0d4a7f7dace3d7b6d9ef8eb"}, - {file = "nvidia_cublas_cu12-12.6.4.1-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:235f728d6e2a409eddf1df58d5b0921cf80cfa9e72b9f2775ccb7b4a87984668"}, - {file = "nvidia_cublas_cu12-12.6.4.1-py3-none-win_amd64.whl", hash = "sha256:9e4fa264f4d8a4eb0cdbd34beadc029f453b3bafae02401e999cf3d5a5af75f8"}, -] - -[[package]] -name = "nvidia-cublas-cu12" -version = "12.8.4.1" -description = "CUBLAS native runtime libraries" -optional = false -python-versions = ">=3" -groups = ["main"] -markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and python_version < \"3.13\"" -files = [ - {file = "nvidia_cublas_cu12-12.8.4.1-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:b86f6dd8935884615a0683b663891d43781b819ac4f2ba2b0c9604676af346d0"}, - {file = "nvidia_cublas_cu12-12.8.4.1-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:8ac4e771d5a348c551b2a426eda6193c19aa630236b418086020df5ba9667142"}, - {file = "nvidia_cublas_cu12-12.8.4.1-py3-none-win_amd64.whl", hash = "sha256:47e9b82132fa8d2b4944e708049229601448aaad7e6f296f630f2d1a32de35af"}, -] - -[[package]] -name = "nvidia-cuda-cupti-cu12" -version = "12.6.80" -description = "CUDA profiling tools runtime libs." -optional = false -python-versions = ">=3" -groups = ["main"] -markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and python_version >= \"3.13\"" -files = [ - {file = "nvidia_cuda_cupti_cu12-12.6.80-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:166ee35a3ff1587f2490364f90eeeb8da06cd867bd5b701bf7f9a02b78bc63fc"}, - {file = "nvidia_cuda_cupti_cu12-12.6.80-py3-none-manylinux2014_aarch64.whl", hash = "sha256:358b4a1d35370353d52e12f0a7d1769fc01ff74a191689d3870b2123156184c4"}, - {file = "nvidia_cuda_cupti_cu12-12.6.80-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6768bad6cab4f19e8292125e5f1ac8aa7d1718704012a0e3272a6f61c4bce132"}, - {file = "nvidia_cuda_cupti_cu12-12.6.80-py3-none-manylinux2014_x86_64.whl", hash = "sha256:a3eff6cdfcc6a4c35db968a06fcadb061cbc7d6dde548609a941ff8701b98b73"}, - {file = "nvidia_cuda_cupti_cu12-12.6.80-py3-none-win_amd64.whl", hash = "sha256:bbe6ae76e83ce5251b56e8c8e61a964f757175682bbad058b170b136266ab00a"}, -] - -[[package]] -name = "nvidia-cuda-cupti-cu12" -version = "12.8.90" -description = "CUDA profiling tools runtime libs." -optional = false -python-versions = ">=3" -groups = ["main"] -markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and python_version < \"3.13\"" -files = [ - {file = "nvidia_cuda_cupti_cu12-12.8.90-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4412396548808ddfed3f17a467b104ba7751e6b58678a4b840675c56d21cf7ed"}, - {file = "nvidia_cuda_cupti_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ea0cb07ebda26bb9b29ba82cda34849e73c166c18162d3913575b0c9db9a6182"}, - {file = "nvidia_cuda_cupti_cu12-12.8.90-py3-none-win_amd64.whl", hash = "sha256:bb479dcdf7e6d4f8b0b01b115260399bf34154a1a2e9fe11c85c517d87efd98e"}, -] - -[[package]] -name = "nvidia-cuda-nvrtc-cu12" -version = "12.6.77" -description = "NVRTC native runtime libraries" -optional = false -python-versions = ">=3" -groups = ["main"] -markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and python_version >= \"3.13\"" -files = [ - {file = "nvidia_cuda_nvrtc_cu12-12.6.77-py3-none-manylinux2014_aarch64.whl", hash = "sha256:5847f1d6e5b757f1d2b3991a01082a44aad6f10ab3c5c0213fa3e25bddc25a13"}, - {file = "nvidia_cuda_nvrtc_cu12-12.6.77-py3-none-manylinux2014_x86_64.whl", hash = "sha256:35b0cc6ee3a9636d5409133e79273ce1f3fd087abb0532d2d2e8fff1fe9efc53"}, - {file = "nvidia_cuda_nvrtc_cu12-12.6.77-py3-none-win_amd64.whl", hash = "sha256:f7007dbd914c56bd80ea31bc43e8e149da38f68158f423ba845fc3292684e45a"}, -] - -[[package]] -name = "nvidia-cuda-nvrtc-cu12" -version = "12.8.93" -description = "NVRTC native runtime libraries" -optional = false -python-versions = ">=3" -groups = ["main"] -markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and python_version < \"3.13\"" -files = [ - {file = "nvidia_cuda_nvrtc_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:a7756528852ef889772a84c6cd89d41dfa74667e24cca16bb31f8f061e3e9994"}, - {file = "nvidia_cuda_nvrtc_cu12-12.8.93-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fc1fec1e1637854b4c0a65fb9a8346b51dd9ee69e61ebaccc82058441f15bce8"}, - {file = "nvidia_cuda_nvrtc_cu12-12.8.93-py3-none-win_amd64.whl", hash = "sha256:7a4b6b2904850fe78e0bd179c4b655c404d4bb799ef03ddc60804247099ae909"}, -] - -[[package]] -name = "nvidia-cuda-runtime-cu12" -version = "12.6.77" -description = "CUDA Runtime native Libraries" -optional = false -python-versions = ">=3" -groups = ["main"] -markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and python_version >= \"3.13\"" -files = [ - {file = "nvidia_cuda_runtime_cu12-12.6.77-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6116fad3e049e04791c0256a9778c16237837c08b27ed8c8401e2e45de8d60cd"}, - {file = "nvidia_cuda_runtime_cu12-12.6.77-py3-none-manylinux2014_aarch64.whl", hash = "sha256:d461264ecb429c84c8879a7153499ddc7b19b5f8d84c204307491989a365588e"}, - {file = "nvidia_cuda_runtime_cu12-12.6.77-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ba3b56a4f896141e25e19ab287cd71e52a6a0f4b29d0d31609f60e3b4d5219b7"}, - {file = "nvidia_cuda_runtime_cu12-12.6.77-py3-none-manylinux2014_x86_64.whl", hash = "sha256:a84d15d5e1da416dd4774cb42edf5e954a3e60cc945698dc1d5be02321c44dc8"}, - {file = "nvidia_cuda_runtime_cu12-12.6.77-py3-none-win_amd64.whl", hash = "sha256:86c58044c824bf3c173c49a2dbc7a6c8b53cb4e4dca50068be0bf64e9dab3f7f"}, -] - -[[package]] -name = "nvidia-cuda-runtime-cu12" -version = "12.8.90" -description = "CUDA Runtime native Libraries" -optional = false -python-versions = ">=3" -groups = ["main"] -markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and python_version < \"3.13\"" -files = [ - {file = "nvidia_cuda_runtime_cu12-12.8.90-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:52bf7bbee900262ffefe5e9d5a2a69a30d97e2bc5bb6cc866688caa976966e3d"}, - {file = "nvidia_cuda_runtime_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:adade8dcbd0edf427b7204d480d6066d33902cab2a4707dcfc48a2d0fd44ab90"}, - {file = "nvidia_cuda_runtime_cu12-12.8.90-py3-none-win_amd64.whl", hash = "sha256:c0c6027f01505bfed6c3b21ec546f69c687689aad5f1a377554bc6ca4aa993a8"}, -] - -[[package]] -name = "nvidia-cudnn-cu12" -version = "9.5.1.17" -description = "cuDNN runtime libraries" -optional = false -python-versions = ">=3" -groups = ["main"] -markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and python_version >= \"3.13\"" -files = [ - {file = "nvidia_cudnn_cu12-9.5.1.17-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:9fd4584468533c61873e5fda8ca41bac3a38bcb2d12350830c69b0a96a7e4def"}, - {file = "nvidia_cudnn_cu12-9.5.1.17-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:30ac3869f6db17d170e0e556dd6cc5eee02647abc31ca856634d5a40f82c15b2"}, - {file = "nvidia_cudnn_cu12-9.5.1.17-py3-none-win_amd64.whl", hash = "sha256:d7af0f8a4f3b4b9dbb3122f2ef553b45694ed9c384d5a75bab197b8eefb79ab8"}, -] - -[package.dependencies] -nvidia-cublas-cu12 = "*" - -[[package]] -name = "nvidia-cudnn-cu12" -version = "9.10.2.21" -description = "cuDNN runtime libraries" -optional = false -python-versions = ">=3" -groups = ["main"] -markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and python_version < \"3.13\"" -files = [ - {file = "nvidia_cudnn_cu12-9.10.2.21-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:c9132cc3f8958447b4910a1720036d9eff5928cc3179b0a51fb6d167c6cc87d8"}, - {file = "nvidia_cudnn_cu12-9.10.2.21-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:949452be657fa16687d0930933f032835951ef0892b37d2d53824d1a84dc97a8"}, - {file = "nvidia_cudnn_cu12-9.10.2.21-py3-none-win_amd64.whl", hash = "sha256:c6288de7d63e6cf62988f0923f96dc339cea362decb1bf5b3141883392a7d65e"}, -] - -[package.dependencies] -nvidia-cublas-cu12 = "*" - -[[package]] -name = "nvidia-cufft-cu12" -version = "11.3.0.4" -description = "CUFFT native runtime libraries" -optional = false -python-versions = ">=3" -groups = ["main"] -markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and python_version >= \"3.13\"" -files = [ - {file = "nvidia_cufft_cu12-11.3.0.4-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d16079550df460376455cba121db6564089176d9bac9e4f360493ca4741b22a6"}, - {file = "nvidia_cufft_cu12-11.3.0.4-py3-none-manylinux2014_aarch64.whl", hash = "sha256:8510990de9f96c803a051822618d42bf6cb8f069ff3f48d93a8486efdacb48fb"}, - {file = "nvidia_cufft_cu12-11.3.0.4-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ccba62eb9cef5559abd5e0d54ceed2d9934030f51163df018532142a8ec533e5"}, - {file = "nvidia_cufft_cu12-11.3.0.4-py3-none-manylinux2014_x86_64.whl", hash = "sha256:768160ac89f6f7b459bee747e8d175dbf53619cfe74b2a5636264163138013ca"}, - {file = "nvidia_cufft_cu12-11.3.0.4-py3-none-win_amd64.whl", hash = "sha256:6048ebddfb90d09d2707efb1fd78d4e3a77cb3ae4dc60e19aab6be0ece2ae464"}, -] - -[package.dependencies] -nvidia-nvjitlink-cu12 = "*" - -[[package]] -name = "nvidia-cufft-cu12" -version = "11.3.3.83" -description = "CUFFT native runtime libraries" -optional = false -python-versions = ">=3" -groups = ["main"] -markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and python_version < \"3.13\"" -files = [ - {file = "nvidia_cufft_cu12-11.3.3.83-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:848ef7224d6305cdb2a4df928759dca7b1201874787083b6e7550dd6765ce69a"}, - {file = "nvidia_cufft_cu12-11.3.3.83-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d2dd21ec0b88cf61b62e6b43564355e5222e4a3fb394cac0db101f2dd0d4f74"}, - {file = "nvidia_cufft_cu12-11.3.3.83-py3-none-win_amd64.whl", hash = "sha256:7a64a98ef2a7c47f905aaf8931b69a3a43f27c55530c698bb2ed7c75c0b42cb7"}, -] - -[package.dependencies] -nvidia-nvjitlink-cu12 = "*" - -[[package]] -name = "nvidia-cufile-cu12" -version = "1.11.1.6" -description = "cuFile GPUDirect libraries" -optional = false -python-versions = ">=3" -groups = ["main"] -markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and python_version >= \"3.13\"" -files = [ - {file = "nvidia_cufile_cu12-1.11.1.6-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc23469d1c7e52ce6c1d55253273d32c565dd22068647f3aa59b3c6b005bf159"}, - {file = "nvidia_cufile_cu12-1.11.1.6-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:8f57a0051dcf2543f6dc2b98a98cb2719c37d3cee1baba8965d57f3bbc90d4db"}, -] - -[[package]] -name = "nvidia-cufile-cu12" -version = "1.13.1.3" -description = "cuFile GPUDirect libraries" -optional = false -python-versions = ">=3" -groups = ["main"] -markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and python_version < \"3.13\"" -files = [ - {file = "nvidia_cufile_cu12-1.13.1.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1d069003be650e131b21c932ec3d8969c1715379251f8d23a1860554b1cb24fc"}, - {file = "nvidia_cufile_cu12-1.13.1.3-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:4beb6d4cce47c1a0f1013d72e02b0994730359e17801d395bdcbf20cfb3bb00a"}, -] - -[[package]] -name = "nvidia-curand-cu12" -version = "10.3.7.77" -description = "CURAND native runtime libraries" -optional = false -python-versions = ">=3" -groups = ["main"] -markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and python_version >= \"3.13\"" -files = [ - {file = "nvidia_curand_cu12-10.3.7.77-py3-none-manylinux2014_aarch64.whl", hash = "sha256:6e82df077060ea28e37f48a3ec442a8f47690c7499bff392a5938614b56c98d8"}, - {file = "nvidia_curand_cu12-10.3.7.77-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a42cd1344297f70b9e39a1e4f467a4e1c10f1da54ff7a85c12197f6c652c8bdf"}, - {file = "nvidia_curand_cu12-10.3.7.77-py3-none-manylinux2014_x86_64.whl", hash = "sha256:99f1a32f1ac2bd134897fc7a203f779303261268a65762a623bf30cc9fe79117"}, - {file = "nvidia_curand_cu12-10.3.7.77-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:7b2ed8e95595c3591d984ea3603dd66fe6ce6812b886d59049988a712ed06b6e"}, - {file = "nvidia_curand_cu12-10.3.7.77-py3-none-win_amd64.whl", hash = "sha256:6d6d935ffba0f3d439b7cd968192ff068fafd9018dbf1b85b37261b13cfc9905"}, -] - -[[package]] -name = "nvidia-curand-cu12" -version = "10.3.9.90" -description = "CURAND native runtime libraries" -optional = false -python-versions = ">=3" -groups = ["main"] -markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and python_version < \"3.13\"" -files = [ - {file = "nvidia_curand_cu12-10.3.9.90-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:dfab99248034673b779bc6decafdc3404a8a6f502462201f2f31f11354204acd"}, - {file = "nvidia_curand_cu12-10.3.9.90-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:b32331d4f4df5d6eefa0554c565b626c7216f87a06a4f56fab27c3b68a830ec9"}, - {file = "nvidia_curand_cu12-10.3.9.90-py3-none-win_amd64.whl", hash = "sha256:f149a8ca457277da854f89cf282d6ef43176861926c7ac85b2a0fbd237c587ec"}, -] - -[[package]] -name = "nvidia-cusolver-cu12" -version = "11.7.1.2" -description = "CUDA solver native runtime libraries" -optional = false -python-versions = ">=3" -groups = ["main"] -markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and python_version >= \"3.13\"" -files = [ - {file = "nvidia_cusolver_cu12-11.7.1.2-py3-none-manylinux2014_aarch64.whl", hash = "sha256:0ce237ef60acde1efc457335a2ddadfd7610b892d94efee7b776c64bb1cac9e0"}, - {file = "nvidia_cusolver_cu12-11.7.1.2-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e9e49843a7707e42022babb9bcfa33c29857a93b88020c4e4434656a655b698c"}, - {file = "nvidia_cusolver_cu12-11.7.1.2-py3-none-manylinux2014_x86_64.whl", hash = "sha256:6cf28f17f64107a0c4d7802be5ff5537b2130bfc112f25d5a30df227058ca0e6"}, - {file = "nvidia_cusolver_cu12-11.7.1.2-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:dbbe4fc38ec1289c7e5230e16248365e375c3673c9c8bac5796e2e20db07f56e"}, - {file = "nvidia_cusolver_cu12-11.7.1.2-py3-none-win_amd64.whl", hash = "sha256:6813f9d8073f555444a8705f3ab0296d3e1cb37a16d694c5fc8b862a0d8706d7"}, -] - -[package.dependencies] -nvidia-cublas-cu12 = "*" -nvidia-cusparse-cu12 = "*" -nvidia-nvjitlink-cu12 = "*" - -[[package]] -name = "nvidia-cusolver-cu12" -version = "11.7.3.90" -description = "CUDA solver native runtime libraries" -optional = false -python-versions = ">=3" -groups = ["main"] -markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and python_version < \"3.13\"" -files = [ - {file = "nvidia_cusolver_cu12-11.7.3.90-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:db9ed69dbef9715071232caa9b69c52ac7de3a95773c2db65bdba85916e4e5c0"}, - {file = "nvidia_cusolver_cu12-11.7.3.90-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:4376c11ad263152bd50ea295c05370360776f8c3427b30991df774f9fb26c450"}, - {file = "nvidia_cusolver_cu12-11.7.3.90-py3-none-win_amd64.whl", hash = "sha256:4a550db115fcabc4d495eb7d39ac8b58d4ab5d8e63274d3754df1c0ad6a22d34"}, -] - -[package.dependencies] -nvidia-cublas-cu12 = "*" -nvidia-cusparse-cu12 = "*" -nvidia-nvjitlink-cu12 = "*" - -[[package]] -name = "nvidia-cusparse-cu12" -version = "12.5.4.2" -description = "CUSPARSE native runtime libraries" -optional = false -python-versions = ">=3" -groups = ["main"] -markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and python_version >= \"3.13\"" -files = [ - {file = "nvidia_cusparse_cu12-12.5.4.2-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d25b62fb18751758fe3c93a4a08eff08effedfe4edf1c6bb5afd0890fe88f887"}, - {file = "nvidia_cusparse_cu12-12.5.4.2-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7aa32fa5470cf754f72d1116c7cbc300b4e638d3ae5304cfa4a638a5b87161b1"}, - {file = "nvidia_cusparse_cu12-12.5.4.2-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7556d9eca156e18184b94947ade0fba5bb47d69cec46bf8660fd2c71a4b48b73"}, - {file = "nvidia_cusparse_cu12-12.5.4.2-py3-none-manylinux2014_x86_64.whl", hash = "sha256:23749a6571191a215cb74d1cdbff4a86e7b19f1200c071b3fcf844a5bea23a2f"}, - {file = "nvidia_cusparse_cu12-12.5.4.2-py3-none-win_amd64.whl", hash = "sha256:4acb8c08855a26d737398cba8fb6f8f5045d93f82612b4cfd84645a2332ccf20"}, -] - -[package.dependencies] -nvidia-nvjitlink-cu12 = "*" - -[[package]] -name = "nvidia-cusparse-cu12" -version = "12.5.8.93" -description = "CUSPARSE native runtime libraries" -optional = false -python-versions = ">=3" -groups = ["main"] -markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and python_version < \"3.13\"" -files = [ - {file = "nvidia_cusparse_cu12-12.5.8.93-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9b6c161cb130be1a07a27ea6923df8141f3c295852f4b260c65f18f3e0a091dc"}, - {file = "nvidia_cusparse_cu12-12.5.8.93-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ec05d76bbbd8b61b06a80e1eaf8cf4959c3d4ce8e711b65ebd0443bb0ebb13b"}, - {file = "nvidia_cusparse_cu12-12.5.8.93-py3-none-win_amd64.whl", hash = "sha256:9a33604331cb2cac199f2e7f5104dfbb8a5a898c367a53dfda9ff2acb6b6b4dd"}, -] - -[package.dependencies] -nvidia-nvjitlink-cu12 = "*" - -[[package]] -name = "nvidia-cusparselt-cu12" -version = "0.6.3" -description = "NVIDIA cuSPARSELt" -optional = false -python-versions = "*" -groups = ["main"] -markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and python_version >= \"3.13\"" -files = [ - {file = "nvidia_cusparselt_cu12-0.6.3-py3-none-manylinux2014_aarch64.whl", hash = "sha256:8371549623ba601a06322af2133c4a44350575f5a3108fb75f3ef20b822ad5f1"}, - {file = "nvidia_cusparselt_cu12-0.6.3-py3-none-manylinux2014_x86_64.whl", hash = "sha256:e5c8a26c36445dd2e6812f1177978a24e2d37cacce7e090f297a688d1ec44f46"}, - {file = "nvidia_cusparselt_cu12-0.6.3-py3-none-win_amd64.whl", hash = "sha256:3b325bcbd9b754ba43df5a311488fca11a6b5dc3d11df4d190c000cf1a0765c7"}, -] - -[[package]] -name = "nvidia-cusparselt-cu12" -version = "0.7.1" -description = "NVIDIA cuSPARSELt" -optional = false -python-versions = "*" -groups = ["main"] -markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and python_version < \"3.13\"" -files = [ - {file = "nvidia_cusparselt_cu12-0.7.1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:8878dce784d0fac90131b6817b607e803c36e629ba34dc5b433471382196b6a5"}, - {file = "nvidia_cusparselt_cu12-0.7.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:f1bb701d6b930d5a7cea44c19ceb973311500847f81b634d802b7b539dc55623"}, - {file = "nvidia_cusparselt_cu12-0.7.1-py3-none-win_amd64.whl", hash = "sha256:f67fbb5831940ec829c9117b7f33807db9f9678dc2a617fbe781cac17b4e1075"}, -] - -[[package]] -name = "nvidia-nccl-cu12" -version = "2.26.2" -description = "NVIDIA Collective Communication Library (NCCL) Runtime" -optional = false -python-versions = ">=3" -groups = ["main"] -markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and python_version >= \"3.13\"" -files = [ - {file = "nvidia_nccl_cu12-2.26.2-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c196e95e832ad30fbbb50381eb3cbd1fadd5675e587a548563993609af19522"}, - {file = "nvidia_nccl_cu12-2.26.2-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:694cf3879a206553cc9d7dbda76b13efaf610fdb70a50cba303de1b0d1530ac6"}, -] - -[[package]] -name = "nvidia-nccl-cu12" -version = "2.27.5" -description = "NVIDIA Collective Communication Library (NCCL) Runtime" -optional = false -python-versions = ">=3" -groups = ["main"] -markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and python_version < \"3.13\"" -files = [ - {file = "nvidia_nccl_cu12-2.27.5-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:31432ad4d1fb1004eb0c56203dc9bc2178a1ba69d1d9e02d64a6938ab5e40e7a"}, - {file = "nvidia_nccl_cu12-2.27.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ad730cf15cb5d25fe849c6e6ca9eb5b76db16a80f13f425ac68d8e2e55624457"}, -] - -[[package]] -name = "nvidia-nvjitlink-cu12" -version = "12.6.85" -description = "Nvidia JIT LTO Library" -optional = false -python-versions = ">=3" -groups = ["main"] -markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and python_version >= \"3.13\"" -files = [ - {file = "nvidia_nvjitlink_cu12-12.6.85-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:eedc36df9e88b682efe4309aa16b5b4e78c2407eac59e8c10a6a47535164369a"}, - {file = "nvidia_nvjitlink_cu12-12.6.85-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cf4eaa7d4b6b543ffd69d6abfb11efdeb2db48270d94dfd3a452c24150829e41"}, - {file = "nvidia_nvjitlink_cu12-12.6.85-py3-none-win_amd64.whl", hash = "sha256:e61120e52ed675747825cdd16febc6a0730537451d867ee58bee3853b1b13d1c"}, -] - -[[package]] -name = "nvidia-nvjitlink-cu12" -version = "12.8.93" -description = "Nvidia JIT LTO Library" -optional = false -python-versions = ">=3" -groups = ["main"] -markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and python_version < \"3.13\"" -files = [ - {file = "nvidia_nvjitlink_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:81ff63371a7ebd6e6451970684f916be2eab07321b73c9d244dc2b4da7f73b88"}, - {file = "nvidia_nvjitlink_cu12-12.8.93-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:adccd7161ace7261e01bb91e44e88da350895c270d23f744f0820c818b7229e7"}, - {file = "nvidia_nvjitlink_cu12-12.8.93-py3-none-win_amd64.whl", hash = "sha256:bd93fbeeee850917903583587f4fc3a4eafa022e34572251368238ab5e6bd67f"}, -] - -[[package]] -name = "nvidia-nvshmem-cu12" -version = "3.3.20" -description = "NVSHMEM creates a global address space that provides efficient and scalable communication for NVIDIA GPU clusters." -optional = false -python-versions = ">=3" -groups = ["main"] -markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and python_version < \"3.13\"" -files = [ - {file = "nvidia_nvshmem_cu12-3.3.20-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0b0b960da3842212758e4fa4696b94f129090b30e5122fea3c5345916545cff0"}, - {file = "nvidia_nvshmem_cu12-3.3.20-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d00f26d3f9b2e3c3065be895e3059d6479ea5c638a3f38c9fec49b1b9dd7c1e5"}, -] - -[[package]] -name = "nvidia-nvtx-cu12" -version = "12.6.77" -description = "NVIDIA Tools Extension" -optional = false -python-versions = ">=3" -groups = ["main"] -markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and python_version >= \"3.13\"" -files = [ - {file = "nvidia_nvtx_cu12-12.6.77-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f44f8d86bb7d5629988d61c8d3ae61dddb2015dee142740536bc7481b022fe4b"}, - {file = "nvidia_nvtx_cu12-12.6.77-py3-none-manylinux2014_aarch64.whl", hash = "sha256:adcaabb9d436c9761fca2b13959a2d237c5f9fd406c8e4b723c695409ff88059"}, - {file = "nvidia_nvtx_cu12-12.6.77-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b90bed3df379fa79afbd21be8e04a0314336b8ae16768b58f2d34cb1d04cd7d2"}, - {file = "nvidia_nvtx_cu12-12.6.77-py3-none-manylinux2014_x86_64.whl", hash = "sha256:6574241a3ec5fdc9334353ab8c479fe75841dbe8f4532a8fc97ce63503330ba1"}, - {file = "nvidia_nvtx_cu12-12.6.77-py3-none-win_amd64.whl", hash = "sha256:2fb11a4af04a5e6c84073e6404d26588a34afd35379f0855a99797897efa75c0"}, -] - -[[package]] -name = "nvidia-nvtx-cu12" -version = "12.8.90" -description = "NVIDIA Tools Extension" -optional = false -python-versions = ">=3" -groups = ["main"] -markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and python_version < \"3.13\"" -files = [ - {file = "nvidia_nvtx_cu12-12.8.90-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d7ad891da111ebafbf7e015d34879f7112832fc239ff0d7d776b6cb685274615"}, - {file = "nvidia_nvtx_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5b17e2001cc0d751a5bc2c6ec6d26ad95913324a4adb86788c944f8ce9ba441f"}, - {file = "nvidia_nvtx_cu12-12.8.90-py3-none-win_amd64.whl", hash = "sha256:619c8304aedc69f02ea82dd244541a83c3d9d40993381b3b590f1adaed3db41e"}, -] - -[[package]] -name = "openai" -version = "1.109.1" -description = "The official Python library for the openai API" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "openai-1.109.1-py3-none-any.whl", hash = "sha256:6bcaf57086cf59159b8e27447e4e7dd019db5d29a438072fbd49c290c7e65315"}, - {file = "openai-1.109.1.tar.gz", hash = "sha256:d173ed8dbca665892a6db099b4a2dfac624f94d20a93f46eb0b56aae940ed869"}, -] - -[package.dependencies] -anyio = ">=3.5.0,<5" -distro = ">=1.7.0,<2" -httpx = ">=0.23.0,<1" -jiter = ">=0.4.0,<1" -pydantic = ">=1.9.0,<3" -sniffio = "*" -tqdm = ">4" -typing-extensions = ">=4.11,<5" - -[package.extras] -aiohttp = ["aiohttp", "httpx-aiohttp (>=0.1.8)"] -datalib = ["numpy (>=1)", "pandas (>=1.2.3)", "pandas-stubs (>=1.1.0.11)"] -realtime = ["websockets (>=13,<16)"] -voice-helpers = ["numpy (>=2.0.2)", "sounddevice (>=0.5.1)"] - -[[package]] -name = "opentelemetry-api" -version = "1.38.0" -description = "OpenTelemetry Python API" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "opentelemetry_api-1.38.0-py3-none-any.whl", hash = "sha256:2891b0197f47124454ab9f0cf58f3be33faca394457ac3e09daba13ff50aa582"}, - {file = "opentelemetry_api-1.38.0.tar.gz", hash = "sha256:f4c193b5e8acb0912b06ac5b16321908dd0843d75049c091487322284a3eea12"}, -] - -[package.dependencies] -importlib-metadata = ">=6.0,<8.8.0" -typing-extensions = ">=4.5.0" - -[[package]] -name = "opentelemetry-exporter-otlp" -version = "1.38.0" -description = "OpenTelemetry Collector Exporters" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "opentelemetry_exporter_otlp-1.38.0-py3-none-any.whl", hash = "sha256:bc6562cef229fac8887ed7109fc5abc52315f39d9c03fd487bb8b4ef8fbbc231"}, - {file = "opentelemetry_exporter_otlp-1.38.0.tar.gz", hash = "sha256:2f55acdd475e4136117eff20fbf1b9488b1b0b665ab64407516e1ac06f9c3f9d"}, -] - -[package.dependencies] -opentelemetry-exporter-otlp-proto-grpc = "1.38.0" -opentelemetry-exporter-otlp-proto-http = "1.38.0" - -[[package]] -name = "opentelemetry-exporter-otlp-proto-common" -version = "1.38.0" -description = "OpenTelemetry Protobuf encoding" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "opentelemetry_exporter_otlp_proto_common-1.38.0-py3-none-any.whl", hash = "sha256:03cb76ab213300fe4f4c62b7d8f17d97fcfd21b89f0b5ce38ea156327ddda74a"}, - {file = "opentelemetry_exporter_otlp_proto_common-1.38.0.tar.gz", hash = "sha256:e333278afab4695aa8114eeb7bf4e44e65c6607d54968271a249c180b2cb605c"}, -] - -[package.dependencies] -opentelemetry-proto = "1.38.0" - -[[package]] -name = "opentelemetry-exporter-otlp-proto-grpc" -version = "1.38.0" -description = "OpenTelemetry Collector Protobuf over gRPC Exporter" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "opentelemetry_exporter_otlp_proto_grpc-1.38.0-py3-none-any.whl", hash = "sha256:7c49fd9b4bd0dbe9ba13d91f764c2d20b0025649a6e4ac35792fb8d84d764bc7"}, - {file = "opentelemetry_exporter_otlp_proto_grpc-1.38.0.tar.gz", hash = "sha256:2473935e9eac71f401de6101d37d6f3f0f1831db92b953c7dcc912536158ebd6"}, -] - -[package.dependencies] -googleapis-common-protos = ">=1.57,<2.0" -grpcio = [ - {version = ">=1.63.2,<2.0.0", markers = "python_version < \"3.13\""}, - {version = ">=1.66.2,<2.0.0", markers = "python_version >= \"3.13\""}, -] -opentelemetry-api = ">=1.15,<2.0" -opentelemetry-exporter-otlp-proto-common = "1.38.0" -opentelemetry-proto = "1.38.0" -opentelemetry-sdk = ">=1.38.0,<1.39.0" -typing-extensions = ">=4.6.0" - -[[package]] -name = "opentelemetry-exporter-otlp-proto-http" -version = "1.38.0" -description = "OpenTelemetry Collector Protobuf over HTTP Exporter" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "opentelemetry_exporter_otlp_proto_http-1.38.0-py3-none-any.whl", hash = "sha256:84b937305edfc563f08ec69b9cb2298be8188371217e867c1854d77198d0825b"}, - {file = "opentelemetry_exporter_otlp_proto_http-1.38.0.tar.gz", hash = "sha256:f16bd44baf15cbe07633c5112ffc68229d0edbeac7b37610be0b2def4e21e90b"}, -] - -[package.dependencies] -googleapis-common-protos = ">=1.52,<2.0" -opentelemetry-api = ">=1.15,<2.0" -opentelemetry-exporter-otlp-proto-common = "1.38.0" -opentelemetry-proto = "1.38.0" -opentelemetry-sdk = ">=1.38.0,<1.39.0" -requests = ">=2.7,<3.0" -typing-extensions = ">=4.5.0" - -[[package]] -name = "opentelemetry-instrumentation" -version = "0.59b0" -description = "Instrumentation Tools & Auto Instrumentation for OpenTelemetry Python" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "opentelemetry_instrumentation-0.59b0-py3-none-any.whl", hash = "sha256:44082cc8fe56b0186e87ee8f7c17c327c4c2ce93bdbe86496e600985d74368ee"}, - {file = "opentelemetry_instrumentation-0.59b0.tar.gz", hash = "sha256:6010f0faaacdaf7c4dff8aac84e226d23437b331dcda7e70367f6d73a7db1adc"}, -] - -[package.dependencies] -opentelemetry-api = ">=1.4,<2.0" -opentelemetry-semantic-conventions = "0.59b0" -packaging = ">=18.0" -wrapt = ">=1.0.0,<2.0.0" - -[[package]] -name = "opentelemetry-instrumentation-asgi" -version = "0.59b0" -description = "ASGI instrumentation for OpenTelemetry" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "opentelemetry_instrumentation_asgi-0.59b0-py3-none-any.whl", hash = "sha256:ba9703e09d2c33c52fa798171f344c8123488fcd45017887981df088452d3c53"}, - {file = "opentelemetry_instrumentation_asgi-0.59b0.tar.gz", hash = "sha256:2509d6fe9fd829399ce3536e3a00426c7e3aa359fc1ed9ceee1628b56da40e7a"}, -] - -[package.dependencies] -asgiref = ">=3.0,<4.0" -opentelemetry-api = ">=1.12,<2.0" -opentelemetry-instrumentation = "0.59b0" -opentelemetry-semantic-conventions = "0.59b0" -opentelemetry-util-http = "0.59b0" - -[package.extras] -instruments = ["asgiref (>=3.0,<4.0)"] - -[[package]] -name = "opentelemetry-instrumentation-fastapi" -version = "0.59b0" -description = "OpenTelemetry FastAPI Instrumentation" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "opentelemetry_instrumentation_fastapi-0.59b0-py3-none-any.whl", hash = "sha256:0d8d00ff7d25cca40a4b2356d1d40a8f001e0668f60c102f5aa6bb721d660c4f"}, - {file = "opentelemetry_instrumentation_fastapi-0.59b0.tar.gz", hash = "sha256:e8fe620cfcca96a7d634003df1bc36a42369dedcdd6893e13fb5903aeeb89b2b"}, -] - -[package.dependencies] -opentelemetry-api = ">=1.12,<2.0" -opentelemetry-instrumentation = "0.59b0" -opentelemetry-instrumentation-asgi = "0.59b0" -opentelemetry-semantic-conventions = "0.59b0" -opentelemetry-util-http = "0.59b0" - -[package.extras] -instruments = ["fastapi (>=0.92,<1.0)"] - -[[package]] -name = "opentelemetry-proto" -version = "1.38.0" -description = "OpenTelemetry Python Proto" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "opentelemetry_proto-1.38.0-py3-none-any.whl", hash = "sha256:b6ebe54d3217c42e45462e2a1ae28c3e2bf2ec5a5645236a490f55f45f1a0a18"}, - {file = "opentelemetry_proto-1.38.0.tar.gz", hash = "sha256:88b161e89d9d372ce723da289b7da74c3a8354a8e5359992be813942969ed468"}, -] - -[package.dependencies] -protobuf = ">=5.0,<7.0" - -[[package]] -name = "opentelemetry-sdk" -version = "1.38.0" -description = "OpenTelemetry Python SDK" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "opentelemetry_sdk-1.38.0-py3-none-any.whl", hash = "sha256:1c66af6564ecc1553d72d811a01df063ff097cdc82ce188da9951f93b8d10f6b"}, - {file = "opentelemetry_sdk-1.38.0.tar.gz", hash = "sha256:93df5d4d871ed09cb4272305be4d996236eedb232253e3ab864c8620f051cebe"}, -] - -[package.dependencies] -opentelemetry-api = "1.38.0" -opentelemetry-semantic-conventions = "0.59b0" -typing-extensions = ">=4.5.0" - -[[package]] -name = "opentelemetry-semantic-conventions" -version = "0.59b0" -description = "OpenTelemetry Semantic Conventions" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "opentelemetry_semantic_conventions-0.59b0-py3-none-any.whl", hash = "sha256:35d3b8833ef97d614136e253c1da9342b4c3c083bbaf29ce31d572a1c3825eed"}, - {file = "opentelemetry_semantic_conventions-0.59b0.tar.gz", hash = "sha256:7a6db3f30d70202d5bf9fa4b69bc866ca6a30437287de6c510fb594878aed6b0"}, -] - -[package.dependencies] -opentelemetry-api = "1.38.0" -typing-extensions = ">=4.5.0" - -[[package]] -name = "opentelemetry-util-http" -version = "0.59b0" -description = "Web util for OpenTelemetry" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "opentelemetry_util_http-0.59b0-py3-none-any.whl", hash = "sha256:6d036a07563bce87bf521839c0671b507a02a0d39d7ea61b88efa14c6e25355d"}, - {file = "opentelemetry_util_http-0.59b0.tar.gz", hash = "sha256:ae66ee91be31938d832f3b4bc4eb8a911f6eddd38969c4a871b1230db2a0a560"}, -] - -[[package]] -name = "overrides" -version = "7.7.0" -description = "A decorator to automatically detect mismatch when overriding a method." -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "overrides-7.7.0-py3-none-any.whl", hash = "sha256:c7ed9d062f78b8e4c1a7b70bd8796b35ead4d9f510227ef9c5dc7626c60d7e49"}, - {file = "overrides-7.7.0.tar.gz", hash = "sha256:55158fa3d93b98cc75299b1e67078ad9003ca27945c76162c1c0766d6f91820a"}, -] - -[[package]] -name = "packaging" -version = "25.0" -description = "Core utilities for Python packages" -optional = false -python-versions = ">=3.8" -groups = ["main", "dev"] -files = [ - {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, - {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, -] - -[[package]] -name = "pluggy" -version = "1.6.0" -description = "plugin and hook calling mechanisms for python" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, - {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, -] - -[package.extras] -dev = ["pre-commit", "tox"] -testing = ["coverage", "pytest", "pytest-benchmark"] - -[[package]] -name = "protobuf" -version = "6.33.0" -description = "" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "protobuf-6.33.0-cp310-abi3-win32.whl", hash = "sha256:d6101ded078042a8f17959eccd9236fb7a9ca20d3b0098bbcb91533a5680d035"}, - {file = "protobuf-6.33.0-cp310-abi3-win_amd64.whl", hash = "sha256:9a031d10f703f03768f2743a1c403af050b6ae1f3480e9c140f39c45f81b13ee"}, - {file = "protobuf-6.33.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:905b07a65f1a4b72412314082c7dbfae91a9e8b68a0cc1577515f8df58ecf455"}, - {file = "protobuf-6.33.0-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:e0697ece353e6239b90ee43a9231318302ad8353c70e6e45499fa52396debf90"}, - {file = "protobuf-6.33.0-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:e0a1715e4f27355afd9570f3ea369735afc853a6c3951a6afe1f80d8569ad298"}, - {file = "protobuf-6.33.0-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:35be49fd3f4fefa4e6e2aacc35e8b837d6703c37a2168a55ac21e9b1bc7559ef"}, - {file = "protobuf-6.33.0-cp39-cp39-win32.whl", hash = "sha256:cd33a8e38ea3e39df66e1bbc462b076d6e5ba3a4ebbde58219d777223a7873d3"}, - {file = "protobuf-6.33.0-cp39-cp39-win_amd64.whl", hash = "sha256:c963e86c3655af3a917962c9619e1a6b9670540351d7af9439d06064e3317cc9"}, - {file = "protobuf-6.33.0-py3-none-any.whl", hash = "sha256:25c9e1963c6734448ea2d308cfa610e692b801304ba0908d7bfa564ac5132995"}, - {file = "protobuf-6.33.0.tar.gz", hash = "sha256:140303d5c8d2037730c548f8c7b93b20bb1dc301be280c378b82b8894589c954"}, -] - -[[package]] -name = "psutil" -version = "7.1.1" -description = "Cross-platform lib for process and system monitoring." -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "psutil-7.1.1-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:8fa59d7b1f01f0337f12cd10dbd76e4312a4d3c730a4fedcbdd4e5447a8b8460"}, - {file = "psutil-7.1.1-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:2a95104eae85d088891716db676f780c1404fc15d47fde48a46a5d61e8f5ad2c"}, - {file = "psutil-7.1.1-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:98629cd8567acefcc45afe2f4ba1e9290f579eacf490a917967decce4b74ee9b"}, - {file = "psutil-7.1.1-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:92ebc58030fb054fa0f26c3206ef01c31c29d67aee1367e3483c16665c25c8d2"}, - {file = "psutil-7.1.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:146a704f224fb2ded2be3da5ac67fc32b9ea90c45b51676f9114a6ac45616967"}, - {file = "psutil-7.1.1-cp37-abi3-win32.whl", hash = "sha256:295c4025b5cd880f7445e4379e6826f7307e3d488947bf9834e865e7847dc5f7"}, - {file = "psutil-7.1.1-cp37-abi3-win_amd64.whl", hash = "sha256:9b4f17c5f65e44f69bd3a3406071a47b79df45cf2236d1f717970afcb526bcd3"}, - {file = "psutil-7.1.1-cp37-abi3-win_arm64.whl", hash = "sha256:5457cf741ca13da54624126cd5d333871b454ab133999a9a103fb097a7d7d21a"}, - {file = "psutil-7.1.1.tar.gz", hash = "sha256:092b6350145007389c1cfe5716050f02030a05219d90057ea867d18fe8d372fc"}, -] - -[package.extras] -dev = ["abi3audit", "black", "check-manifest", "coverage", "packaging", "pylint", "pyperf", "pypinfo", "pyreadline ; os_name == \"nt\"", "pytest", "pytest-cov", "pytest-instafail", "pytest-subtests", "pytest-xdist", "pywin32 ; os_name == \"nt\" and platform_python_implementation != \"PyPy\"", "requests", "rstcheck", "ruff", "setuptools", "sphinx", "sphinx_rtd_theme", "toml-sort", "twine", "validate-pyproject[all]", "virtualenv", "vulture", "wheel", "wheel ; os_name == \"nt\" and platform_python_implementation != \"PyPy\"", "wmi ; os_name == \"nt\" and platform_python_implementation != \"PyPy\""] -test = ["pytest", "pytest-instafail", "pytest-subtests", "pytest-xdist", "pywin32 ; os_name == \"nt\" and platform_python_implementation != \"PyPy\"", "setuptools", "wheel ; os_name == \"nt\" and platform_python_implementation != \"PyPy\"", "wmi ; os_name == \"nt\" and platform_python_implementation != \"PyPy\""] - -[[package]] -name = "pydantic" -version = "2.12.3" -description = "Data validation using Python type hints" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "pydantic-2.12.3-py3-none-any.whl", hash = "sha256:6986454a854bc3bc6e5443e1369e06a3a456af9d339eda45510f517d9ea5c6bf"}, - {file = "pydantic-2.12.3.tar.gz", hash = "sha256:1da1c82b0fc140bb0103bc1441ffe062154c8d38491189751ee00fd8ca65ce74"}, -] - -[package.dependencies] -annotated-types = ">=0.6.0" -pydantic-core = "2.41.4" -typing-extensions = ">=4.14.1" -typing-inspection = ">=0.4.2" - -[package.extras] -email = ["email-validator (>=2.0.0)"] -timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""] - -[[package]] -name = "pydantic-core" -version = "2.41.4" -description = "Core functionality for Pydantic validation and serialization" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "pydantic_core-2.41.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2442d9a4d38f3411f22eb9dd0912b7cbf4b7d5b6c92c4173b75d3e1ccd84e36e"}, - {file = "pydantic_core-2.41.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:30a9876226dda131a741afeab2702e2d127209bde3c65a2b8133f428bc5d006b"}, - {file = "pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d55bbac04711e2980645af68b97d445cdbcce70e5216de444a6c4b6943ebcccd"}, - {file = "pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e1d778fb7849a42d0ee5927ab0f7453bf9f85eef8887a546ec87db5ddb178945"}, - {file = "pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1b65077a4693a98b90ec5ad8f203ad65802a1b9b6d4a7e48066925a7e1606706"}, - {file = "pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:62637c769dee16eddb7686bf421be48dfc2fae93832c25e25bc7242e698361ba"}, - {file = "pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dfe3aa529c8f501babf6e502936b9e8d4698502b2cfab41e17a028d91b1ac7b"}, - {file = "pydantic_core-2.41.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ca2322da745bf2eeb581fc9ea3bbb31147702163ccbcbf12a3bb630e4bf05e1d"}, - {file = "pydantic_core-2.41.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e8cd3577c796be7231dcf80badcf2e0835a46665eaafd8ace124d886bab4d700"}, - {file = "pydantic_core-2.41.4-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:1cae8851e174c83633f0833e90636832857297900133705ee158cf79d40f03e6"}, - {file = "pydantic_core-2.41.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a26d950449aae348afe1ac8be5525a00ae4235309b729ad4d3399623125b43c9"}, - {file = "pydantic_core-2.41.4-cp310-cp310-win32.whl", hash = "sha256:0cf2a1f599efe57fa0051312774280ee0f650e11152325e41dfd3018ef2c1b57"}, - {file = "pydantic_core-2.41.4-cp310-cp310-win_amd64.whl", hash = "sha256:a8c2e340d7e454dc3340d3d2e8f23558ebe78c98aa8f68851b04dcb7bc37abdc"}, - {file = "pydantic_core-2.41.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:28ff11666443a1a8cf2a044d6a545ebffa8382b5f7973f22c36109205e65dc80"}, - {file = "pydantic_core-2.41.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:61760c3925d4633290292bad462e0f737b840508b4f722247d8729684f6539ae"}, - {file = "pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eae547b7315d055b0de2ec3965643b0ab82ad0106a7ffd29615ee9f266a02827"}, - {file = "pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ef9ee5471edd58d1fcce1c80ffc8783a650e3e3a193fe90d52e43bb4d87bff1f"}, - {file = "pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:15dd504af121caaf2c95cb90c0ebf71603c53de98305621b94da0f967e572def"}, - {file = "pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3a926768ea49a8af4d36abd6a8968b8790f7f76dd7cbd5a4c180db2b4ac9a3a2"}, - {file = "pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6916b9b7d134bff5440098a4deb80e4cb623e68974a87883299de9124126c2a8"}, - {file = "pydantic_core-2.41.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5cf90535979089df02e6f17ffd076f07237efa55b7343d98760bde8743c4b265"}, - {file = "pydantic_core-2.41.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:7533c76fa647fade2d7ec75ac5cc079ab3f34879626dae5689b27790a6cf5a5c"}, - {file = "pydantic_core-2.41.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:37e516bca9264cbf29612539801ca3cd5d1be465f940417b002905e6ed79d38a"}, - {file = "pydantic_core-2.41.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0c19cb355224037c83642429b8ce261ae108e1c5fbf5c028bac63c77b0f8646e"}, - {file = "pydantic_core-2.41.4-cp311-cp311-win32.whl", hash = "sha256:09c2a60e55b357284b5f31f5ab275ba9f7f70b7525e18a132ec1f9160b4f1f03"}, - {file = "pydantic_core-2.41.4-cp311-cp311-win_amd64.whl", hash = "sha256:711156b6afb5cb1cb7c14a2cc2c4a8b4c717b69046f13c6b332d8a0a8f41ca3e"}, - {file = "pydantic_core-2.41.4-cp311-cp311-win_arm64.whl", hash = "sha256:6cb9cf7e761f4f8a8589a45e49ed3c0d92d1d696a45a6feaee8c904b26efc2db"}, - {file = "pydantic_core-2.41.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:ab06d77e053d660a6faaf04894446df7b0a7e7aba70c2797465a0a1af00fc887"}, - {file = "pydantic_core-2.41.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c53ff33e603a9c1179a9364b0a24694f183717b2e0da2b5ad43c316c956901b2"}, - {file = "pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:304c54176af2c143bd181d82e77c15c41cbacea8872a2225dd37e6544dce9999"}, - {file = "pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:025ba34a4cf4fb32f917d5d188ab5e702223d3ba603be4d8aca2f82bede432a4"}, - {file = "pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b9f5f30c402ed58f90c70e12eff65547d3ab74685ffe8283c719e6bead8ef53f"}, - {file = "pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd96e5d15385d301733113bcaa324c8bcf111275b7675a9c6e88bfb19fc05e3b"}, - {file = "pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98f348cbb44fae6e9653c1055db7e29de67ea6a9ca03a5fa2c2e11a47cff0e47"}, - {file = "pydantic_core-2.41.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ec22626a2d14620a83ca583c6f5a4080fa3155282718b6055c2ea48d3ef35970"}, - {file = "pydantic_core-2.41.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3a95d4590b1f1a43bf33ca6d647b990a88f4a3824a8c4572c708f0b45a5290ed"}, - {file = "pydantic_core-2.41.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:f9672ab4d398e1b602feadcffcdd3af44d5f5e6ddc15bc7d15d376d47e8e19f8"}, - {file = "pydantic_core-2.41.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:84d8854db5f55fead3b579f04bda9a36461dab0730c5d570e1526483e7bb8431"}, - {file = "pydantic_core-2.41.4-cp312-cp312-win32.whl", hash = "sha256:9be1c01adb2ecc4e464392c36d17f97e9110fbbc906bcbe1c943b5b87a74aabd"}, - {file = "pydantic_core-2.41.4-cp312-cp312-win_amd64.whl", hash = "sha256:d682cf1d22bab22a5be08539dca3d1593488a99998f9f412137bc323179067ff"}, - {file = "pydantic_core-2.41.4-cp312-cp312-win_arm64.whl", hash = "sha256:833eebfd75a26d17470b58768c1834dfc90141b7afc6eb0429c21fc5a21dcfb8"}, - {file = "pydantic_core-2.41.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:85e050ad9e5f6fe1004eec65c914332e52f429bc0ae12d6fa2092407a462c746"}, - {file = "pydantic_core-2.41.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7393f1d64792763a48924ba31d1e44c2cfbc05e3b1c2c9abb4ceeadd912cced"}, - {file = "pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94dab0940b0d1fb28bcab847adf887c66a27a40291eedf0b473be58761c9799a"}, - {file = "pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:de7c42f897e689ee6f9e93c4bec72b99ae3b32a2ade1c7e4798e690ff5246e02"}, - {file = "pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:664b3199193262277b8b3cd1e754fb07f2c6023289c815a1e1e8fb415cb247b1"}, - {file = "pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d95b253b88f7d308b1c0b417c4624f44553ba4762816f94e6986819b9c273fb2"}, - {file = "pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1351f5bbdbbabc689727cb91649a00cb9ee7203e0a6e54e9f5ba9e22e384b84"}, - {file = "pydantic_core-2.41.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1affa4798520b148d7182da0615d648e752de4ab1a9566b7471bc803d88a062d"}, - {file = "pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7b74e18052fea4aa8dea2fb7dbc23d15439695da6cbe6cfc1b694af1115df09d"}, - {file = "pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:285b643d75c0e30abda9dc1077395624f314a37e3c09ca402d4015ef5979f1a2"}, - {file = "pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:f52679ff4218d713b3b33f88c89ccbf3a5c2c12ba665fb80ccc4192b4608dbab"}, - {file = "pydantic_core-2.41.4-cp313-cp313-win32.whl", hash = "sha256:ecde6dedd6fff127c273c76821bb754d793be1024bc33314a120f83a3c69460c"}, - {file = "pydantic_core-2.41.4-cp313-cp313-win_amd64.whl", hash = "sha256:d081a1f3800f05409ed868ebb2d74ac39dd0c1ff6c035b5162356d76030736d4"}, - {file = "pydantic_core-2.41.4-cp313-cp313-win_arm64.whl", hash = "sha256:f8e49c9c364a7edcbe2a310f12733aad95b022495ef2a8d653f645e5d20c1564"}, - {file = "pydantic_core-2.41.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:ed97fd56a561f5eb5706cebe94f1ad7c13b84d98312a05546f2ad036bafe87f4"}, - {file = "pydantic_core-2.41.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a870c307bf1ee91fc58a9a61338ff780d01bfae45922624816878dce784095d2"}, - {file = "pydantic_core-2.41.4-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d25e97bc1f5f8f7985bdc2335ef9e73843bb561eb1fa6831fdfc295c1c2061cf"}, - {file = "pydantic_core-2.41.4-cp313-cp313t-win_amd64.whl", hash = "sha256:d405d14bea042f166512add3091c1af40437c2e7f86988f3915fabd27b1e9cd2"}, - {file = "pydantic_core-2.41.4-cp313-cp313t-win_arm64.whl", hash = "sha256:19f3684868309db5263a11bace3c45d93f6f24afa2ffe75a647583df22a2ff89"}, - {file = "pydantic_core-2.41.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:e9205d97ed08a82ebb9a307e92914bb30e18cdf6f6b12ca4bedadb1588a0bfe1"}, - {file = "pydantic_core-2.41.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:82df1f432b37d832709fbcc0e24394bba04a01b6ecf1ee87578145c19cde12ac"}, - {file = "pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc3b4cc4539e055cfa39a3763c939f9d409eb40e85813257dcd761985a108554"}, - {file = "pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b1eb1754fce47c63d2ff57fdb88c351a6c0150995890088b33767a10218eaa4e"}, - {file = "pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e6ab5ab30ef325b443f379ddb575a34969c333004fca5a1daa0133a6ffaad616"}, - {file = "pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:31a41030b1d9ca497634092b46481b937ff9397a86f9f51bd41c4767b6fc04af"}, - {file = "pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a44ac1738591472c3d020f61c6df1e4015180d6262ebd39bf2aeb52571b60f12"}, - {file = "pydantic_core-2.41.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d72f2b5e6e82ab8f94ea7d0d42f83c487dc159c5240d8f83beae684472864e2d"}, - {file = "pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:c4d1e854aaf044487d31143f541f7aafe7b482ae72a022c664b2de2e466ed0ad"}, - {file = "pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b568af94267729d76e6ee5ececda4e283d07bbb28e8148bb17adad93d025d25a"}, - {file = "pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:6d55fb8b1e8929b341cc313a81a26e0d48aa3b519c1dbaadec3a6a2b4fcad025"}, - {file = "pydantic_core-2.41.4-cp314-cp314-win32.whl", hash = "sha256:5b66584e549e2e32a1398df11da2e0a7eff45d5c2d9db9d5667c5e6ac764d77e"}, - {file = "pydantic_core-2.41.4-cp314-cp314-win_amd64.whl", hash = "sha256:557a0aab88664cc552285316809cab897716a372afaf8efdbef756f8b890e894"}, - {file = "pydantic_core-2.41.4-cp314-cp314-win_arm64.whl", hash = "sha256:3f1ea6f48a045745d0d9f325989d8abd3f1eaf47dd00485912d1a3a63c623a8d"}, - {file = "pydantic_core-2.41.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6c1fe4c5404c448b13188dd8bd2ebc2bdd7e6727fa61ff481bcc2cca894018da"}, - {file = "pydantic_core-2.41.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:523e7da4d43b113bf8e7b49fa4ec0c35bf4fe66b2230bfc5c13cc498f12c6c3e"}, - {file = "pydantic_core-2.41.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5729225de81fb65b70fdb1907fcf08c75d498f4a6f15af005aabb1fdadc19dfa"}, - {file = "pydantic_core-2.41.4-cp314-cp314t-win_amd64.whl", hash = "sha256:de2cfbb09e88f0f795fd90cf955858fc2c691df65b1f21f0aa00b99f3fbc661d"}, - {file = "pydantic_core-2.41.4-cp314-cp314t-win_arm64.whl", hash = "sha256:d34f950ae05a83e0ede899c595f312ca976023ea1db100cd5aa188f7005e3ab0"}, - {file = "pydantic_core-2.41.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:646e76293345954acea6966149683047b7b2ace793011922208c8e9da12b0062"}, - {file = "pydantic_core-2.41.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cc8e85a63085a137d286e2791037f5fdfff0aabb8b899483ca9c496dd5797338"}, - {file = "pydantic_core-2.41.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:692c622c8f859a17c156492783902d8370ac7e121a611bd6fe92cc71acf9ee8d"}, - {file = "pydantic_core-2.41.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d1e2906efb1031a532600679b424ef1d95d9f9fb507f813951f23320903adbd7"}, - {file = "pydantic_core-2.41.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e04e2f7f8916ad3ddd417a7abdd295276a0bf216993d9318a5d61cc058209166"}, - {file = "pydantic_core-2.41.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df649916b81822543d1c8e0e1d079235f68acdc7d270c911e8425045a8cfc57e"}, - {file = "pydantic_core-2.41.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66c529f862fdba70558061bb936fe00ddbaaa0c647fd26e4a4356ef1d6561891"}, - {file = "pydantic_core-2.41.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3b4c5a1fd3a311563ed866c2c9b62da06cb6398bee186484ce95c820db71cb"}, - {file = "pydantic_core-2.41.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6e0fc40d84448f941df9b3334c4b78fe42f36e3bf631ad54c3047a0cdddc2514"}, - {file = "pydantic_core-2.41.4-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:44e7625332683b6c1c8b980461475cde9595eff94447500e80716db89b0da005"}, - {file = "pydantic_core-2.41.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:170ee6835f6c71081d031ef1c3b4dc4a12b9efa6a9540f93f95b82f3c7571ae8"}, - {file = "pydantic_core-2.41.4-cp39-cp39-win32.whl", hash = "sha256:3adf61415efa6ce977041ba9745183c0e1f637ca849773afa93833e04b163feb"}, - {file = "pydantic_core-2.41.4-cp39-cp39-win_amd64.whl", hash = "sha256:a238dd3feee263eeaeb7dc44aea4ba1364682c4f9f9467e6af5596ba322c2332"}, - {file = "pydantic_core-2.41.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:a1b2cfec3879afb742a7b0bcfa53e4f22ba96571c9e54d6a3afe1052d17d843b"}, - {file = "pydantic_core-2.41.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:d175600d975b7c244af6eb9c9041f10059f20b8bbffec9e33fdd5ee3f67cdc42"}, - {file = "pydantic_core-2.41.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f184d657fa4947ae5ec9c47bd7e917730fa1cbb78195037e32dcbab50aca5ee"}, - {file = "pydantic_core-2.41.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ed810568aeffed3edc78910af32af911c835cc39ebbfacd1f0ab5dd53028e5c"}, - {file = "pydantic_core-2.41.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:4f5d640aeebb438517150fdeec097739614421900e4a08db4a3ef38898798537"}, - {file = "pydantic_core-2.41.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:4a9ab037b71927babc6d9e7fc01aea9e66dc2a4a34dff06ef0724a4049629f94"}, - {file = "pydantic_core-2.41.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4dab9484ec605c3016df9ad4fd4f9a390bc5d816a3b10c6550f8424bb80b18c"}, - {file = "pydantic_core-2.41.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8a5028425820731d8c6c098ab642d7b8b999758e24acae03ed38a66eca8335"}, - {file = "pydantic_core-2.41.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:1e5ab4fc177dd41536b3c32b2ea11380dd3d4619a385860621478ac2d25ceb00"}, - {file = "pydantic_core-2.41.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:3d88d0054d3fa11ce936184896bed3c1c5441d6fa483b498fac6a5d0dd6f64a9"}, - {file = "pydantic_core-2.41.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b2a054a8725f05b4b6503357e0ac1c4e8234ad3b0c2ac130d6ffc66f0e170e2"}, - {file = "pydantic_core-2.41.4-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b0d9db5a161c99375a0c68c058e227bee1d89303300802601d76a3d01f74e258"}, - {file = "pydantic_core-2.41.4-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:6273ea2c8ffdac7b7fda2653c49682db815aebf4a89243a6feccf5e36c18c347"}, - {file = "pydantic_core-2.41.4-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:4c973add636efc61de22530b2ef83a65f39b6d6f656df97f678720e20de26caa"}, - {file = "pydantic_core-2.41.4-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b69d1973354758007f46cf2d44a4f3d0933f10b6dc9bf15cf1356e037f6f731a"}, - {file = "pydantic_core-2.41.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:3619320641fd212aaf5997b6ca505e97540b7e16418f4a241f44cdf108ffb50d"}, - {file = "pydantic_core-2.41.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:491535d45cd7ad7e4a2af4a5169b0d07bebf1adfd164b0368da8aa41e19907a5"}, - {file = "pydantic_core-2.41.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:54d86c0cada6aba4ec4c047d0e348cbad7063b87ae0f005d9f8c9ad04d4a92a2"}, - {file = "pydantic_core-2.41.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eca1124aced216b2500dc2609eade086d718e8249cb9696660ab447d50a758bd"}, - {file = "pydantic_core-2.41.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6c9024169becccf0cb470ada03ee578d7348c119a0d42af3dcf9eda96e3a247c"}, - {file = "pydantic_core-2.41.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:26895a4268ae5a2849269f4991cdc97236e4b9c010e51137becf25182daac405"}, - {file = "pydantic_core-2.41.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:ca4df25762cf71308c446e33c9b1fdca2923a3f13de616e2a949f38bf21ff5a8"}, - {file = "pydantic_core-2.41.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:5a28fcedd762349519276c36634e71853b4541079cab4acaaac60c4421827308"}, - {file = "pydantic_core-2.41.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c173ddcd86afd2535e2b695217e82191580663a1d1928239f877f5a1649ef39f"}, - {file = "pydantic_core-2.41.4.tar.gz", hash = "sha256:70e47929a9d4a1905a67e4b687d5946026390568a8e952b92824118063cee4d5"}, -] - -[package.dependencies] -typing-extensions = ">=4.14.1" - -[[package]] -name = "pygments" -version = "2.19.2" -description = "Pygments is a syntax highlighting package written in Python." -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, - {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, -] - -[package.extras] -windows-terminal = ["colorama (>=0.4.6)"] - -[[package]] -name = "pytest" -version = "8.4.2" -description = "pytest: simple powerful testing with Python" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79"}, - {file = "pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01"}, -] - -[package.dependencies] -colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} -exceptiongroup = {version = ">=1", markers = "python_version < \"3.11\""} -iniconfig = ">=1" -packaging = ">=20" -pluggy = ">=1.5,<2" -pygments = ">=2.7.2" -tomli = {version = ">=1", markers = "python_version < \"3.11\""} - -[package.extras] -dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] - -[[package]] -name = "pytest-asyncio" -version = "1.2.0" -description = "Pytest support for asyncio" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "pytest_asyncio-1.2.0-py3-none-any.whl", hash = "sha256:8e17ae5e46d8e7efe51ab6494dd2010f4ca8dae51652aa3c8d55acf50bfb2e99"}, - {file = "pytest_asyncio-1.2.0.tar.gz", hash = "sha256:c609a64a2a8768462d0c99811ddb8bd2583c33fd33cf7f21af1c142e824ffb57"}, -] - -[package.dependencies] -backports-asyncio-runner = {version = ">=1.1,<2", markers = "python_version < \"3.11\""} -pytest = ">=8.2,<9" -typing-extensions = {version = ">=4.12", markers = "python_version < \"3.13\""} - -[package.extras] -docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1)"] -testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] - -[[package]] -name = "pytest-httpserver" -version = "1.1.3" -description = "pytest-httpserver is a httpserver for pytest" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "pytest_httpserver-1.1.3-py3-none-any.whl", hash = "sha256:5f84757810233e19e2bb5287f3826a71c97a3740abe3a363af9155c0f82fdbb9"}, - {file = "pytest_httpserver-1.1.3.tar.gz", hash = "sha256:af819d6b533f84b4680b9416a5b3f67f1df3701f1da54924afd4d6e4ba5917ec"}, -] - -[package.dependencies] -Werkzeug = ">=2.0.0" - -[[package]] -name = "pytest-retry" -version = "1.7.0" -description = "Adds the ability to retry flaky tests in CI environments" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "pytest_retry-1.7.0-py3-none-any.whl", hash = "sha256:a2dac85b79a4e2375943f1429479c65beb6c69553e7dae6b8332be47a60954f4"}, - {file = "pytest_retry-1.7.0.tar.gz", hash = "sha256:f8d52339f01e949df47c11ba9ee8d5b362f5824dff580d3870ec9ae0057df80f"}, -] - -[package.dependencies] -pytest = ">=7.0.0" - -[package.extras] -dev = ["black", "flake8", "isort", "mypy"] - -[[package]] -name = "python-dateutil" -version = "2.9.0.post0" -description = "Extensions to the standard Python datetime module" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -groups = ["main"] -files = [ - {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, - {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, -] - -[package.dependencies] -six = ">=1.5" - -[[package]] -name = "pytz" -version = "2025.2" -description = "World timezone definitions, modern and historical" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00"}, - {file = "pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3"}, -] - -[[package]] -name = "pyyaml" -version = "6.0.3" -description = "YAML parser and emitter for Python" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "PyYAML-6.0.3-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f"}, - {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4"}, - {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3"}, - {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6"}, - {file = "PyYAML-6.0.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369"}, - {file = "PyYAML-6.0.3-cp38-cp38-win32.whl", hash = "sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295"}, - {file = "PyYAML-6.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b"}, - {file = "pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b"}, - {file = "pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956"}, - {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8"}, - {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198"}, - {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b"}, - {file = "pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0"}, - {file = "pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69"}, - {file = "pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e"}, - {file = "pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c"}, - {file = "pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e"}, - {file = "pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824"}, - {file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c"}, - {file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00"}, - {file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d"}, - {file = "pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a"}, - {file = "pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4"}, - {file = "pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b"}, - {file = "pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf"}, - {file = "pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196"}, - {file = "pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0"}, - {file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28"}, - {file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c"}, - {file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc"}, - {file = "pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e"}, - {file = "pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea"}, - {file = "pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5"}, - {file = "pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b"}, - {file = "pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd"}, - {file = "pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8"}, - {file = "pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1"}, - {file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c"}, - {file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5"}, - {file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6"}, - {file = "pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6"}, - {file = "pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be"}, - {file = "pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26"}, - {file = "pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c"}, - {file = "pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb"}, - {file = "pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac"}, - {file = "pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310"}, - {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7"}, - {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788"}, - {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5"}, - {file = "pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764"}, - {file = "pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35"}, - {file = "pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac"}, - {file = "pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3"}, - {file = "pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3"}, - {file = "pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba"}, - {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c"}, - {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702"}, - {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c"}, - {file = "pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065"}, - {file = "pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65"}, - {file = "pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9"}, - {file = "pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b"}, - {file = "pyyaml-6.0.3-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da"}, - {file = "pyyaml-6.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917"}, - {file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9"}, - {file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5"}, - {file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a"}, - {file = "pyyaml-6.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926"}, - {file = "pyyaml-6.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7"}, - {file = "pyyaml-6.0.3-cp39-cp39-win32.whl", hash = "sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0"}, - {file = "pyyaml-6.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007"}, - {file = "pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f"}, -] - -[[package]] -name = "regex" -version = "2025.10.23" -description = "Alternative regular expression module, to replace re." -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "regex-2025.10.23-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:17bbcde374bef1c5fad9b131f0e28a6a24856dd90368d8c0201e2b5a69533daa"}, - {file = "regex-2025.10.23-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b4e10434279cc8567f99ca6e018e9025d14f2fded2a603380b6be2090f476426"}, - {file = "regex-2025.10.23-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9c9bb421cbe7012c744a5a56cf4d6c80829c72edb1a2991677299c988d6339c8"}, - {file = "regex-2025.10.23-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:275cd1c2ed8c4a78ebfa489618d7aee762e8b4732da73573c3e38236ec5f65de"}, - {file = "regex-2025.10.23-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7b426ae7952f3dc1e73a86056d520bd4e5f021397484a6835902fc5648bcacce"}, - {file = "regex-2025.10.23-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c5cdaf5b6d37c7da1967dbe729d819461aab6a98a072feef65bbcff0a6e60649"}, - {file = "regex-2025.10.23-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3bfeff0b08f296ab28b4332a7e03ca31c437ee78b541ebc874bbf540e5932f8d"}, - {file = "regex-2025.10.23-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5f97236a67307b775f30a74ef722b64b38b7ab7ba3bb4a2508518a5de545459c"}, - {file = "regex-2025.10.23-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:be19e7de499940cd72475fb8e46ab2ecb1cf5906bebdd18a89f9329afb1df82f"}, - {file = "regex-2025.10.23-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:883df76ee42d9ecb82b37ff8d01caea5895b3f49630a64d21111078bbf8ef64c"}, - {file = "regex-2025.10.23-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2e9117d1d35fc2addae6281019ecc70dc21c30014b0004f657558b91c6a8f1a7"}, - {file = "regex-2025.10.23-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0ff1307f531a5d8cf5c20ea517254551ff0a8dc722193aab66c656c5a900ea68"}, - {file = "regex-2025.10.23-cp310-cp310-win32.whl", hash = "sha256:7888475787cbfee4a7cd32998eeffe9a28129fa44ae0f691b96cb3939183ef41"}, - {file = "regex-2025.10.23-cp310-cp310-win_amd64.whl", hash = "sha256:ec41a905908496ce4906dab20fb103c814558db1d69afc12c2f384549c17936a"}, - {file = "regex-2025.10.23-cp310-cp310-win_arm64.whl", hash = "sha256:b2b7f19a764d5e966d5a62bf2c28a8b4093cc864c6734510bdb4aeb840aec5e6"}, - {file = "regex-2025.10.23-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6c531155bf9179345e85032052a1e5fe1a696a6abf9cea54b97e8baefff970fd"}, - {file = "regex-2025.10.23-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:912e9df4e89d383681268d38ad8f5780d7cccd94ba0e9aa09ca7ab7ab4f8e7eb"}, - {file = "regex-2025.10.23-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4f375c61bfc3138b13e762fe0ae76e3bdca92497816936534a0177201666f44f"}, - {file = "regex-2025.10.23-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e248cc9446081119128ed002a3801f8031e0c219b5d3c64d3cc627da29ac0a33"}, - {file = "regex-2025.10.23-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b52bf9282fdf401e4f4e721f0f61fc4b159b1307244517789702407dd74e38ca"}, - {file = "regex-2025.10.23-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c084889ab2c59765a0d5ac602fd1c3c244f9b3fcc9a65fdc7ba6b74c5287490"}, - {file = "regex-2025.10.23-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d80e8eb79009bdb0936658c44ca06e2fbbca67792013e3818eea3f5f228971c2"}, - {file = "regex-2025.10.23-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b6f259118ba87b814a8ec475380aee5f5ae97a75852a3507cf31d055b01b5b40"}, - {file = "regex-2025.10.23-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:9b8c72a242683dcc72d37595c4f1278dfd7642b769e46700a8df11eab19dfd82"}, - {file = "regex-2025.10.23-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:a8d7b7a0a3df9952f9965342159e0c1f05384c0f056a47ce8b61034f8cecbe83"}, - {file = "regex-2025.10.23-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:413bfea20a484c524858125e92b9ce6ffdd0a4b97d4ff96b5859aa119b0f1bdd"}, - {file = "regex-2025.10.23-cp311-cp311-win32.whl", hash = "sha256:f76deef1f1019a17dad98f408b8f7afc4bd007cbe835ae77b737e8c7f19ae575"}, - {file = "regex-2025.10.23-cp311-cp311-win_amd64.whl", hash = "sha256:59bba9f7125536f23fdab5deeea08da0c287a64c1d3acc1c7e99515809824de8"}, - {file = "regex-2025.10.23-cp311-cp311-win_arm64.whl", hash = "sha256:b103a752b6f1632ca420225718d6ed83f6a6ced3016dd0a4ab9a6825312de566"}, - {file = "regex-2025.10.23-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:7a44d9c00f7a0a02d3b777429281376370f3d13d2c75ae74eb94e11ebcf4a7fc"}, - {file = "regex-2025.10.23-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b83601f84fde939ae3478bb32a3aef36f61b58c3208d825c7e8ce1a735f143f2"}, - {file = "regex-2025.10.23-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ec13647907bb9d15fd192bbfe89ff06612e098a5709e7d6ecabbdd8f7908fc45"}, - {file = "regex-2025.10.23-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78d76dd2957d62501084e7012ddafc5fcd406dd982b7a9ca1ea76e8eaaf73e7e"}, - {file = "regex-2025.10.23-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8668e5f067e31a47699ebb354f43aeb9c0ef136f915bd864243098524482ac43"}, - {file = "regex-2025.10.23-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a32433fe3deb4b2d8eda88790d2808fed0dc097e84f5e683b4cd4f42edef6cca"}, - {file = "regex-2025.10.23-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d97d73818c642c938db14c0668167f8d39520ca9d983604575ade3fda193afcc"}, - {file = "regex-2025.10.23-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bca7feecc72ee33579e9f6ddf8babbe473045717a0e7dbc347099530f96e8b9a"}, - {file = "regex-2025.10.23-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7e24af51e907d7457cc4a72691ec458320b9ae67dc492f63209f01eecb09de32"}, - {file = "regex-2025.10.23-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:d10bcde58bbdf18146f3a69ec46dd03233b94a4a5632af97aa5378da3a47d288"}, - {file = "regex-2025.10.23-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:44383bc0c933388516c2692c9a7503e1f4a67e982f20b9a29d2fb70c6494f147"}, - {file = "regex-2025.10.23-cp312-cp312-win32.whl", hash = "sha256:6040a86f95438a0114bba16e51dfe27f1bc004fd29fe725f54a586f6d522b079"}, - {file = "regex-2025.10.23-cp312-cp312-win_amd64.whl", hash = "sha256:436b4c4352fe0762e3bfa34a5567079baa2ef22aa9c37cf4d128979ccfcad842"}, - {file = "regex-2025.10.23-cp312-cp312-win_arm64.whl", hash = "sha256:f4b1b1991617055b46aff6f6db24888c1f05f4db9801349d23f09ed0714a9335"}, - {file = "regex-2025.10.23-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:b7690f95404a1293923a296981fd943cca12c31a41af9c21ba3edd06398fc193"}, - {file = "regex-2025.10.23-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1a32d77aeaea58a13230100dd8797ac1a84c457f3af2fdf0d81ea689d5a9105b"}, - {file = "regex-2025.10.23-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b24b29402f264f70a3c81f45974323b41764ff7159655360543b7cabb73e7d2f"}, - {file = "regex-2025.10.23-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:563824a08c7c03d96856d84b46fdb3bbb7cfbdf79da7ef68725cda2ce169c72a"}, - {file = "regex-2025.10.23-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a0ec8bdd88d2e2659c3518087ee34b37e20bd169419ffead4240a7004e8ed03b"}, - {file = "regex-2025.10.23-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b577601bfe1d33913fcd9276d7607bbac827c4798d9e14d04bf37d417a6c41cb"}, - {file = "regex-2025.10.23-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c9f2c68ac6cb3de94eea08a437a75eaa2bd33f9e97c84836ca0b610a5804368"}, - {file = "regex-2025.10.23-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:89f8b9ea3830c79468e26b0e21c3585f69f105157c2154a36f6b7839f8afb351"}, - {file = "regex-2025.10.23-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:98fd84c4e4ea185b3bb5bf065261ab45867d8875032f358a435647285c722673"}, - {file = "regex-2025.10.23-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:1e11d3e5887b8b096f96b4154dfb902f29c723a9556639586cd140e77e28b313"}, - {file = "regex-2025.10.23-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f13450328a6634348d47a88367e06b64c9d84980ef6a748f717b13f8ce64e87"}, - {file = "regex-2025.10.23-cp313-cp313-win32.whl", hash = "sha256:37be9296598a30c6a20236248cb8b2c07ffd54d095b75d3a2a2ee5babdc51df1"}, - {file = "regex-2025.10.23-cp313-cp313-win_amd64.whl", hash = "sha256:ea7a3c283ce0f06fe789365841e9174ba05f8db16e2fd6ae00a02df9572c04c0"}, - {file = "regex-2025.10.23-cp313-cp313-win_arm64.whl", hash = "sha256:d9a4953575f300a7bab71afa4cd4ac061c7697c89590a2902b536783eeb49a4f"}, - {file = "regex-2025.10.23-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:7d6606524fa77b3912c9ef52a42ef63c6cfbfc1077e9dc6296cd5da0da286044"}, - {file = "regex-2025.10.23-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:c037aadf4d64bdc38af7db3dbd34877a057ce6524eefcb2914d6d41c56f968cc"}, - {file = "regex-2025.10.23-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:99018c331fb2529084a0c9b4c713dfa49fafb47c7712422e49467c13a636c656"}, - {file = "regex-2025.10.23-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fd8aba965604d70306eb90a35528f776e59112a7114a5162824d43b76fa27f58"}, - {file = "regex-2025.10.23-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:238e67264b4013e74136c49f883734f68656adf8257bfa13b515626b31b20f8e"}, - {file = "regex-2025.10.23-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b2eb48bd9848d66fd04826382f5e8491ae633de3233a3d64d58ceb4ecfa2113a"}, - {file = "regex-2025.10.23-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d36591ce06d047d0c0fe2fc5f14bfbd5b4525d08a7b6a279379085e13f0e3d0e"}, - {file = "regex-2025.10.23-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b5d4ece8628d6e364302006366cea3ee887db397faebacc5dacf8ef19e064cf8"}, - {file = "regex-2025.10.23-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:39a7e8083959cb1c4ff74e483eecb5a65d3b3e1d821b256e54baf61782c906c6"}, - {file = "regex-2025.10.23-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:842d449a8fefe546f311656cf8c0d6729b08c09a185f1cad94c756210286d6a8"}, - {file = "regex-2025.10.23-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d614986dc68506be8f00474f4f6960e03e4ca9883f7df47744800e7d7c08a494"}, - {file = "regex-2025.10.23-cp313-cp313t-win32.whl", hash = "sha256:a5b7a26b51a9df473ec16a1934d117443a775ceb7b39b78670b2e21893c330c9"}, - {file = "regex-2025.10.23-cp313-cp313t-win_amd64.whl", hash = "sha256:ce81c5544a5453f61cb6f548ed358cfb111e3b23f3cd42d250a4077a6be2a7b6"}, - {file = "regex-2025.10.23-cp313-cp313t-win_arm64.whl", hash = "sha256:e9bf7f6699f490e4e43c44757aa179dab24d1960999c84ab5c3d5377714ed473"}, - {file = "regex-2025.10.23-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:5b5cb5b6344c4c4c24b2dc87b0bfee78202b07ef7633385df70da7fcf6f7cec6"}, - {file = "regex-2025.10.23-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a6ce7973384c37bdf0f371a843f95a6e6f4e1489e10e0cf57330198df72959c5"}, - {file = "regex-2025.10.23-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2ee3663f2c334959016b56e3bd0dd187cbc73f948e3a3af14c3caaa0c3035d10"}, - {file = "regex-2025.10.23-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2003cc82a579107e70d013482acce8ba773293f2db534fb532738395c557ff34"}, - {file = "regex-2025.10.23-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:182c452279365a93a9f45874f7f191ec1c51e1f1eb41bf2b16563f1a40c1da3a"}, - {file = "regex-2025.10.23-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b1249e9ff581c5b658c8f0437f883b01f1edcf424a16388591e7c05e5e9e8b0c"}, - {file = "regex-2025.10.23-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b841698f93db3ccc36caa1900d2a3be281d9539b822dc012f08fc80b46a3224"}, - {file = "regex-2025.10.23-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:956d89e0c92d471e8f7eee73f73fdff5ed345886378c45a43175a77538a1ffe4"}, - {file = "regex-2025.10.23-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5c259cb363299a0d90d63b5c0d7568ee98419861618a95ee9d91a41cb9954462"}, - {file = "regex-2025.10.23-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:185d2b18c062820b3a40d8fefa223a83f10b20a674bf6e8c4a432e8dfd844627"}, - {file = "regex-2025.10.23-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:281d87fa790049c2b7c1b4253121edd80b392b19b5a3d28dc2a77579cb2a58ec"}, - {file = "regex-2025.10.23-cp314-cp314-win32.whl", hash = "sha256:63b81eef3656072e4ca87c58084c7a9c2b81d41a300b157be635a8a675aacfb8"}, - {file = "regex-2025.10.23-cp314-cp314-win_amd64.whl", hash = "sha256:0967c5b86f274800a34a4ed862dfab56928144d03cb18821c5153f8777947796"}, - {file = "regex-2025.10.23-cp314-cp314-win_arm64.whl", hash = "sha256:c70dfe58b0a00b36aa04cdb0f798bf3e0adc31747641f69e191109fd8572c9a9"}, - {file = "regex-2025.10.23-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:1f5799ea1787aa6de6c150377d11afad39a38afd033f0c5247aecb997978c422"}, - {file = "regex-2025.10.23-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:a9639ab7540cfea45ef57d16dcbea2e22de351998d614c3ad2f9778fa3bdd788"}, - {file = "regex-2025.10.23-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:08f52122c352eb44c3421dab78b9b73a8a77a282cc8314ae576fcaa92b780d10"}, - {file = "regex-2025.10.23-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ebf1baebef1c4088ad5a5623decec6b52950f0e4d7a0ae4d48f0a99f8c9cb7d7"}, - {file = "regex-2025.10.23-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:16b0f1c2e2d566c562d5c384c2b492646be0a19798532fdc1fdedacc66e3223f"}, - {file = "regex-2025.10.23-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7ada5d9dceafaab92646aa00c10a9efd9b09942dd9b0d7c5a4b73db92cc7e61"}, - {file = "regex-2025.10.23-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3a36b4005770044bf08edecc798f0e41a75795b9e7c9c12fe29da8d792ef870c"}, - {file = "regex-2025.10.23-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:af7b2661dcc032da1fae82069b5ebf2ac1dfcd5359ef8b35e1367bfc92181432"}, - {file = "regex-2025.10.23-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:1cb976810ac1416a67562c2e5ba0accf6f928932320fef302e08100ed681b38e"}, - {file = "regex-2025.10.23-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:1a56a54be3897d62f54290190fbcd754bff6932934529fbf5b29933da28fcd43"}, - {file = "regex-2025.10.23-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8f3e6d202fb52c2153f532043bbcf618fd177df47b0b306741eb9b60ba96edc3"}, - {file = "regex-2025.10.23-cp314-cp314t-win32.whl", hash = "sha256:1fa1186966b2621b1769fd467c7b22e317e6ba2d2cdcecc42ea3089ef04a8521"}, - {file = "regex-2025.10.23-cp314-cp314t-win_amd64.whl", hash = "sha256:08a15d40ce28362eac3e78e83d75475147869c1ff86bc93285f43b4f4431a741"}, - {file = "regex-2025.10.23-cp314-cp314t-win_arm64.whl", hash = "sha256:a93e97338e1c8ea2649e130dcfbe8cd69bba5e1e163834752ab64dcb4de6d5ed"}, - {file = "regex-2025.10.23-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d8d286760ee5b77fd21cf6b33cc45e0bffd1deeda59ca65b9be996f590a9828a"}, - {file = "regex-2025.10.23-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9e72e3b84b170fec02193d32620a0a7060a22e52c46e45957dcd14742e0d28fb"}, - {file = "regex-2025.10.23-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ec506e8114fa12d21616deb44800f536d6bf2e1a69253dbf611f69af92395c99"}, - {file = "regex-2025.10.23-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d7e481f9710e8e24228ce2c77b41db7662a3f68853395da86a292b49dadca2aa"}, - {file = "regex-2025.10.23-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4663ff2fc367735ae7b90b4f0e05b25554446df4addafc76fdaacaaa0ba852b5"}, - {file = "regex-2025.10.23-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0879dd3251a42d2e9b938e1e03b1e9f60de90b4d153015193f5077a376a18439"}, - {file = "regex-2025.10.23-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:651c58aecbab7e97bdf8ec76298a28d2bf2b6238c099ec6bf32e6d41e2f9a9cb"}, - {file = "regex-2025.10.23-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ceabc62a0e879169cd1bf066063bd6991c3e41e437628936a2ce66e0e2071c32"}, - {file = "regex-2025.10.23-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:bfdf4e9aa3e7b7d02fda97509b4ceeed34542361694ecc0a81db1688373ecfbd"}, - {file = "regex-2025.10.23-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:92f565ff9beb9f51bc7cc8c578a7e92eb5c4576b69043a4c58cd05d73fda83c5"}, - {file = "regex-2025.10.23-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:abbea548b1076eaf8635caf1071c9d86efdf0fa74abe71fca26c05a2d64cda80"}, - {file = "regex-2025.10.23-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:33535dcf34f47821381e341f7b715cbd027deda4223af4d3932adcd371d3192a"}, - {file = "regex-2025.10.23-cp39-cp39-win32.whl", hash = "sha256:345c9df49a15bf6460534b104b336581bc5f35c286cac526416e7a63d389b09b"}, - {file = "regex-2025.10.23-cp39-cp39-win_amd64.whl", hash = "sha256:f668fe1fd3358c5423355a289a4a003e58005ce829d217b828f80bd605a90145"}, - {file = "regex-2025.10.23-cp39-cp39-win_arm64.whl", hash = "sha256:07a3fd25d9074923e4d7258b551ae35ab6bdfe01904b8f0d5341c7d8b20eb18d"}, - {file = "regex-2025.10.23.tar.gz", hash = "sha256:8cbaf8ceb88f96ae2356d01b9adf5e6306fa42fa6f7eab6b97794e37c959ac26"}, -] - -[[package]] -name = "requests" -version = "2.32.5" -description = "Python HTTP for Humans." -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6"}, - {file = "requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf"}, -] - -[package.dependencies] -certifi = ">=2017.4.17" -charset_normalizer = ">=2,<4" -idna = ">=2.5,<4" -urllib3 = ">=1.21.1,<3" - -[package.extras] -socks = ["PySocks (>=1.5.6,!=1.5.7)"] -use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] - -[[package]] -name = "safetensors" -version = "0.6.2" -description = "" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "safetensors-0.6.2-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:9c85ede8ec58f120bad982ec47746981e210492a6db876882aa021446af8ffba"}, - {file = "safetensors-0.6.2-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:d6675cf4b39c98dbd7d940598028f3742e0375a6b4d4277e76beb0c35f4b843b"}, - {file = "safetensors-0.6.2-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d2d2b3ce1e2509c68932ca03ab8f20570920cd9754b05063d4368ee52833ecd"}, - {file = "safetensors-0.6.2-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:93de35a18f46b0f5a6a1f9e26d91b442094f2df02e9fd7acf224cfec4238821a"}, - {file = "safetensors-0.6.2-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:89a89b505f335640f9120fac65ddeb83e40f1fd081cb8ed88b505bdccec8d0a1"}, - {file = "safetensors-0.6.2-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fc4d0d0b937e04bdf2ae6f70cd3ad51328635fe0e6214aa1fc811f3b576b3bda"}, - {file = "safetensors-0.6.2-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8045db2c872db8f4cbe3faa0495932d89c38c899c603f21e9b6486951a5ecb8f"}, - {file = "safetensors-0.6.2-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:81e67e8bab9878bb568cffbc5f5e655adb38d2418351dc0859ccac158f753e19"}, - {file = "safetensors-0.6.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b0e4d029ab0a0e0e4fdf142b194514695b1d7d3735503ba700cf36d0fc7136ce"}, - {file = "safetensors-0.6.2-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:fa48268185c52bfe8771e46325a1e21d317207bcabcb72e65c6e28e9ffeb29c7"}, - {file = "safetensors-0.6.2-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:d83c20c12c2d2f465997c51b7ecb00e407e5f94d7dec3ea0cc11d86f60d3fde5"}, - {file = "safetensors-0.6.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:d944cea65fad0ead848b6ec2c37cc0b197194bec228f8020054742190e9312ac"}, - {file = "safetensors-0.6.2-cp38-abi3-win32.whl", hash = "sha256:cab75ca7c064d3911411461151cb69380c9225798a20e712b102edda2542ddb1"}, - {file = "safetensors-0.6.2-cp38-abi3-win_amd64.whl", hash = "sha256:c7b214870df923cbc1593c3faee16bec59ea462758699bd3fee399d00aac072c"}, - {file = "safetensors-0.6.2.tar.gz", hash = "sha256:43ff2aa0e6fa2dc3ea5524ac7ad93a9839256b8703761e76e2d0b2a3fa4f15d9"}, -] - -[package.extras] -all = ["safetensors[jax]", "safetensors[numpy]", "safetensors[paddlepaddle]", "safetensors[pinned-tf]", "safetensors[quality]", "safetensors[testing]", "safetensors[torch]"] -dev = ["safetensors[all]"] -jax = ["flax (>=0.6.3)", "jax (>=0.3.25)", "jaxlib (>=0.3.25)", "safetensors[numpy]"] -mlx = ["mlx (>=0.0.9)"] -numpy = ["numpy (>=1.21.6)"] -paddlepaddle = ["paddlepaddle (>=2.4.1)", "safetensors[numpy]"] -pinned-tf = ["safetensors[numpy]", "tensorflow (==2.18.0)"] -quality = ["ruff"] -tensorflow = ["safetensors[numpy]", "tensorflow (>=2.11.0)"] -testing = ["h5py (>=3.7.0)", "huggingface-hub (>=0.12.1)", "hypothesis (>=6.70.2)", "pytest (>=7.2.0)", "pytest-benchmark (>=4.0.0)", "safetensors[numpy]", "setuptools-rust (>=1.5.2)"] -testingfree = ["huggingface-hub (>=0.12.1)", "hypothesis (>=6.70.2)", "pytest (>=7.2.0)", "pytest-benchmark (>=4.0.0)", "safetensors[numpy]", "setuptools-rust (>=1.5.2)"] -torch = ["safetensors[numpy]", "torch (>=1.10)"] - -[[package]] -name = "setuptools" -version = "80.9.0" -description = "Easily download, build, install, upgrade, and uninstall Python packages" -optional = false -python-versions = ">=3.9" -groups = ["main"] -markers = "python_version >= \"3.12\"" -files = [ - {file = "setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922"}, - {file = "setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c"}, -] - -[package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.8.0) ; sys_platform != \"cygwin\""] -core = ["importlib_metadata (>=6) ; python_version < \"3.10\"", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1) ; python_version < \"3.11\"", "wheel (>=0.43.0)"] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] -enabler = ["pytest-enabler (>=2.2)"] -test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] -type = ["importlib_metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.14.*)", "pytest-mypy"] - -[[package]] -name = "six" -version = "1.17.0" -description = "Python 2 and 3 compatibility utilities" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -groups = ["main"] -files = [ - {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, - {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, -] - -[[package]] -name = "sniffio" -version = "1.3.1" -description = "Sniff out which async library your code is running under" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, - {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, -] - -[[package]] -name = "starlette" -version = "0.38.6" -description = "The little ASGI library that shines." -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "starlette-0.38.6-py3-none-any.whl", hash = "sha256:4517a1409e2e73ee4951214ba012052b9e16f60e90d73cfb06192c19203bbb05"}, - {file = "starlette-0.38.6.tar.gz", hash = "sha256:863a1588f5574e70a821dadefb41e4881ea451a47a3cd1b4df359d4ffefe5ead"}, -] - -[package.dependencies] -anyio = ">=3.4.0,<5" - -[package.extras] -full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.7)", "pyyaml"] - -[[package]] -name = "sympy" -version = "1.14.0" -description = "Computer algebra system (CAS) in Python" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5"}, - {file = "sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517"}, -] - -[package.dependencies] -mpmath = ">=1.1.0,<1.4" - -[package.extras] -dev = ["hypothesis (>=6.70.0)", "pytest (>=7.1.0)"] - -[[package]] -name = "tokenizers" -version = "0.22.1" -description = "" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "tokenizers-0.22.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:59fdb013df17455e5f950b4b834a7b3ee2e0271e6378ccb33aa74d178b513c73"}, - {file = "tokenizers-0.22.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:8d4e484f7b0827021ac5f9f71d4794aaef62b979ab7608593da22b1d2e3c4edc"}, - {file = "tokenizers-0.22.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19d2962dd28bc67c1f205ab180578a78eef89ac60ca7ef7cbe9635a46a56422a"}, - {file = "tokenizers-0.22.1-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:38201f15cdb1f8a6843e6563e6e79f4abd053394992b9bbdf5213ea3469b4ae7"}, - {file = "tokenizers-0.22.1-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d1cbe5454c9a15df1b3443c726063d930c16f047a3cc724b9e6e1a91140e5a21"}, - {file = "tokenizers-0.22.1-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e7d094ae6312d69cc2a872b54b91b309f4f6fbce871ef28eb27b52a98e4d0214"}, - {file = "tokenizers-0.22.1-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afd7594a56656ace95cdd6df4cca2e4059d294c5cfb1679c57824b605556cb2f"}, - {file = "tokenizers-0.22.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2ef6063d7a84994129732b47e7915e8710f27f99f3a3260b8a38fc7ccd083f4"}, - {file = "tokenizers-0.22.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ba0a64f450b9ef412c98f6bcd2a50c6df6e2443b560024a09fa6a03189726879"}, - {file = "tokenizers-0.22.1-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:331d6d149fa9c7d632cde4490fb8bbb12337fa3a0232e77892be656464f4b446"}, - {file = "tokenizers-0.22.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:607989f2ea68a46cb1dfbaf3e3aabdf3f21d8748312dbeb6263d1b3b66c5010a"}, - {file = "tokenizers-0.22.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a0f307d490295717726598ef6fa4f24af9d484809223bbc253b201c740a06390"}, - {file = "tokenizers-0.22.1-cp39-abi3-win32.whl", hash = "sha256:b5120eed1442765cd90b903bb6cfef781fd8fe64e34ccaecbae4c619b7b12a82"}, - {file = "tokenizers-0.22.1-cp39-abi3-win_amd64.whl", hash = "sha256:65fd6e3fb11ca1e78a6a93602490f134d1fdeb13bcef99389d5102ea318ed138"}, - {file = "tokenizers-0.22.1.tar.gz", hash = "sha256:61de6522785310a309b3407bac22d99c4db5dba349935e99e4d15ea2226af2d9"}, -] - -[package.dependencies] -huggingface-hub = ">=0.16.4,<2.0" - -[package.extras] -dev = ["tokenizers[testing]"] -docs = ["setuptools-rust", "sphinx", "sphinx-rtd-theme"] -testing = ["black (==22.3)", "datasets", "numpy", "pytest", "pytest-asyncio", "requests", "ruff"] - -[[package]] -name = "tomli" -version = "2.3.0" -description = "A lil' TOML parser" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -markers = "python_version == \"3.10\"" -files = [ - {file = "tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45"}, - {file = "tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba"}, - {file = "tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf"}, - {file = "tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441"}, - {file = "tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845"}, - {file = "tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c"}, - {file = "tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456"}, - {file = "tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be"}, - {file = "tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac"}, - {file = "tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22"}, - {file = "tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f"}, - {file = "tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52"}, - {file = "tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8"}, - {file = "tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6"}, - {file = "tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876"}, - {file = "tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878"}, - {file = "tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b"}, - {file = "tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae"}, - {file = "tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b"}, - {file = "tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf"}, - {file = "tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f"}, - {file = "tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05"}, - {file = "tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606"}, - {file = "tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999"}, - {file = "tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e"}, - {file = "tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3"}, - {file = "tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc"}, - {file = "tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0"}, - {file = "tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879"}, - {file = "tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005"}, - {file = "tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463"}, - {file = "tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8"}, - {file = "tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77"}, - {file = "tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf"}, - {file = "tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530"}, - {file = "tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b"}, - {file = "tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67"}, - {file = "tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f"}, - {file = "tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0"}, - {file = "tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba"}, - {file = "tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b"}, - {file = "tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549"}, -] - -[[package]] -name = "torch" -version = "2.7.1" -description = "Tensors and Dynamic neural networks in Python with strong GPU acceleration" -optional = false -python-versions = ">=3.9.0" -groups = ["main"] -markers = "python_version >= \"3.13\"" -files = [ - {file = "torch-2.7.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:a103b5d782af5bd119b81dbcc7ffc6fa09904c423ff8db397a1e6ea8fd71508f"}, - {file = "torch-2.7.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:fe955951bdf32d182ee8ead6c3186ad54781492bf03d547d31771a01b3d6fb7d"}, - {file = "torch-2.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:885453d6fba67d9991132143bf7fa06b79b24352f4506fd4d10b309f53454162"}, - {file = "torch-2.7.1-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:d72acfdb86cee2a32c0ce0101606f3758f0d8bb5f8f31e7920dc2809e963aa7c"}, - {file = "torch-2.7.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:236f501f2e383f1cb861337bdf057712182f910f10aeaf509065d54d339e49b2"}, - {file = "torch-2.7.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:06eea61f859436622e78dd0cdd51dbc8f8c6d76917a9cf0555a333f9eac31ec1"}, - {file = "torch-2.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:8273145a2e0a3c6f9fd2ac36762d6ee89c26d430e612b95a99885df083b04e52"}, - {file = "torch-2.7.1-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:aea4fc1bf433d12843eb2c6b2204861f43d8364597697074c8d38ae2507f8730"}, - {file = "torch-2.7.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:27ea1e518df4c9de73af7e8a720770f3628e7f667280bce2be7a16292697e3fa"}, - {file = "torch-2.7.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:c33360cfc2edd976c2633b3b66c769bdcbbf0e0b6550606d188431c81e7dd1fc"}, - {file = "torch-2.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:d8bf6e1856ddd1807e79dc57e54d3335f2b62e6f316ed13ed3ecfe1fc1df3d8b"}, - {file = "torch-2.7.1-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:787687087412c4bd68d315e39bc1223f08aae1d16a9e9771d95eabbb04ae98fb"}, - {file = "torch-2.7.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:03563603d931e70722dce0e11999d53aa80a375a3d78e6b39b9f6805ea0a8d28"}, - {file = "torch-2.7.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:d632f5417b6980f61404a125b999ca6ebd0b8b4bbdbb5fbbba44374ab619a412"}, - {file = "torch-2.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:23660443e13995ee93e3d844786701ea4ca69f337027b05182f5ba053ce43b38"}, - {file = "torch-2.7.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:0da4f4dba9f65d0d203794e619fe7ca3247a55ffdcbd17ae8fb83c8b2dc9b585"}, - {file = "torch-2.7.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:e08d7e6f21a617fe38eeb46dd2213ded43f27c072e9165dc27300c9ef9570934"}, - {file = "torch-2.7.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:30207f672328a42df4f2174b8f426f354b2baa0b7cca3a0adb3d6ab5daf00dc8"}, - {file = "torch-2.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:79042feca1c634aaf6603fe6feea8c6b30dfa140a6bbc0b973e2260c7e79a22e"}, - {file = "torch-2.7.1-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:988b0cbc4333618a1056d2ebad9eb10089637b659eb645434d0809d8d937b946"}, - {file = "torch-2.7.1-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:e0d81e9a12764b6f3879a866607c8ae93113cbcad57ce01ebde63eb48a576369"}, - {file = "torch-2.7.1-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:8394833c44484547ed4a47162318337b88c97acdb3273d85ea06e03ffff44998"}, - {file = "torch-2.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:df41989d9300e6e3c19ec9f56f856187a6ef060c3662fe54f4b6baf1fc90bd19"}, - {file = "torch-2.7.1-cp39-none-macosx_11_0_arm64.whl", hash = "sha256:a737b5edd1c44a5c1ece2e9f3d00df9d1b3fb9541138bee56d83d38293fb6c9d"}, -] - -[package.dependencies] -filelock = "*" -fsspec = "*" -jinja2 = "*" -networkx = "*" -nvidia-cublas-cu12 = {version = "12.6.4.1", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} -nvidia-cuda-cupti-cu12 = {version = "12.6.80", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} -nvidia-cuda-nvrtc-cu12 = {version = "12.6.77", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} -nvidia-cuda-runtime-cu12 = {version = "12.6.77", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} -nvidia-cudnn-cu12 = {version = "9.5.1.17", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} -nvidia-cufft-cu12 = {version = "11.3.0.4", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} -nvidia-cufile-cu12 = {version = "1.11.1.6", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} -nvidia-curand-cu12 = {version = "10.3.7.77", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} -nvidia-cusolver-cu12 = {version = "11.7.1.2", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} -nvidia-cusparse-cu12 = {version = "12.5.4.2", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} -nvidia-cusparselt-cu12 = {version = "0.6.3", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} -nvidia-nccl-cu12 = {version = "2.26.2", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} -nvidia-nvjitlink-cu12 = {version = "12.6.85", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} -nvidia-nvtx-cu12 = {version = "12.6.77", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} -setuptools = {version = "*", markers = "python_version >= \"3.12\""} -sympy = ">=1.13.3" -triton = {version = "3.3.1", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} -typing-extensions = ">=4.10.0" - -[package.extras] -opt-einsum = ["opt-einsum (>=3.3)"] -optree = ["optree (>=0.13.0)"] - -[[package]] -name = "torch" -version = "2.9.0" -description = "Tensors and Dynamic neural networks in Python with strong GPU acceleration" -optional = false -python-versions = ">=3.10" -groups = ["main"] -markers = "python_version < \"3.13\"" -files = [ - {file = "torch-2.9.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:030bbfe367379ae6a4ae4042b6c44da25383343b8b3c68abaa9c7231efbaf2dd"}, - {file = "torch-2.9.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:51cb63902182a78e90886e8068befd8ea102af4b00e420263591a3d70c7d3c6c"}, - {file = "torch-2.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:3f6aad4d2f0ee2248bac25339d74858ff846c3969b27d14ac235821f055af83d"}, - {file = "torch-2.9.0-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:413e1654c9203733138858780e184d9fc59442f0b3b209e16f39354eb893db9b"}, - {file = "torch-2.9.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:c596708b5105d0b199215acf0c9be7c1db5f1680d88eddadf4b75a299259a677"}, - {file = "torch-2.9.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:51de31219c97c51cf4bf2be94d622e3deb5dcc526c6dc00e97c17eaec0fc1d67"}, - {file = "torch-2.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:dd515c70059afd95f48b8192733764c08ca37a1d19803af6401b5ecad7c8676e"}, - {file = "torch-2.9.0-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:614a185e4986326d526a91210c8fc1397e76e8cfafa78baf6296a790e53a9eec"}, - {file = "torch-2.9.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:e5f7af1dc4c0a7c4a260c2534f41ddaf209714f7c89145e644c44712fbd6b642"}, - {file = "torch-2.9.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:01cff95ecd9a212ea2f141db28acccdceb6a4c54f64e6c51091146f5e2a772c6"}, - {file = "torch-2.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:4582b162f541651f0cb184d3e291c05c2f556c7117c64a9873e2ee158d40062b"}, - {file = "torch-2.9.0-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:33f58e9a102a91259af289d50525c30323b5c9ae1d31322b6447c0814da68695"}, - {file = "torch-2.9.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:c30a17fc83eeab346913e237c64b15b5ba6407fff812f6c541e322e19bc9ea0e"}, - {file = "torch-2.9.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:8f25033b8667b57857dfd01458fbf2a9e6a6df1f8def23aef0dc46292f6aa642"}, - {file = "torch-2.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:d037f1b4ffd25013be4a7bf3651a0a910c68554956c7b2c92ebe87c76475dece"}, - {file = "torch-2.9.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e4e5b5cba837a2a8d1a497ba9a58dae46fa392593eaa13b871c42f71847503a5"}, - {file = "torch-2.9.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:64693568f5dc4dbd5f880a478b1cea0201cc6b510d91d1bc54fea86ac5d1a637"}, - {file = "torch-2.9.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:f8ed31ddd7d10bfb3fbe0b9fe01b1243577f13d75e6f4a0839a283915ce3791e"}, - {file = "torch-2.9.0-cp313-cp313t-win_amd64.whl", hash = "sha256:eff527d4e4846e6f70d2afd8058b73825761203d66576a7e04ea2ecfebcb4ab8"}, - {file = "torch-2.9.0-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:f8877779cf56d1ce431a7636703bdb13307f5960bb1af49716d8b179225e0e6a"}, - {file = "torch-2.9.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7e614fae699838038d888729f82b687c03413c5989ce2a9481f9a7e7a396e0bb"}, - {file = "torch-2.9.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:dfb5b8cd310ba3436c7e14e8b7833ef658cf3045e50d2bdaed23c8fc517065eb"}, - {file = "torch-2.9.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:b3d29524993a478e46f5d598b249cd824b7ed98d7fba538bd9c4cde6c803948f"}, - {file = "torch-2.9.0-cp314-cp314-win_amd64.whl", hash = "sha256:71c7578984f5ec0eb645eb4816ac8435fcf3e3e2ae1901bcd2f519a9cafb5125"}, - {file = "torch-2.9.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:71d9309aee457bbe0b164bce2111cd911c4ed4e847e65d5077dbbcd3aba6befc"}, - {file = "torch-2.9.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:c08fb654d783899e204a32cca758a7ce8a45b2d78eeb89517cc937088316f78e"}, - {file = "torch-2.9.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:ec8feb0099b2daa5728fbc7abb0b05730fd97e0f359ff8bda09865aaa7bd7d4b"}, - {file = "torch-2.9.0-cp314-cp314t-win_amd64.whl", hash = "sha256:695ba920f234ad4170c9c50e28d56c848432f8f530e6bc7f88fcb15ddf338e75"}, -] - -[package.dependencies] -filelock = "*" -fsspec = ">=0.8.5" -jinja2 = "*" -networkx = ">=2.5.1" -nvidia-cublas-cu12 = {version = "12.8.4.1", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} -nvidia-cuda-cupti-cu12 = {version = "12.8.90", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} -nvidia-cuda-nvrtc-cu12 = {version = "12.8.93", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} -nvidia-cuda-runtime-cu12 = {version = "12.8.90", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} -nvidia-cudnn-cu12 = {version = "9.10.2.21", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} -nvidia-cufft-cu12 = {version = "11.3.3.83", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} -nvidia-cufile-cu12 = {version = "1.13.1.3", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} -nvidia-curand-cu12 = {version = "10.3.9.90", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} -nvidia-cusolver-cu12 = {version = "11.7.3.90", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} -nvidia-cusparse-cu12 = {version = "12.5.8.93", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} -nvidia-cusparselt-cu12 = {version = "0.7.1", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} -nvidia-nccl-cu12 = {version = "2.27.5", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} -nvidia-nvjitlink-cu12 = {version = "12.8.93", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} -nvidia-nvshmem-cu12 = {version = "3.3.20", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} -nvidia-nvtx-cu12 = {version = "12.8.90", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} -setuptools = {version = "*", markers = "python_version >= \"3.12\""} -sympy = ">=1.13.3" -triton = {version = "3.5.0", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} -typing-extensions = ">=4.10.0" - -[package.extras] -opt-einsum = ["opt-einsum (>=3.3)"] -optree = ["optree (>=0.13.0)"] -pyyaml = ["pyyaml"] - -[[package]] -name = "tqdm" -version = "4.67.1" -description = "Fast, Extensible Progress Meter" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2"}, - {file = "tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "platform_system == \"Windows\""} - -[package.extras] -dev = ["nbval", "pytest (>=6)", "pytest-asyncio (>=0.24)", "pytest-cov", "pytest-timeout"] -discord = ["requests"] -notebook = ["ipywidgets (>=6)"] -slack = ["slack-sdk"] -telegram = ["requests"] - -[[package]] -name = "transformers" -version = "4.57.1" -description = "State-of-the-art Machine Learning for JAX, PyTorch and TensorFlow" -optional = false -python-versions = ">=3.9.0" -groups = ["main"] -files = [ - {file = "transformers-4.57.1-py3-none-any.whl", hash = "sha256:b10d05da8fa67dc41644dbbf9bc45a44cb86ae33da6f9295f5fbf5b7890bd267"}, - {file = "transformers-4.57.1.tar.gz", hash = "sha256:f06c837959196c75039809636cd964b959f6604b75b8eeec6fdfc0440b89cc55"}, -] - -[package.dependencies] -filelock = "*" -huggingface-hub = ">=0.34.0,<1.0" -numpy = ">=1.17" -packaging = ">=20.0" -pyyaml = ">=5.1" -regex = "!=2019.12.17" -requests = "*" -safetensors = ">=0.4.3" -tokenizers = ">=0.22.0,<=0.23.0" -tqdm = ">=4.27" - -[package.extras] -accelerate = ["accelerate (>=0.26.0)"] -all = ["Pillow (>=10.0.1,<=15.0)", "Pillow (>=10.0.1,<=15.0)", "accelerate (>=0.26.0)", "accelerate (>=0.26.0)", "av", "codecarbon (>=2.8.1)", "flax (>=0.4.1,<=0.7.0)", "jax (>=0.4.1,<=0.4.13)", "jaxlib (>=0.4.1,<=0.4.13)", "jinja2 (>=3.1.0)", "kenlm", "keras-nlp (>=0.3.1,<0.14.0)", "kernels (>=0.6.1,<=0.9)", "librosa", "mistral-common[opencv] (>=1.6.3)", "num2words", "onnxconverter-common", "optax (>=0.0.8,<=0.1.4)", "optuna", "phonemizer", "protobuf", "pyctcdecode (>=0.4.0)", "ray[tune] (>=2.7.0)", "scipy (<1.13.0)", "sentencepiece (>=0.1.91,!=0.1.92)", "tensorflow (>2.9,<2.16)", "tensorflow-text (<2.16)", "tf2onnx", "timm (!=1.0.18,<=1.0.19)", "tokenizers (>=0.22.0,<=0.23.0)", "torch (>=2.2)", "torchaudio", "torchvision"] -audio = ["kenlm", "librosa", "phonemizer", "pyctcdecode (>=0.4.0)"] -benchmark = ["optimum-benchmark (>=0.3.0)"] -chat-template = ["jinja2 (>=3.1.0)"] -codecarbon = ["codecarbon (>=2.8.1)"] -deepspeed = ["accelerate (>=0.26.0)", "deepspeed (>=0.9.3)"] -deepspeed-testing = ["GitPython (<3.1.19)", "accelerate (>=0.26.0)", "accelerate (>=0.26.0)", "beautifulsoup4", "cookiecutter (==1.7.3)", "datasets (>=2.15.0)", "datasets (>=2.15.0)", "deepspeed (>=0.9.3)", "dill (<0.3.5)", "evaluate (>=0.2.0)", "faiss-cpu", "fastapi", "libcst", "mistral-common[opencv] (>=1.6.3)", "nltk (<=3.8.1)", "openai (>=1.98.0)", "optuna", "parameterized (>=0.9)", "protobuf", "psutil", "pydantic (>=2)", "pydantic (>=2)", "pytest (>=7.2.0)", "pytest-asyncio", "pytest-order", "pytest-rerunfailures (<16.0)", "pytest-rich", "pytest-timeout", "pytest-xdist", "rjieba", "rouge-score (!=0.0.7,!=0.0.8,!=0.1,!=0.1.1)", "ruff (==0.13.1)", "sacrebleu (>=1.4.12,<2.0.0)", "sacremoses", "sentencepiece (>=0.1.91,!=0.1.92)", "sentencepiece (>=0.1.91,!=0.1.92)", "starlette", "tensorboard", "timeout-decorator", "torch (>=2.2)", "uvicorn"] -dev = ["GitPython (<3.1.19)", "GitPython (<3.1.19)", "Pillow (>=10.0.1,<=15.0)", "Pillow (>=10.0.1,<=15.0)", "accelerate (>=0.26.0)", "accelerate (>=0.26.0)", "accelerate (>=0.26.0)", "av", "beautifulsoup4", "codecarbon (>=2.8.1)", "cookiecutter (==1.7.3)", "cookiecutter (==1.7.3)", "datasets (>=2.15.0)", "datasets (>=2.15.0)", "datasets (>=2.15.0)", "dill (<0.3.5)", "evaluate (>=0.2.0)", "faiss-cpu", "fastapi", "flax (>=0.4.1,<=0.7.0)", "fugashi (>=1.0)", "ipadic (>=1.0.0,<2.0)", "jax (>=0.4.1,<=0.4.13)", "jaxlib (>=0.4.1,<=0.4.13)", "jinja2 (>=3.1.0)", "kenlm", "keras-nlp (>=0.3.1,<0.14.0)", "kernels (>=0.6.1,<=0.9)", "libcst", "libcst", "librosa", "mistral-common[opencv] (>=1.6.3)", "mistral-common[opencv] (>=1.6.3)", "nltk (<=3.8.1)", "num2words", "onnxconverter-common", "openai (>=1.98.0)", "optax (>=0.0.8,<=0.1.4)", "optuna", "pandas (<2.3.0)", "parameterized (>=0.9)", "phonemizer", "protobuf", "psutil", "pyctcdecode (>=0.4.0)", "pydantic (>=2)", "pydantic (>=2)", "pytest (>=7.2.0)", "pytest-asyncio", "pytest-order", "pytest-rerunfailures (<16.0)", "pytest-rich", "pytest-timeout", "pytest-xdist", "ray[tune] (>=2.7.0)", "rhoknp (>=1.1.0,<1.3.1)", "rich", "rjieba", "rouge-score (!=0.0.7,!=0.0.8,!=0.1,!=0.1.1)", "ruff (==0.13.1)", "ruff (==0.13.1)", "sacrebleu (>=1.4.12,<2.0.0)", "sacremoses", "scikit-learn", "scipy (<1.13.0)", "sentencepiece (>=0.1.91,!=0.1.92)", "sentencepiece (>=0.1.91,!=0.1.92)", "starlette", "sudachidict_core (>=20220729)", "sudachipy (>=0.6.6)", "tensorboard", "tensorflow (>2.9,<2.16)", "tensorflow-text (<2.16)", "tf2onnx", "timeout-decorator", "timm (!=1.0.18,<=1.0.19)", "tokenizers (>=0.22.0,<=0.23.0)", "torch (>=2.2)", "torch (>=2.2)", "torchaudio", "torchvision", "unidic (>=1.0.2)", "unidic_lite (>=1.0.7)", "urllib3 (<2.0.0)", "uvicorn"] -dev-tensorflow = ["GitPython (<3.1.19)", "GitPython (<3.1.19)", "Pillow (>=10.0.1,<=15.0)", "accelerate (>=0.26.0)", "beautifulsoup4", "cookiecutter (==1.7.3)", "cookiecutter (==1.7.3)", "datasets (>=2.15.0)", "datasets (>=2.15.0)", "datasets (>=2.15.0)", "dill (<0.3.5)", "evaluate (>=0.2.0)", "faiss-cpu", "fastapi", "kenlm", "keras-nlp (>=0.3.1,<0.14.0)", "libcst", "libcst", "librosa", "mistral-common[opencv] (>=1.6.3)", "nltk (<=3.8.1)", "onnxconverter-common", "onnxconverter-common", "onnxruntime (>=1.4.0)", "onnxruntime-tools (>=1.4.2)", "openai (>=1.98.0)", "pandas (<2.3.0)", "parameterized (>=0.9)", "phonemizer", "protobuf", "psutil", "pyctcdecode (>=0.4.0)", "pydantic (>=2)", "pydantic (>=2)", "pytest (>=7.2.0)", "pytest-asyncio", "pytest-order", "pytest-rerunfailures (<16.0)", "pytest-rich", "pytest-timeout", "pytest-xdist", "rich", "rjieba", "rouge-score (!=0.0.7,!=0.0.8,!=0.1,!=0.1.1)", "ruff (==0.13.1)", "ruff (==0.13.1)", "sacrebleu (>=1.4.12,<2.0.0)", "sacremoses", "scikit-learn", "sentencepiece (>=0.1.91,!=0.1.92)", "sentencepiece (>=0.1.91,!=0.1.92)", "starlette", "tensorboard", "tensorflow (>2.9,<2.16)", "tensorflow-text (<2.16)", "tf2onnx", "tf2onnx", "timeout-decorator", "tokenizers (>=0.22.0,<=0.23.0)", "torch (>=2.2)", "urllib3 (<2.0.0)", "uvicorn"] -dev-torch = ["GitPython (<3.1.19)", "GitPython (<3.1.19)", "Pillow (>=10.0.1,<=15.0)", "Pillow (>=10.0.1,<=15.0)", "accelerate (>=0.26.0)", "accelerate (>=0.26.0)", "beautifulsoup4", "codecarbon (>=2.8.1)", "cookiecutter (==1.7.3)", "cookiecutter (==1.7.3)", "datasets (>=2.15.0)", "datasets (>=2.15.0)", "datasets (>=2.15.0)", "dill (<0.3.5)", "evaluate (>=0.2.0)", "faiss-cpu", "fastapi", "fugashi (>=1.0)", "ipadic (>=1.0.0,<2.0)", "kenlm", "kernels (>=0.6.1,<=0.9)", "libcst", "libcst", "librosa", "mistral-common[opencv] (>=1.6.3)", "nltk (<=3.8.1)", "num2words", "onnxruntime (>=1.4.0)", "onnxruntime-tools (>=1.4.2)", "openai (>=1.98.0)", "optuna", "pandas (<2.3.0)", "parameterized (>=0.9)", "phonemizer", "protobuf", "psutil", "pyctcdecode (>=0.4.0)", "pydantic (>=2)", "pydantic (>=2)", "pytest (>=7.2.0)", "pytest-asyncio", "pytest-order", "pytest-rerunfailures (<16.0)", "pytest-rich", "pytest-timeout", "pytest-xdist", "ray[tune] (>=2.7.0)", "rhoknp (>=1.1.0,<1.3.1)", "rich", "rjieba", "rouge-score (!=0.0.7,!=0.0.8,!=0.1,!=0.1.1)", "ruff (==0.13.1)", "ruff (==0.13.1)", "sacrebleu (>=1.4.12,<2.0.0)", "sacremoses", "scikit-learn", "sentencepiece (>=0.1.91,!=0.1.92)", "sentencepiece (>=0.1.91,!=0.1.92)", "starlette", "sudachidict_core (>=20220729)", "sudachipy (>=0.6.6)", "tensorboard", "timeout-decorator", "timm (!=1.0.18,<=1.0.19)", "tokenizers (>=0.22.0,<=0.23.0)", "torch (>=2.2)", "torch (>=2.2)", "torchaudio", "torchvision", "unidic (>=1.0.2)", "unidic_lite (>=1.0.7)", "urllib3 (<2.0.0)", "uvicorn"] -flax = ["flax (>=0.4.1,<=0.7.0)", "jax (>=0.4.1,<=0.4.13)", "jaxlib (>=0.4.1,<=0.4.13)", "optax (>=0.0.8,<=0.1.4)", "scipy (<1.13.0)"] -flax-speech = ["kenlm", "librosa", "phonemizer", "pyctcdecode (>=0.4.0)"] -ftfy = ["ftfy"] -hf-xet = ["hf_xet"] -hub-kernels = ["kernels (>=0.6.1,<=0.9)"] -integrations = ["kernels (>=0.6.1,<=0.9)", "optuna", "ray[tune] (>=2.7.0)"] -ja = ["fugashi (>=1.0)", "ipadic (>=1.0.0,<2.0)", "rhoknp (>=1.1.0,<1.3.1)", "sudachidict_core (>=20220729)", "sudachipy (>=0.6.6)", "unidic (>=1.0.2)", "unidic_lite (>=1.0.7)"] -mistral-common = ["mistral-common[opencv] (>=1.6.3)"] -modelcreation = ["cookiecutter (==1.7.3)"] -natten = ["natten (>=0.14.6,<0.15.0)"] -num2words = ["num2words"] -onnx = ["onnxconverter-common", "onnxruntime (>=1.4.0)", "onnxruntime-tools (>=1.4.2)", "tf2onnx"] -onnxruntime = ["onnxruntime (>=1.4.0)", "onnxruntime-tools (>=1.4.2)"] -open-telemetry = ["opentelemetry-api", "opentelemetry-exporter-otlp", "opentelemetry-sdk"] -optuna = ["optuna"] -quality = ["GitPython (<3.1.19)", "datasets (>=2.15.0)", "libcst", "pandas (<2.3.0)", "rich", "ruff (==0.13.1)", "urllib3 (<2.0.0)"] -ray = ["ray[tune] (>=2.7.0)"] -retrieval = ["datasets (>=2.15.0)", "faiss-cpu"] -ruff = ["ruff (==0.13.1)"] -sagemaker = ["sagemaker (>=2.31.0)"] -sentencepiece = ["protobuf", "sentencepiece (>=0.1.91,!=0.1.92)"] -serving = ["accelerate (>=0.26.0)", "fastapi", "openai (>=1.98.0)", "pydantic (>=2)", "starlette", "torch (>=2.2)", "uvicorn"] -sigopt = ["sigopt"] -sklearn = ["scikit-learn"] -speech = ["kenlm", "librosa", "phonemizer", "pyctcdecode (>=0.4.0)", "torchaudio"] -testing = ["GitPython (<3.1.19)", "accelerate (>=0.26.0)", "beautifulsoup4", "cookiecutter (==1.7.3)", "datasets (>=2.15.0)", "datasets (>=2.15.0)", "dill (<0.3.5)", "evaluate (>=0.2.0)", "faiss-cpu", "fastapi", "libcst", "mistral-common[opencv] (>=1.6.3)", "nltk (<=3.8.1)", "openai (>=1.98.0)", "parameterized (>=0.9)", "psutil", "pydantic (>=2)", "pydantic (>=2)", "pytest (>=7.2.0)", "pytest-asyncio", "pytest-order", "pytest-rerunfailures (<16.0)", "pytest-rich", "pytest-timeout", "pytest-xdist", "rjieba", "rouge-score (!=0.0.7,!=0.0.8,!=0.1,!=0.1.1)", "ruff (==0.13.1)", "sacrebleu (>=1.4.12,<2.0.0)", "sacremoses", "sentencepiece (>=0.1.91,!=0.1.92)", "starlette", "tensorboard", "timeout-decorator", "torch (>=2.2)", "uvicorn"] -tf = ["keras-nlp (>=0.3.1,<0.14.0)", "onnxconverter-common", "tensorflow (>2.9,<2.16)", "tensorflow-text (<2.16)", "tf2onnx"] -tf-cpu = ["keras (>2.9,<2.16)", "keras-nlp (>=0.3.1,<0.14.0)", "onnxconverter-common", "tensorflow-cpu (>2.9,<2.16)", "tensorflow-probability (<0.24)", "tensorflow-text (<2.16)", "tf2onnx"] -tf-speech = ["kenlm", "librosa", "phonemizer", "pyctcdecode (>=0.4.0)"] -tiktoken = ["blobfile", "tiktoken"] -timm = ["timm (!=1.0.18,<=1.0.19)"] -tokenizers = ["tokenizers (>=0.22.0,<=0.23.0)"] -torch = ["accelerate (>=0.26.0)", "torch (>=2.2)"] -torch-speech = ["kenlm", "librosa", "phonemizer", "pyctcdecode (>=0.4.0)", "torchaudio"] -torch-vision = ["Pillow (>=10.0.1,<=15.0)", "torchvision"] -torchhub = ["filelock", "huggingface-hub (>=0.34.0,<1.0)", "importlib_metadata", "numpy (>=1.17)", "packaging (>=20.0)", "protobuf", "regex (!=2019.12.17)", "requests", "sentencepiece (>=0.1.91,!=0.1.92)", "tokenizers (>=0.22.0,<=0.23.0)", "torch (>=2.2)", "tqdm (>=4.27)"] -video = ["av"] -vision = ["Pillow (>=10.0.1,<=15.0)"] - -[[package]] -name = "triton" -version = "3.3.1" -description = "A language and compiler for custom Deep Learning operations" -optional = false -python-versions = "*" -groups = ["main"] -markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and python_version >= \"3.13\"" -files = [ - {file = "triton-3.3.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b74db445b1c562844d3cfad6e9679c72e93fdfb1a90a24052b03bb5c49d1242e"}, - {file = "triton-3.3.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b31e3aa26f8cb3cc5bf4e187bf737cbacf17311e1112b781d4a059353dfd731b"}, - {file = "triton-3.3.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9999e83aba21e1a78c1f36f21bce621b77bcaa530277a50484a7cb4a822f6e43"}, - {file = "triton-3.3.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b89d846b5a4198317fec27a5d3a609ea96b6d557ff44b56c23176546023c4240"}, - {file = "triton-3.3.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3198adb9d78b77818a5388bff89fa72ff36f9da0bc689db2f0a651a67ce6a42"}, - {file = "triton-3.3.1-cp39-cp39-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f6139aeb04a146b0b8e0fbbd89ad1e65861c57cfed881f21d62d3cb94a36bab7"}, -] - -[package.dependencies] -setuptools = ">=40.8.0" - -[package.extras] -build = ["cmake (>=3.20)", "lit"] -tests = ["autopep8", "isort", "llnl-hatchet", "numpy", "pytest", "pytest-forked", "pytest-xdist", "scipy (>=1.7.1)"] -tutorials = ["matplotlib", "pandas", "tabulate"] - -[[package]] -name = "triton" -version = "3.5.0" -description = "A language and compiler for custom Deep Learning operations" -optional = false -python-versions = "<3.15,>=3.10" -groups = ["main"] -markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and python_version < \"3.13\"" -files = [ - {file = "triton-3.5.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6f90de6a6566bb619b4c0adc9855729e1b1b5e26533fca1bf6206e96b6d277a3"}, - {file = "triton-3.5.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5d3b3d480debf24eaa739623c9a42446b0b77f95593d30eb1f64cd2278cc1f0"}, - {file = "triton-3.5.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8457b22148defefdcb7fa8144b05ce211b9faefad650a1ce85b23df488d5549c"}, - {file = "triton-3.5.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f34bfa21c5b3a203c0f0eab28dcc1e49bd1f67d22724e77fb6665a659200a4ec"}, - {file = "triton-3.5.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7da21fccceafc163e3a5e857abe34351ef76345af06cabf9637a914742671f0b"}, - {file = "triton-3.5.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9e71db82261c4ffa3921cd050cd5faa18322d2d405c30eb56084afaff3b0833"}, - {file = "triton-3.5.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:188da5b81fa2f8322c27fec1627703eac24cb9bb7ab0dfbe9925973bc1b070d3"}, - {file = "triton-3.5.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e6bb9aa5519c084a333acdba443789e50012a4b851cd486c54f0b8dc2a8d3a12"}, - {file = "triton-3.5.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:03127d9b33aaf979c856676b394bc059ec1d68cb6da68ae03f62dd8ad77a04ae"}, - {file = "triton-3.5.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c83f2343e1a220a716c7b3ab9fccfcbe3ad4020d189549200e2d2e8d5868bed9"}, - {file = "triton-3.5.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:468936651d383f4a6d10068d34a627505e13af55be5d002b9f27b987e7a5f0ac"}, - {file = "triton-3.5.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da0fa67ccd76c3dcfb0bffe1b1c57c685136a6bd33d141c24d9655d4185b1289"}, - {file = "triton-3.5.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7ceef21410229ac23173a28eee5cfc0e37c1dfdb8b4bc11ecda2e3ecec7c686"}, - {file = "triton-3.5.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:317fe477ea8fd4524a6a8c499fb0a36984a56d0b75bf9c9cb6133a1c56d5a6e7"}, -] - -[package.extras] -build = ["cmake (>=3.20,<4.0)", "lit"] -tests = ["autopep8", "isort", "llnl-hatchet", "numpy", "pytest", "pytest-forked", "pytest-xdist", "scipy (>=1.7.1)"] -tutorials = ["matplotlib", "pandas", "tabulate"] - -[[package]] -name = "typing-extensions" -version = "4.15.0" -description = "Backported and Experimental Type Hints for Python 3.9+" -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -files = [ - {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, - {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, -] -markers = {dev = "python_version < \"3.13\""} - -[[package]] -name = "typing-inspection" -version = "0.4.2" -description = "Runtime typing introspection tools" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7"}, - {file = "typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464"}, -] - -[package.dependencies] -typing-extensions = ">=4.12.0" - -[[package]] -name = "tzdata" -version = "2025.2" -description = "Provider of IANA time zone data" -optional = false -python-versions = ">=2" -groups = ["main"] -markers = "platform_system == \"Windows\"" -files = [ - {file = "tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8"}, - {file = "tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9"}, -] - -[[package]] -name = "tzlocal" -version = "5.3.1" -description = "tzinfo object for the local timezone" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "tzlocal-5.3.1-py3-none-any.whl", hash = "sha256:eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d"}, - {file = "tzlocal-5.3.1.tar.gz", hash = "sha256:cceffc7edecefea1f595541dbd6e990cb1ea3d19bf01b2809f362a03dd7921fd"}, -] - -[package.dependencies] -tzdata = {version = "*", markers = "platform_system == \"Windows\""} - -[package.extras] -devenv = ["check-manifest", "pytest (>=4.3)", "pytest-cov", "pytest-mock (>=3.3)", "zest.releaser"] - -[[package]] -name = "urllib3" -version = "2.5.0" -description = "HTTP library with thread-safe connection pooling, file post, and more." -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc"}, - {file = "urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760"}, -] - -[package.extras] -brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""] -h2 = ["h2 (>=4,<5)"] -socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] -zstd = ["zstandard (>=0.18.0)"] - -[[package]] -name = "uvicorn" -version = "0.31.0" -description = "The lightning-fast ASGI server." -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "uvicorn-0.31.0-py3-none-any.whl", hash = "sha256:cac7be4dd4d891c363cd942160a7b02e69150dcbc7a36be04d5f4af4b17c8ced"}, - {file = "uvicorn-0.31.0.tar.gz", hash = "sha256:13bc21373d103859f68fe739608e2eb054a816dea79189bc3ca08ea89a275906"}, -] - -[package.dependencies] -click = ">=7.0" -h11 = ">=0.8" -typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""} - -[package.extras] -standard = ["colorama (>=0.4) ; sys_platform == \"win32\"", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1) ; sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\"", "watchfiles (>=0.13)", "websockets (>=10.4)"] - -[[package]] -name = "werkzeug" -version = "3.1.3" -description = "The comprehensive WSGI web application library." -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "werkzeug-3.1.3-py3-none-any.whl", hash = "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e"}, - {file = "werkzeug-3.1.3.tar.gz", hash = "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746"}, -] - -[package.dependencies] -MarkupSafe = ">=2.1.1" - -[package.extras] -watchdog = ["watchdog (>=2.3)"] - -[[package]] -name = "wrapt" -version = "1.17.3" -description = "Module for decorators, wrappers and monkey patching." -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "wrapt-1.17.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88bbae4d40d5a46142e70d58bf664a89b6b4befaea7b2ecc14e03cedb8e06c04"}, - {file = "wrapt-1.17.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6b13af258d6a9ad602d57d889f83b9d5543acd471eee12eb51f5b01f8eb1bc2"}, - {file = "wrapt-1.17.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd341868a4b6714a5962c1af0bd44f7c404ef78720c7de4892901e540417111c"}, - {file = "wrapt-1.17.3-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f9b2601381be482f70e5d1051a5965c25fb3625455a2bf520b5a077b22afb775"}, - {file = "wrapt-1.17.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:343e44b2a8e60e06a7e0d29c1671a0d9951f59174f3709962b5143f60a2a98bd"}, - {file = "wrapt-1.17.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:33486899acd2d7d3066156b03465b949da3fd41a5da6e394ec49d271baefcf05"}, - {file = "wrapt-1.17.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e6f40a8aa5a92f150bdb3e1c44b7e98fb7113955b2e5394122fa5532fec4b418"}, - {file = "wrapt-1.17.3-cp310-cp310-win32.whl", hash = "sha256:a36692b8491d30a8c75f1dfee65bef119d6f39ea84ee04d9f9311f83c5ad9390"}, - {file = "wrapt-1.17.3-cp310-cp310-win_amd64.whl", hash = "sha256:afd964fd43b10c12213574db492cb8f73b2f0826c8df07a68288f8f19af2ebe6"}, - {file = "wrapt-1.17.3-cp310-cp310-win_arm64.whl", hash = "sha256:af338aa93554be859173c39c85243970dc6a289fa907402289eeae7543e1ae18"}, - {file = "wrapt-1.17.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:273a736c4645e63ac582c60a56b0acb529ef07f78e08dc6bfadf6a46b19c0da7"}, - {file = "wrapt-1.17.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5531d911795e3f935a9c23eb1c8c03c211661a5060aab167065896bbf62a5f85"}, - {file = "wrapt-1.17.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0610b46293c59a3adbae3dee552b648b984176f8562ee0dba099a56cfbe4df1f"}, - {file = "wrapt-1.17.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b32888aad8b6e68f83a8fdccbf3165f5469702a7544472bdf41f582970ed3311"}, - {file = "wrapt-1.17.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cccf4f81371f257440c88faed6b74f1053eef90807b77e31ca057b2db74edb1"}, - {file = "wrapt-1.17.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8a210b158a34164de8bb68b0e7780041a903d7b00c87e906fb69928bf7890d5"}, - {file = "wrapt-1.17.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:79573c24a46ce11aab457b472efd8d125e5a51da2d1d24387666cd85f54c05b2"}, - {file = "wrapt-1.17.3-cp311-cp311-win32.whl", hash = "sha256:c31eebe420a9a5d2887b13000b043ff6ca27c452a9a22fa71f35f118e8d4bf89"}, - {file = "wrapt-1.17.3-cp311-cp311-win_amd64.whl", hash = "sha256:0b1831115c97f0663cb77aa27d381237e73ad4f721391a9bfb2fe8bc25fa6e77"}, - {file = "wrapt-1.17.3-cp311-cp311-win_arm64.whl", hash = "sha256:5a7b3c1ee8265eb4c8f1b7d29943f195c00673f5ab60c192eba2d4a7eae5f46a"}, - {file = "wrapt-1.17.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ab232e7fdb44cdfbf55fc3afa31bcdb0d8980b9b95c38b6405df2acb672af0e0"}, - {file = "wrapt-1.17.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9baa544e6acc91130e926e8c802a17f3b16fbea0fd441b5a60f5cf2cc5c3deba"}, - {file = "wrapt-1.17.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b538e31eca1a7ea4605e44f81a48aa24c4632a277431a6ed3f328835901f4fd"}, - {file = "wrapt-1.17.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:042ec3bb8f319c147b1301f2393bc19dba6e176b7da446853406d041c36c7828"}, - {file = "wrapt-1.17.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3af60380ba0b7b5aeb329bc4e402acd25bd877e98b3727b0135cb5c2efdaefe9"}, - {file = "wrapt-1.17.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b02e424deef65c9f7326d8c19220a2c9040c51dc165cddb732f16198c168396"}, - {file = "wrapt-1.17.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:74afa28374a3c3a11b3b5e5fca0ae03bef8450d6aa3ab3a1e2c30e3a75d023dc"}, - {file = "wrapt-1.17.3-cp312-cp312-win32.whl", hash = "sha256:4da9f45279fff3543c371d5ababc57a0384f70be244de7759c85a7f989cb4ebe"}, - {file = "wrapt-1.17.3-cp312-cp312-win_amd64.whl", hash = "sha256:e71d5c6ebac14875668a1e90baf2ea0ef5b7ac7918355850c0908ae82bcb297c"}, - {file = "wrapt-1.17.3-cp312-cp312-win_arm64.whl", hash = "sha256:604d076c55e2fdd4c1c03d06dc1a31b95130010517b5019db15365ec4a405fc6"}, - {file = "wrapt-1.17.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0"}, - {file = "wrapt-1.17.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77"}, - {file = "wrapt-1.17.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7"}, - {file = "wrapt-1.17.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6fd1ad24dc235e4ab88cda009e19bf347aabb975e44fd5c2fb22a3f6e4141277"}, - {file = "wrapt-1.17.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ed61b7c2d49cee3c027372df5809a59d60cf1b6c2f81ee980a091f3afed6a2d"}, - {file = "wrapt-1.17.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:423ed5420ad5f5529db9ce89eac09c8a2f97da18eb1c870237e84c5a5c2d60aa"}, - {file = "wrapt-1.17.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e01375f275f010fcbf7f643b4279896d04e571889b8a5b3f848423d91bf07050"}, - {file = "wrapt-1.17.3-cp313-cp313-win32.whl", hash = "sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8"}, - {file = "wrapt-1.17.3-cp313-cp313-win_amd64.whl", hash = "sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb"}, - {file = "wrapt-1.17.3-cp313-cp313-win_arm64.whl", hash = "sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16"}, - {file = "wrapt-1.17.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39"}, - {file = "wrapt-1.17.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235"}, - {file = "wrapt-1.17.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c"}, - {file = "wrapt-1.17.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b"}, - {file = "wrapt-1.17.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa"}, - {file = "wrapt-1.17.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7"}, - {file = "wrapt-1.17.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4"}, - {file = "wrapt-1.17.3-cp314-cp314-win32.whl", hash = "sha256:fbd3c8319de8e1dc79d346929cd71d523622da527cca14e0c1d257e31c2b8b10"}, - {file = "wrapt-1.17.3-cp314-cp314-win_amd64.whl", hash = "sha256:e1a4120ae5705f673727d3253de3ed0e016f7cd78dc463db1b31e2463e1f3cf6"}, - {file = "wrapt-1.17.3-cp314-cp314-win_arm64.whl", hash = "sha256:507553480670cab08a800b9463bdb881b2edeed77dc677b0a5915e6106e91a58"}, - {file = "wrapt-1.17.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a"}, - {file = "wrapt-1.17.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067"}, - {file = "wrapt-1.17.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454"}, - {file = "wrapt-1.17.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e"}, - {file = "wrapt-1.17.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f"}, - {file = "wrapt-1.17.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056"}, - {file = "wrapt-1.17.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804"}, - {file = "wrapt-1.17.3-cp314-cp314t-win32.whl", hash = "sha256:41b1d2bc74c2cac6f9074df52b2efbef2b30bdfe5f40cb78f8ca22963bc62977"}, - {file = "wrapt-1.17.3-cp314-cp314t-win_amd64.whl", hash = "sha256:73d496de46cd2cdbdbcce4ae4bcdb4afb6a11234a1df9c085249d55166b95116"}, - {file = "wrapt-1.17.3-cp314-cp314t-win_arm64.whl", hash = "sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6"}, - {file = "wrapt-1.17.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:70d86fa5197b8947a2fa70260b48e400bf2ccacdcab97bb7de47e3d1e6312225"}, - {file = "wrapt-1.17.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:df7d30371a2accfe4013e90445f6388c570f103d61019b6b7c57e0265250072a"}, - {file = "wrapt-1.17.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:caea3e9c79d5f0d2c6d9ab96111601797ea5da8e6d0723f77eabb0d4068d2b2f"}, - {file = "wrapt-1.17.3-cp38-cp38-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:758895b01d546812d1f42204bd443b8c433c44d090248bf22689df673ccafe00"}, - {file = "wrapt-1.17.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02b551d101f31694fc785e58e0720ef7d9a10c4e62c1c9358ce6f63f23e30a56"}, - {file = "wrapt-1.17.3-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:656873859b3b50eeebe6db8b1455e99d90c26ab058db8e427046dbc35c3140a5"}, - {file = "wrapt-1.17.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:a9a2203361a6e6404f80b99234fe7fb37d1fc73487b5a78dc1aa5b97201e0f22"}, - {file = "wrapt-1.17.3-cp38-cp38-win32.whl", hash = "sha256:55cbbc356c2842f39bcc553cf695932e8b30e30e797f961860afb308e6b1bb7c"}, - {file = "wrapt-1.17.3-cp38-cp38-win_amd64.whl", hash = "sha256:ad85e269fe54d506b240d2d7b9f5f2057c2aa9a2ea5b32c66f8902f768117ed2"}, - {file = "wrapt-1.17.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:30ce38e66630599e1193798285706903110d4f057aab3168a34b7fdc85569afc"}, - {file = "wrapt-1.17.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:65d1d00fbfb3ea5f20add88bbc0f815150dbbde3b026e6c24759466c8b5a9ef9"}, - {file = "wrapt-1.17.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a7c06742645f914f26c7f1fa47b8bc4c91d222f76ee20116c43d5ef0912bba2d"}, - {file = "wrapt-1.17.3-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7e18f01b0c3e4a07fe6dfdb00e29049ba17eadbc5e7609a2a3a4af83ab7d710a"}, - {file = "wrapt-1.17.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f5f51a6466667a5a356e6381d362d259125b57f059103dd9fdc8c0cf1d14139"}, - {file = "wrapt-1.17.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:59923aa12d0157f6b82d686c3fd8e1166fa8cdfb3e17b42ce3b6147ff81528df"}, - {file = "wrapt-1.17.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:46acc57b331e0b3bcb3e1ca3b421d65637915cfcd65eb783cb2f78a511193f9b"}, - {file = "wrapt-1.17.3-cp39-cp39-win32.whl", hash = "sha256:3e62d15d3cfa26e3d0788094de7b64efa75f3a53875cdbccdf78547aed547a81"}, - {file = "wrapt-1.17.3-cp39-cp39-win_amd64.whl", hash = "sha256:1f23fa283f51c890eda8e34e4937079114c74b4c81d2b2f1f1d94948f5cc3d7f"}, - {file = "wrapt-1.17.3-cp39-cp39-win_arm64.whl", hash = "sha256:24c2ed34dc222ed754247a2702b1e1e89fdbaa4016f324b4b8f1a802d4ffe87f"}, - {file = "wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22"}, - {file = "wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0"}, -] - -[[package]] -name = "zipp" -version = "3.23.0" -description = "Backport of pathlib-compatible object wrapper for zip files" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e"}, - {file = "zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166"}, -] - -[package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -enabler = ["pytest-enabler (>=2.2)"] -test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more_itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] -type = ["pytest-mypy"] - -[metadata] -lock-version = "2.1" -python-versions = ">=3.10" -content-hash = "961764e7424b803ec8bac17dbf75b186a128726fde5ba47eb678150d459a31cc" diff --git a/model_server/pyproject.toml b/model_server/pyproject.toml deleted file mode 100644 index 9077d07c..00000000 --- a/model_server/pyproject.toml +++ /dev/null @@ -1,49 +0,0 @@ -[project] -name = "archgw_modelserver" -version = "0.3.18" -description = "A model server for serving models" -authors = [{name = "Katanemo Labs, Inc", email = "info@katanemo.com"}] -license = "Apache 2.0" -readme = "README.md" -requires-python = ">=3.10" -dependencies = [ - "fastapi==0.115.0", - "torch>=2.6.0", - "uvicorn==0.31.0", - "transformers>=4.37.0,<5.0.0", - "accelerate>=1.0.0,<2.0.0", - "pydantic>=2.10.1,<3.0.0", - "dateparser", - "openai>=1.50.2,<2.0.0", - "httpx==0.27.2", # https://community.openai.com/t/typeerror-asyncclient-init-got-an-unexpected-keyword-argument-proxies/1040287 - "opentelemetry-api>=1.28.0,<2.0.0", - "opentelemetry-sdk>=1.28.0,<2.0.0", - "opentelemetry-exporter-otlp>=1.28.0,<2.0.0", - "opentelemetry-instrumentation-fastapi>=0.49b0,<1.0", - "overrides>=7.7.0,<8.0.0", -] - -[project.scripts] -archgw_modelserver = "src.cli:main" - -[dependency-groups] -dev = [ - "pytest", - "pytest-asyncio", - "pytest-httpserver>=1.1.0,<2.0.0", - "pytest-retry>=1.6.3,<2.0.0", -] - -[tool.poetry] -packages = [{ include = "src" }] - -[build-system] -requires = ["poetry-core>=2.0.0"] -build-backend = "poetry.core.masonry.api" - -[tool.pytest.ini_options] -python_files = ["test*.py"] -addopts = ["-v", "-s"] -retries = 2 -retry_delay = 0.5 -cumulative_timing = false diff --git a/model_server/src/__init__.py b/model_server/src/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/model_server/src/cli.py b/model_server/src/cli.py deleted file mode 100644 index d0d9e9e1..00000000 --- a/model_server/src/cli.py +++ /dev/null @@ -1,196 +0,0 @@ -import importlib -import os -import sys -import subprocess -import argparse -import signal -import tempfile -import time -import requests - -import src.commons.utils as utils - - -logger = utils.get_model_server_logger() - - -def get_version(): - try: - version = importlib.metadata.version("archgw_modelserver") - return version - except importlib.metadata.PackageNotFoundError: - return "version not found" - - -def wait_for_health_check(url, timeout=300): - """Wait for the Uvicorn server to respond to health-check requests.""" - - start_time = time.time() - while time.time() - start_time < timeout: - try: - response = requests.get(url) - if response.status_code == 200: - return True - except requests.ConnectionError: - time.sleep(1) - - return False - - -def get_pid_file(): - temp_dir = tempfile.gettempdir() - return os.path.join(temp_dir, "model_server.pid") - - -def ensure_killed(process): - process.terminate() - # if the process is not terminated, kill it - now = time.time() - # wait for 5 seconds - while time.time() - now < 5: - if process.poll() is not None: - break - time.sleep(1) - if process.poll() is None: - logger.info("Killing model server") - process.kill() - - -def start_server(port=51000, foreground=False): - """Start the Uvicorn server.""" - - logger.info("model server version: %s", get_version()) - - stop_server() - - logger.info( - "starting model server, port: %s, foreground: %s. Please wait ...", - port, - foreground, - ) - - if foreground: - process = subprocess.Popen( - [ - sys.executable, - "-m", - "uvicorn", - "src.main:app", - "--host", - "0.0.0.0", - "--port", - str(port), - ], - ) - else: - process = subprocess.Popen( - [ - sys.executable, - "-m", - "uvicorn", - "src.main:app", - "--host", - "0.0.0.0", - "--port", - str(port), - ], - stderr=subprocess.PIPE, - stdout=subprocess.PIPE, - ) - - try: - if wait_for_health_check(f"http://0.0.0.0:{port}/healthz"): - logger.info( - f"model server health check passed, port {port}, pid: {process.pid}" - ) - else: - logger.error("health check failed, shutting it down.") - process.terminate() - except KeyboardInterrupt: - logger.info("model server stopped by user during initialization.") - ensure_killed(process) - - # write process id to temp file in temp folder - pid_file = get_pid_file() - logger.info(f"writing pid {process.pid} to {pid_file}") - with open(pid_file, "w") as f: - f.write(str(process.pid)) - - if foreground: - try: - process.wait() - except KeyboardInterrupt: - logger.info("model server stopped by user.") - ensure_killed(process) - - -def stop_server(): - """Stop the Uvicorn server.""" - - pid_file = get_pid_file() - if os.path.exists(pid_file): - logger.info("PID file found, shutting down the server.") - # read pid from file - with open(pid_file, "r") as f: - pid = int(f.read()) - logger.info(f"Killing model server {pid}") - try: - os.kill(pid, signal.SIGKILL) - except ProcessLookupError: - logger.info(f"Process {pid} not found") - os.remove(pid_file) - else: - logger.info("No PID file found, server is not running.") - - -def restart_server(port=51000, foreground=False): - """Restart the Uvicorn server.""" - stop_server() - start_server(port, foreground) - - -def parse_args(): - parser = argparse.ArgumentParser(description="Manage the Uvicorn server.") - parser.add_argument( - "action", - choices=["start", "stop", "restart"], - default="start", - nargs="?", - help="Action to perform on the server (default: start).", - ) - parser.add_argument( - "--port", - type=int, - default=51000, - help="Port number for the server (default: 51000).", - ) - - parser.add_argument( - "--foreground", - default=False, - action="store_true", - help="Run the server in the foreground (default: False).", - ) - - return parser.parse_args() - - -def main(): - """ - Start, stop, or restart the Uvicorn server based on command-line arguments. - """ - - args = parse_args() - - if args.action == "start": - logger.info("[CLI] - Starting server") - start_server(args.port, args.foreground) - elif args.action == "stop": - logger.info("[CLI] - Stopping server") - stop_server() - elif args.action == "restart": - logger.info("[CLI] - Restarting server") - restart_server(args.port) - else: - logger.error(f"[CLI] - Unknown action: {args.action}") - sys.exit(1) diff --git a/model_server/src/commons/__init__.py b/model_server/src/commons/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/model_server/src/commons/globals.py b/model_server/src/commons/globals.py deleted file mode 100644 index 97da3e20..00000000 --- a/model_server/src/commons/globals.py +++ /dev/null @@ -1,38 +0,0 @@ -import os -from openai import OpenAI -from src.commons.utils import get_model_server_logger -from src.core.guardrails import get_guardrail_handler -from src.core.function_calling import ( - ArchAgentConfig, - ArchAgentHandler, - ArchFunctionConfig, - ArchFunctionHandler, -) - - -# Define logger -logger = get_model_server_logger() - - -# Define the client -ARCH_ENDPOINT = os.getenv("ARCH_ENDPOINT", "https://archfc.katanemo.dev/v1") -ARCH_API_KEY = "EMPTY" -ARCH_CLIENT = OpenAI(base_url=ARCH_ENDPOINT, api_key=ARCH_API_KEY) -ARCH_AGENT_CLIENT = ARCH_CLIENT - -# Define model names -ARCH_INTENT_MODEL_ALIAS = "Arch-Intent" -ARCH_FUNCTION_MODEL_ALIAS = "Arch-Function" -ARCH_AGENT_MODEL_ALIAS = ARCH_FUNCTION_MODEL_ALIAS -ARCH_GUARD_MODEL_ALIAS = "katanemo/Arch-Guard" - -# Define model handlers -handler_map = { - "Arch-Function": ArchFunctionHandler( - ARCH_CLIENT, ARCH_FUNCTION_MODEL_ALIAS, ArchFunctionConfig - ), - "Arch-Agent": ArchAgentHandler( - ARCH_AGENT_CLIENT, ARCH_AGENT_MODEL_ALIAS, ArchAgentConfig - ), - "Arch-Guard": get_guardrail_handler(ARCH_GUARD_MODEL_ALIAS), -} diff --git a/model_server/src/commons/utils.py b/model_server/src/commons/utils.py deleted file mode 100644 index 76e46d13..00000000 --- a/model_server/src/commons/utils.py +++ /dev/null @@ -1,50 +0,0 @@ -import torch -import logging - -from datetime import datetime - - -def get_model_server_logger(): - """ - Get or initialize the logger instance for the model server. - - Returns: - - logging.Logger: Configured logger instance. - """ - - # Check if the logger is already configured - logger = logging.getLogger("model_server") - - # Return existing logger instance if already configured - if logger.hasHandlers(): - return logger - - # Configure logging to only log to console - logging.basicConfig( - level=logging.INFO, - format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", - handlers=[logging.StreamHandler()], - ) - - return logger - - -def get_device(): - if torch.cuda.is_available(): - device = "cuda" - elif torch.backends.mps.is_available(): - device = "mps" - else: - device = "cpu" - - return device - - -def get_today_date(): - # Get today's date - today = datetime.now() - - # Get full date with day of week - full_date = today.strftime("%Y-%m-%d") - - return full_date diff --git a/model_server/src/core/__init__.py b/model_server/src/core/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/model_server/src/core/function_calling.py b/model_server/src/core/function_calling.py deleted file mode 100644 index bec5dfdf..00000000 --- a/model_server/src/core/function_calling.py +++ /dev/null @@ -1,520 +0,0 @@ -import ast -import copy -import json -import random -import builtins -import src.commons.utils as utils - -from openai import OpenAI -from typing import Any, Dict, List -from overrides import override -from src.core.utils.hallucination_utils import HallucinationState -from src.core.utils.model_utils import ( - Message, - ChatMessage, - Choice, - ChatCompletionResponse, - ArchBaseHandler, -) - - -logger = utils.get_model_server_logger() - - -# ============================================================================================================================================== - - -class ArchFunctionConfig: - TASK_PROMPT = ( - "You are a helpful assistant designed to assist with the user query by making one or more function calls if needed." - "\n\nYou are provided with function signatures within XML tags:\n\n{tools}\n" - "\n\nYour task is to decide which functions are needed and collect missing parameters if necessary." - ) - - FORMAT_PROMPT = ( - "\n\nBased on your analysis, provide your response in one of the following JSON formats:" - '\n1. If no functions are needed:\n```json\n{"response": "Your response text here"}\n```' - '\n2. If functions are needed but some required parameters are missing:\n```json\n{"required_functions": ["func_name1", "func_name2", ...], "clarification": "Text asking for missing parameters"}\n```' - '\n3. If functions are needed and all required parameters are available:\n```json\n{"tool_calls": [{"name": "func_name1", "arguments": {"argument1": "value1", "argument2": "value2"}},... (more tool calls as required)]}\n```' - ) - - GENERATION_PARAMS = { - "temperature": 0.1, - "top_p": 1.0, - "top_k": 10, - "max_tokens": 1024, - "stop_token_ids": [151645], - "logprobs": True, - "top_logprobs": 10, - } - - SUPPORT_DATA_TYPES = ["int", "float", "bool", "str", "list", "tuple", "set", "dict"] - - -class ArchFunctionHandler(ArchBaseHandler): - def __init__( - self, - client: OpenAI, - model_name: str, - config: ArchFunctionConfig, - ): - """ - Initializes the function handler. - - Args: - client (OpenAI): An OpenAI client instance. - model_name (str): Name of the model to use. - config (ArchFunctionConfig): The configuration for Arch-Function - """ - - super().__init__( - client, - model_name, - config.TASK_PROMPT, - config.FORMAT_PROMPT, - config.GENERATION_PARAMS, - ) - - self.generation_params = self.generation_params | { - "continue_final_message": True, - "add_generation_prompt": False, - } - - self.default_prefix = '```json\n{"' - self.clarify_prefix = '```json\n{"required_functions":' - - self.hallucination_state = None - - # Predefine data types for verification. Only support Python for now. - # TODO: Extend the list of support data types - self.support_data_types = { - type_name: getattr(builtins, type_name) - for type_name in config.SUPPORT_DATA_TYPES - } - - @override - def _convert_tools(self, tools: List[Dict[str, Any]]) -> str: - """ - Converts a list of tools into JSON format. - - Args: - tools (List[Dict[str, Any]]): A list of tools represented as dictionaries. - - Returns: - str: A string representation of converted tools. - """ - - converted = [json.dumps(tool["function"], ensure_ascii=False) for tool in tools] - return "\n".join(converted) - - def _fix_json_string(self, json_str: str) -> str: - """ - Fixes malformed JSON strings by ensuring proper bracket matching. - - Args: - json_str (str): A JSON string that might be malformed. - - Returns: - str: A corrected JSON string. - """ - - # Remove any leading or trailing whitespace or newline characters - json_str = json_str.strip() - - # Stack to keep track of brackets - stack = [] - - # Clean string to collect valid characters - fixed_str = "" - - # Dictionary for matching brackets - matching_bracket = {")": "(", "}": "{", "]": "["} - - # Dictionary for the opposite of matching_bracket - opening_bracket = {v: k for k, v in matching_bracket.items()} - - for char in json_str: - if char in "{[(": - stack.append(char) - fixed_str += char - elif char in "}])": - if stack and stack[-1] == matching_bracket[char]: - stack.pop() - fixed_str += char - else: - # Ignore the unmatched closing brackets - continue - else: - fixed_str += char - - # If there are unmatched opening brackets left in the stack, add corresponding closing brackets - while stack: - unmatched_opening = stack.pop() - fixed_str += opening_bracket[unmatched_opening] - - try: - fixed_str = json.loads(fixed_str) - except Exception: - fixed_str = json.loads(fixed_str.replace("'", '"')) - - return json.dumps(fixed_str) - - def _parse_model_response(self, content: str) -> Dict[str, any]: - """ - Extracts tool call information from a given string. - - Args: - content (str): The content string containing potential tool call information. - - Returns: - Dict: A dictionary of extraction, including: - - "required_functions": A list of detected intents. - - "clarification": Text to collect missing parameters - - "tool_calls": A list of tool call dictionaries. - - "is_valid": A boolean indicating if the extraction was valid. - - "error_message": An error message or exception if parsing failed. - """ - - response_dict = { - "raw_response": [], - "response": [], - "required_functions": [], - "clarification": "", - "tool_calls": [], - "is_valid": True, - "error_message": "", - } - - try: - if content.startswith("```") and content.endswith("```"): - content = content.strip("```").strip() - if content.startswith("json"): - content = content[4:].strip() - - content = self._fix_json_string(content) - response_dict["raw_response"] = f"```json\n{content}\n```" - - model_response = json.loads(content) - response_dict["response"] = model_response.get("response", "") - response_dict["required_functions"] = model_response.get( - "required_functions", [] - ) - response_dict["clarification"] = model_response.get("clarification", "") - - for tool_call in model_response.get("tool_calls", []): - response_dict["tool_calls"].append( - { - "id": f"call_{random.randint(1000, 10000)}", - "type": "function", - "function": { - "name": tool_call.get("name", ""), - "arguments": tool_call.get("arguments", {}), - }, - } - ) - except Exception as e: - response_dict["is_valid"] = False - response_dict["error_message"] = f"Fail to parse model responses: {e}" - - return response_dict - - def _convert_data_type(self, value: str, target_type: str): - # TODO: Add more conversion rules as needed - try: - if target_type is float and isinstance(value, int): - return float(value) - elif target_type is list and isinstance(value, str): - return ast.literal_eval(value) - elif target_type is str and not isinstance(value, str): - return str(value) - except (ValueError, TypeError, json.JSONDecodeError): - pass - return value - - def _verify_tool_calls( - self, tools: List[Dict[str, Any]], tool_calls: List[Dict[str, Any]] - ) -> Dict[str, any]: - """ - Verifies the validity of extracted tool calls against the provided tools. - - Args: - tools (List[Dict[str, Any]]): A list of available tools. - tool_calls (List[Dict[str, Any]]): A list of tool calls to verify. - - Returns: - Dict: A dictionary of verification, including: - - "status": A boolean indicating if the tool calls are valid. - - "invalid_tool_call": A dictionary of the invalid tool call if any. - - "message": An error message. - """ - - verification_dict = { - "is_valid": True, - "invalid_tool_call": {}, - "error_message": "", - } - - functions = {} - for tool in tools: - functions[tool["function"]["name"]] = tool["function"]["parameters"] - - for tool_call in tool_calls: - if not verification_dict["is_valid"]: - break - - func_name = tool_call["function"]["name"] - func_args = tool_call["function"]["arguments"] - - # Check whether the function is available or not - if func_name not in functions: - verification_dict["is_valid"] = False - verification_dict["invalid_tool_call"] = tool_call - verification_dict["error_message"] = f"{func_name} is not available!" - else: - # Check if all the requried parameters can be found in the tool calls - for required_param in functions[func_name].get("required", []): - if required_param not in func_args: - verification_dict["is_valid"] = False - verification_dict["invalid_tool_call"] = tool_call - verification_dict[ - "error_message" - ] = f"`{required_param}` is required by the function `{func_name}` but not found in the tool call!" - break - - # Verify the data type of each parameter in the tool calls - function_properties = functions[func_name]["properties"] - - logger.info("== func_args ==") - logger.info(func_args) - for param_name in func_args: - if param_name not in function_properties: - verification_dict["is_valid"] = False - verification_dict["invalid_tool_call"] = tool_call - verification_dict[ - "error_message" - ] = f"Parameter `{param_name}` is not defined in the function `{func_name}`." - break - else: - param_value = func_args[param_name] - target_type = function_properties[param_name]["type"] - - if target_type in self.support_data_types: - data_type = self.support_data_types[target_type] - - if not isinstance(param_value, data_type): - param_value = self._convert_data_type( - param_value, data_type - ) - if not isinstance(param_value, data_type): - verification_dict["is_valid"] = False - verification_dict["invalid_tool_call"] = tool_call - verification_dict[ - "error_message" - ] = f"Parameter `{param_name}` is expected to have the data type `{data_type}`, got `{type(param_value)}`." - break - else: - verification_dict["is_valid"] = False - verification_dict["invalid_tool_call"] = tool_call - verification_dict[ - "error_message" - ] = f"Data type `{target_type}` is not supported." - - return verification_dict - - def _prefill_message(self, messages: List[Dict[str, str]], prefill_message): - """ - Update messages and generation params for prompt prefilling - - Args: - messages (List[Dict[str, str]]): A list of messages. - - Returns: - prefill_messages (List[Dict[str, str]]): A list of messages. - """ - return messages + [{"role": "assistant", "content": prefill_message}] - - @override - async def chat_completion(self, req: ChatMessage) -> ChatCompletionResponse: - """ - Generates a chat completion response for a given request. - - Args: - req (ChatMessage): A chat message request object. - enable_prefilling (bool, optional): Whether to enable prefill responses. Defaults to True. - Returns: - ChatCompletionResponse: The model's response to the chat request. - - Note: - Currently only support vllm inference - """ - logger.info("[Arch-Function] - ChatCompletion") - - messages = self._process_messages( - req.messages, req.tools, metadata=req.metadata - ) - - logger.info( - f"[request to arch-fc]: model: {self.model_name}, extra_body: {self.generation_params}, body: {json.dumps(messages)}" - ) - - # always enable `stream=True` to collect model responses - response = self.client.chat.completions.create( - messages=self._prefill_message(messages, self.default_prefix), - model=self.model_name, - stream=True, - extra_body=self.generation_params, - ) - - use_agent_orchestrator = req.metadata.get("use_agent_orchestrator", False) - model_response = "" - if use_agent_orchestrator: - for chunk in response: - if len(chunk.choices) > 0 and chunk.choices[0].delta.content: - model_response += chunk.choices[0].delta.content - logger.info(f"[Agent Orchestrator]: response received: {model_response}") - else: - # initialize the hallucination handler, which is an iterator - self.hallucination_state = HallucinationState( - response_iterator=response, function=req.tools - ) - - has_tool_calls, has_hallucination = None, False - for _ in self.hallucination_state: - # check if moodel response starts with tool calls, we do it after 5 tokens because we only check the first part of the response. - if len(self.hallucination_state.tokens) > 5 and has_tool_calls is None: - content = "".join(self.hallucination_state.tokens) - if "tool_calls" in content: - has_tool_calls = True - else: - has_tool_calls = False - - # if the model is hallucinating, start parameter gathering - if self.hallucination_state.hallucination is True: - has_hallucination = True - break - - if has_tool_calls and has_hallucination: - # start prompt prefilling if hallcuination is found in tool calls - logger.info( - f"[Hallucination]: {self.hallucination_state.error_message}" - ) - response = self.client.chat.completions.create( - messages=self._prefill_message(messages, self.clarify_prefix), - model=self.model_name, - stream=False, - extra_body=self.generation_params, - ) - model_response = response.choices[0].message.content - else: - model_response = "".join(self.hallucination_state.tokens) - - # Extract tool calls from model response - response_dict = self._parse_model_response(model_response) - logger.info(f"[arch-fc]: raw model response: {response_dict['raw_response']}") - - # General model response - if response_dict.get("response", ""): - model_message = Message(content="", tool_calls=[]) - # Parameter gathering - elif response_dict.get("required_functions", []): - if not use_agent_orchestrator: - clarification = response_dict.get("clarification", "") - model_message = Message(content=clarification, tool_calls=[]) - else: - model_message = Message(content="", tool_calls=[]) - # Function Calling - elif response_dict.get("tool_calls", []): - if response_dict["is_valid"]: - if not use_agent_orchestrator: - verification_dict = self._verify_tool_calls( - tools=req.tools, tool_calls=response_dict["tool_calls"] - ) - - if verification_dict["is_valid"]: - logger.info( - f"[Tool calls]: {json.dumps([tool_call['function'] for tool_call in response_dict['tool_calls']])}" - ) - model_message = Message( - content="", tool_calls=response_dict["tool_calls"] - ) - else: - logger.error( - f"Invalid tool call - {verification_dict['error_message']}" - ) - model_message = Message(content="", tool_calls=[]) - else: - # skip tool call verification if using agent orchestrator - logger.info( - f"[Tool calls]: {json.dumps([tool_call['function'] for tool_call in response_dict['tool_calls']])}" - ) - model_message = Message( - content="", tool_calls=response_dict["tool_calls"] - ) - - else: - # Response with tool calls but invalid - model_message = Message(content="", tool_calls=[]) - # Response not in the desired format - else: - logger.error(f"Invalid model response - {model_response}") - model_message = Message(content="", tool_calls=[]) - - chat_completion_response = ChatCompletionResponse( - choices=[Choice(message=model_message)], - model=self.model_name, - metadata={"x-arch-fc-model-response": response_dict["raw_response"]}, - role="assistant", - ) - - logger.info( - f"[response arch-fc]: {json.dumps(chat_completion_response.model_dump(exclude_none=True))}" - ) - - return chat_completion_response - - -# ============================================================================================================================================== - - -class ArchAgentConfig(ArchFunctionConfig): - GENERATION_PARAMS = { - "temperature": 0.01, - "top_p": 1.0, - "top_k": 10, - "max_tokens": 1024, - "stop_token_ids": [151645], - "logprobs": True, - "top_logprobs": 10, - } - - -class ArchAgentHandler(ArchFunctionHandler): - def __init__(self, client: OpenAI, model_name: str, config: ArchAgentConfig): - super().__init__(client, model_name, config) - - @override - def _convert_tools(self, tools: List[Dict[str, Any]]) -> str: - """ - Converts a list of tools into JSON format. - - Args: - tools (List[Dict[str, Any]]): A list of tools represented as dictionaries. - - Returns: - str: A string representation of converted tools. - """ - - converted = [] - # delete parameters key if its empty in tool - for tool in tools: - if ( - "parameters" in tool["function"] - and "properties" in tool["function"]["parameters"] - and not tool["function"]["parameters"]["properties"] - ): - tool_copy = copy.deepcopy(tool) - del tool_copy["function"]["parameters"] - converted.append(json.dumps(tool_copy["function"], ensure_ascii=False)) - else: - converted.append(json.dumps(tool["function"], ensure_ascii=False)) - return "\n".join(converted) diff --git a/model_server/src/core/guardrails.py b/model_server/src/core/guardrails.py deleted file mode 100644 index fae4e5ba..00000000 --- a/model_server/src/core/guardrails.py +++ /dev/null @@ -1,160 +0,0 @@ -import torch -import numpy as np -import src.commons.utils as utils - -from transformers import AutoTokenizer, AutoModelForSequenceClassification -from src.core.utils.model_utils import GuardRequest, GuardResponse - - -logger = utils.get_model_server_logger() - - -class ArchGuardHanlder: - def __init__(self, model_dict): - """ - Initializes the ArchGuardHanlder with the given model dictionary. - - Args: - model_dict (dict): A dictionary containing the model, tokenizer, and device information. - """ - - self.model = model_dict["model"] - self.model_name = model_dict["model_name"] - self.tokenizer = model_dict["tokenizer"] - self.device = model_dict["device"] - - self.support_tasks = {"jailbreak": {"positive_class": 2, "threshold": 0.5}} - - def _split_text_into_chunks(self, text, max_num_words=300): - """ - Splits the input text into chunks of up to `max_num_words` words. - - Args: - text (str): The input text to be split. - max_num_words (int, optional): The maximum number of words in each chunk. Defaults to 300. - - Returns: - List[str]: A list of text chunks. - """ - - words = text.split() - - chunks = [ - " ".join(words[i : i + max_num_words]) - for i in range(0, len(words), max_num_words) - ] - - return chunks - - @staticmethod - def softmax(x): - """ - Computes the softmax of the input array. - - Args: - x (np.ndarray): The input array. - - Returns: - np.ndarray: The softmax of the input. - """ - return np.exp(x) / np.exp(x).sum(axis=0) - - def _predict_text(self, task, text, max_length=512) -> GuardResponse: - """ - Predicts the result for the provided text for a specific task. - - Args: - task (str): The task to perform (e.g., "jailbreak"). - text (str): The input text to classify. - max_length (int, optional): The maximum length for tokenization. Defaults to 512. - - Returns: - GuardResponse: A GuardResponse object containing the prediction. - """ - - inputs = self.tokenizer( - text, truncation=True, max_length=max_length, return_tensors="pt" - ).to(self.device) - - with torch.no_grad(): - logits = self.model(**inputs).logits.cpu().detach().numpy()[0] - prob = ArchGuardHanlder.softmax(logits)[ - self.support_tasks[task]["positive_class"] - ].item() - - verdict = prob > self.support_tasks[task]["threshold"] - - return GuardResponse(task=task, input=text, prob=prob, verdict=verdict) - - def predict(self, req: GuardRequest, max_num_words=300) -> GuardResponse: - """ - Makes a prediction based on the GuardRequest input. - - Args: - req (GuardRequest): The GuardRequest object containing the input text and task. - max_num_words (int, optional): The maximum number of words in each chunk if splitting is needed. Defaults to 300. - - Returns: - GuardResponse: A GuardResponse object containing the prediction. - - Note: - currently only support jailbreak check - """ - - if req.task not in self.support_tasks: - raise NotImplementedError(f"{req.task} is not supported!") - - logger.info("[Arch-Guard] - Prediction") - logger.info(f"[request arch-guard]: {req.input}") - - if len(req.input.split()) < max_num_words: - result = self._predict_text(req.task, req.input) - else: - prob, verdict = 0.0, False - - # split into chunks if text is long - text_chunks = self._split_text_into_chunks(req.input) - - for chunk in text_chunks: - chunk_result = self._predict_text(req.task, chunk) - - if chunk_result.verdict: - prob = chunk_result.prob - verdict = True - break - - result = GuardResponse( - task=req.task, input=req.input, prob=prob, verdict=verdict - ) - - logger.info( - f"[response]: {req.task}: {'True' if result.verdict else 'False'} (prob: {result.prob:.2f})" - ) - - return result - - -def get_guardrail_handler(model_name: str = "katanemo/Arch-Guard", device: str = None): - """ - Initializes and returns an instance of ArchGuardHanlder based on the specified device. - - Args: - device (str, optional): The device to use for model inference (e.g., "cpu" or "cuda"). Defaults to None. - - Returns: - ArchGuardHanlder: An instance of ArchGuardHanlder configured for the specified device. - """ - - if device is None: - device = utils.get_device() - - guardrail_dict = { - "device": device, - "model_name": model_name, - "tokenizer": AutoTokenizer.from_pretrained(model_name, trust_remote_code=True), - "model": AutoModelForSequenceClassification.from_pretrained( - model_name, device_map=device, low_cpu_mem_usage=True - ), - } - - return ArchGuardHanlder(model_dict=guardrail_dict) diff --git a/model_server/src/core/utils/__init__.py b/model_server/src/core/utils/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/model_server/src/core/utils/hallucination_utils.py b/model_server/src/core/utils/hallucination_utils.py deleted file mode 100644 index 992f8aa4..00000000 --- a/model_server/src/core/utils/hallucination_utils.py +++ /dev/null @@ -1,393 +0,0 @@ -import json -import math -import torch -import itertools - - -from typing import Dict, List, Tuple -from enum import Enum -import string - -from src.commons.utils import get_model_server_logger - -logger = get_model_server_logger() - -# constants -FUNC_NAME_START_PATTERN = ('{"name":"', "{'name':'") -FUNC_NAME_END_TOKEN = ('",', "',") -END_TOOL_CALL_TOKEN = "}}" - -FIRST_PARAM_NAME_START_PATTERN = ('"arguments":{"', "'arguments':{'") -PARAMETER_NAME_END_TOKENS = ('":', ':"', "':", ":'", '":"', "':'") -PARAMETER_NAME_START_PATTERN = ('","', "','") -PARAMETER_VALUE_START_PATTERN = ('":', "':") -PARAMETER_VALUE_END_TOKEN = ('",', '"}') - -BRACKETS = {"(": ")", "{": "}", "[": "]"} - - -# Thresholds -class MaskToken(Enum): - FUNCTION_NAME = "f" - PARAMETER_VALUE = "v" - PARAMETER_NAME = "p" - NOT_USED = "e" - TOOL_CALL = "t" - - -HALLUCINATION_THRESHOLD_DICT = { - "entropy": 0.0001, - "varentropy": 0.0001, - "probability": 0.8, -} - - -def check_threshold(entropy: float, varentropy: float, thd: Dict) -> bool: - """ - Check if the given entropy or variance of entropy exceeds the specified thresholds. - - Args: - entropy (float): The entropy value to check. - varentropy (float): The variance of entropy value to check. - thd (dict): A dictionary containing the threshold values with keys 'entropy' and 'varentropy'. - - Returns: - bool: True if both the entropy and varentropy exceeds their respective thresholds, False otherwise. - """ - return entropy > thd["entropy"] and varentropy > thd["varentropy"] - - -def calculate_uncertainty(log_probs: List[float]) -> Tuple[float, float]: - """ - Calculate the entropy and variance of entropy (varentropy) from log probabilities. - - Args: - log_probs (list of float): A list of log probabilities. - - Returns: - tuple: A tuple containing: - - log_probs (list of float): The input log probabilities as a list. - - entropy (float): The calculated entropy. - - varentropy (float): The calculated variance of entropy. - """ - log_probs = torch.tensor(log_probs) - token_probs = torch.exp(log_probs) - entropy = -torch.sum(log_probs * token_probs, dim=-1) / math.log(2, math.e) - varentropy = torch.sum( - token_probs * (log_probs / math.log(2, math.e) + entropy.unsqueeze(-1)) ** 2, - dim=-1, - ) - return entropy.item(), varentropy.item(), token_probs[0].item() - - -def is_parameter_required( - function_description: Dict, - parameter_name: str, -) -> bool: - """ - Check if a parameter in required list - - Args: - function_description (dict): The API description in JSON format. - parameter_name (str): The name of the parameter to check. - - Returns: - bool: True if the parameter has the specified property, False otherwise. - """ - required_parameters = function_description.get("required", {}) - - return parameter_name in required_parameters - - -def is_parameter_property( - function_description: Dict, parameter_name: str, property_name: str -) -> bool: - """ - Check if a parameter in an API description has a specific property. - - Args: - function_description (dict): The API description in JSON format. - parameter_name (str): The name of the parameter to check. - property_name (str): The property to look for (e.g., 'format', 'default'). - - Returns: - bool: True if the parameter has the specified property, False otherwise. - """ - parameters = function_description.get("properties", {}) - parameter_info = parameters.get(parameter_name, {}) - - return property_name in parameter_info - - -class HallucinationState: - """ - A class to handle the state of hallucination detection in token processing. - - Attributes: - tokens (list): List of tokens processed. - logprobs (list): List of log probabilities for each token. - state (str): Current state of the handler. - mask (list): List of masks indicating the type of each token. - parameter_name_done (bool): Flag indicating if parameter name extraction is done. - hallucination (bool): Flag indicating if a hallucination is detected. - hallucination_message (str): Message describing the hallucination. - parameter_name (list): List of extracted parameter names. - token_probs_map (list): List mapping tokens to their entropy and variance of entropy. - """ - - def __init__(self, response_iterator=None, function=None): - """ - Initializes the HallucinationState with default values. - """ - self.tokens: List[str] = [] - self.logprobs: List[float] = [] - self.state: str = None - self.mask: List[str] = [] - self.parameter_name_done: bool = False - self.hallucination: bool = False - self.error_message: str = "" - self.parameter_name: List[str] = [] - self.token_probs_map: List[Tuple[str, float, float]] = [] - self.response_iterator = response_iterator - self._process_function(function) - self.open_bracket = False - self.bracket = None - self.function_name = "" - self.check_parameter_name = {} - self.HALLUCINATION_THRESHOLD_DICT = HALLUCINATION_THRESHOLD_DICT - - def _process_function(self, function): - self.function = function - if self.function is None: - raise ValueError("API descriptions not set.") - self.function_properties = { - x["function"]["name"]: x["function"]["parameters"] for x in self.function - } - - def _reset_parameters(self): - """ - Resets all parameters in the HallucinationState to their default values. - """ - self.state = None - self.parameter_name_done = False - self.hallucination = False - self.error_message = "" - self.open_bracket = False - self.bracket = None - self.check_parameter_name = {} - - def append_and_check_token_hallucination(self, token, logprob): - """ - Check if the given token is hallucinated based on the log probability. - - Args: - token (str): The token to check. - logprob (float): The log probability of the token. - - Returns: - bool: True if the token is hallucinated, False otherwise. - """ - self.tokens.append(token) - self.logprobs.append(logprob) - self._process_token() - return self.hallucination - - def __iter__(self): - return self - - def __next__(self): - if self.response_iterator is not None: - try: - r = next(self.response_iterator) - if hasattr(r.choices[0].delta, "content"): - token_content = r.choices[0].delta.content - if token_content != "": - try: - logprobs = [ - p.logprob - for p in r.choices[0].logprobs.content[0].top_logprobs - ] - self.append_and_check_token_hallucination( - token_content, logprobs - ) - except Exception as e: - self.append_and_check_token_hallucination( - token_content, [None] - ) - - return token_content - except StopIteration: - raise StopIteration - - def _process_token(self): - """ - Processes the current token and updates the state and mask accordingly. - Detects hallucinations based on the token type and log probabilities. - """ - content = "".join(self.tokens).replace(" ", "") - - # Function name extraction logic - # If the state is function name and the token is not an end token, add to the mask - if content.endswith(END_TOOL_CALL_TOKEN): - self._reset_parameters() - - if self.state == "function_name": - if self.tokens[-1] not in FUNC_NAME_END_TOKEN: - self.mask.append(MaskToken.FUNCTION_NAME) - else: - self.state = None - self._get_function_name() - - # Check if the token is a function name start token, change the state - if content.endswith(FUNC_NAME_START_PATTERN): - self.state = "function_name" - - # Parameter name extraction logic - # if the state is parameter name and the token is not an end token, add to the mask - if self.state == "parameter_name" and not content.endswith( - PARAMETER_NAME_END_TOKENS - ): - self.mask.append(MaskToken.PARAMETER_NAME) - # if the state is parameter name and the token is an end token, change the state, check hallucination and set the flag parameter name done - # The need for parameter name done is to allow the check of parameter value pattern - elif self.state == "parameter_name" and content.endswith( - PARAMETER_NAME_END_TOKENS - ): - self.state = None - self.parameter_name_done = True - self._get_parameter_name() - # if the parameter name is done and the token is a parameter name start token, change the state - elif ( - self.parameter_name_done - and not self.open_bracket - and content.endswith(PARAMETER_NAME_START_PATTERN) - ): - self.state = "parameter_name" - - # if token is a first parameter value start token, change the state - if content.endswith(FIRST_PARAM_NAME_START_PATTERN): - self.state = "parameter_name" - - # Parameter value extraction logic - # if the state is parameter value and the token is not an end token, add to the mask - if self.state == "parameter_value" and not content.endswith( - PARAMETER_VALUE_END_TOKEN - ): - # checking if the token is a value token and is not empty - open_brackets = [ - char for char in self.tokens[-1].strip() if char in BRACKETS - ] - if open_brackets: - self.open_bracket = True - self.bracket = open_brackets[0] - - if self.open_bracket and BRACKETS[self.bracket] in self.tokens[-1].strip(): - self.open_bracket = False - self.bracket = None - - if ( - not all( - char in set(string.punctuation) for char in self.tokens[-1].strip() - ) - and self.tokens[-1].strip() != "" - ): - self.mask.append(MaskToken.PARAMETER_VALUE) - - # checking if the parameter doesn't have enum and the token is the first parameter value token - # check if function name is in function properties - if self.function_name in self.function_properties: - if ( - len(self.mask) > 1 - and self.mask[-2] != MaskToken.PARAMETER_VALUE - and is_parameter_required( - self.function_properties[self.function_name], - self.parameter_name[-1], - ) - and not is_parameter_property( - self.function_properties[self.function_name], - self.parameter_name[-1], - "enum", - ) - ): - if self.parameter_name[-1] not in self.check_parameter_name: - self._check_logprob() - self.check_parameter_name[self.parameter_name[-1]] = True - else: - self._check_logprob() - self.error_message = f"Function name {self.function_name} not found in function properties" - logger.warning( - f"Function name {self.function_name} not found in function properties" - ) - else: - self.mask.append(MaskToken.NOT_USED) - # if the state is parameter value and the token is an end token, change the state - elif ( - self.state == "parameter_value" - and not self.open_bracket - and content.endswith(PARAMETER_VALUE_END_TOKEN) - ): - self.state = None - # if the parameter name is done and the token is a parameter value start token, change the state - elif self.parameter_name_done and content.endswith( - PARAMETER_VALUE_START_PATTERN - ): - self.state = "parameter_value" - - # Maintain consistency between stack and mask - # If the mask length is less than tokens, add an not used (e) token to the mask - if len(self.mask) != len(self.tokens): - self.mask.append(MaskToken.NOT_USED) - - def _check_logprob(self): - """ - Checks the log probability of the current token and updates the token probability map. - Detects hallucinations based on entropy and variance of entropy. - """ - probs = self.logprobs[-1] - entropy, varentropy, probability = calculate_uncertainty(probs) - self.token_probs_map.append((self.tokens[-1], entropy, varentropy, probability)) - - if check_threshold( - entropy, - varentropy, - self.HALLUCINATION_THRESHOLD_DICT, - ): - self.hallucination = True - self.error_message = f"token '{self.tokens[-1]}' is uncertain. Generated response:\n{''.join(self.tokens)}" - - def _count_consecutive_token(self, token=MaskToken.PARAMETER_VALUE) -> int: - """ - Counts the number of consecutive occurrences of a given token in the mask. - - Args: - token (str): The token to count in the mask. - - Returns: - int: The number of consecutive occurrences of the token. - """ - return ( - len(list(itertools.takewhile(lambda x: x == token, reversed(self.mask)))) - if self.mask and self.mask[-1] == token - else 0 - ) - - def _get_parameter_name(self): - """ - Get the parameter name from the tokens. - - Returns: - str: The extracted parameter name. - """ - p_len = self._count_consecutive_token(MaskToken.PARAMETER_NAME) - parameter_name = "".join(self.tokens[:-1][-p_len:]) - self.parameter_name.append(parameter_name) - - def _get_function_name(self): - """ - Get the function name from the tokens. - - Returns: - str: The extracted function name. - """ - f_len = self._count_consecutive_token(MaskToken.FUNCTION_NAME) - self.function_name = "".join(self.tokens[:-1][-f_len:]) diff --git a/model_server/src/core/utils/model_utils.py b/model_server/src/core/utils/model_utils.py deleted file mode 100644 index 9dfac528..00000000 --- a/model_server/src/core/utils/model_utils.py +++ /dev/null @@ -1,217 +0,0 @@ -import json -import src.commons.utils as utils - -from openai import OpenAI -from pydantic import BaseModel -from typing import Any, Dict, List, Optional -from overrides import final - - -class Message(BaseModel): - role: Optional[str] = "" - content: Optional[str] = "" - tool_call_id: Optional[str] = "" - tool_calls: Optional[List[Dict[str, Any]]] = [] - - -class ChatMessage(BaseModel): - messages: List[Message] = [] - tools: List[Dict[str, Any]] = [] - metadata: Optional[Dict[str, str]] = {} - - -class Choice(BaseModel): - id: Optional[int] = 0 - message: Message - finish_reason: Optional[str] = "stop" - - -class ChatCompletionResponse(BaseModel): - id: Optional[int] = 0 - object: Optional[str] = "chat_completion" - created: Optional[str] = "" - choices: List[Choice] = [] - model: str = "" - metadata: Optional[Dict[str, str]] = {} - - -class GuardRequest(BaseModel): - input: str - task: str - - -class GuardResponse(BaseModel): - task: str = "" - input: str = "" - prob: float = 0.0 - verdict: bool = False - metadata: Optional[Dict[str, str]] = {} - - -# ================================================================================================ - - -class ArchBaseHandler: - def __init__( - self, - client: OpenAI, - model_name: str, - task_prompt: str, - format_prompt: str, - generation_params: Dict, - ): - """ - Initializes the base handler. - - Args: - client (OpenAI): An OpenAI client instance. - model_name (str): Name of the model to use. - task_prompt (str): The main task prompt for the system. - format_prompt (str): A prompt specifying the desired output format. - generation_params (Dict): Generation parameters for the model. - """ - self.client = client - self.model_name = model_name - - self.task_prompt = task_prompt - self.format_prompt = format_prompt - - self.generation_params = generation_params - - def _convert_tools(self, tools: List[Dict[str, Any]]) -> str: - """ - Converts a list of tools into the desired internal representation. - - Args: - tools (List[Dict[str, Any]]): A list of tools represented as dictionaries. - - Raises: - NotImplementedError: Method should be overridden in subclasses. - """ - - raise NotImplementedError() - - @final - def _format_system_prompt(self, tools: List[Dict[str, Any]]) -> str: - """ - Formats the system prompt using provided tools. - - Args: - tools (List[Dict[str, Any]]): A list of tools represented as dictionaries. - - Returns: - str: A formatted system prompt. - """ - - today_date = utils.get_today_date() - tools = self._convert_tools(tools) - - system_prompt = ( - self.task_prompt.format(today_date=today_date, tools=tools) - + self.format_prompt - ) - - return system_prompt - - @final - def _process_messages( - self, - messages: List[Message], - tools: List[Dict[str, Any]] = None, - extra_instruction: str = None, - max_tokens=4096, - metadata: Dict[str, str] = {}, - ): - """ - Processes a list of messages and formats them appropriately. - - Args: - messages (List[Message]): A list of message objects. - tools (List[Dict[str, Any]], optional): A list of tools to include in the system prompt. - extra_instruction (str, optional): Additional instructions to append to the last user message. - max_tokens (int): Maximum allowed token count, assuming ~4 characters per token on average. - - Returns: - List[Dict[str, Any]]: A list of processed message dictionaries. - """ - - processed_messages = [] - - if tools: - processed_messages.append( - {"role": "system", "content": self._format_system_prompt(tools)} - ) - - for idx, message in enumerate(messages): - role, content, tool_calls = ( - message.role, - message.content, - message.tool_calls, - ) - - if tool_calls: - # TODO: Extend to support multiple function calls - role = "assistant" - content = f"\n{json.dumps(tool_calls[0]['function'])}\n" - elif role == "tool": - role = "user" - if metadata.get("optimize_context_window", "false").lower() == "true": - content = f"\n\n" - else: - # sample response below - # "content": "\n{'name': 'get_stock_price', 'result': '$196.66'}\n" - # msg[idx-1] contains tool call = '{"tool_calls": [{"name": "currency_exchange", "arguments": {"currency_symbol": "NZD"}}]}' - tool_call_msg = messages[idx - 1].content - if tool_call_msg.startswith("```") and tool_call_msg.endswith( - "```" - ): - tool_call_msg = tool_call_msg.strip("```").strip() - if tool_call_msg.startswith("json"): - tool_call_msg = tool_call_msg[4:].strip() - func_name = json.loads(tool_call_msg)["tool_calls"][0].get( - "name", "no_name" - ) - tool_response = { - "name": func_name, - "result": content, - } - content = f"\n{json.dumps(tool_response)}\n" - - processed_messages.append({"role": role, "content": content}) - - assert processed_messages[-1]["role"] == "user" - - if extra_instruction: - processed_messages[-1]["content"] += "\n" + extra_instruction - - # keep the first system message and shift conversation if the total token length exceeds the limit - def truncate_messages(messages: List[Dict[str, Any]]): - num_tokens, conversation_idx = 0, 0 - if messages[0]["role"] == "system": - num_tokens += len(messages[0]["content"]) // 4 - conversation_idx = 1 - - for message_idx in range(len(messages) - 1, conversation_idx - 1, -1): - num_tokens += len(messages[message_idx]["content"]) // 4 - if num_tokens >= max_tokens: - if messages[message_idx]["role"] == "user": - break - - return messages[:conversation_idx] + messages[message_idx:] - - processed_messages = truncate_messages(processed_messages) - - return processed_messages - - async def chat_completion(self, req: ChatMessage) -> ChatCompletionResponse: - """ - Abstract method for generating chat completions. - - Args: - req (ChatMessage): A chat message request object. - - Raises: - NotImplementedError: Method should be overridden in subclasses. - """ - - raise NotImplementedError() diff --git a/model_server/src/main.py b/model_server/src/main.py deleted file mode 100644 index 34856498..00000000 --- a/model_server/src/main.py +++ /dev/null @@ -1,158 +0,0 @@ -import json -import os -import time -import logging -import src.commons.utils as utils - -from src.commons.globals import ARCH_ENDPOINT, handler_map -from src.core.function_calling import ArchFunctionHandler -from src.core.utils.model_utils import ( - ChatMessage, - ChatCompletionResponse, - GuardRequest, - GuardResponse, -) - -from fastapi import FastAPI, Response -from opentelemetry import trace -from opentelemetry.sdk.trace import TracerProvider -from opentelemetry.sdk.trace.export import BatchSpanProcessor -from opentelemetry.sdk.resources import Resource -from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor -from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter - - -resource = Resource.create( - { - "service.name": "model-server", - } -) - -# Initialize the tracer provider -trace.set_tracer_provider(TracerProvider(resource=resource)) -tracer = trace.get_tracer(__name__) - -# DEFAULT_OTLP_HOST = "http://localhost:4317" -DEFAULT_OTLP_HOST = "none" - -# Configure the OTLP exporter (Jaeger, Zipkin, etc.) -otlp_exporter = OTLPSpanExporter( - endpoint=os.getenv("OTLP_HOST", DEFAULT_OTLP_HOST) # noqa: F821 -) - -trace.get_tracer_provider().add_span_processor(BatchSpanProcessor(otlp_exporter)) - - -logger = utils.get_model_server_logger() -logging.getLogger("httpx").setLevel(logging.ERROR) -logging.getLogger("opentelemetry.exporter.otlp.proto.grpc.exporter").setLevel( - logging.ERROR -) - -app = FastAPI() -FastAPIInstrumentor().instrument_app(app) - -logger.info(f"using archfc endpoint: {ARCH_ENDPOINT}") - - -@app.get("/healthz") -async def healthz(): - return {"status": "ok"} - - -@app.get("/models") -async def models(): - return { - "object": "list", - "data": [{"id": model_name, "object": "model"} for model_name in handler_map], - } - - -@app.post("/function_calling") -async def function_calling(req: ChatMessage, res: Response): - logger.info("[Endpoint: /function_calling]") - logger.info(f"[request body]: {json.dumps(req.model_dump(exclude_none=True))}") - - final_response: ChatCompletionResponse = None - error_messages = None - - use_agent_orchestrator = req.metadata.get("use_agent_orchestrator", False) - logger.info(f"Use agent orchestrator: {use_agent_orchestrator}") - - try: - handler_name = "Arch-Agent" if use_agent_orchestrator else "Arch-Function" - model_handler: ArchFunctionHandler = handler_map[handler_name] - - start_time = time.perf_counter() - final_response = await model_handler.chat_completion(req) - latency = time.perf_counter() - start_time - - if not final_response.metadata: - final_response.metadata = {} - - # Parameter gathering for detected intents - if final_response.choices[0].message.content: - final_response.metadata["function_latency"] = str(round(latency * 1000, 3)) - # Function Calling - elif final_response.choices[0].message.tool_calls: - final_response.metadata["function_latency"] = str(round(latency * 1000, 3)) - - if not use_agent_orchestrator: - final_response.metadata["hallucination"] = str( - model_handler.hallucination_state.hallucination - ) - # No intent detected - else: - final_response.metadata["intent_latency"] = str(round(latency * 1000, 3)) - - if not use_agent_orchestrator: - final_response.metadata["intent_latency"] = str(round(latency * 1000, 3)) - - final_response.metadata["hallucination"] = str( - model_handler.hallucination_state.hallucination - ) - - except ValueError as e: - res.statuscode = 503 - error_messages = f"[{handler_name}] - Error in tool call extraction: {e}" - raise - except StopIteration as e: - res.statuscode = 500 - error_messages = f"[{handler_name}] - Error in hallucination check: {e}" - raise - except Exception as e: - res.status_code = 500 - error_messages = f"[{handler_name}] - Error in ChatCompletion: {e}" - raise - - if error_messages is not None: - logger.error(error_messages) - final_response = ChatCompletionResponse(metadata={"error": error_messages}) - - return final_response - - -@app.post("/guardrails") -async def guardrails(req: GuardRequest, res: Response, max_num_words=300): - logger.info("[Endpoint: /guardrails] - Gateway") - logger.info(f"[request body]: {json.dumps(req.model_dump(exclude_none=True))}") - - final_response: GuardResponse = None - error_messages = None - - try: - guard_start_time = time.perf_counter() - final_response = handler_map["Arch-Guard"].predict(req) - guard_latency = time.perf_counter() - guard_start_time - final_response.metadata = { - "guard_latency": round(guard_latency * 1000, 3), - } - except Exception as e: - res.status_code = 500 - error_messages = f"[Arch-Guard]: {e}" - - if error_messages is not None: - logger.error(error_messages) - final_response = GuardResponse(metadata={"error": error_messages}) - - return final_response diff --git a/model_server/tests/__init__.py b/model_server/tests/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/model_server/tests/core/__init__.py b/model_server/tests/core/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/model_server/tests/core/test_function_calling.py b/model_server/tests/core/test_function_calling.py deleted file mode 100644 index 6005d5e6..00000000 --- a/model_server/tests/core/test_function_calling.py +++ /dev/null @@ -1,115 +0,0 @@ -import pytest -import time -from src.commons.globals import handler_map -from src.core.utils.model_utils import ChatMessage, Message - - -# define function -get_weather_api = { - "type": "function", - "function": { - "name": "get_current_weather", - "description": "Get current weather at a location.", - "parameters": { - "type": "object", - "properties": { - "location": { - "type": "str", - "description": "The location to get the weather for", - "format": "City, State", - }, - "unit": { - "type": "str", - "description": "The unit to return the weather in.", - "enum": ["celsius", "fahrenheit"], - "default": "celsius", - }, - "days": { - "type": "str", - "description": "the number of days for the request.", - }, - }, - "required": ["location", "days"], - }, - }, -} - -# get_data class return request, intent, hallucination, parameter_gathering - - -def get_hallucination_data(): - # Create instances of the Message class - message1 = Message(role="user", content="How is the weather in Seattle in days?") - - # Create a list of tools - tools = [get_weather_api] - - # Create an instance of the ChatMessage class - req = ChatMessage(messages=[message1], tools=tools) - - # first token will not be tool call - return req, False, True - - -def get_success_tool_call_data(): - # Create instances of the Message class - message1 = Message(role="user", content="How is the weather in Seattle in 7 days?") - - # Create a list of tools - tools = [get_weather_api] - - # Create an instance of the ChatMessage class - req = ChatMessage(messages=[message1], tools=tools) - - return req, True, False - - -def get_irrelevant_data(): - # Create instances of the Message class - message1 = Message(role="user", content="What is 1+1?") - - # Create a list of tools - tools = [get_weather_api] - - # Create an instance of the ChatMessage class - req = ChatMessage(messages=[message1], tools=tools) - - return req, False, False - - -def get_greeting_data(): - # Create instances of the Message class - message1 = Message(role="user", content="Hello how are you?") - - # Create a list of tools - tools = [get_weather_api] - - # Create an instance of the ChatMessage class - req = ChatMessage(messages=[message1], tools=tools) - - return req, False, False - - -@pytest.mark.asyncio -@pytest.mark.parametrize( - "get_data_func", - [ - get_hallucination_data, - get_greeting_data, - get_irrelevant_data, - get_success_tool_call_data, - ], -) -async def test_function_calling(get_data_func): - req, intent, hallucination = get_data_func() - handler_name = "Arch-Function" - use_agent_orchestrator = False - model_handler: ArchFunctionHandler = handler_map[handler_name] - - start_time = time.perf_counter() - final_response = await model_handler.chat_completion(req) - latency = time.perf_counter() - start_time - - assert intent == (len(final_response.choices[0].message.tool_calls) >= 1) - - assert hallucination == model_handler.hallucination_state.hallucination diff --git a/model_server/tests/core/test_guardrails.py b/model_server/tests/core/test_guardrails.py deleted file mode 100644 index 74d2dc97..00000000 --- a/model_server/tests/core/test_guardrails.py +++ /dev/null @@ -1,42 +0,0 @@ -from unittest.mock import patch, MagicMock -from src.core.guardrails import get_guardrail_handler - - -# Test for `get_guardrail_handler()` function on `cuda` -@patch("src.core.guardrails.AutoTokenizer.from_pretrained") -@patch("src.core.guardrails.AutoModelForSequenceClassification.from_pretrained") -def test_guardrail_handler_on_cuda(mock_auto_model, mock_tokenizer): - device = "cuda" - - mock_auto_model.return_value = MagicMock() - mock_tokenizer.return_value = MagicMock() - - guardrail = get_guardrail_handler(device=device) - - mock_tokenizer.assert_called_once_with(guardrail.model_name, trust_remote_code=True) - - mock_auto_model.assert_called_once_with( - guardrail.model_name, - device_map=device, - low_cpu_mem_usage=True, - ) - - -# Test for `get_guardrail_handler()` function on `mps` -@patch("src.core.guardrails.AutoTokenizer.from_pretrained") -@patch("src.core.guardrails.AutoModelForSequenceClassification.from_pretrained") -def test_guardrail_handler_on_mps(mock_auto_model, mock_tokenizer): - device = "mps" - - mock_auto_model.return_value = MagicMock() - mock_tokenizer.return_value = MagicMock() - - guardrail = get_guardrail_handler(device=device) - - mock_tokenizer.assert_called_once_with(guardrail.model_name, trust_remote_code=True) - - mock_auto_model.assert_called_once_with( - guardrail.model_name, - device_map=device, - low_cpu_mem_usage=True, - ) diff --git a/model_server/tests/core/test_state.py b/model_server/tests/core/test_state.py deleted file mode 100644 index 331b8d3f..00000000 --- a/model_server/tests/core/test_state.py +++ /dev/null @@ -1,36 +0,0 @@ -from src.commons.globals import handler_map -from src.core.function_calling import ArchFunctionHandler, Message - - -test_input_history = [ - {"role": "user", "content": "how is the weather in chicago for next 5 days?"}, - { - "role": "assistant", - "model": "Arch-Function", - "content": '```json\n{"tool_calls": [{"name": "get_current_weather", "arguments": {"days": 5, "location": "Chicago, Illinois"}}]}\n```', - }, - { - "role": "tool", - "model": "Arch-Function", - "content": '{"location":"Chicago%2C%20Illinois","temperature":[{"date":"2025-04-14","temperature":{"min":53,"max":65},"units":"Farenheit","query_time":"2025-04-14 17:01:52.432817+00:00"},{"date":"2025-04-15","temperature":{"min":85,"max":97},"units":"Farenheit","query_time":"2025-04-14 17:01:52.432830+00:00"},{"date":"2025-04-16","temperature":{"min":62,"max":78},"units":"Farenheit","query_time":"2025-04-14 17:01:52.432835+00:00"},{"date":"2025-04-17","temperature":{"min":89,"max":101},"units":"Farenheit","query_time":"2025-04-14 17:01:52.432839+00:00"},{"date":"2025-04-18","temperature":{"min":86,"max":104},"units":"Farenheit","query_time":"2025-04-14 17:01:52.432843+00:00"}],"units":"Farenheit"}', - }, - { - "role": "assistant", - "model": "gpt-4o-2024-08-06", - "content": '{"response": "Based on the forecast data you provided, here is the weather for the next 5 days in Chicago:\\n\\n- **April 14, 2025**: The temperature will range between 53\\u00b0F and 65\\u00b0F. \\n- **April 15, 2025**: The temperature will range between 85\\u00b0F and 97\\u00b0F.\\n- **April 16, 2025**: The temperature will range between 62\\u00b0F and 78\\u00b0F.\\n- **April 17, 2025**: The temperature will range between 89\\u00b0F and 101\\u00b0F.\\n- **April 18, 2025**: The temperature will range between 86\\u00b0F and 104\\u00b0F.\\n\\nPlease note that the temperatures are given in Fahrenheit."}', - }, - {"role": "user", "content": "what about seattle?"}, -] - - -def test_update_fc_history(): - message_history = [] - - for h in test_input_history: - message_history.append(Message(**h)) - - handler: ArchFunctionHandler = handler_map["Arch-Function"] - updated_history = handler._process_messages(message_history) - assert len(updated_history) == 5 - # ensure that tool role does not exist anymore - assert all([h["role"] != "tool" for h in updated_history]) diff --git a/model_server/tests/test_app.py b/model_server/tests/test_app.py deleted file mode 100644 index 0029e4b5..00000000 --- a/model_server/tests/test_app.py +++ /dev/null @@ -1,47 +0,0 @@ -import pytest -import httpx - -from fastapi.testclient import TestClient -from src.main import app - - -client = TestClient(app) - - -# Unit tests for the health check endpoint -@pytest.mark.asyncio -async def test_healthz(): - response = client.get("/healthz") - assert response.status_code == 200 - assert response.json() == {"status": "ok"} - - -# Unit test for the models endpoint -@pytest.mark.asyncio -async def test_models(): - response = client.get("/models") - assert response.status_code == 200 - assert response.json()["object"] == "list" - assert len(response.json()["data"]) > 0 - - -# Unit test for the guardrail endpoint -@pytest.mark.asyncio -async def test_guardrail_endpoint(): - request_data = {"input": "Test for jailbreak and toxicity", "task": "jailbreak"} - response = client.post("/guardrails", json=request_data) - assert response.status_code == 200 - - -# Unit test for the function calling endpoint -@pytest.mark.asyncio -async def test_function_calling_endpoint(): - async with httpx.AsyncClient(app=app, base_url="http://test") as client: - request_data = { - "messages": [{"role": "user", "content": "Hello!"}], - "model": "Arch-Function", - "tools": [], - "metadata": {"x-arch-state": "[]"}, - } - response = await client.post("/function_calling", json=request_data) - assert response.status_code == 200 diff --git a/tests/archgw/docker-compose.yaml b/tests/archgw/docker-compose.yaml index 3cafd86e..cd317b38 100644 --- a/tests/archgw/docker-compose.yaml +++ b/tests/archgw/docker-compose.yaml @@ -12,4 +12,3 @@ services: environment: - OPENAI_API_KEY=${OPENAI_API_KEY:?error} - MISTRAL_API_KEY=${MISTRAL_API_KEY:?error} - - MODEL_SERVER_PORT=${MODEL_SERVER_PORT:-51001} diff --git a/tests/archgw/test_prompt_gateway.py b/tests/archgw/test_prompt_gateway.py index b207ebe0..0e1b4317 100644 --- a/tests/archgw/test_prompt_gateway.py +++ b/tests/archgw/test_prompt_gateway.py @@ -22,6 +22,28 @@ from common import ( ) +def normalize_tool_call_arguments(tool_call): + """ + Normalize tool call arguments to ensure they are always a dict. + + According to OpenAI API spec, the 'arguments' field should be a JSON string, + but for easier testing we parse it into a dict here. + + Args: + tool_call: A tool call dict that may have 'arguments' as either a string or dict + + Returns: + A tool call dict with 'arguments' guaranteed to be a dict + """ + if "arguments" in tool_call and isinstance(tool_call["arguments"], str): + try: + tool_call["arguments"] = json.loads(tool_call["arguments"]) + except (json.JSONDecodeError, TypeError): + # If parsing fails, keep it as is + pass + return tool_call + + def test_prompt_gateway(httpserver: HTTPServer): simple_fixture = TEST_CASE_FIXTURES["SIMPLE"] input = simple_fixture["input"] @@ -67,7 +89,7 @@ def test_prompt_gateway(httpserver: HTTPServer): tool_calls_message = arch_messages[0] tool_calls = tool_calls_message.get("tool_calls", []) assert len(tool_calls) > 0 - tool_call = tool_calls[0]["function"] + tool_call = normalize_tool_call_arguments(tool_calls[0]["function"]) diff = DeepDiff(tool_call, expected_tool_call, ignore_string_case=True) assert not diff diff --git a/tests/e2e/arch_config_memory_state_v1_responses.yaml b/tests/e2e/arch_config_memory_state_v1_responses.yaml new file mode 100644 index 00000000..afc40910 --- /dev/null +++ b/tests/e2e/arch_config_memory_state_v1_responses.yaml @@ -0,0 +1,25 @@ +version: v0.1 + +listeners: + egress_traffic: + address: 0.0.0.0 + port: 12000 + message_format: openai + timeout: 30s + +llm_providers: + + # OpenAI Models + - model: openai/gpt-5-mini-2025-08-07 + access_key: $OPENAI_API_KEY + default: true + + # Anthropic Models + - model: anthropic/claude-sonnet-4-20250514 + access_key: $ANTHROPIC_API_KEY + +# State storage configuration for v1/responses API +# Manages conversation state for multi-turn conversations +state_storage: + # Type: memory | postgres + type: memory diff --git a/tests/e2e/docker-compose.yaml b/tests/e2e/docker-compose.yaml index a78c5632..2dd18374 100644 --- a/tests/e2e/docker-compose.yaml +++ b/tests/e2e/docker-compose.yaml @@ -10,7 +10,6 @@ services: volumes: - ../../demos/samples_python/weather_forecast/arch_config.yaml:/app/arch_config.yaml - /etc/ssl/cert.pem:/etc/ssl/cert.pem - - ~/archgw_logs:/var/log/ extra_hosts: - "host.docker.internal:host-gateway" environment: @@ -18,4 +17,3 @@ services: - MISTRAL_API_KEY=${MISTRAL_API_KEY:?error} - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY:?error} - OTEL_TRACING_HTTP_ENDPOINT=http://host.docker.internal:4318/v1/traces - - MODEL_SERVER_PORT=${MODEL_SERVER_PORT:-51000} diff --git a/tests/e2e/run_e2e_tests.sh b/tests/e2e/run_e2e_tests.sh index 54a53b1f..a6e66121 100644 --- a/tests/e2e/run_e2e_tests.sh +++ b/tests/e2e/run_e2e_tests.sh @@ -11,9 +11,6 @@ touch ~/archgw_logs/modelserver.log print_debug() { log "Received signal to stop" - log "Printing debug logs for model_server" - log "====================================" - tail -n 100 ~/archgw_logs/modelserver.log log "Printing debug logs for docker" log "====================================" tail -n 100 ../build.log @@ -30,12 +27,6 @@ cd ../../demos/samples_python/weather_forecast/ docker compose up weather_forecast_service --build -d cd - -log building and install model server -log ================================= -cd ../../model_server -poetry install -cd - - log building and installing archgw cli log ================================== cd ../../arch/tools @@ -53,11 +44,8 @@ poetry install log startup arch gateway with function calling demo cd ../../ -tail -F ~/archgw_logs/modelserver.log & -model_server_tail_pid=$! archgw down archgw up demos/samples_python/weather_forecast/arch_config.yaml -kill $model_server_tail_pid cd - log running e2e tests for prompt gateway @@ -77,6 +65,18 @@ log running e2e tests for model alias routing log ======================================== poetry run pytest test_model_alias_routing.py +log running e2e tests for openai responses api client +log ======================================== +poetry run pytest test_openai_responses_api_client.py + +log startup arch gateway with state storage for openai responses api client demo +archgw down +archgw up arch_config_memory_state_v1_responses.yaml + +log running e2e tests for openai responses api client +log ======================================== +poetry run pytest test_openai_responses_api_client_with_state.py + log shutting down the weather_forecast demo log ======================================= cd ../../demos/samples_python/weather_forecast diff --git a/tests/e2e/test_openai_responses_api_client.py b/tests/e2e/test_openai_responses_api_client.py new file mode 100644 index 00000000..282af7c4 --- /dev/null +++ b/tests/e2e/test_openai_responses_api_client.py @@ -0,0 +1,671 @@ +import openai +import pytest +import os +import logging +import sys + +# Set up logging +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", + handlers=[logging.StreamHandler(sys.stdout)], +) +logger = logging.getLogger(__name__) + +LLM_GATEWAY_ENDPOINT = os.getenv( + "LLM_GATEWAY_ENDPOINT", "http://localhost:12000/v1/chat/completions" +) + + +# ----------------------- +# v1/responses API tests +# ----------------------- +def test_openai_responses_api_non_streaming_passthrough(): + """Build a v1/responses API request (pass-through) and ensure gateway accepts it""" + base_url = LLM_GATEWAY_ENDPOINT.replace("/v1/chat/completions", "") + client = openai.OpenAI(api_key="test-key", base_url=f"{base_url}/v1") + + # Simple responses API request using a direct model (pass-through) + resp = client.responses.create( + model="gpt-4o", input="Hello via responses passthrough" + ) + + # Print the response content - handle both responses format and chat completions format + print(f"\n{'='*80}") + print(f"Model: {resp.model}") + print(f"Output: {resp.output_text}") + print(f"{'='*80}\n") + + # Minimal sanity checks + assert resp is not None + assert ( + getattr(resp, "id", None) is not None + or getattr(resp, "output", None) is not None + ) + + +def test_openai_responses_api_with_streaming_passthrough(): + """Build a v1/responses API streaming request (pass-through) and ensure gateway accepts it""" + base_url = LLM_GATEWAY_ENDPOINT.replace("/v1/chat/completions", "") + client = openai.OpenAI(api_key="test-key", base_url=f"{base_url}/v1") + + # Simple streaming responses API request using a direct model (pass-through) + stream = client.responses.create( + model="gpt-4o", + input="Write a short haiku about coding", + stream=True, + ) + + # Collect streamed content using the official Responses API streaming shape + text_chunks = [] + final_message = None + + for event in stream: + # The Python SDK surfaces a high-level Responses streaming interface. + # We rely on its typed helpers instead of digging into model_extra. + if getattr(event, "type", None) == "response.output_text.delta" and getattr( + event, "delta", None + ): + # Each delta contains a text fragment + text_chunks.append(event.delta) + + # Track the final response message if provided by the SDK + if getattr(event, "type", None) == "response.completed" and getattr( + event, "response", None + ): + final_message = event.response + + full_content = "".join(text_chunks) + + # Print the streaming response + print(f"\n{'='*80}") + print( + f"Model: {getattr(final_message, 'model', 'unknown') if final_message else 'unknown'}" + ) + print(f"Streamed Output: {full_content}") + print(f"{'='*80}\n") + + assert len(text_chunks) > 0, "Should have received streaming text deltas" + assert len(full_content) > 0, "Should have received content" + + +def test_openai_responses_api_non_streaming_with_tools_passthrough(): + """Responses API with a function/tool definition (pass-through)""" + base_url = LLM_GATEWAY_ENDPOINT.replace("/v1/chat/completions", "") + client = openai.OpenAI(api_key="test-key", base_url=f"{base_url}/v1", max_retries=0) + + # Define a simple tool/function for the Responses API + tools = [ + { + "type": "function", + "name": "echo_tool", + "description": "Echo back the provided input", + "parameters": { + "type": "object", + "properties": {"text": {"type": "string"}}, + "required": ["text"], + }, + } + ] + + resp = client.responses.create( + model="gpt-5", + input="Call the echo tool", + tools=tools, + ) + + assert resp is not None + assert ( + getattr(resp, "id", None) is not None + or getattr(resp, "output", None) is not None + ) + + +def test_openai_responses_api_with_streaming_with_tools_passthrough(): + """Responses API with a function/tool definition (streaming, pass-through)""" + base_url = LLM_GATEWAY_ENDPOINT.replace("/v1/chat/completions", "") + client = openai.OpenAI(api_key="test-key", base_url=f"{base_url}/v1", max_retries=0) + + tools = [ + { + "type": "function", + "name": "echo_tool", + "description": "Echo back the provided input", + "parameters": { + "type": "object", + "properties": {"text": {"type": "string"}}, + "required": ["text"], + }, + } + ] + + stream = client.responses.create( + model="gpt-5", + input="Call the echo tool", + tools=tools, + stream=True, + ) + + text_chunks = [] + tool_calls = [] + + for event in stream: + etype = getattr(event, "type", None) + + # Collect streamed text output + if etype == "response.output_text.delta" and getattr(event, "delta", None): + text_chunks.append(event.delta) + + # Collect streamed tool call arguments + if etype == "response.function_call_arguments.delta" and getattr( + event, "delta", None + ): + tool_calls.append(event.delta) + + full_text = "".join(text_chunks) + + print(f"\n{'='*80}") + print("Responses tools streaming test") + print(f"Streamed text: {full_text}") + print(f"Tool call argument chunks: {len(tool_calls)}") + print(f"{'='*80}\n") + + # We expect either streamed text output or streamed tool-call arguments + assert ( + full_text or tool_calls + ), "Expected streamed text or tool call argument deltas from Responses tools stream" + + +def test_openai_responses_api_non_streaming_upstream_chat_completions(): + """Send a v1/responses request using the grok alias to verify translation/routing""" + base_url = LLM_GATEWAY_ENDPOINT.replace("/v1/chat/completions", "") + client = openai.OpenAI(api_key="test-key", base_url=f"{base_url}/v1") + + resp = client.responses.create( + model="arch.grok.v1", input="Hello, translate this via grok alias" + ) + + # Print the response content - handle both responses format and chat completions format + print(f"\n{'='*80}") + print(f"Model: {resp.model}") + print(f"Output: {resp.output_text}") + print(f"{'='*80}\n") + + assert resp is not None + assert resp.id is not None + + +def test_openai_responses_api_with_streaming_upstream_chat_completions(): + """Build a v1/responses API streaming request (pass-through) and ensure gateway accepts it""" + base_url = LLM_GATEWAY_ENDPOINT.replace("/v1/chat/completions", "") + client = openai.OpenAI(api_key="test-key", base_url=f"{base_url}/v1") + + # Simple streaming responses API request using a direct model (pass-through) + stream = client.responses.create( + model="arch.grok.v1", + input="Write a short haiku about coding", + stream=True, + ) + + # Collect streamed content using the official Responses API streaming shape + text_chunks = [] + final_message = None + + for event in stream: + # The Python SDK surfaces a high-level Responses streaming interface. + # We rely on its typed helpers instead of digging into model_extra. + if getattr(event, "type", None) == "response.output_text.delta" and getattr( + event, "delta", None + ): + # Each delta contains a text fragment + text_chunks.append(event.delta) + + # Track the final response message if provided by the SDK + if getattr(event, "type", None) == "response.completed" and getattr( + event, "response", None + ): + final_message = event.response + + full_content = "".join(text_chunks) + + # Print the streaming response + print(f"\n{'='*80}") + print( + f"Model: {getattr(final_message, 'model', 'unknown') if final_message else 'unknown'}" + ) + print(f"Streamed Output: {full_content}") + print(f"{'='*80}\n") + + assert len(text_chunks) > 0, "Should have received streaming text deltas" + assert len(full_content) > 0, "Should have received content" + + +def test_openai_responses_api_non_streaming_with_tools_upstream_chat_completions(): + """Responses API wioutputling routed to grok via alias""" + base_url = LLM_GATEWAY_ENDPOINT.replace("/v1/chat/completions", "") + client = openai.OpenAI(api_key="test-key", base_url=f"{base_url}/v1") + + tools = [ + { + "type": "function", + "name": "echo_tool", + "description": "Echo back the provided input", + "parameters": { + "type": "object", + "properties": {"text": {"type": "string"}}, + "required": ["text"], + }, + } + ] + + resp = client.responses.create( + model="arch.grok.v1", + input="Call the echo tool", + tools=tools, + ) + + assert resp.id is not None + + print(f"\n{'='*80}") + print(f"Model: {resp.model}") + print(f"Output: {resp.output_text}") + print(f"{'='*80}\n") + + +def test_openai_responses_api_streaming_with_tools_upstream_chat_completions(): + """Responses API with a function/tool definition (streaming, pass-through)""" + base_url = LLM_GATEWAY_ENDPOINT.replace("/v1/chat/completions", "") + client = openai.OpenAI(api_key="test-key", base_url=f"{base_url}/v1", max_retries=0) + + tools = [ + { + "type": "function", + "name": "echo_tool", + "description": "Echo back the provided input", + "parameters": { + "type": "object", + "properties": {"text": {"type": "string"}}, + "required": ["text"], + }, + } + ] + + stream = client.responses.create( + model="arch.grok.v1", + input="Call the echo tool", + tools=tools, + stream=True, + ) + + text_chunks = [] + tool_calls = [] + + for event in stream: + etype = getattr(event, "type", None) + + # Collect streamed text output + if etype == "response.output_text.delta" and getattr(event, "delta", None): + text_chunks.append(event.delta) + + # Collect streamed tool call arguments + if etype == "response.function_call_arguments.delta" and getattr( + event, "delta", None + ): + tool_calls.append(event.delta) + + full_text = "".join(text_chunks) + + print(f"\n{'='*80}") + print("Responses tools streaming test") + print(f"Streamed text: {full_text}") + print(f"Tool call argument chunks: {len(tool_calls)}") + print(f"{'='*80}\n") + + # We expect either streamed text output or streamed tool-call arguments + assert ( + full_text or tool_calls + ), "Expected streamed text or tool call argument deltas from Responses tools stream" + + +def test_openai_responses_api_non_streaming_upstream_bedrock(): + """Send a v1/responses request using the coding-model alias to verify Bedrock translation/routing""" + base_url = LLM_GATEWAY_ENDPOINT.replace("/v1/chat/completions", "") + client = openai.OpenAI(api_key="test-key", base_url=f"{base_url}/v1") + + resp = client.responses.create( + model="coding-model", + input="Hello, translate this via coding-model alias to Bedrock", + ) + + # Print the response content - handle both responses format and chat completions format + print(f"\n{'='*80}") + print(f"Model: {resp.model}") + print(f"Output: {resp.output_text}") + print(f"{'='*80}\n") + + assert resp is not None + assert resp.id is not None + + +def test_openai_responses_api_with_streaming_upstream_bedrock(): + """Build a v1/responses API streaming request routed to Bedrock via coding-model alias""" + base_url = LLM_GATEWAY_ENDPOINT.replace("/v1/chat/completions", "") + client = openai.OpenAI(api_key="test-key", base_url=f"{base_url}/v1") + + # Simple streaming responses API request using coding-model alias + stream = client.responses.create( + model="coding-model", + input="Write a short haiku about coding", + stream=True, + ) + + # Collect streamed content using the official Responses API streaming shape + text_chunks = [] + final_message = None + + for event in stream: + # The Python SDK surfaces a high-level Responses streaming interface. + # We rely on its typed helpers instead of digging into model_extra. + if getattr(event, "type", None) == "response.output_text.delta" and getattr( + event, "delta", None + ): + # Each delta contains a text fragment + text_chunks.append(event.delta) + + # Track the final response message if provided by the SDK + if getattr(event, "type", None) == "response.completed" and getattr( + event, "response", None + ): + final_message = event.response + + full_content = "".join(text_chunks) + + # Print the streaming response + print(f"\n{'='*80}") + print( + f"Model: {getattr(final_message, 'model', 'unknown') if final_message else 'unknown'}" + ) + print(f"Streamed Output: {full_content}") + print(f"{'='*80}\n") + + assert len(text_chunks) > 0, "Should have received streaming text deltas" + assert len(full_content) > 0, "Should have received content" + + +def test_openai_responses_api_non_streaming_with_tools_upstream_bedrock(): + """Responses API with tools routed to Bedrock via coding-model alias""" + base_url = LLM_GATEWAY_ENDPOINT.replace("/v1/chat/completions", "") + client = openai.OpenAI(api_key="test-key", base_url=f"{base_url}/v1") + + tools = [ + { + "type": "function", + "name": "echo_tool", + "description": "Echo back the provided input", + "parameters": { + "type": "object", + "properties": {"text": {"type": "string"}}, + "required": ["text"], + }, + } + ] + + resp = client.responses.create( + model="coding-model", + input="Call the echo tool", + tools=tools, + ) + + assert resp.id is not None + + print(f"\n{'='*80}") + print(f"Model: {resp.model}") + print(f"Output: {resp.output_text}") + print(f"{'='*80}\n") + + +def test_openai_responses_api_streaming_with_tools_upstream_bedrock(): + """Responses API with a function/tool definition streaming to Bedrock via coding-model alias""" + base_url = LLM_GATEWAY_ENDPOINT.replace("/v1/chat/completions", "") + client = openai.OpenAI(api_key="test-key", base_url=f"{base_url}/v1", max_retries=0) + + tools = [ + { + "type": "function", + "name": "echo_tool", + "description": "Echo back the provided input", + "parameters": { + "type": "object", + "properties": {"text": {"type": "string"}}, + "required": ["text"], + }, + } + ] + + stream = client.responses.create( + model="coding-model", + input="Call the echo tool", + tools=tools, + stream=True, + ) + + text_chunks = [] + tool_calls = [] + + for event in stream: + etype = getattr(event, "type", None) + + # Collect streamed text output + if etype == "response.output_text.delta" and getattr(event, "delta", None): + text_chunks.append(event.delta) + + # Collect streamed tool call arguments + if etype == "response.function_call_arguments.delta" and getattr( + event, "delta", None + ): + tool_calls.append(event.delta) + + full_text = "".join(text_chunks) + + print(f"\n{'='*80}") + print("Responses tools streaming test (Bedrock)") + print(f"Streamed text: {full_text}") + print(f"Tool call argument chunks: {len(tool_calls)}") + print(f"{'='*80}\n") + + # We expect either streamed text output or streamed tool-call arguments + assert ( + full_text or tool_calls + ), "Expected streamed text or tool call argument deltas from Responses tools stream" + + +def test_openai_responses_api_non_streaming_upstream_anthropic(): + """Send a v1/responses request using the grok alias to verify translation/routing""" + base_url = LLM_GATEWAY_ENDPOINT.replace("/v1/chat/completions", "") + client = openai.OpenAI(api_key="test-key", base_url=f"{base_url}/v1") + + resp = client.responses.create( + model="claude-sonnet-4-20250514", input="Hello, translate this via grok alias" + ) + + # Print the response content - handle both responses format and chat completions format + print(f"\n{'='*80}") + print(f"Model: {resp.model}") + print(f"Output: {resp.output_text}") + print(f"{'='*80}\n") + + assert resp is not None + assert resp.id is not None + + +def test_openai_responses_api_with_streaming_upstream_anthropic(): + """Build a v1/responses API streaming request (pass-through) and ensure gateway accepts it""" + base_url = LLM_GATEWAY_ENDPOINT.replace("/v1/chat/completions", "") + client = openai.OpenAI(api_key="test-key", base_url=f"{base_url}/v1") + + # Simple streaming responses API request using a direct model (pass-through) + stream = client.responses.create( + model="claude-sonnet-4-20250514", + input="Write a short haiku about coding", + stream=True, + ) + + # Collect streamed content using the official Responses API streaming shape + text_chunks = [] + final_message = None + + for event in stream: + # The Python SDK surfaces a high-level Responses streaming interface. + # We rely on its typed helpers instead of digging into model_extra. + if getattr(event, "type", None) == "response.output_text.delta" and getattr( + event, "delta", None + ): + # Each delta contains a text fragment + text_chunks.append(event.delta) + + # Track the final response message if provided by the SDK + if getattr(event, "type", None) == "response.completed" and getattr( + event, "response", None + ): + final_message = event.response + + full_content = "".join(text_chunks) + + # Print the streaming response + print(f"\n{'='*80}") + print( + f"Model: {getattr(final_message, 'model', 'unknown') if final_message else 'unknown'}" + ) + print(f"Streamed Output: {full_content}") + print(f"{'='*80}\n") + + assert len(text_chunks) > 0, "Should have received streaming text deltas" + assert len(full_content) > 0, "Should have received content" + + +def test_openai_responses_api_non_streaming_with_tools_upstream_anthropic(): + """Responses API with tools routed to grok via alias""" + base_url = LLM_GATEWAY_ENDPOINT.replace("/v1/chat/completions", "") + client = openai.OpenAI(api_key="test-key", base_url=f"{base_url}/v1") + + tools = [ + { + "type": "function", + "name": "echo_tool", + "description": "Echo back the provided input: hello_world", + "parameters": { + "type": "object", + "properties": {"text": {"type": "string"}}, + "required": ["text"], + }, + } + ] + + resp = client.responses.create( + model="claude-sonnet-4-20250514", + input="Call the echo tool", + tools=tools, + ) + + assert resp.id is not None + + print(f"\n{'='*80}") + print(f"Model: {resp.model}") + print(f"Output: {resp.output_text}") + print(f"{'='*80}\n") + + +def test_openai_responses_api_streaming_with_tools_upstream_anthropic(): + """Responses API with a function/tool definition (streaming, pass-through)""" + base_url = LLM_GATEWAY_ENDPOINT.replace("/v1/chat/completions", "") + client = openai.OpenAI(api_key="test-key", base_url=f"{base_url}/v1", max_retries=0) + + tools = [ + { + "type": "function", + "name": "echo_tool", + "description": "Echo back the provided input: hello_world", + "parameters": { + "type": "object", + "properties": {"text": {"type": "string"}}, + "required": ["text"], + }, + } + ] + + stream = client.responses.create( + model="claude-sonnet-4-20250514", + input="Call the echo tool with hello_world", + tools=tools, + stream=True, + ) + + text_chunks = [] + tool_calls = [] + + for event in stream: + etype = getattr(event, "type", None) + + # Collect streamed text output + if etype == "response.output_text.delta" and getattr(event, "delta", None): + text_chunks.append(event.delta) + + # Collect streamed tool call arguments + if etype == "response.function_call_arguments.delta" and getattr( + event, "delta", None + ): + tool_calls.append(event.delta) + + full_text = "".join(text_chunks) + + print(f"\n{'='*80}") + print("Responses tools streaming test") + print(f"Streamed text: {full_text}") + print(f"Tool call argument chunks: {len(tool_calls)}") + print(f"{'='*80}\n") + + # We expect either streamed text output or streamed tool-call arguments + assert ( + full_text or tool_calls + ), "Expected streamed text or tool call argument deltas from Responses tools stream" + + +def test_openai_responses_api_mixed_content_types(): + """Test Responses API with mixed content types (string and array) in input messages""" + base_url = LLM_GATEWAY_ENDPOINT.replace("/v1/chat/completions", "") + client = openai.OpenAI(api_key="test-key", base_url=f"{base_url}/v1") + + # This test mimics the request that was failing: + # One message with string content, another with array content + resp = client.responses.create( + model="arch.title.v1", + input=[ + { + "role": "developer", + "content": "Generate a very short chat title (2-5 words max) based on the user's message.\n" + "Rules:\n" + "- Maximum 30 characters\n" + "- No quotes, colons, hashtags, or markdown\n" + "- Just the topic/intent, not a full sentence\n" + '- If the message is a greeting like "hi" or "hello", respond with just "New conversation"\n' + '- Be concise: "Weather in NYC" not "User asking about the weather in New York City"', + }, + { + "role": "user", + "content": [ + {"type": "input_text", "text": "What is the weather in Seattle"} + ], + }, + ], + ) + + # Print the response + print(f"\n{'='*80}") + print(f"Model: {resp.model}") + print(f"Output: {resp.output_text}") + print(f"{'='*80}\n") + + assert resp is not None + assert resp.id is not None + # Verify we got a reasonable title + assert len(resp.output_text) > 0 diff --git a/tests/e2e/test_openai_responses_api_client_with_state.py b/tests/e2e/test_openai_responses_api_client_with_state.py new file mode 100644 index 00000000..c23307e6 --- /dev/null +++ b/tests/e2e/test_openai_responses_api_client_with_state.py @@ -0,0 +1,218 @@ +import openai +import pytest +import os +import logging +import sys + +# Set up logging +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", + handlers=[logging.StreamHandler(sys.stdout)], +) +logger = logging.getLogger(__name__) + +LLM_GATEWAY_ENDPOINT = os.getenv( + "LLM_GATEWAY_ENDPOINT", "http://localhost:12000/v1/chat/completions" +) + + +def test_conversation_state_management_two_turn(): + """ + Test conversation state management across two turns: + 1. Send initial message to non-OpenAI model via v1/responses + 2. Capture response_id from first response + 3. Send second message with previous_response_id + 4. Verify model receives both messages in correct order + """ + base_url = LLM_GATEWAY_ENDPOINT.replace("/v1/chat/completions", "") + client = openai.OpenAI(api_key="test-key", base_url=f"{base_url}/v1") + + logger.info("\n" + "=" * 80) + logger.info("TEST: Conversation State Management - Two Turn Flow") + logger.info("=" * 80) + + # Turn 1: Send initial message to Anthropic (non-OpenAI model) + logger.info("\n[TURN 1] Sending initial message...") + resp1 = client.responses.create( + model="claude-sonnet-4-20250514", + input="My name is Alice and I like pizza.", + ) + + # Extract response_id from first response + response_id_1 = resp1.id + logger.info(f"[TURN 1] Received response_id: {response_id_1}") + logger.info(f"[TURN 1] Model response: {resp1.output_text}") + + assert response_id_1 is not None, "First response should have an id" + assert len(resp1.output_text) > 0, "First response should have content" + + # Turn 2: Send follow-up message with previous_response_id + # Ask the model to list all messages to verify state was combined + logger.info( + f"\n[TURN 2] Sending follow-up with previous_response_id={response_id_1}" + ) + resp2 = client.responses.create( + model="claude-sonnet-4-20250514", + input="Please list all the messages you have received in our conversation, numbering each one.", + previous_response_id=response_id_1, + ) + + response_id_2 = resp2.id + logger.info(f"[TURN 2] Received response_id: {response_id_2}") + logger.info(f"[TURN 2] Model response: {resp2.output_text}") + + assert response_id_2 is not None, "Second response should have an id" + assert response_id_2 != response_id_1, "Second response should have different id" + + # Verify the model received the conversation history + # The response should reference both the initial message and the follow-up + response_lower = resp2.output_text.lower() + + # Check if the model acknowledges receiving multiple messages + # Different models might format this differently, so we check for various indicators + has_conversation_context = ( + "alice" in response_lower + or "pizza" in response_lower # References the name from turn 1 + or "two" in response_lower # References the preference from turn 1 + or "2" in response_lower # Mentions number of messages + or "first" in response_lower # Numeric indicator + or "second" # References first message + in response_lower # References second message + ) + + logger.info( + f"\n[VALIDATION] Conversation context preserved: {has_conversation_context}" + ) + logger.info( + f"[VALIDATION] Response contains conversation markers: {has_conversation_context}" + ) + + print(f"\n{'='*80}") + print("Conversation State Test Results:") + print(f"Turn 1 Response ID: {response_id_1}") + print(f"Turn 2 Response ID: {response_id_2}") + print(f"Turn 1 Output: {resp1.output_text[:100]}...") + print(f"Turn 2 Output: {resp2.output_text}") + print(f"Conversation Context Preserved: {has_conversation_context}") + print(f"{'='*80}\n") + + assert has_conversation_context, ( + f"Model should have received conversation history. " + f"Response: {resp2.output_text}" + ) + + +def test_conversation_state_management_two_turn_streaming(): + """ + Test conversation state management across two turns with streaming: + 1. Send initial streaming message to non-OpenAI model via v1/responses + 2. Capture response_id from first response + 3. Send second streaming message with previous_response_id + 4. Verify model receives both messages in correct order + """ + base_url = LLM_GATEWAY_ENDPOINT.replace("/v1/chat/completions", "") + client = openai.OpenAI(api_key="test-key", base_url=f"{base_url}/v1") + + logger.info("\n" + "=" * 80) + logger.info("TEST: Conversation State Management - Two Turn Streaming Flow") + logger.info("=" * 80) + + # Turn 1: Send initial streaming message to Anthropic (non-OpenAI model) + logger.info("\n[TURN 1] Sending initial streaming message...") + stream1 = client.responses.create( + model="claude-sonnet-4-20250514", + input="My name is Alice and I like pizza.", + stream=True, + ) + + # Collect streamed content and capture response_id + text_chunks_1 = [] + response_id_1 = None + + for event in stream1: + if getattr(event, "type", None) == "response.output_text.delta" and getattr( + event, "delta", None + ): + text_chunks_1.append(event.delta) + + # Capture response_id from response.completed event + if getattr(event, "type", None) == "response.completed" and getattr( + event, "response", None + ): + response_id_1 = event.response.id + + output_1 = "".join(text_chunks_1) + logger.info(f"[TURN 1] Received response_id: {response_id_1}") + logger.info(f"[TURN 1] Model response: {output_1}") + + assert response_id_1 is not None, "First response should have an id" + assert len(output_1) > 0, "First response should have content" + + # Turn 2: Send follow-up streaming message with previous_response_id + logger.info( + f"\n[TURN 2] Sending follow-up streaming request with previous_response_id={response_id_1}" + ) + stream2 = client.responses.create( + model="claude-sonnet-4-20250514", + input="Please list all the messages you have received in our conversation, numbering each one.", + previous_response_id=response_id_1, + stream=True, + ) + + # Collect streamed content from second response + text_chunks_2 = [] + response_id_2 = None + + for event in stream2: + if getattr(event, "type", None) == "response.output_text.delta" and getattr( + event, "delta", None + ): + text_chunks_2.append(event.delta) + + # Capture response_id from response.completed event + if getattr(event, "type", None) == "response.completed" and getattr( + event, "response", None + ): + response_id_2 = event.response.id + + output_2 = "".join(text_chunks_2) + logger.info(f"[TURN 2] Received response_id: {response_id_2}") + logger.info(f"[TURN 2] Model response: {output_2}") + + assert response_id_2 is not None, "Second response should have an id" + assert response_id_2 != response_id_1, "Second response should have different id" + + # Verify the model received the conversation history + response_lower = output_2.lower() + + # Check if the model acknowledges receiving multiple messages + has_conversation_context = ( + "alice" in response_lower + or "pizza" in response_lower # References the name from turn 1 + or "two" in response_lower # References the preference from turn 1 + or "2" in response_lower # Mentions number of messages + or "first" in response_lower # Numeric indicator + or "second" # References first message + in response_lower # References second message + ) + + logger.info( + f"\n[VALIDATION] Conversation context preserved: {has_conversation_context}" + ) + logger.info( + f"[VALIDATION] Response contains conversation markers: {has_conversation_context}" + ) + + print(f"\n{'='*80}") + print("Streaming Conversation State Test Results:") + print(f"Turn 1 Response ID: {response_id_1}") + print(f"Turn 2 Response ID: {response_id_2}") + print(f"Turn 1 Output: {output_1[:100]}...") + print(f"Turn 2 Output: {output_2}") + print(f"Conversation Context Preserved: {has_conversation_context}") + print(f"{'='*80}\n") + + assert has_conversation_context, ( + f"Model should have received conversation history. " f"Response: {output_2}" + ) diff --git a/tests/e2e/test_prompt_gateway.py b/tests/e2e/test_prompt_gateway.py index 2edab55d..a55e740c 100644 --- a/tests/e2e/test_prompt_gateway.py +++ b/tests/e2e/test_prompt_gateway.py @@ -24,6 +24,28 @@ def cleanup_tool_call(tool_call): return tool_call.strip() +def normalize_tool_call_arguments(tool_call): + """ + Normalize tool call arguments to ensure they are always a dict. + + According to OpenAI API spec, the 'arguments' field should be a JSON string, + but for easier testing we parse it into a dict here. + + Args: + tool_call: A tool call dict that may have 'arguments' as either a string or dict + + Returns: + A tool call dict with 'arguments' guaranteed to be a dict + """ + if "arguments" in tool_call and isinstance(tool_call["arguments"], str): + try: + tool_call["arguments"] = json.loads(tool_call["arguments"]) + except (json.JSONDecodeError, TypeError): + # If parsing fails, keep it as is + pass + return tool_call + + @pytest.mark.parametrize("stream", [True, False]) def test_prompt_gateway(stream): expected_tool_call = { @@ -62,7 +84,7 @@ def test_prompt_gateway(stream): print("cleaned_tool_call_str: ", cleaned_tool_call_str) tool_calls = json.loads(cleaned_tool_call_str).get("tool_calls", []) assert len(tool_calls) > 0 - tool_call = tool_calls[0] + tool_call = normalize_tool_call_arguments(tool_calls[0]) location = tool_call["arguments"]["location"] assert expected_tool_call["arguments"]["location"] in location.lower() del expected_tool_call["arguments"]["location"] @@ -106,7 +128,7 @@ def test_prompt_gateway(stream): print("cleaned_tool_call_json: ", json.dumps(cleaned_tool_call_json)) tool_calls_list = cleaned_tool_call_json.get("tool_calls", []) assert len(tool_calls_list) > 0 - tool_call = tool_calls_list[0] + tool_call = normalize_tool_call_arguments(tool_calls_list[0]) location = tool_call["arguments"]["location"] assert expected_tool_call["arguments"]["location"] in location.lower() del expected_tool_call["arguments"]["location"] @@ -241,7 +263,7 @@ def test_prompt_gateway_param_tool_call(stream): assert role == "assistant" tool_calls = choices[0].get("delta", {}).get("tool_calls", []) assert len(tool_calls) > 0 - tool_call = tool_calls[0]["function"] + tool_call = normalize_tool_call_arguments(tool_calls[0]["function"]) diff = DeepDiff(tool_call, expected_tool_call, ignore_string_case=True) assert not diff @@ -275,7 +297,7 @@ def test_prompt_gateway_param_tool_call(stream): tool_calls_message = arch_messages[0] tool_calls = tool_calls_message.get("tool_calls", []) assert len(tool_calls) > 0 - tool_call = tool_calls[0]["function"] + tool_call = normalize_tool_call_arguments(tool_calls[0]["function"]) diff = DeepDiff(tool_call, expected_tool_call, ignore_string_case=True) assert not diff diff --git a/tests/modelserver/.vscode/launch.json b/tests/modelserver/.vscode/launch.json deleted file mode 100644 index 6a211d8e..00000000 --- a/tests/modelserver/.vscode/launch.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "name": "Python Debugger: Current File", - "type": "debugpy", - "request": "launch", - "program": "${file}", - "console": "integratedTerminal" - } - ] -} diff --git a/tests/modelserver/.vscode/settings.json b/tests/modelserver/.vscode/settings.json deleted file mode 100644 index 98ba633e..00000000 --- a/tests/modelserver/.vscode/settings.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "python.testing.pytestArgs": [ - "." - ], - "python.testing.unittestEnabled": false, - "python.testing.pytestEnabled": true -} diff --git a/tests/modelserver/poetry.lock b/tests/modelserver/poetry.lock deleted file mode 100644 index d4a0640f..00000000 --- a/tests/modelserver/poetry.lock +++ /dev/null @@ -1,899 +0,0 @@ -# This file is automatically @generated by Poetry 1.8.5 and should not be changed by hand. - -[[package]] -name = "attrs" -version = "24.3.0" -description = "Classes Without Boilerplate" -optional = false -python-versions = ">=3.8" -files = [ - {file = "attrs-24.3.0-py3-none-any.whl", hash = "sha256:ac96cd038792094f438ad1f6ff80837353805ac950cd2aa0e0625ef19850c308"}, - {file = "attrs-24.3.0.tar.gz", hash = "sha256:8f5c07333d543103541ba7be0e2ce16eeee8130cb0b3f9238ab904ce1e85baff"}, -] - -[package.extras] -benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier (<24.7)"] -tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] - -[[package]] -name = "certifi" -version = "2024.12.14" -description = "Python package for providing Mozilla's CA Bundle." -optional = false -python-versions = ">=3.6" -files = [ - {file = "certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56"}, - {file = "certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db"}, -] - -[[package]] -name = "cffi" -version = "1.17.1" -description = "Foreign Function Interface for Python calling C code." -optional = false -python-versions = ">=3.8" -files = [ - {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, - {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, - {file = "cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382"}, - {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702"}, - {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3"}, - {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6"}, - {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17"}, - {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8"}, - {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e"}, - {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be"}, - {file = "cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c"}, - {file = "cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15"}, - {file = "cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401"}, - {file = "cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf"}, - {file = "cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4"}, - {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41"}, - {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1"}, - {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6"}, - {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d"}, - {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6"}, - {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f"}, - {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"}, - {file = "cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655"}, - {file = "cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0"}, - {file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"}, - {file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"}, - {file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"}, - {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"}, - {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"}, - {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"}, - {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"}, - {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"}, - {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"}, - {file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"}, - {file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"}, - {file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"}, - {file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"}, - {file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"}, - {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"}, - {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"}, - {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"}, - {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"}, - {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"}, - {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"}, - {file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"}, - {file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"}, - {file = "cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b"}, - {file = "cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964"}, - {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9"}, - {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc"}, - {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c"}, - {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1"}, - {file = "cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8"}, - {file = "cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1"}, - {file = "cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16"}, - {file = "cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36"}, - {file = "cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8"}, - {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576"}, - {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87"}, - {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0"}, - {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3"}, - {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595"}, - {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a"}, - {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e"}, - {file = "cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7"}, - {file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"}, - {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"}, -] - -[package.dependencies] -pycparser = "*" - -[[package]] -name = "charset-normalizer" -version = "3.4.1" -description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -optional = false -python-versions = ">=3.7" -files = [ - {file = "charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f30bf9fd9be89ecb2360c7d94a711f00c09b976258846efe40db3d05828e8089"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:97f68b8d6831127e4787ad15e6757232e14e12060bec17091b85eb1486b91d8d"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7974a0b5ecd505609e3b19742b60cee7aa2aa2fb3151bc917e6e2646d7667dcf"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc54db6c8593ef7d4b2a331b58653356cf04f67c960f584edb7c3d8c97e8f39e"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:311f30128d7d333eebd7896965bfcfbd0065f1716ec92bd5638d7748eb6f936a"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:7d053096f67cd1241601111b698f5cad775f97ab25d81567d3f59219b5f1adbd"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:807f52c1f798eef6cf26beb819eeb8819b1622ddfeef9d0977a8502d4db6d534"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:dccbe65bd2f7f7ec22c4ff99ed56faa1e9f785482b9bbd7c717e26fd723a1d1e"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:2fb9bd477fdea8684f78791a6de97a953c51831ee2981f8e4f583ff3b9d9687e"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:01732659ba9b5b873fc117534143e4feefecf3b2078b0a6a2e925271bb6f4cfa"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-win32.whl", hash = "sha256:7a4f97a081603d2050bfaffdefa5b02a9ec823f8348a572e39032caa8404a487"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:7b1bef6280950ee6c177b326508f86cad7ad4dff12454483b51d8b7d673a2c5d"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ecddf25bee22fe4fe3737a399d0d177d72bc22be6913acfab364b40bce1ba83c"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c60ca7339acd497a55b0ea5d506b2a2612afb2826560416f6894e8b5770d4a9"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b7b2d86dd06bfc2ade3312a83a5c364c7ec2e3498f8734282c6c3d4b07b346b8"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd78cfcda14a1ef52584dbb008f7ac81c1328c0f58184bf9a84c49c605002da6"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e27f48bcd0957c6d4cb9d6fa6b61d192d0b13d5ef563e5f2ae35feafc0d179c"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01ad647cdd609225c5350561d084b42ddf732f4eeefe6e678765636791e78b9a"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:619a609aa74ae43d90ed2e89bdd784765de0a25ca761b93e196d938b8fd1dbbd"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:89149166622f4db9b4b6a449256291dc87a99ee53151c74cbd82a53c8c2f6ccd"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:7709f51f5f7c853f0fb938bcd3bc59cdfdc5203635ffd18bf354f6967ea0f824"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:345b0426edd4e18138d6528aed636de7a9ed169b4aaf9d61a8c19e39d26838ca"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0907f11d019260cdc3f94fbdb23ff9125f6b5d1039b76003b5b0ac9d6a6c9d5b"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-win32.whl", hash = "sha256:ea0d8d539afa5eb2728aa1932a988a9a7af94f18582ffae4bc10b3fbdad0626e"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:329ce159e82018d646c7ac45b01a430369d526569ec08516081727a20e9e4af4"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-win32.whl", hash = "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765"}, - {file = "charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85"}, - {file = "charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3"}, -] - -[[package]] -name = "colorama" -version = "0.4.6" -description = "Cross-platform colored terminal text." -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -files = [ - {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, - {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, -] - -[[package]] -name = "coverage" -version = "7.6.10" -description = "Code coverage measurement for Python" -optional = false -python-versions = ">=3.9" -files = [ - {file = "coverage-7.6.10-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5c912978f7fbf47ef99cec50c4401340436d200d41d714c7a4766f377c5b7b78"}, - {file = "coverage-7.6.10-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a01ec4af7dfeb96ff0078ad9a48810bb0cc8abcb0115180c6013a6b26237626c"}, - {file = "coverage-7.6.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3b204c11e2b2d883946fe1d97f89403aa1811df28ce0447439178cc7463448a"}, - {file = "coverage-7.6.10-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32ee6d8491fcfc82652a37109f69dee9a830e9379166cb73c16d8dc5c2915165"}, - {file = "coverage-7.6.10-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675cefc4c06e3b4c876b85bfb7c59c5e2218167bbd4da5075cbe3b5790a28988"}, - {file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f4f620668dbc6f5e909a0946a877310fb3d57aea8198bde792aae369ee1c23b5"}, - {file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4eea95ef275de7abaef630c9b2c002ffbc01918b726a39f5a4353916ec72d2f3"}, - {file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e2f0280519e42b0a17550072861e0bc8a80a0870de260f9796157d3fca2733c5"}, - {file = "coverage-7.6.10-cp310-cp310-win32.whl", hash = "sha256:bc67deb76bc3717f22e765ab3e07ee9c7a5e26b9019ca19a3b063d9f4b874244"}, - {file = "coverage-7.6.10-cp310-cp310-win_amd64.whl", hash = "sha256:0f460286cb94036455e703c66988851d970fdfd8acc2a1122ab7f4f904e4029e"}, - {file = "coverage-7.6.10-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ea3c8f04b3e4af80e17bab607c386a830ffc2fb88a5484e1df756478cf70d1d3"}, - {file = "coverage-7.6.10-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:507a20fc863cae1d5720797761b42d2d87a04b3e5aeb682ef3b7332e90598f43"}, - {file = "coverage-7.6.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d37a84878285b903c0fe21ac8794c6dab58150e9359f1aaebbeddd6412d53132"}, - {file = "coverage-7.6.10-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a534738b47b0de1995f85f582d983d94031dffb48ab86c95bdf88dc62212142f"}, - {file = "coverage-7.6.10-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d7a2bf79378d8fb8afaa994f91bfd8215134f8631d27eba3e0e2c13546ce994"}, - {file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6713ba4b4ebc330f3def51df1d5d38fad60b66720948112f114968feb52d3f99"}, - {file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ab32947f481f7e8c763fa2c92fd9f44eeb143e7610c4ca9ecd6a36adab4081bd"}, - {file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7bbd8c8f1b115b892e34ba66a097b915d3871db7ce0e6b9901f462ff3a975377"}, - {file = "coverage-7.6.10-cp311-cp311-win32.whl", hash = "sha256:299e91b274c5c9cdb64cbdf1b3e4a8fe538a7a86acdd08fae52301b28ba297f8"}, - {file = "coverage-7.6.10-cp311-cp311-win_amd64.whl", hash = "sha256:489a01f94aa581dbd961f306e37d75d4ba16104bbfa2b0edb21d29b73be83609"}, - {file = "coverage-7.6.10-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:27c6e64726b307782fa5cbe531e7647aee385a29b2107cd87ba7c0105a5d3853"}, - {file = "coverage-7.6.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c56e097019e72c373bae32d946ecf9858fda841e48d82df7e81c63ac25554078"}, - {file = "coverage-7.6.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7827a5bc7bdb197b9e066cdf650b2887597ad124dd99777332776f7b7c7d0d0"}, - {file = "coverage-7.6.10-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:204a8238afe787323a8b47d8be4df89772d5c1e4651b9ffa808552bdf20e1d50"}, - {file = "coverage-7.6.10-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e67926f51821b8e9deb6426ff3164870976fe414d033ad90ea75e7ed0c2e5022"}, - {file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e78b270eadb5702938c3dbe9367f878249b5ef9a2fcc5360ac7bff694310d17b"}, - {file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:714f942b9c15c3a7a5fe6876ce30af831c2ad4ce902410b7466b662358c852c0"}, - {file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:abb02e2f5a3187b2ac4cd46b8ced85a0858230b577ccb2c62c81482ca7d18852"}, - {file = "coverage-7.6.10-cp312-cp312-win32.whl", hash = "sha256:55b201b97286cf61f5e76063f9e2a1d8d2972fc2fcfd2c1272530172fd28c359"}, - {file = "coverage-7.6.10-cp312-cp312-win_amd64.whl", hash = "sha256:e4ae5ac5e0d1e4edfc9b4b57b4cbecd5bc266a6915c500f358817a8496739247"}, - {file = "coverage-7.6.10-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:05fca8ba6a87aabdd2d30d0b6c838b50510b56cdcfc604d40760dae7153b73d9"}, - {file = "coverage-7.6.10-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9e80eba8801c386f72e0712a0453431259c45c3249f0009aff537a517b52942b"}, - {file = "coverage-7.6.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a372c89c939d57abe09e08c0578c1d212e7a678135d53aa16eec4430adc5e690"}, - {file = "coverage-7.6.10-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ec22b5e7fe7a0fa8509181c4aac1db48f3dd4d3a566131b313d1efc102892c18"}, - {file = "coverage-7.6.10-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26bcf5c4df41cad1b19c84af71c22cbc9ea9a547fc973f1f2cc9a290002c8b3c"}, - {file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e4630c26b6084c9b3cb53b15bd488f30ceb50b73c35c5ad7871b869cb7365fd"}, - {file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2396e8116db77789f819d2bc8a7e200232b7a282c66e0ae2d2cd84581a89757e"}, - {file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:79109c70cc0882e4d2d002fe69a24aa504dec0cc17169b3c7f41a1d341a73694"}, - {file = "coverage-7.6.10-cp313-cp313-win32.whl", hash = "sha256:9e1747bab246d6ff2c4f28b4d186b205adced9f7bd9dc362051cc37c4a0c7bd6"}, - {file = "coverage-7.6.10-cp313-cp313-win_amd64.whl", hash = "sha256:254f1a3b1eef5f7ed23ef265eaa89c65c8c5b6b257327c149db1ca9d4a35f25e"}, - {file = "coverage-7.6.10-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2ccf240eb719789cedbb9fd1338055de2761088202a9a0b73032857e53f612fe"}, - {file = "coverage-7.6.10-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0c807ca74d5a5e64427c8805de15b9ca140bba13572d6d74e262f46f50b13273"}, - {file = "coverage-7.6.10-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bcfa46d7709b5a7ffe089075799b902020b62e7ee56ebaed2f4bdac04c508d8"}, - {file = "coverage-7.6.10-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e0de1e902669dccbf80b0415fb6b43d27edca2fbd48c74da378923b05316098"}, - {file = "coverage-7.6.10-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7b444c42bbc533aaae6b5a2166fd1a797cdb5eb58ee51a92bee1eb94a1e1cb"}, - {file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b330368cb99ef72fcd2dc3ed260adf67b31499584dc8a20225e85bfe6f6cfed0"}, - {file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:9a7cfb50515f87f7ed30bc882f68812fd98bc2852957df69f3003d22a2aa0abf"}, - {file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f93531882a5f68c28090f901b1d135de61b56331bba82028489bc51bdd818d2"}, - {file = "coverage-7.6.10-cp313-cp313t-win32.whl", hash = "sha256:89d76815a26197c858f53c7f6a656686ec392b25991f9e409bcef020cd532312"}, - {file = "coverage-7.6.10-cp313-cp313t-win_amd64.whl", hash = "sha256:54a5f0f43950a36312155dae55c505a76cd7f2b12d26abeebbe7a0b36dbc868d"}, - {file = "coverage-7.6.10-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:656c82b8a0ead8bba147de9a89bda95064874c91a3ed43a00e687f23cc19d53a"}, - {file = "coverage-7.6.10-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ccc2b70a7ed475c68ceb548bf69cec1e27305c1c2606a5eb7c3afff56a1b3b27"}, - {file = "coverage-7.6.10-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5e37dc41d57ceba70956fa2fc5b63c26dba863c946ace9705f8eca99daecdc4"}, - {file = "coverage-7.6.10-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0aa9692b4fdd83a4647eeb7db46410ea1322b5ed94cd1715ef09d1d5922ba87f"}, - {file = "coverage-7.6.10-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa744da1820678b475e4ba3dfd994c321c5b13381d1041fe9c608620e6676e25"}, - {file = "coverage-7.6.10-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c0b1818063dc9e9d838c09e3a473c1422f517889436dd980f5d721899e66f315"}, - {file = "coverage-7.6.10-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:59af35558ba08b758aec4d56182b222976330ef8d2feacbb93964f576a7e7a90"}, - {file = "coverage-7.6.10-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7ed2f37cfce1ce101e6dffdfd1c99e729dd2ffc291d02d3e2d0af8b53d13840d"}, - {file = "coverage-7.6.10-cp39-cp39-win32.whl", hash = "sha256:4bcc276261505d82f0ad426870c3b12cb177752834a633e737ec5ee79bbdff18"}, - {file = "coverage-7.6.10-cp39-cp39-win_amd64.whl", hash = "sha256:457574f4599d2b00f7f637a0700a6422243b3565509457b2dbd3f50703e11f59"}, - {file = "coverage-7.6.10-pp39.pp310-none-any.whl", hash = "sha256:fd34e7b3405f0cc7ab03d54a334c17a9e802897580d964bd8c2001f4b9fd488f"}, - {file = "coverage-7.6.10.tar.gz", hash = "sha256:7fb105327c8f8f0682e29843e2ff96af9dcbe5bab8eeb4b398c6a33a16d80a23"}, -] - -[package.dependencies] -tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} - -[package.extras] -toml = ["tomli"] - -[[package]] -name = "deepdiff" -version = "8.1.1" -description = "Deep Difference and Search of any Python object/data. Recreate objects by adding adding deltas to each other." -optional = false -python-versions = ">=3.8" -files = [ - {file = "deepdiff-8.1.1-py3-none-any.whl", hash = "sha256:b0231fa3afb0f7184e82535f2b4a36636442ed21e94a0cf3aaa7982157e7ebca"}, - {file = "deepdiff-8.1.1.tar.gz", hash = "sha256:dd7bc7d5c8b51b5b90f01b0e2fe23c801fd8b4c6a7ee7e31c5a3c3663fcc7ceb"}, -] - -[package.dependencies] -orderly-set = ">=5.2.3,<6" - -[package.extras] -cli = ["click (==8.1.7)", "pyyaml (==6.0.2)"] -optimize = ["orjson"] - -[[package]] -name = "exceptiongroup" -version = "1.2.2" -description = "Backport of PEP 654 (exception groups)" -optional = false -python-versions = ">=3.7" -files = [ - {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, - {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, -] - -[package.extras] -test = ["pytest (>=6)"] - -[[package]] -name = "h11" -version = "0.14.0" -description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" -optional = false -python-versions = ">=3.7" -files = [ - {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, - {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, -] - -[[package]] -name = "idna" -version = "3.10" -description = "Internationalized Domain Names in Applications (IDNA)" -optional = false -python-versions = ">=3.6" -files = [ - {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, - {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, -] - -[package.extras] -all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] - -[[package]] -name = "iniconfig" -version = "2.0.0" -description = "brain-dead simple config-ini parsing" -optional = false -python-versions = ">=3.7" -files = [ - {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, - {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, -] - -[[package]] -name = "markupsafe" -version = "3.0.2" -description = "Safely add untrusted strings to HTML/XML markup." -optional = false -python-versions = ">=3.9" -files = [ - {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"}, - {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, -] - -[[package]] -name = "orderly-set" -version = "5.2.3" -description = "Orderly set" -optional = false -python-versions = ">=3.8" -files = [ - {file = "orderly_set-5.2.3-py3-none-any.whl", hash = "sha256:d357cedcf67f4ebff0d4cbd5b0997e98eeb65dd24fdf5c990a501ae9e82c7d34"}, - {file = "orderly_set-5.2.3.tar.gz", hash = "sha256:571ed97c5a5fca7ddeb6b2d26c19aca896b0ed91f334d9c109edd2f265fb3017"}, -] - -[[package]] -name = "outcome" -version = "1.3.0.post0" -description = "Capture the outcome of Python function calls." -optional = false -python-versions = ">=3.7" -files = [ - {file = "outcome-1.3.0.post0-py2.py3-none-any.whl", hash = "sha256:e771c5ce06d1415e356078d3bdd68523f284b4ce5419828922b6871e65eda82b"}, - {file = "outcome-1.3.0.post0.tar.gz", hash = "sha256:9dcf02e65f2971b80047b377468e72a268e15c0af3cf1238e6ff14f7f91143b8"}, -] - -[package.dependencies] -attrs = ">=19.2.0" - -[[package]] -name = "packaging" -version = "24.2" -description = "Core utilities for Python packages" -optional = false -python-versions = ">=3.8" -files = [ - {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, - {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, -] - -[[package]] -name = "pluggy" -version = "1.5.0" -description = "plugin and hook calling mechanisms for python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, - {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, -] - -[package.extras] -dev = ["pre-commit", "tox"] -testing = ["pytest", "pytest-benchmark"] - -[[package]] -name = "pycparser" -version = "2.22" -description = "C parser in Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, - {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, -] - -[[package]] -name = "pysocks" -version = "1.7.1" -description = "A Python SOCKS client module. See https://github.com/Anorov/PySocks for more information." -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -files = [ - {file = "PySocks-1.7.1-py27-none-any.whl", hash = "sha256:08e69f092cc6dbe92a0fdd16eeb9b9ffbc13cadfe5ca4c7bd92ffb078b293299"}, - {file = "PySocks-1.7.1-py3-none-any.whl", hash = "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5"}, - {file = "PySocks-1.7.1.tar.gz", hash = "sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0"}, -] - -[[package]] -name = "pytest" -version = "8.3.4" -description = "pytest: simple powerful testing with Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"}, - {file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "sys_platform == \"win32\""} -exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} -iniconfig = "*" -packaging = "*" -pluggy = ">=1.5,<2" -tomli = {version = ">=1", markers = "python_version < \"3.11\""} - -[package.extras] -dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] - -[[package]] -name = "pytest-cov" -version = "4.1.0" -description = "Pytest plugin for measuring coverage." -optional = false -python-versions = ">=3.7" -files = [ - {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, - {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, -] - -[package.dependencies] -coverage = {version = ">=5.2.1", extras = ["toml"]} -pytest = ">=4.6" - -[package.extras] -testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] - -[[package]] -name = "pytest-httpserver" -version = "1.1.0" -description = "pytest-httpserver is a httpserver for pytest" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pytest_httpserver-1.1.0-py3-none-any.whl", hash = "sha256:7ef88be8ed3354b6784daa3daa75a422370327c634053cefb124903fa8d73a41"}, - {file = "pytest_httpserver-1.1.0.tar.gz", hash = "sha256:6b1cb0199e2ed551b1b94d43f096863bbf6ae5bcd7c75c2c06845e5ce2dc8701"}, -] - -[package.dependencies] -Werkzeug = ">=2.0.0" - -[[package]] -name = "pytest-retry" -version = "1.6.3" -description = "Adds the ability to retry flaky tests in CI environments" -optional = false -python-versions = ">=3.9" -files = [ - {file = "pytest_retry-1.6.3-py3-none-any.whl", hash = "sha256:e96f7df77ee70b0838d1085f9c3b8b5b7d74bf8947a0baf32e2b8c71b27683c8"}, - {file = "pytest_retry-1.6.3.tar.gz", hash = "sha256:36ccfa11c8c8f9ddad5e20375182146d040c20c4a791745139c5a99ddf1b557d"}, -] - -[package.dependencies] -pytest = ">=7.0.0" - -[package.extras] -dev = ["black", "flake8", "isort", "mypy"] - -[[package]] -name = "pytest-sugar" -version = "1.0.0" -description = "pytest-sugar is a plugin for pytest that changes the default look and feel of pytest (e.g. progressbar, show tests that fail instantly)." -optional = false -python-versions = "*" -files = [ - {file = "pytest-sugar-1.0.0.tar.gz", hash = "sha256:6422e83258f5b0c04ce7c632176c7732cab5fdb909cb39cca5c9139f81276c0a"}, - {file = "pytest_sugar-1.0.0-py3-none-any.whl", hash = "sha256:70ebcd8fc5795dc457ff8b69d266a4e2e8a74ae0c3edc749381c64b5246c8dfd"}, -] - -[package.dependencies] -packaging = ">=21.3" -pytest = ">=6.2.0" -termcolor = ">=2.1.0" - -[package.extras] -dev = ["black", "flake8", "pre-commit"] - -[[package]] -name = "pyyaml" -version = "6.0.2" -description = "YAML parser and emitter for Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, - {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, - {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, - {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, - {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, - {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, - {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, - {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, - {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, - {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, - {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, - {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, - {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, - {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, - {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, - {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, - {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, - {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, - {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, - {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, - {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, - {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, - {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, - {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, - {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, - {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, - {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, - {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, - {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, - {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, - {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, - {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, - {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, - {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, - {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, - {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, - {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, - {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, - {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, - {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, - {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, - {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, - {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, - {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, - {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, - {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, - {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, - {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, - {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, - {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, - {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, - {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, - {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, -] - -[[package]] -name = "requests" -version = "2.32.3" -description = "Python HTTP for Humans." -optional = false -python-versions = ">=3.8" -files = [ - {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, - {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, -] - -[package.dependencies] -certifi = ">=2017.4.17" -charset-normalizer = ">=2,<4" -idna = ">=2.5,<4" -urllib3 = ">=1.21.1,<3" - -[package.extras] -socks = ["PySocks (>=1.5.6,!=1.5.7)"] -use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] - -[[package]] -name = "selenium" -version = "4.27.1" -description = "Official Python bindings for Selenium WebDriver" -optional = false -python-versions = ">=3.8" -files = [ - {file = "selenium-4.27.1-py3-none-any.whl", hash = "sha256:b89b1f62b5cfe8025868556fe82360d6b649d464f75d2655cb966c8f8447ea18"}, - {file = "selenium-4.27.1.tar.gz", hash = "sha256:5296c425a75ff1b44d0d5199042b36a6d1ef76c04fb775b97b40be739a9caae2"}, -] - -[package.dependencies] -certifi = ">=2021.10.8" -trio = ">=0.17,<1.0" -trio-websocket = ">=0.9,<1.0" -typing_extensions = ">=4.9,<5.0" -urllib3 = {version = ">=1.26,<3", extras = ["socks"]} -websocket-client = ">=1.8,<2.0" - -[[package]] -name = "sniffio" -version = "1.3.1" -description = "Sniff out which async library your code is running under" -optional = false -python-versions = ">=3.7" -files = [ - {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, - {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, -] - -[[package]] -name = "sortedcontainers" -version = "2.4.0" -description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set" -optional = false -python-versions = "*" -files = [ - {file = "sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"}, - {file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"}, -] - -[[package]] -name = "termcolor" -version = "2.5.0" -description = "ANSI color formatting for output in terminal" -optional = false -python-versions = ">=3.9" -files = [ - {file = "termcolor-2.5.0-py3-none-any.whl", hash = "sha256:37b17b5fc1e604945c2642c872a3764b5d547a48009871aea3edd3afa180afb8"}, - {file = "termcolor-2.5.0.tar.gz", hash = "sha256:998d8d27da6d48442e8e1f016119076b690d962507531df4890fcd2db2ef8a6f"}, -] - -[package.extras] -tests = ["pytest", "pytest-cov"] - -[[package]] -name = "tomli" -version = "2.2.1" -description = "A lil' TOML parser" -optional = false -python-versions = ">=3.8" -files = [ - {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, - {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, - {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, - {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, - {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, - {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, - {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, - {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, - {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, - {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, - {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, - {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, - {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, - {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, - {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, - {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, - {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, - {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, - {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, - {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, - {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, - {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, - {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, - {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, - {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, - {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, - {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, - {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, - {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, - {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, - {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, - {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, -] - -[[package]] -name = "trio" -version = "0.28.0" -description = "A friendly Python library for async concurrency and I/O" -optional = false -python-versions = ">=3.9" -files = [ - {file = "trio-0.28.0-py3-none-any.whl", hash = "sha256:56d58977acc1635735a96581ec70513cc781b8b6decd299c487d3be2a721cd94"}, - {file = "trio-0.28.0.tar.gz", hash = "sha256:4e547896fe9e8a5658e54e4c7c5fa1db748cbbbaa7c965e7d40505b928c73c05"}, -] - -[package.dependencies] -attrs = ">=23.2.0" -cffi = {version = ">=1.14", markers = "os_name == \"nt\" and implementation_name != \"pypy\""} -exceptiongroup = {version = "*", markers = "python_version < \"3.11\""} -idna = "*" -outcome = "*" -sniffio = ">=1.3.0" -sortedcontainers = "*" - -[[package]] -name = "trio-websocket" -version = "0.11.1" -description = "WebSocket library for Trio" -optional = false -python-versions = ">=3.7" -files = [ - {file = "trio-websocket-0.11.1.tar.gz", hash = "sha256:18c11793647703c158b1f6e62de638acada927344d534e3c7628eedcb746839f"}, - {file = "trio_websocket-0.11.1-py3-none-any.whl", hash = "sha256:520d046b0d030cf970b8b2b2e00c4c2245b3807853ecd44214acd33d74581638"}, -] - -[package.dependencies] -exceptiongroup = {version = "*", markers = "python_version < \"3.11\""} -trio = ">=0.11" -wsproto = ">=0.14" - -[[package]] -name = "typing-extensions" -version = "4.12.2" -description = "Backported and Experimental Type Hints for Python 3.8+" -optional = false -python-versions = ">=3.8" -files = [ - {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, - {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, -] - -[[package]] -name = "urllib3" -version = "2.3.0" -description = "HTTP library with thread-safe connection pooling, file post, and more." -optional = false -python-versions = ">=3.9" -files = [ - {file = "urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df"}, - {file = "urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d"}, -] - -[package.dependencies] -pysocks = {version = ">=1.5.6,<1.5.7 || >1.5.7,<2.0", optional = true, markers = "extra == \"socks\""} - -[package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] -h2 = ["h2 (>=4,<5)"] -socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] -zstd = ["zstandard (>=0.18.0)"] - -[[package]] -name = "websocket-client" -version = "1.8.0" -description = "WebSocket client for Python with low level API options" -optional = false -python-versions = ">=3.8" -files = [ - {file = "websocket_client-1.8.0-py3-none-any.whl", hash = "sha256:17b44cc997f5c498e809b22cdf2d9c7a9e71c02c8cc2b6c56e7c2d1239bfa526"}, - {file = "websocket_client-1.8.0.tar.gz", hash = "sha256:3239df9f44da632f96012472805d40a23281a991027ce11d2f45a6f24ac4c3da"}, -] - -[package.extras] -docs = ["Sphinx (>=6.0)", "myst-parser (>=2.0.0)", "sphinx-rtd-theme (>=1.1.0)"] -optional = ["python-socks", "wsaccel"] -test = ["websockets"] - -[[package]] -name = "werkzeug" -version = "3.1.3" -description = "The comprehensive WSGI web application library." -optional = false -python-versions = ">=3.9" -files = [ - {file = "werkzeug-3.1.3-py3-none-any.whl", hash = "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e"}, - {file = "werkzeug-3.1.3.tar.gz", hash = "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746"}, -] - -[package.dependencies] -MarkupSafe = ">=2.1.1" - -[package.extras] -watchdog = ["watchdog (>=2.3)"] - -[[package]] -name = "wsproto" -version = "1.2.0" -description = "WebSockets state-machine based protocol implementation" -optional = false -python-versions = ">=3.7.0" -files = [ - {file = "wsproto-1.2.0-py3-none-any.whl", hash = "sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736"}, - {file = "wsproto-1.2.0.tar.gz", hash = "sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065"}, -] - -[package.dependencies] -h11 = ">=0.9.0,<1" - -[metadata] -lock-version = "2.0" -python-versions = "^3.10" -content-hash = "d0ba3f1875425991d473c9b6fee9f9cb35de5df5835fd7f7e27e3863ff7d37fe" diff --git a/tests/modelserver/pyproject.toml b/tests/modelserver/pyproject.toml deleted file mode 100644 index af13bfd5..00000000 --- a/tests/modelserver/pyproject.toml +++ /dev/null @@ -1,29 +0,0 @@ -[tool.poetry] -name = "modelserver_mock_tests" -version = "0.0.1" -description = "modelserver tests" -authors = ["Katanemo Labs, Inc "] -license = "Apache 2.0" -readme = "README.md" -package-mode = false - -[tool.poetry.dependencies] -python = "^3.10" -pytest = "^8.3.3" -requests = "^2.29.0" -selenium = "^4.11.2" -pytest-sugar = "^1.0.0" -deepdiff = "^8.0.1" -pytest-retry = "^1.6.3" -pytest-httpserver = "^1.1.0" -pyyaml = "*" - -[tool.poetry.dev-dependencies] -pytest-cov = "^4.1.0" - -[tool.pytest.ini_options] -python_files = ["test*.py"] -addopts = ["-v", "-s"] -retries = 2 -retry_delay = 0.5 -cumulative_timing = false diff --git a/tests/modelserver/test_hallucination.py b/tests/modelserver/test_hallucination.py deleted file mode 100644 index 323db3fc..00000000 --- a/tests/modelserver/test_hallucination.py +++ /dev/null @@ -1,44 +0,0 @@ -import os -import pytest -import requests -import logging -import yaml - -pytestmark = pytest.mark.skip( - reason="Skipping entire test file as hallucination is not enabled for archfc 1.1 yet" -) - -MODEL_SERVER_ENDPOINT = os.getenv( - "MODEL_SERVER_ENDPOINT", "http://localhost:51000/function_calling" -) - -# Load test data from YAML file -script_dir = os.path.dirname(__file__) - -# Construct the full path to the YAML file -yaml_file_path = os.path.join(script_dir, "test_hallucination_data.yaml") - -# Load test data from YAML file -with open(yaml_file_path, "r") as file: - test_data_yaml = yaml.safe_load(file) - - -@pytest.mark.parametrize( - "test_data", - [ - pytest.param(test_case, id=test_case["id"]) - for test_case in test_data_yaml["test_cases"] - ], -) -def test_model_server(test_data): - input = test_data["input"] - expected = test_data["expected"] - - response = requests.post(MODEL_SERVER_ENDPOINT, json=input) - assert response.status_code == 200 - assert response.headers["content-type"] == "application/json" - - response_json = response.json() - assert response_json - metadata = response_json.get("metadata", {}) - assert (metadata["hallucination"].lower() == "true") == expected[0]["hallucination"] diff --git a/tests/modelserver/test_hallucination_data.yaml b/tests/modelserver/test_hallucination_data.yaml deleted file mode 100644 index 935a8f5f..00000000 --- a/tests/modelserver/test_hallucination_data.yaml +++ /dev/null @@ -1,257 +0,0 @@ -test_cases: - - id: "[WEATHER AGENT] - single turn, single tool, prompt prefilling" - input: - messages: - - role: "user" - content: "what is the weather forecast for seattle?" - tools: - - type: "function" - function: - name: "get_current_weather" - description: "Get current weather at a location." - parameters: - type: "object" - properties: - location: - type: "string" - description: "The location to get the weather for" - format: "City, State" - days: - type: "integer" - description: "The number of days for the request." - required: - - location - - days - expected: - - type: "metadata" - hallucination: false - - - id: "[WEATHER AGENT] - single turn, single tool, hallucination" - input: - messages: - - role: "user" - content: "what is the weather in Seattle in days?" - tools: - - type: "function" - function: - name: "get_current_weather" - description: "Get current weather at a location." - parameters: - type: "object" - properties: - location: - type: "str" - description: "The location to get the weather for" - format: "City, State" - days: - type: "int" - description: "the number of days for the request." - required: ["location", "days"] - expected: - - type: "metadata" - hallucination: true - - - id: "[WEATHER AGENT] - multi turn, single tool, all params passed" - input: - messages: - - role: "user" - content: "how is the weather in chicago for next 5 days?" - - role: "assistant" - content: "Can you tell me your location and how many days you want?" - - role: "user" - content: "Seattle" - - role: "assistant" - content: "Can you please provide me the days for the weather forecast?" - - role: "user" - content: "5 days" - tools: - - type: "function" - function: - name: "get_current_weather" - description: "Get current weather at a location." - parameters: - type: "object" - properties: - location: - type: "str" - description: "The location to get the weather for" - format: "City, State" - days: - type: "int" - description: "the number of days for the request." - required: ["location", "days"] - expected: - - type: "metadata" - hallucination: false - - - id: "[WEATHER AGENT] - multi turn, single tool, clarification" - input: - messages: - - role: "user" - content: "how is the weather for next 5 days?" - - role: "assistant" - content: "Can you tell me your location and how many days you want?" - - role: "user" - content: "Seattle" - - role: "assistant" - content: "Can you please provide me the days for the weather forecast?" - - role: "user" - content: "Sorry, the location is actually los angeles in 5 days" - tools: - - type: "function" - function: - name: "get_current_weather" - description: "Get current weather at a location." - parameters: - type: "object" - properties: - location: - type: "str" - description: "The location to get the weather for" - format: "City, State" - days: - type: "int" - description: "the number of days for the request." - required: ["location", "days"] - expected: - - type: "metadata" - hallucination: false - - - id: "[SALE AGENT] - single turn, single tool, hallucination region" - input: - messages: - - role: "user" - content: "get me sales opportunities of tech" - tools: - - type: "function" - function: - name: "sales_opportunity" - description: "Retrieve potential sales opportunities based for a particular industry type in a region." - parameters: - type: "object" - properties: - region: - type: "str" - description: "Geographical region to identify sales opportunities." - industry: - type: "str" - description: "Industry type." - max_results: - type: "int" - description: "Maximum number of sales opportunities to retrieve." - default: 20 - required: ["region", "industry"] - expected: - - type: "metadata" - hallucination: true - - - id: "[SALE AGENT] - single turn, single tool, hallucination industry" - input: - messages: - - role: "user" - content: "get me sales opportunities in NA" - tools: - - type: "function" - function: - name: "sales_opportunity" - description: "Retrieve potential sales opportunities based for a particular industry type in a region." - parameters: - type: "object" - properties: - region: - type: "str" - description: "Geographical region to identify sales opportunities." - industry: - type: "str" - description: "Industry type." - max_results: - type: "int" - description: "Maximum number of sales opportunities to retrieve." - default: 20 - required: ["region", "industry"] - expected: - - type: "metadata" - hallucination: true - - - id: "[PRODUCT AGENT] - single turn, single tool, hallucination industry" - input: - messages: - - role: "user" - content: "get me sales opportunities in NA" - tools: - - type: "function" - function: - name: "product_recommendation" - description: "Place an order for an iphone with user_id 195 and location is 1600 pensylvania ave" - parameters: - type: "object" - properties: - user_id: - type: "str" - description: "Unique identifier for the user." - category: - type: "str" - description: "Product category for recommendations." - max_results: - type: "int" - description: "Maximum number of recommended products to show." - default: 10 - required: ["user_id", "category"] - - type: "function" - function: - name: "place_order" - description: "Place and pay for an order for one or more products to ship to the an address." - parameters: - type: "object" - properties: - user_id: - type: "str" - description: "Unique identifier for the user placing the order." - product_ids: - type: "array" - description: "List of product IDs to include in the order." - shipping_address: - type: "str" - description: "Shipping address for the order." - payment_method: - type: "str" - description: "Payment method for the order." - required: ["user_id", "product_ids", "shipping_address", "payment_method"] - - type: "function" - function: - name: "sales_opportunity" - description: "Retrieve potential sales opportunities based for a particular industry type in a region." - parameters: - type: "object" - properties: - region: - type: "str" - description: "Geographical region to identify sales opportunities." - industry: - type: "str" - description: "Industry type." - max_results: - type: "int" - description: "Maximum number of sales opportunities to retrieve." - default: 20 - required: ["region", "industry"] - - type: "function" - function: - name: "query_database" - description: "Perform a database query to retrieve or update information." - parameters: - type: "object" - properties: - query: - type: "str" - description: "SQL query string to execute against the database." - parameters: - type: "array" - description: "List of parameters to safely inject into the SQL query (to prevent SQL injection)." - operation: - type: "str" - description: "Type of operation." - required: ["query", "operation"] - expected: - - type: "metadata" - hallucination: true diff --git a/tests/modelserver/test_modelserver.py b/tests/modelserver/test_modelserver.py deleted file mode 100644 index 4596606f..00000000 --- a/tests/modelserver/test_modelserver.py +++ /dev/null @@ -1,58 +0,0 @@ -import os -import pytest -import requests -import yaml - -from deepdiff import DeepDiff - -pytestmark = pytest.mark.skip( - reason="Skipping entire test file as this these tests are heavily dependent on model output" -) - -MODEL_SERVER_ENDPOINT = os.getenv( - "MODEL_SERVER_ENDPOINT", "http://localhost:51000/function_calling" -) - -# Load test data from YAML file -script_dir = os.path.dirname(__file__) - -# Construct the full path to the YAML file -yaml_file_path = os.path.join(script_dir, "test_success_data.yaml") - -# Load test data from YAML file -with open(yaml_file_path, "r") as file: - test_data_yaml = yaml.safe_load(file) - - -@pytest.mark.parametrize( - "test_data", - [ - pytest.param(test_case, id=test_case["id"]) - for test_case in test_data_yaml["test_cases"] - ], -) -def test_model_server(test_data): - input = test_data["input"] - expected = test_data["expected"] - - response = requests.post(MODEL_SERVER_ENDPOINT, json=input) - assert response.status_code == 200 - # ensure that response is json - assert response.headers["content-type"] == "application/json" - response_json = response.json() - assert response_json - choices = response_json.get("choices", []) - assert len(choices) == 1 - choice = choices[0] - assert "message" in choice - message = choice["message"] - assert "tool_calls" in message - tool_calls = message["tool_calls"] - assert len(tool_calls) == len(expected) - - for tool_call, expected_tool_call in zip(tool_calls, expected): - assert "id" in tool_call - del tool_call["id"] - # ensure that the tool call matches the expected tool call - diff = DeepDiff(expected_tool_call, tool_call, ignore_string_case=True) - assert not diff diff --git a/tests/modelserver/test_success_data.yaml b/tests/modelserver/test_success_data.yaml deleted file mode 100644 index 36e3dcfe..00000000 --- a/tests/modelserver/test_success_data.yaml +++ /dev/null @@ -1,561 +0,0 @@ -test_cases: - - id: "[WEATHER AGENT] - single turn, single tool, all parameters" - input: - messages: - - role: "user" - content: "what is the weather forecast for Seattle, WA in the next 10 days?" - tools: - - type: "function" - function: - name: "get_current_weather" - description: "Get current weather at a location." - parameters: - type: "object" - properties: - location: - type: "str" - description: "The location to get the weather for" - format: "City, State" - days: - type: "int" - description: "the number of days for the request." - required: ["location", "days"] - expected: - - type: "function" - function: - name: "get_current_weather" - arguments: - location: "Seattle, WA" - days: 10 - - - id: "[WEATHER AGENT] - single turn, single tool, param gathering" - input: - messages: - - role: "user" - content: "what is the weather in Seattle?" - - role: "assistant" - content: "May I know the location and number of days you want to get the weather for?" - model: "Arch-Function" - - role: "user" - content: "5 days" - tools: - - type: "function" - function: - name: "get_current_weather" - description: "Get current weather at a location." - parameters: - type: "object" - properties: - location: - type: "str" - description: "The location to get the weather for" - format: "City, State" - days: - type: "int" - description: "the number of days for the request." - required: ["location", "days"] - expected: - - type: "function" - function: - name: "get_current_weather" - arguments: - location: "Seattle, WA" - days: 5 - - - id: "[WEATHER AGENT] - multi turn, single tool, all params passed" - input: - messages: - - role: "user" - content: "how is the weather in chicago for next 5 days?" - - role: "assistant" - tool_calls: - - id: "call_3394" - type: "function" - function: - name: "get_current_weather" - arguments: - location: "Chicago, IL" - days: 5 - - role: "tool" - content: "--" - tool_call_id: "call_3394" - - role: "assistant" - content: "--" - - role: "user" - content: "how is the weather in LA for next 5 days?" - tools: - - type: "function" - function: - name: "get_current_weather" - description: "Get current weather at a location." - parameters: - type: "object" - properties: - location: - type: "str" - description: "The location to get the weather for" - format: "City, State" - days: - type: "int" - description: "the number of days for the request." - required: ["location", "days"] - expected: - - type: "function" - function: - name: "get_current_weather" - arguments: - location: "Los Angeles, CA" - days: 5 - - # Skip! - # - id: "[WEATHER AGENT] - multi turn, single tool, infer param from context" - # input: - # messages: - # - role: "user" - # content: "how is the weather in chicago for next 5 days?" - # - role: "assistant" - # tool_calls: - # - id: "call_3394" - # type: "function" - # function: - # name: "get_current_weather" - # arguments: - # location: "Chicago, IL" - # days: 5 - # - role: "tool" - # content: "--" - # tool_call_id: "call_3394" - # - role: "assistant" - # content: "--" - # - role: "user" - # content: "how is the weather in LA?" - # tools: - # - type: "function" - # function: - # name: "get_current_weather" - # description: "Get current weather at a location." - # parameters: - # type: "object" - # properties: - # location: - # type: "str" - # description: "The location to get the weather for" - # format: "City, State" - # days: - # type: "int" - # description: "the number of days for the request." - # required: ["location", "days"] - # expected: - # - type: "function" - # function: - # name: "get_current_weather" - # arguments: - # location: "Los Angeles, CA" - # days: 5 - - - id: "[WEATHER AGENT] - multi turn, single tool, infer param from context 2nd try" - input: - messages: - - role: "user" - content: "how is the weather in seattle for 5 days?" - tool_call_id: "" - - role: "assistant" - content: "" - tool_call_id: "" - tool_calls: - - id: "call_7134" - type: "function" - function: - name: "get_current_weather" - arguments: - location: "Seattle, WA" - days: 5 - - role: "tool" - content: "{\"location\":\"Seattle, WA\",\"temperature\":[{\"date\":\"2024-12-19\",\"temperature\":{\"min\":74,\"max\":90},\"units\":\"Farenheit\",\"query_time\":\"2024-12-19 00:14:35.853372+00:00\"},{\"date\":\"2024-12-20\",\"temperature\":{\"min\":79,\"max\":88},\"units\":\"Farenheit\",\"query_time\":\"2024-12-19 00:14:35.853402+00:00\"}],\"units\":\"Farenheit\"}" - tool_call_id: "" - - role: "assistant" - content: "The weather in Seattle for the next two days is as follows:\n\n- **December 19, 2024**: The temperature will range from a minimum of 74°F to a maximum of 90°F.\n- **December 20, 2024**: The temperature will range from a minimum of 79°F to a maximum of 88°F.\n\nIt seems to be quite warm for Seattle during these dates!" - tool_call_id: "" - - role: "user" - content: "what about weather in chicago?" - tool_call_id: "" - tools: - - type: "function" - function: - name: "get_current_weather" - description: "Get current weather at a location." - parameters: - properties: - days: - type: "int" - description: "the number of days for the request" - location: - type: "str" - description: "The location to get the weather for" - format: "city, state" - required: ["days", "location"] - - type: "function" - function: - name: "default_target" - description: "This is the default target for all unmatched prompts." - parameters: - properties: {} - expected: - - type: "function" - function: - name: "get_current_weather" - arguments: - location: "Chicago, IL" - days: 5 - - id: "[HR AGENT] - single turn, single tool, all parameters" - input: - messages: - - role: "user" - content: "Can you show the workforce data for agency staff in america?" - tools: - - type: "function" - function: - name: "get_hr_data" - description: "Get workforce data like headcount and satisfacton levels by region and staffing type." - parameters: - type: "object" - properties: - staffing_type: - type: "str" - description: "Staffing type of employees" - region: - type: "str" - description: "Geographical region for which you want workforce data." - enum: ["america", "emea", "apac"] - point_in_time: - type: "str" - description: "the point in time for which to retrieve data." - default: "1" - required: ["staffing_type", "region"] - expected: - - type: "function" - function: - name: "get_hr_data" - arguments: - region: "america" - staffing_type: "agency" - - id: "[HR AGENT] - multi turn, single tool, all parameters, enum" - input: - messages: - - role: "user" - content: "Can you show the workforce data for agency staff?" - - role: "assistant" - content: "Of course, I can help with that. However, I need the region and staffing type to provide the workforce data. Could you please provide that information?" - - role: "user" - content: "ameriza" - tools: - - type: "function" - function: - name: "get_hr_data" - description: "Get workforce data like headcount and satisfacton levels by region and staffing type." - parameters: - type: "object" - properties: - staffing_type: - type: "str" - description: "Staffing type of employees" - region: - type: "str" - description: "Geographical region for which you want workforce data." - enum: ["america", "emea", "apac"] - point_in_time: - type: "str" - description: "the point in time for which to retrieve data." - default: "1" - required: ["staffing_type", "region"] - expected: - - type: "function" - function: - name: "get_hr_data" - arguments: - region: "america" - staffing_type: "agency" - - id: "[HR AGENT] - multi turn, multi tool, all parameters, enum" - input: - messages: - - role: "user" - content: "Can you show the workforce data for agency staff?" - - role: "assistant" - content: "Of course, I can help with that. However, I need the region and staffing type to provide the workforce data. Could you please provide that information?" - - role: "user" - content: "america. Also, please get the satisfaction levels for the full_time staff in emea" - tools: - - type: "function" - function: - name: "get_hr_data" - description: "Get workforce data like headcount and satisfacton levels by region and staffing type." - parameters: - type: "object" - properties: - staffing_type: - type: "str" - description: "Staffing type of employees" - region: - type: "str" - description: "Geographical region for which you want workforce data." - enum: ["america", "emea", "apac"] - point_in_time: - type: "str" - description: "the point in time for which to retrieve data." - default: "1" - required: ["staffing_type", "region"] - expected: - - type: "function" - function: - name: "get_hr_data" - arguments: - region: "america" - staffing_type: "agency" - - type: "function" - function: - name: "get_hr_data" - arguments: - region: "emea" - staffing_type: "full_time" - - id: "[INSURANCE AGENT] - single turn, multi tool, all parameters" - input: - messages: - - role: "user" - content: " i want to start an insurance policy with 500 deductible for car and update deductible my boat insurance policy with id boawd123 to 1000" - tools: - - type: "function" - function: - name: "policy_qa" - description: "Handle general Q/A related to insurance." - parameters: - type: "object" - properties: {} - required: [] - - type: "function" - function: - name: "get_policy_coverage" - description: "Retrieve the coverage details for an insurance policy." - parameters: - type: "object" - properties: - policy_type: - type: "str" - description: "The type of insurance policy." - required: ["policy_type"] - - type: "function" - function: - name: "initiate_policy" - description: "Start a policy coverage for an insurance policy." - parameters: - type: "object" - properties: - policy_type: - type: "str" - description: "The type of insurance policy." - deductible: - type: "float" - description: "The deductible amount set for the policy." - required: ["policy_type", "deductible"] - - type: "function" - function: - name: "update_claim" - description: "Update the notes on the claim." - parameters: - type: "object" - properties: - claim_id: - type: "str" - description: "The claim number." - notes: - type: "str" - description: "Notes about the claim number for your adjustor to see." - required: ["claim_id"] - - type: "function" - function: - name: "update_deductible" - description: "Update the deductible amount for a specific insurance policy coverage." - parameters: - type: "object" - properties: - policy_id: - type: "str" - description: "The ID of the insurance policy." - deductible: - type: "float" - description: "The deductible amount set for the policy." - required: ["policy_id", "deductible"] - expected: - - type: "function" - function: - name: "initiate_policy" - arguments: - policy_type: "car" - deductible: 500 - - type: "function" - function: - name: "update_deductible" - arguments: - policy_id: "boawd123" - deductible: 1000 - - id: "[INSURANCE AGENT] - multi turn, multi tool, all parameters" - input: - messages: - - role: "user" - content: "hi what can you do?" - - role: "assistant" - content: "Certainly! I'm here to assist you with various questions and tasks. Whether it's answering specific questions, providing information, or helping with something else, feel free to let me know how I can assist you." - - role: "user" - content: "i want to start a new insurance policy" - - role: "assistant" - content: "Certainly! To start a new insurance policy, I'll need the type of insurance policy you're interested in and the deductible amount you'd like to set for that policy. Could you please provide this information?" - - role: "user" - content: "car insurance, 500. Also, please get me the coverage details for a house insurance" - tools: - - type: "function" - function: - name: "policy_qa" - description: "Handle general Q/A related to insurance." - parameters: - type: "object" - properties: {} - required: [] - - type: "function" - function: - name: "get_policy_coverage" - description: "Retrieve the coverage details for an insurance policy." - parameters: - type: "object" - properties: - policy_type: - type: "str" - description: "The type of insurance policy." - required: ["policy_type"] - - type: "function" - function: - name: "initiate_policy" - description: "Start a policy coverage for an insurance policy." - parameters: - type: "object" - properties: - policy_type: - type: "str" - description: "The type of insurance policy." - deductible: - type: "float" - description: "The deductible amount set for the policy." - required: ["policy_type", "deductible"] - - type: "function" - function: - name: "update_claim" - description: "Update the notes on the claim." - parameters: - type: "object" - properties: - claim_id: - type: "str" - description: "The claim number." - notes: - type: "str" - description: "Notes about the claim number for your adjustor to see." - required: ["claim_id"] - - type: "function" - function: - name: "update_deductible" - description: "Update the deductible amount for a specific insurance policy coverage." - parameters: - type: "object" - properties: - policy_id: - type: "str" - description: "The ID of the insurance policy." - deductible: - type: "float" - description: "The deductible amount set for the policy." - required: ["policy_id", "deductible"] - expected: - - type: "function" - function: - name: "initiate_policy" - arguments: - policy_type: "car insurance" - deductible: 500 - - type: "function" - function: - name: "get_policy_coverage" - arguments: - policy_type: "house insurance" - - - id: "[INSURANCE AGENT] - single turn, single tool, all parameters" - input: - messages: - - role: "user" - content: "i want to start a insurance policy for car with 500 deductible" - tools: - - type: "function" - function: - name: "policy_qa" - description: "Handle general Q/A related to insurance." - parameters: - type: "object" - properties: {} - required: [] - - type: "function" - function: - name: "get_policy_coverage" - description: "Retrieve the coverage details for an insurance policy." - parameters: - type: "object" - properties: - policy_type: - type: "str" - description: "The type of insurance policy." - required: ["policy_type"] - - type: "function" - function: - name: "initiate_policy" - description: "Start a policy coverage for an insurance policy." - parameters: - type: "object" - properties: - policy_type: - type: "str" - description: "The type of insurance policy." - deductible: - type: "float" - description: "The deductible amount set for the policy." - required: ["policy_type", "deductible"] - - type: "function" - function: - name: "update_claim" - description: "Update the notes on the claim." - parameters: - type: "object" - properties: - claim_id: - type: "str" - description: "The claim number." - notes: - type: "str" - description: "Notes about the claim number for your adjustor to see." - required: ["claim_id"] - - type: "function" - function: - name: "update_deductible" - description: "Update the deductible amount for a specific insurance policy coverage." - parameters: - type: "object" - properties: - policy_id: - type: "str" - description: "The ID of the insurance policy." - deductible: - type: "float" - description: "The deductible amount set for the policy." - required: ["policy_id", "deductible"] - expected: - - type: "function" - function: - name: "initiate_policy" - arguments: - policy_type: "car" - deductible: 500 diff --git a/tests/rest/api_model_server.rest b/tests/rest/api_model_server.rest index 5fdbf968..9c094c19 100644 --- a/tests/rest/api_model_server.rest +++ b/tests/rest/api_model_server.rest @@ -1,4 +1,4 @@ -@model_server_endpoint = http://localhost:51000 +@model_server_endpoint = http://localhost:12000 @archfc_endpoint = https://archfc.katanemo.dev ### talk to function calling endpoint diff --git a/tests/rest/insurance_agent.rest b/tests/rest/insurance_agent.rest index c45ebb85..f5a86f8f 100644 --- a/tests/rest/insurance_agent.rest +++ b/tests/rest/insurance_agent.rest @@ -1,4 +1,4 @@ -@model_server_endpoint = http://localhost:51000 +@model_server_endpoint = http://localhost:12000 @archfc_endpoint = https://archfc.katanemo.dev ### multi turn conversation with intent, except parameter gathering @@ -54,26 +54,8 @@ Content-Type: application/json } ] } -### talk to Arch-Intent directly for completion -POST https://archfc.katanemo.dev/v1/chat/completions HTTP/1.1 -Content-Type: application/json - -{ - "model": "Arch-Intent", - "messages": [ - { - "role": "system", - "content": "You are a helpful assistant.\n\nYou task is to check if there are any tools that can be used to help the last user message in conversations according to the available tools listed below.\n\n\n{\"index\": \"T0\", \"type\": \"function\", \"function\": {\"name\": \"weather_forecast\", \"parameters\": {\"type\": \"object\", \"properties\": {\"city\": {\"type\": \"str\"}, \"days\": {\"type\": \"int\"}}, \"required\": [\"city\", \"days\"]}}}\n\n\nProvide your tool assessment for ONLY THE LAST USER MESSAGE in the above conversation:\n- First line must read 'Yes' or 'No'.\n- If yes, a second line must include a comma-separated list of tool indexes.\n" - }, - { "role": "user", "content": "hi" } - ], - "stream": false -} - - - -### multi turn conversation with correct parameters +### multi turn conversation with intent, except parameter gathering POST {{model_server_endpoint}}/function_calling HTTP/1.1 Content-Type: application/json @@ -125,21 +107,6 @@ Content-Type: application/json } ] } -### talk to Arch-Intent directly for completion, expect No -POST https://archfc.katanemo.dev/v1/chat/completions HTTP/1.1 -Content-Type: application/json - -{ - "model": "Arch-Intent", - "messages": [ - { - "role": "system", - "content": "You are a helpful assistant.\n\nYou task is to check if there are any tools that can be used to help the last user message in conversations according to the available tools listed below.\n\n\n{\"index\": \"T0\", \"type\": \"function\", \"function\": {\"name\": \"weather_forecast\", \"parameters\": {\"type\": \"object\", \"properties\": {\"city\": {\"type\": \"str\"}, \"days\": {\"type\": \"int\"}}, \"required\": [\"city\", \"days\"]}}}\n\n\nProvide your tool assessment for ONLY THE LAST USER MESSAGE in the above conversation:\n- First line must read 'Yes' or 'No'.\n- If yes, a second line must include a comma-separated list of tool indexes.\n" - }, - { "role": "user", "content": "what is your name" } - ], - "stream": false -} ### multi turn conversation with correct parameters POST {{model_server_endpoint}}/function_calling HTTP/1.1 diff --git a/tests/rest/network_agent.rest b/tests/rest/network_agent.rest index dc03fa6c..07f746ca 100644 --- a/tests/rest/network_agent.rest +++ b/tests/rest/network_agent.rest @@ -1,4 +1,4 @@ -@model_server_endpoint = http://localhost:51000 +@model_server_endpoint = http://localhost:12000 @archfc_endpoint = https://archfc.katanemo.dev ### single turn function calling all parameters insurance agent summary