Merge remote-tracking branch 'origin/main' into musa/cli

This commit is contained in:
Musa 2026-02-17 07:28:07 -08:00
commit 0416573c82
293 changed files with 8728 additions and 9045 deletions

View file

@ -33,8 +33,8 @@ cli/__pycache__/
cli/planoai/__pycache__/
# Python model server
archgw_modelserver/
arch_tools/
plano_modelserver/
plano_tools/
# Misc
*.md
@ -44,4 +44,4 @@ turbo.json
package.json
*.sh
!cli/build_cli.sh
arch_config.yaml_rendered
plano_config.yaml_rendered

479
.github/workflows/ci.yml vendored Normal file
View file

@ -0,0 +1,479 @@
name: CI
on:
push:
branches: [main]
pull_request:
permissions:
contents: read
security-events: write
env:
PLANO_DOCKER_IMAGE: katanemo/plano:e2e
DOCKER_IMAGE: katanemo/plano
jobs:
# ──────────────────────────────────────────────
# Pre-commit (fmt, clippy, cargo test, black, yaml)
# ──────────────────────────────────────────────
pre-commit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
- uses: pre-commit/action@v3.0.1
# ──────────────────────────────────────────────
# Plano tools (CLI) tests — no Docker needed
# ──────────────────────────────────────────────
plano-tools-tests:
runs-on: ubuntu-latest-m
defaults:
run:
working-directory: ./cli
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.14"
- name: Install uv
run: curl -LsSf https://astral.sh/uv/install.sh | sh
- name: Install plano tools
run: uv sync --extra dev
- name: Run tests
run: uv run pytest
# ──────────────────────────────────────────────
# Single Docker build — shared by all downstream jobs
# ──────────────────────────────────────────────
docker-build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Free disk space on runner
run: |
sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc
docker system prune -af || true
docker volume prune -f || true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build plano image (with GHA cache)
uses: docker/build-push-action@v6
with:
context: .
file: Dockerfile
load: true
tags: |
${{ env.PLANO_DOCKER_IMAGE }}
${{ env.DOCKER_IMAGE }}:0.4.7
${{ env.DOCKER_IMAGE }}:latest
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Save image as artifact
run: docker save ${{ env.PLANO_DOCKER_IMAGE }} ${{ env.DOCKER_IMAGE }}:0.4.7 ${{ env.DOCKER_IMAGE }}:latest -o /tmp/plano-image.tar
- name: Upload image artifact
uses: actions/upload-artifact@v4
with:
name: plano-image
path: /tmp/plano-image.tar
retention-days: 1
# ──────────────────────────────────────────────
# Validate plano config
# ──────────────────────────────────────────────
validate-config:
needs: docker-build
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.14"
- name: Download plano image
uses: actions/download-artifact@v4
with:
name: plano-image
path: /tmp
- name: Load plano image
run: docker load -i /tmp/plano-image.tar
- name: Validate plano config
run: bash config/validate_plano_config.sh
# ──────────────────────────────────────────────
# Docker security scan (Trivy)
# ──────────────────────────────────────────────
security-scan:
needs: docker-build
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Download plano image
uses: actions/download-artifact@v4
with:
name: plano-image
path: /tmp
- name: Load plano image
run: docker load -i /tmp/plano-image.tar
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
with:
image-ref: ${{ env.DOCKER_IMAGE }}:latest
format: table
exit-code: ${{ github.event_name == 'pull_request' && '1' || '0' }}
ignore-unfixed: true
severity: CRITICAL,HIGH
- name: Run Trivy scanner (SARIF for GitHub Security tab)
if: always()
uses: aquasecurity/trivy-action@master
with:
image-ref: ${{ env.DOCKER_IMAGE }}:latest
format: sarif
output: trivy-results.sarif
ignore-unfixed: true
severity: CRITICAL,HIGH
- name: Upload Trivy results to GitHub Security tab
if: always()
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: trivy-results.sarif
# ──────────────────────────────────────────────
# E2E: prompt_gateway tests
# ──────────────────────────────────────────────
test-prompt-gateway:
needs: docker-build
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Free disk space on runner
run: |
sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc
docker system prune -af || true
docker volume prune -f || true
- name: Download plano image
uses: actions/download-artifact@v4
with:
name: plano-image
path: /tmp
- name: Load plano image
run: docker load -i /tmp/plano-image.tar
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.14"
- name: Install uv
uses: astral-sh/setup-uv@v5
with:
enable-cache: true
cache-dependency-glob: |
tests/e2e/uv.lock
cli/uv.lock
- name: Run prompt_gateway tests
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY }}
GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}
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: |
cd tests/e2e && bash run_prompt_gateway_tests.sh
# ──────────────────────────────────────────────
# E2E: model_alias_routing tests
# ──────────────────────────────────────────────
test-model-alias-routing:
needs: docker-build
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Free disk space on runner
run: |
sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc
docker system prune -af || true
docker volume prune -f || true
- name: Download plano image
uses: actions/download-artifact@v4
with:
name: plano-image
path: /tmp
- name: Load plano image
run: docker load -i /tmp/plano-image.tar
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.14"
- name: Install uv
uses: astral-sh/setup-uv@v5
with:
enable-cache: true
cache-dependency-glob: |
tests/e2e/uv.lock
cli/uv.lock
- name: Run model alias routing tests
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY }}
GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}
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: |
cd tests/e2e && bash run_model_alias_tests.sh
# ──────────────────────────────────────────────
# E2E: responses API with state tests
# ──────────────────────────────────────────────
test-responses-api-with-state:
needs: docker-build
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Free disk space on runner
run: |
sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc
docker system prune -af || true
docker volume prune -f || true
- name: Download plano image
uses: actions/download-artifact@v4
with:
name: plano-image
path: /tmp
- name: Load plano image
run: docker load -i /tmp/plano-image.tar
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.14"
- name: Install uv
uses: astral-sh/setup-uv@v5
with:
enable-cache: true
cache-dependency-glob: |
tests/e2e/uv.lock
cli/uv.lock
- name: Run responses API with state tests
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY }}
GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}
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: |
cd tests/e2e && bash run_responses_state_tests.sh
# ──────────────────────────────────────────────
# E2E: plano tests (multi-Python matrix)
# ──────────────────────────────────────────────
e2e-plano-tests:
needs: docker-build
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/archgw
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Download plano image
uses: actions/download-artifact@v4
with:
name: plano-image
path: /tmp
- name: Load plano image
run: docker load -i /tmp/plano-image.tar
- name: Start plano
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY }}
GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
AZURE_API_KEY: ${{ secrets.AZURE_API_KEY }}
AWS_BEARER_TOKEN_BEDROCK: ${{ secrets.AWS_BEARER_TOKEN_BEDROCK }}
run: |
docker compose up | tee &> plano.logs &
- name: Wait for plano to be healthy
run: |
source common.sh && wait_for_healthz http://localhost:10000/healthz
- name: Install uv
run: curl -LsSf https://astral.sh/uv/install.sh | sh
- name: Install test dependencies
run: uv sync
- name: Run plano tests
run: |
uv run pytest || tail -100 plano.logs
- name: Stop plano docker container
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY }}
GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: |
docker compose down
# ──────────────────────────────────────────────
# E2E: demo — preference based routing
# ──────────────────────────────────────────────
e2e-demo-preference:
needs: docker-build
runs-on: ubuntu-latest-m
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.14"
- name: Download plano image
uses: actions/download-artifact@v4
with:
name: plano-image
path: /tmp
- name: Load plano image
run: docker load -i /tmp/plano-image.tar
- name: Install uv
run: curl -LsSf https://astral.sh/uv/install.sh | sh
- name: Setup python venv
run: python -m venv venv
- name: Install hurl
run: |
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 plano gateway and test dependencies
run: |
source venv/bin/activate
cd cli && echo "installing plano cli" && uv sync && uv tool install .
cd ../demos/shared/test_runner && echo "installing test dependencies" && uv sync
- name: Run demo tests
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY }}
GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}
ARCH_API_KEY: ${{ secrets.ARCH_API_KEY }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: |
source venv/bin/activate
cd demos/shared/test_runner && sh run_demo_tests.sh llm_routing/preference_based_routing
# ──────────────────────────────────────────────
# E2E: demo — currency conversion
# ──────────────────────────────────────────────
e2e-demo-currency:
needs: docker-build
runs-on: ubuntu-latest-m
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.14"
- name: Download plano image
uses: actions/download-artifact@v4
with:
name: plano-image
path: /tmp
- name: Load plano image
run: docker load -i /tmp/plano-image.tar
- name: Install uv
run: curl -LsSf https://astral.sh/uv/install.sh | sh
- name: Setup python venv
run: python -m venv venv
- name: Install hurl
run: |
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 plano gateway and test dependencies
run: |
source venv/bin/activate
cd cli && echo "installing plano cli" && uv sync && uv tool install .
cd ../demos/shared/test_runner && echo "installing test dependencies" && uv sync
- name: Run demo tests
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY }}
GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}
run: |
source venv/bin/activate
cd demos/shared/test_runner && sh run_demo_tests.sh advanced/currency_exchange

View file

@ -2,14 +2,19 @@ name: Publish docker image (latest)
env:
DOCKER_IMAGE: katanemo/plano
GHCR_IMAGE: ghcr.io/${{ github.repository_owner }}/plano
on:
push:
branches:
- main
permissions:
contents: read
packages: write
jobs:
# Build ARM64 image on native ARM64 runner.
# Build ARM64 image on native ARM64 runner — push to both registries
build-arm64:
runs-on: [linux-arm64]
steps:
@ -22,13 +27,12 @@ jobs:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
- name: Log in to GHCR
uses: docker/login-action@v3
with:
images: ${{ env.DOCKER_IMAGE }}
tags: |
type=raw,value=latest # Force the tag to be "latest"
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and Push ARM64 Image
uses: docker/build-push-action@v5
@ -37,9 +41,11 @@ jobs:
file: ./Dockerfile
platforms: linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}-arm64
tags: |
${{ env.DOCKER_IMAGE }}:latest-arm64
${{ env.GHCR_IMAGE }}:latest-arm64
# Build AMD64 image on GitHub's AMD64 runner
# Build AMD64 image on GitHub's AMD64 runner — push to both registries
build-amd64:
runs-on: ubuntu-latest
steps:
@ -52,13 +58,12 @@ jobs:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
- name: Log in to GHCR
uses: docker/login-action@v3
with:
images: ${{ env.DOCKER_IMAGE }}
tags: |
type=raw,value=latest # Force the tag to be "latest"
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and Push AMD64 Image
uses: docker/build-push-action@v5
@ -67,13 +72,14 @@ jobs:
file: ./Dockerfile
platforms: linux/amd64
push: true
tags: ${{ steps.meta.outputs.tags }}-amd64
tags: |
${{ env.DOCKER_IMAGE }}:latest-amd64
${{ env.GHCR_IMAGE }}:latest-amd64
# Combine ARM64 and AMD64 images into a multi-arch manifest
# Combine ARM64 and AMD64 images into multi-arch manifests for both registries
create-manifest:
runs-on: ubuntu-latest
needs: [build-arm64, build-amd64] # Wait for both builds
needs: [build-arm64, build-amd64]
steps:
- name: Log in to Docker Hub
uses: docker/login-action@v3
@ -81,17 +87,23 @@ jobs:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
- name: Log in to GHCR
uses: docker/login-action@v3
with:
images: ${{ env.DOCKER_IMAGE }}
tags: |
type=raw,value=latest # Force the tag to be "latest"
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Create Multi-Arch Manifest
- name: Create Docker Hub Multi-Arch Manifest
run: |
# Combine the architecture-specific images into a "latest" manifest
docker buildx imagetools create -t ${{ steps.meta.outputs.tags }} \
docker buildx imagetools create \
-t ${{ env.DOCKER_IMAGE }}:latest \
${{ env.DOCKER_IMAGE }}:latest-arm64 \
${{ env.DOCKER_IMAGE }}:latest-amd64
- name: Create GHCR Multi-Arch Manifest
run: |
docker buildx imagetools create \
-t ${{ env.GHCR_IMAGE }}:latest \
${{ env.GHCR_IMAGE }}:latest-arm64 \
${{ env.GHCR_IMAGE }}:latest-amd64

View file

@ -2,13 +2,18 @@ name: Publish docker image (release)
env:
DOCKER_IMAGE: katanemo/plano
GHCR_IMAGE: ghcr.io/${{ github.repository_owner }}/plano
on:
release:
types: [published]
permissions:
contents: read
packages: write
jobs:
# Build ARM64 image on native ARM64 runner
# Build ARM64 image on native ARM64 runner — push to both registries
build-arm64:
runs-on: [linux-arm64]
steps:
@ -21,7 +26,14 @@ jobs:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Extract metadata (tags, labels) for Docker
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata for Docker
id: meta
uses: docker/metadata-action@v5
with:
@ -36,9 +48,11 @@ jobs:
file: ./Dockerfile
platforms: linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}-arm64
tags: |
${{ steps.meta.outputs.tags }}-arm64
${{ env.GHCR_IMAGE }}:${{ github.event.release.tag_name }}-arm64
# Build AMD64 image on GitHub's AMD64 runner
# Build AMD64 image on GitHub's AMD64 runner — push to both registries
build-amd64:
runs-on: ubuntu-latest
steps:
@ -51,7 +65,14 @@ jobs:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Extract metadata (tags, labels) for Docker
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata for Docker
id: meta
uses: docker/metadata-action@v5
with:
@ -66,12 +87,14 @@ jobs:
file: ./Dockerfile
platforms: linux/amd64
push: true
tags: ${{ steps.meta.outputs.tags }}-amd64
tags: |
${{ steps.meta.outputs.tags }}-amd64
${{ env.GHCR_IMAGE }}:${{ github.event.release.tag_name }}-amd64
# Combine ARM64 and AMD64 images into a multi-arch manifest
# Combine ARM64 and AMD64 images into multi-arch manifests for both registries
create-manifest:
runs-on: ubuntu-latest
needs: [build-arm64, build-amd64] # Wait for both builds
needs: [build-arm64, build-amd64]
steps:
- name: Log in to Docker Hub
uses: docker/login-action@v3
@ -79,7 +102,14 @@ jobs:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Extract metadata (tags, labels) for Docker
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata for Docker
id: meta
uses: docker/metadata-action@v5
with:
@ -87,9 +117,17 @@ jobs:
tags: |
type=raw,value={{tag}}
- name: Create Multi-Arch Manifest
- name: Create Docker Hub Multi-Arch Manifest
run: |
# Combine the architecture-specific images into a single manifest
docker buildx imagetools create -t ${{ steps.meta.outputs.tags }} \
docker buildx imagetools create \
-t ${{ steps.meta.outputs.tags }} \
${{ steps.meta.outputs.tags }}-arm64 \
${{ steps.meta.outputs.tags }}-amd64
- name: Create GHCR Multi-Arch Manifest
run: |
TAG=${{ github.event.release.tag_name }}
docker buildx imagetools create \
-t ${{ env.GHCR_IMAGE }}:${TAG} \
${{ env.GHCR_IMAGE }}:${TAG}-arm64 \
${{ env.GHCR_IMAGE }}:${TAG}-amd64

View file

@ -1,69 +0,0 @@
name: e2e plano tests
on:
push:
branches:
- main
pull_request:
jobs:
e2e_plano_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/archgw
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: build arch docker image
run: |
cd ../../ && docker build -f Dockerfile . -t katanemo/plano -t katanemo/plano:0.4.6 -t katanemo/plano:latest
- name: start plano
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY }}
GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
AZURE_API_KEY: ${{ secrets.AZURE_API_KEY }}
AWS_BEARER_TOKEN_BEDROCK: ${{ secrets.AWS_BEARER_TOKEN_BEDROCK }}
run: |
docker compose up | tee &> plano.logs &
- name: wait for plano to be healthy
run: |
source common.sh && wait_for_healthz http://localhost:10000/healthz
- name: install uv
run: curl -LsSf https://astral.sh/uv/install.sh | sh
- name: install test dependencies
run: |
uv sync
- name: run plano tests
run: |
uv run pytest || tail -100 plano.logs
- name: stop plano docker container
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY }}
GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: |
docker compose down

View file

@ -1,54 +0,0 @@
name: e2e demo tests currency conversion
permissions:
contents: read
on:
push:
branches:
- main
pull_request:
jobs:
e2e_demo_tests:
runs-on: ubuntu-latest-m
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.12"
- name: build plano docker image
run: |
docker build -f Dockerfile . -t katanemo/plano -t katanemo/plano:0.4.6
- name: install uv
run: curl -LsSf https://astral.sh/uv/install.sh | sh
- name: setup python venv
run: |
python -m venv venv
- name: install hurl
run: |
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 arch gateway and test dependencies
run: |
source venv/bin/activate
cd cli && echo "installing plano cli" && uv sync && uv tool install .
cd ../demos/shared/test_runner && echo "installing test dependencies" && uv sync
- name: run demo tests
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY }}
GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}
run: |
source venv/bin/activate
cd demos/shared/test_runner && sh run_demo_tests.sh samples_python/currency_exchange

View file

@ -1,56 +0,0 @@
name: e2e demo preference based routing tests
permissions:
contents: read
on:
push:
branches:
- main
pull_request:
jobs:
e2e_demo_tests:
runs-on: ubuntu-latest-m
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.12"
- name: build arch docker image
run: |
docker build -f Dockerfile . -t katanemo/plano -t katanemo/plano:0.4.6
- name: install uv
run: curl -LsSf https://astral.sh/uv/install.sh | sh
- name: setup python venv
run: |
python -m venv venv
- name: install hurl
run: |
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 arch gateway and test dependencies
run: |
source venv/bin/activate
cd cli && echo "installing plano cli" && uv sync && uv tool install .
cd ../demos/shared/test_runner && echo "installing test dependencies" && uv sync
- name: run demo tests
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY }}
GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}
ARCH_API_KEY: ${{ secrets.ARCH_API_KEY }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: |
source venv/bin/activate
cd demos/shared/test_runner && sh run_demo_tests.sh use_cases/preference_based_routing

View file

@ -1,203 +0,0 @@
name: e2e tests
on:
push:
branches:
- main
pull_request:
permissions:
contents: read
# Shared env vars for all jobs that run tests
env:
PLANO_DOCKER_IMAGE: katanemo/plano:e2e
jobs:
# ──────────────────────────────────────────────
# Job 1: Build the Docker image once, with cache
# ──────────────────────────────────────────────
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Free disk space on runner
run: |
sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc
docker system prune -af || true
docker volume prune -f || true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build plano image (with GHA cache)
uses: docker/build-push-action@v6
with:
context: .
file: Dockerfile
load: true
tags: ${{ env.PLANO_DOCKER_IMAGE }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Save image as artifact
run: docker save ${{ env.PLANO_DOCKER_IMAGE }} -o /tmp/plano-image.tar
- name: Upload image artifact
uses: actions/upload-artifact@v4
with:
name: plano-image
path: /tmp/plano-image.tar
retention-days: 1
# ──────────────────────────────────────────────
# Job 2a: prompt_gateway tests
# ──────────────────────────────────────────────
test-prompt-gateway:
needs: build
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Free disk space on runner
run: |
sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc
docker system prune -af || true
docker volume prune -f || true
- name: Download plano image
uses: actions/download-artifact@v4
with:
name: plano-image
path: /tmp
- name: Load plano image
run: docker load -i /tmp/plano-image.tar
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install uv
uses: astral-sh/setup-uv@v5
with:
enable-cache: true
cache-dependency-glob: |
tests/e2e/uv.lock
cli/uv.lock
- name: Run prompt_gateway tests
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY }}
GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}
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: |
cd tests/e2e && bash run_prompt_gateway_tests.sh
# ──────────────────────────────────────────────
# Job 2b: model_alias_routing + responses API tests
# ──────────────────────────────────────────────
test-model-alias-routing:
needs: build
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Free disk space on runner
run: |
sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc
docker system prune -af || true
docker volume prune -f || true
- name: Download plano image
uses: actions/download-artifact@v4
with:
name: plano-image
path: /tmp
- name: Load plano image
run: docker load -i /tmp/plano-image.tar
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install uv
uses: astral-sh/setup-uv@v5
with:
enable-cache: true
cache-dependency-glob: |
tests/e2e/uv.lock
cli/uv.lock
- name: Run model alias routing tests
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY }}
GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}
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: |
cd tests/e2e && bash run_model_alias_tests.sh
# ──────────────────────────────────────────────
# Job 2c: responses API with state storage tests
# ──────────────────────────────────────────────
test-responses-api-with-state:
needs: build
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Free disk space on runner
run: |
sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc
docker system prune -af || true
docker volume prune -f || true
- name: Download plano image
uses: actions/download-artifact@v4
with:
name: plano-image
path: /tmp
- name: Load plano image
run: docker load -i /tmp/plano-image.tar
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install uv
uses: astral-sh/setup-uv@v5
with:
enable-cache: true
cache-dependency-glob: |
tests/e2e/uv.lock
cli/uv.lock
- name: Run responses API with state tests
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY }}
GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}
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: |
cd tests/e2e && bash run_responses_state_tests.sh

View file

@ -1,88 +0,0 @@
name: Publish docker image to ghcr (latest)
env:
IMAGE_NAME: ghcr.io/${{ github.repository_owner }}/plano
on:
push:
branches: [main]
jobs:
build-arm64:
runs-on: [linux-arm64]
permissions: { contents: read, packages: write }
steps:
- uses: actions/checkout@v4
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.IMAGE_NAME }}
tags: |
type=raw,value=latest
- name: Build and Push ARM64 Image
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
platforms: linux/arm64
push: true
# produce ghcr.io/<owner>/plano:latest-arm64
tags: ${{ steps.meta.outputs.tags }}-arm64
build-amd64:
runs-on: ubuntu-latest
permissions: { contents: read, packages: write }
steps:
- uses: actions/checkout@v4
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.IMAGE_NAME }}
tags: |
type=raw,value=latest
- name: Build and Push AMD64 Image
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
platforms: linux/amd64
push: true
tags: ${{ steps.meta.outputs.tags }}-amd64
create-manifest:
runs-on: ubuntu-latest
needs: [build-arm64, build-amd64]
permissions: { contents: read, packages: write }
steps:
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.IMAGE_NAME }}
tags: |
type=raw,value=latest
- name: Create Multi-Arch Manifest
run: |
docker buildx imagetools create -t ${{ steps.meta.outputs.tags }} \
${{ env.IMAGE_NAME }}:latest-arm64 \
${{ env.IMAGE_NAME }}:latest-amd64

View file

@ -1,87 +0,0 @@
name: release - publish docker image to ghcr (latest)
env:
IMAGE_NAME: ghcr.io/${{ github.repository_owner }}/plano
on:
release:
types: [published]
jobs:
build-arm64:
runs-on: [linux-arm64]
permissions: { contents: read, packages: write }
steps:
- uses: actions/checkout@v4
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.IMAGE_NAME }}
tags: |
type=raw,value={{tag}}
- name: Build and Push ARM64 Image
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
platforms: linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}-arm64
build-amd64:
runs-on: ubuntu-latest
permissions: { contents: read, packages: write }
steps:
- uses: actions/checkout@v4
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.IMAGE_NAME }}
tags: |
type=raw,value={{tag}}
- name: Build and Push AMD64 Image
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
platforms: linux/amd64
push: true
tags: ${{ steps.meta.outputs.tags }}-amd64
create-manifest:
runs-on: ubuntu-latest
needs: [build-arm64, build-amd64]
permissions: { contents: read, packages: write }
steps:
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.IMAGE_NAME }}
tags: |
type=raw,value={{tag}}
- name: Create Multi-Arch Manifest
run: |
docker buildx imagetools create -t ${{ steps.meta.outputs.tags }} \
${{ steps.meta.outputs.tags }}-arm64 \
${{ steps.meta.outputs.tags }}-amd64

View file

@ -1,37 +0,0 @@
name: plano tools tests
permissions:
contents: read
on:
push:
branches:
- main
pull_request:
jobs:
plano_tools_tests:
runs-on: ubuntu-latest-m
defaults:
run:
working-directory: ./cli
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.12"
- name: install uv
run: curl -LsSf https://astral.sh/uv/install.sh | sh
- name: install plano tools
run: |
uv sync --extra dev
- name: run tests
run: |
uv run pytest

View file

@ -1,14 +0,0 @@
name: pre-commit
on:
pull_request:
push:
branches: [main]
jobs:
pre-commit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v3
- uses: pre-commit/action@v3.0.1

View file

@ -22,7 +22,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.12"
python-version: "3.14"
- name: Install uv
uses: astral-sh/setup-uv@v4

View file

@ -1,31 +0,0 @@
name: rust tests (prompt and llm gateway)
on:
pull_request:
push:
branches: [main]
jobs:
test:
name: Test
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./crates
steps:
- name: Setup | Checkout
uses: actions/checkout@v4
- name: Setup | Rust
run: rustup toolchain install 1.92 --profile minimal
- name: Setup | Install wasm toolchain
run: rustup target add wasm32-wasip1
- name: Build wasm module
run: |
cargo build --release --target=wasm32-wasip1 -p llm_gateway -p prompt_gateway
- name: Run unit tests
run: cargo test --lib

View file

@ -3,6 +3,9 @@ on:
push:
branches: [main]
permissions:
contents: write
jobs:
build:
runs-on: ubuntu-latest

View file

@ -1,31 +0,0 @@
name: arch config tests
on:
push:
branches:
- main
pull_request:
jobs:
validate_arch_config:
runs-on: ubuntu-latest
defaults:
run:
working-directory: .
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.12"
- name: build arch docker image
run: |
docker build -f Dockerfile . -t katanemo/plano -t katanemo/plano:0.4.6
- name: validate arch config
run: |
bash config/validate_plano_config.sh

16
.gitignore vendored
View file

@ -80,6 +80,8 @@ celerybeat-schedule
# Environments
.env
.env.local
.env*.local
.venv
env/
venv/
@ -107,30 +109,30 @@ venv.bak/
# =========================================
# Arch
# Plano
cli/config
cli/build
# Archgw - Docs
# Plano - Docs
docs/build/
# Archgw - Demos
# Plano - Demos
demos/function_calling/ollama/models/
demos/function_calling/ollama/id_ed*
demos/function_calling/open-webui/
demos/function_calling/open-webui/
demos/shared/signoz/data
# Arch - Miscellaneous
# Plano - Miscellaneous
grafana-data
prom_data
arch_log/
arch_logs/
plano_log/
plano_logs/
crates/*/target/
crates/target/
build.log
archgw.log
plano.log
# Next.js / Turborepo
.next/

View file

@ -30,6 +30,11 @@ repos:
entry: bash -c "cd crates && cargo test --lib"
pass_filenames: false
- repo: https://github.com/gitleaks/gitleaks
rev: v8.21.2
hooks:
- id: gitleaks
- repo: https://github.com/psf/black
rev: 23.1.0
hooks:

View file

@ -58,7 +58,7 @@ docker build -t katanemo/plano:latest .
### E2E Tests (tests/e2e/)
E2E tests require a built Docker image and API keys. They run via `tests/e2e/run_e2e_tests.sh` which executes three test suites: `test_prompt_gateway.py`, `test_model_alias_routing.py`, and `test_openai_responses_api_client_with_state.py`.
E2E tests require a built Docker image and API keys. They run via `tests/e2e/run_e2e_tests.sh` which executes four test suites: `test_prompt_gateway.py`, `test_model_alias_routing.py`, `test_openai_responses_api_client.py`, and `test_openai_responses_api_client_with_state.py`.
## Architecture
@ -91,12 +91,14 @@ The `planoai` CLI manages the Plano lifecycle. Key commands:
- `planoai logs` — Stream access/debug logs
- `planoai trace` — OTEL trace collection and analysis
- `planoai init` — Initialize new project
- `planoai cli_agent` — Start a CLI agent connected to Plano
- `planoai generate_prompt_targets` — Generate prompt_targets from python methods
Entry point: `cli/planoai/main.py`. Container lifecycle in `core.py`. Docker operations in `docker_cli.py`.
### Configuration System (config/)
- `arch_config_schema.yaml` — JSON Schema (draft-07) for validating user config files
- `plano_config_schema.yaml` — JSON Schema (draft-07) for validating user config files
- `envoy.template.yaml` — Jinja2 template rendered into Envoy proxy config
- `supervisord.conf` — Process supervisor for Envoy + brightstaff in the container
@ -106,6 +108,35 @@ User configs define: `agents` (id + url), `model_providers` (model + access_key)
Turbo monorepo with Next.js 16 / React 19 applications and shared packages (UI components, Tailwind config, TypeScript config). Not part of the core proxy — these are web applications.
## Release Process
To prepare a release (e.g., bumping from `0.4.6` to `0.4.7`), update the version string in all of the following files:
**CI Workflow:**
- `.github/workflows/ci.yml` — docker build/save tags
**CLI:**
- `cli/planoai/__init__.py``__version__`
- `cli/planoai/consts.py``PLANO_DOCKER_IMAGE` default
- `cli/pyproject.toml``version`
**Build & Config:**
- `build_filter_image.sh` — docker build tag
- `config/validate_plano_config.sh` — docker image tag
**Docs:**
- `docs/source/conf.py``release`
- `docs/source/get_started/quickstart.rst` — install commands and example output
- `docs/source/resources/deployment.rst` — docker image tag
**Website & Demos:**
- `apps/www/src/components/Hero.tsx` — version badge
- `demos/llm_routing/preference_based_routing/README.md` — example output
**Important:** Do NOT change `0.4.6` references in `*.lock` files or `Cargo.lock` — those refer to the `colorama` and `http-body` dependency versions, not Plano.
Commit message format: `release X.Y.Z`
## Key Conventions
- Rust edition 2021, formatted with `cargo fmt`, linted with `cargo clippy -D warnings`

View file

@ -42,13 +42,15 @@ RUN cargo build --release -p brightstaff
FROM docker.io/envoyproxy/envoy:v1.37.0 AS envoy
FROM python:3.13.6-slim AS arch
FROM python:3.14-slim AS arch
RUN set -eux; \
apt-get update; \
apt-get install -y --no-install-recommends supervisor gettext-base curl; \
apt-get install -y --no-install-recommends gettext-base curl; \
apt-get clean; rm -rf /var/lib/apt/lists/*
RUN pip install --no-cache-dir supervisor
# Remove PAM packages (CVE-2025-6020)
RUN set -eux; \
dpkg -r --force-depends libpam-modules libpam-modules-bin libpam-runtime libpam0g || true; \
@ -69,7 +71,8 @@ RUN uv run pip install --no-cache-dir .
COPY cli/planoai planoai/
COPY config/envoy.template.yaml .
COPY config/arch_config_schema.yaml .
COPY config/plano_config_schema.yaml .
RUN mkdir -p /etc/supervisor/conf.d
COPY config/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
COPY --from=wasm-builder /arch/target/wasm32-wasip1/release/prompt_gateway.wasm /etc/envoy/proxy-wasm-plugins/prompt_gateway.wasm
@ -81,4 +84,4 @@ RUN mkdir -p /var/log/supervisor && \
/var/log/access_ingress.log /var/log/access_ingress_prompt.log \
/var/log/access_internal.log /var/log/access_llm.log /var/log/access_agent.log
ENTRYPOINT ["/usr/bin/supervisord"]
ENTRYPOINT ["/usr/local/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]

View file

@ -12,9 +12,8 @@
[Documentation](https://docs.planoai.dev) •
[Contact](#Contact)
[![pre-commit](https://github.com/katanemo/plano/actions/workflows/pre-commit.yml/badge.svg)](https://github.com/katanemo/plano/actions/workflows/pre-commit.yml)
[![rust tests (prompt and llm gateway)](https://github.com/katanemo/plano/actions/workflows/rust_tests.yml/badge.svg)](https://github.com/katanemo/plano/actions/workflows/rust_tests.yml)
[![e2e tests](https://github.com/katanemo/plano/actions/workflows/e2e_tests.yml/badge.svg)](https://github.com/katanemo/plano/actions/workflows/e2e_tests.yml)
[![CI](https://github.com/katanemo/plano/actions/workflows/ci.yml/badge.svg)](https://github.com/katanemo/plano/actions/workflows/ci.yml)
[![Docker Image](https://github.com/katanemo/plano/actions/workflows/docker-push-main.yml/badge.svg)](https://github.com/katanemo/plano/actions/workflows/docker-push-main.yml)
[![Build and Deploy Documentation](https://github.com/katanemo/plano/actions/workflows/static.yml/badge.svg)](https://github.com/katanemo/plano/actions/workflows/static.yml)
Star ⭐️ the repo if you found Plano useful — new releases and updates land here first.
@ -46,7 +45,7 @@ Plano pulls rote plumbing out of your framework so you can stay focused on what
Plano handles **orchestration, model management, and observability** as modular building blocks - letting you configure only what you need (edge proxying for agentic orchestration and guardrails, or LLM routing from your services, or both together) to fit cleanly into existing architectures. Below is a simple multi-agent travel agent built with Plano that showcases all three core capabilities
> 📁 **Full working code:** See [`demos/use_cases/travel_agents/`](demos/use_cases/travel_agents/) for complete weather and flight agents you can run locally.
> 📁 **Full working code:** See [`demos/agent_orchestration/travel_agents/`](demos/agent_orchestration/travel_agents/) for complete weather and flight agents you can run locally.
@ -114,7 +113,7 @@ async def chat(request: Request):
days = 7
# Your agent logic: fetch data, call APIs, run tools
# See demos/use_cases/travel_agents/ for the full implementation
# See demos/agent_orchestration/travel_agents/ for the full implementation
weather_data = await get_weather_data(request, messages, days)
# Stream the response back through Plano

View file

@ -4,6 +4,28 @@ import { client, urlFor } from "@/lib/sanity";
export const runtime = "edge";
const ALLOWED_HOSTS = new Set([
"archgw-tau.vercel.app",
"planoai.dev",
"localhost",
]);
function getSafeBaseUrl(requestOrigin: string): string {
if (process.env.NEXT_PUBLIC_APP_URL) {
return process.env.NEXT_PUBLIC_APP_URL;
}
if (process.env.VERCEL_URL) {
return `https://${process.env.VERCEL_URL}`;
}
try {
const parsed = new URL(requestOrigin);
if (ALLOWED_HOSTS.has(parsed.hostname)) {
return parsed.origin;
}
} catch {}
return "http://localhost:3000";
}
// Font loading function that uses the request origin
function loadFont(fileName: string, baseUrl: string) {
return fetch(new URL(`/fonts/${fileName}`, baseUrl)).then((res) => {
@ -55,12 +77,8 @@ export async function GET(
{ params }: { params: Promise<{ slug: string }> },
) {
try {
// Get base URL for font loading - use request origin in production
const fontBaseUrl =
process.env.NEXT_PUBLIC_APP_URL ||
(process.env.VERCEL_URL
? `https://${process.env.VERCEL_URL}`
: request.nextUrl.origin);
// Get base URL for font loading - use validated origin
const fontBaseUrl = getSafeBaseUrl(request.nextUrl.origin);
// Load fonts with error handling
let fontData;
@ -116,11 +134,7 @@ export async function GET(
}
// Use logo PNG
const baseUrl =
process.env.NEXT_PUBLIC_APP_URL ||
(process.env.VERCEL_URL
? `https://${process.env.VERCEL_URL}`
: request.nextUrl.origin);
const baseUrl = getSafeBaseUrl(request.nextUrl.origin);
const logoUrl = `${baseUrl}/Logomark.png`;
return new ImageResponse(

View file

@ -47,24 +47,29 @@ export async function generateMetadata({
}
// Get baseUrl - use NEXT_PUBLIC_APP_URL if set, otherwise construct from VERCEL_URL
// Restrict to allowed hosts: localhost:3000, archgw-tau.vercel.app, or plano.katanemo.com
// Restrict to allowed hosts: localhost:3000, archgw-tau.vercel.app, or planoai.dev
let baseUrl = "http://localhost:3000";
if (process.env.NEXT_PUBLIC_APP_URL) {
const url = process.env.NEXT_PUBLIC_APP_URL;
if (
url.includes("archgw-tau.vercel.app") ||
url.includes("plano.katanemo.com") ||
url.includes("localhost:3000")
) {
baseUrl = url;
try {
const parsed = new URL(process.env.NEXT_PUBLIC_APP_URL);
const allowedHosts = new Set([
"archgw-tau.vercel.app",
"planoai.dev",
"localhost",
]);
if (allowedHosts.has(parsed.hostname)) {
baseUrl = parsed.origin;
}
} catch {
// Invalid URL, keep default
}
} else if (process.env.VERCEL_URL) {
const hostname = process.env.VERCEL_URL;
// VERCEL_URL is just the hostname, not the full URL
if (hostname === "archgw-tau.vercel.app") {
baseUrl = `https://${hostname}`;
} else if (hostname === "plano.katanemo.com") {
if (
hostname === "archgw-tau.vercel.app" ||
hostname === "planoai.dev"
) {
baseUrl = `https://${hostname}`;
}
}

View file

@ -24,7 +24,7 @@ export function Hero() {
>
<div className="inline-flex flex-wrap items-center gap-1.5 sm:gap-2 px-3 sm:px-4 py-1 rounded-full bg-[rgba(185,191,255,0.4)] border border-[var(--secondary)] shadow backdrop-blur hover:bg-[rgba(185,191,255,0.6)] transition-colors cursor-pointer">
<span className="text-xs sm:text-sm font-medium text-black/65">
v0.4.6
v0.4.7
</span>
<span className="text-xs sm:text-sm font-medium text-black ">

View file

@ -115,7 +115,6 @@ export const siteConfig = {
// Brand (minimal, necessary)
"Plano AI",
"Plano gateway",
"Arch gateway",
],
authors: [{ name: "Katanemo", url: "https://github.com/katanemo/plano" }],
creator: "Katanemo",
@ -240,7 +239,7 @@ export const pageMetadata = {
"agentic AI",
"Plano blog",
"Plano blog posts",
"Arch gateway blog",
"Plano gateway blog",
],
}),

View file

@ -1 +1 @@
docker build -f Dockerfile . -t katanemo/plano -t katanemo/plano:0.4.6
docker build -f Dockerfile . -t katanemo/plano -t katanemo/plano:0.4.7

View file

@ -1,3 +1,3 @@
"""Plano CLI - Intelligent Prompt Gateway."""
__version__ = "0.4.6"
__version__ = "0.4.7"

View file

@ -53,34 +53,34 @@ def validate_and_render_schema():
ENVOY_CONFIG_TEMPLATE_FILE = os.getenv(
"ENVOY_CONFIG_TEMPLATE_FILE", "envoy.template.yaml"
)
ARCH_CONFIG_FILE = os.getenv("ARCH_CONFIG_FILE", "/app/arch_config.yaml")
ARCH_CONFIG_FILE_RENDERED = os.getenv(
"ARCH_CONFIG_FILE_RENDERED", "/app/arch_config_rendered.yaml"
PLANO_CONFIG_FILE = os.getenv("PLANO_CONFIG_FILE", "/app/plano_config.yaml")
PLANO_CONFIG_FILE_RENDERED = os.getenv(
"PLANO_CONFIG_FILE_RENDERED", "/app/plano_config_rendered.yaml"
)
ENVOY_CONFIG_FILE_RENDERED = os.getenv(
"ENVOY_CONFIG_FILE_RENDERED", "/etc/envoy/envoy.yaml"
)
ARCH_CONFIG_SCHEMA_FILE = os.getenv(
"ARCH_CONFIG_SCHEMA_FILE", "arch_config_schema.yaml"
PLANO_CONFIG_SCHEMA_FILE = os.getenv(
"PLANO_CONFIG_SCHEMA_FILE", "plano_config_schema.yaml"
)
env = Environment(loader=FileSystemLoader(os.getenv("TEMPLATE_ROOT", "./")))
template = env.get_template(ENVOY_CONFIG_TEMPLATE_FILE)
try:
validate_prompt_config(ARCH_CONFIG_FILE, ARCH_CONFIG_SCHEMA_FILE)
validate_prompt_config(PLANO_CONFIG_FILE, PLANO_CONFIG_SCHEMA_FILE)
except Exception as e:
print(str(e))
exit(1) # validate_prompt_config failed. Exit
with open(ARCH_CONFIG_FILE, "r") as file:
arch_config = file.read()
with open(PLANO_CONFIG_FILE, "r") as file:
plano_config = file.read()
with open(ARCH_CONFIG_SCHEMA_FILE, "r") as file:
arch_config_schema = file.read()
with open(PLANO_CONFIG_SCHEMA_FILE, "r") as file:
plano_config_schema = file.read()
config_yaml = yaml.safe_load(arch_config)
_ = yaml.safe_load(arch_config_schema)
config_yaml = yaml.safe_load(plano_config)
_ = yaml.safe_load(plano_config_schema)
inferred_clusters = {}
# Convert legacy llm_providers to model_providers
@ -145,7 +145,7 @@ def validate_and_render_schema():
inferred_clusters[name]["port"],
) = get_endpoint_and_port(endpoint, protocol)
print("defined clusters from arch_config.yaml: ", json.dumps(inferred_clusters))
print("defined clusters from plano_config.yaml: ", json.dumps(inferred_clusters))
if "prompt_targets" in config_yaml:
for prompt_target in config_yaml["prompt_targets"]:
@ -154,13 +154,13 @@ def validate_and_render_schema():
continue
if name not in inferred_clusters:
raise Exception(
f"Unknown endpoint {name}, please add it in endpoints section in your arch_config.yaml file"
f"Unknown endpoint {name}, please add it in endpoints section in your plano_config.yaml file"
)
arch_tracing = config_yaml.get("tracing", {})
plano_tracing = config_yaml.get("tracing", {})
# Resolution order: config yaml > OTEL_TRACING_GRPC_ENDPOINT env var > hardcoded default
opentracing_grpc_endpoint = arch_tracing.get(
opentracing_grpc_endpoint = plano_tracing.get(
"opentracing_grpc_endpoint",
os.environ.get(
"OTEL_TRACING_GRPC_ENDPOINT", DEFAULT_OTEL_TRACING_GRPC_ENDPOINT
@ -172,7 +172,7 @@ def validate_and_render_schema():
print(
f"Resolved opentracing_grpc_endpoint to {opentracing_grpc_endpoint} after expanding environment variables"
)
arch_tracing["opentracing_grpc_endpoint"] = opentracing_grpc_endpoint
plano_tracing["opentracing_grpc_endpoint"] = opentracing_grpc_endpoint
# ensure that opentracing_grpc_endpoint is a valid URL if present and start with http and must not have any path
if opentracing_grpc_endpoint:
urlparse_result = urlparse(opentracing_grpc_endpoint)
@ -436,8 +436,8 @@ def validate_and_render_schema():
f"Model alias 2 - '{alias_name}' targets '{target}' which is not defined as a model. Available models: {', '.join(sorted(model_name_keys))}"
)
arch_config_string = yaml.dump(config_yaml)
arch_llm_config_string = yaml.dump(config_yaml)
plano_config_string = yaml.dump(config_yaml)
plano_llm_config_string = yaml.dump(config_yaml)
use_agent_orchestrator = config_yaml.get("overrides", {}).get(
"use_agent_orchestrator", False
@ -449,11 +449,11 @@ def validate_and_render_schema():
if len(endpoints) == 0:
raise Exception(
"Please provide agent orchestrator in the endpoints section in your arch_config.yaml file"
"Please provide agent orchestrator in the endpoints section in your plano_config.yaml file"
)
elif len(endpoints) > 1:
raise Exception(
"Please provide single agent orchestrator in the endpoints section in your arch_config.yaml file"
"Please provide single agent orchestrator in the endpoints section in your plano_config.yaml file"
)
else:
agent_orchestrator = list(endpoints.keys())[0]
@ -463,11 +463,11 @@ def validate_and_render_schema():
data = {
"prompt_gateway_listener": prompt_gateway,
"llm_gateway_listener": llm_gateway,
"arch_config": arch_config_string,
"arch_llm_config": arch_llm_config_string,
"arch_clusters": inferred_clusters,
"arch_model_providers": updated_model_providers,
"arch_tracing": arch_tracing,
"plano_config": plano_config_string,
"plano_llm_config": plano_llm_config_string,
"plano_clusters": inferred_clusters,
"plano_model_providers": updated_model_providers,
"plano_tracing": plano_tracing,
"local_llms": llms_with_endpoint,
"agent_orchestrator": agent_orchestrator,
"listeners": listeners,
@ -479,25 +479,25 @@ def validate_and_render_schema():
with open(ENVOY_CONFIG_FILE_RENDERED, "w") as file:
file.write(rendered)
with open(ARCH_CONFIG_FILE_RENDERED, "w") as file:
file.write(arch_config_string)
with open(PLANO_CONFIG_FILE_RENDERED, "w") as file:
file.write(plano_config_string)
def validate_prompt_config(arch_config_file, arch_config_schema_file):
with open(arch_config_file, "r") as file:
arch_config = file.read()
def validate_prompt_config(plano_config_file, plano_config_schema_file):
with open(plano_config_file, "r") as file:
plano_config = file.read()
with open(arch_config_schema_file, "r") as file:
arch_config_schema = file.read()
with open(plano_config_schema_file, "r") as file:
plano_config_schema = file.read()
config_yaml = yaml.safe_load(arch_config)
config_schema_yaml = yaml.safe_load(arch_config_schema)
config_yaml = yaml.safe_load(plano_config)
config_schema_yaml = yaml.safe_load(plano_config_schema)
try:
validate(config_yaml, config_schema_yaml)
except Exception as e:
print(
f"Error validating arch_config file: {arch_config_file}, schema file: {arch_config_schema_file}, error: {e}"
f"Error validating plano_config file: {plano_config_file}, schema file: {plano_config_schema_file}, error: {e}"
)
raise e

View file

@ -5,5 +5,5 @@ PLANO_COLOR = "#969FF4"
SERVICE_NAME_ARCHGW = "plano"
PLANO_DOCKER_NAME = "plano"
PLANO_DOCKER_IMAGE = os.getenv("PLANO_DOCKER_IMAGE", "katanemo/plano:0.4.6")
PLANO_DOCKER_IMAGE = os.getenv("PLANO_DOCKER_IMAGE", "katanemo/plano:0.4.7")
DEFAULT_OTEL_TRACING_GRPC_ENDPOINT = "http://host.docker.internal:4317"

View file

@ -24,17 +24,17 @@ from planoai.docker_cli import (
log = getLogger(__name__)
def _get_gateway_ports(arch_config_file: str) -> list[int]:
def _get_gateway_ports(plano_config_file: str) -> list[int]:
PROMPT_GATEWAY_DEFAULT_PORT = 10000
LLM_GATEWAY_DEFAULT_PORT = 12000
# parse arch_config_file yaml file and get prompt_gateway_port
arch_config_dict = {}
with open(arch_config_file) as f:
arch_config_dict = yaml.safe_load(f)
# parse plano_config_file yaml file and get prompt_gateway_port
plano_config_dict = {}
with open(plano_config_file) as f:
plano_config_dict = yaml.safe_load(f)
listeners, _, _ = convert_legacy_listeners(
arch_config_dict.get("listeners"), arch_config_dict.get("llm_providers")
plano_config_dict.get("listeners"), plano_config_dict.get("llm_providers")
)
all_ports = [listener.get("port") for listener in listeners]
@ -45,7 +45,7 @@ def _get_gateway_ports(arch_config_file: str) -> list[int]:
return all_ports
def start_arch(arch_config_file, env, log_timeout=120, foreground=False):
def start_plano(plano_config_file, env, log_timeout=120, foreground=False):
"""
Start Docker Compose in detached mode and stream logs until services are healthy.
@ -54,7 +54,7 @@ def start_arch(arch_config_file, env, log_timeout=120, foreground=False):
log_timeout (int): Time in seconds to show logs before checking for healthy state.
"""
log.info(
f"Starting arch gateway, image name: {PLANO_DOCKER_NAME}, tag: {PLANO_DOCKER_IMAGE}"
f"Starting plano gateway, image name: {PLANO_DOCKER_NAME}, tag: {PLANO_DOCKER_IMAGE}"
)
try:
@ -64,10 +64,10 @@ def start_arch(arch_config_file, env, log_timeout=120, foreground=False):
docker_stop_container(PLANO_DOCKER_NAME)
docker_remove_container(PLANO_DOCKER_NAME)
gateway_ports = _get_gateway_ports(arch_config_file)
gateway_ports = _get_gateway_ports(plano_config_file)
return_code, _, plano_stderr = docker_start_plano_detached(
arch_config_file,
plano_config_file,
env,
gateway_ports,
)
@ -117,7 +117,7 @@ def start_arch(arch_config_file, env, log_timeout=120, foreground=False):
stream_gateway_logs(follow=True)
except KeyboardInterrupt:
log.info("Keyboard interrupt received, stopping arch gateway service.")
log.info("Keyboard interrupt received, stopping plano gateway service.")
stop_docker_container()
@ -144,15 +144,15 @@ def stop_docker_container(service=PLANO_DOCKER_NAME):
log.info(f"Failed to shut down services: {str(e)}")
def start_cli_agent(arch_config_file=None, settings_json="{}"):
def start_cli_agent(plano_config_file=None, settings_json="{}"):
"""Start a CLI client connected to Plano."""
with open(arch_config_file, "r") as file:
arch_config = file.read()
arch_config_yaml = yaml.safe_load(arch_config)
with open(plano_config_file, "r") as file:
plano_config = file.read()
plano_config_yaml = yaml.safe_load(plano_config)
# Get egress listener configuration
egress_config = arch_config_yaml.get("listeners", {}).get("egress_traffic", {})
egress_config = plano_config_yaml.get("listeners", {}).get("egress_traffic", {})
host = egress_config.get("host", "127.0.0.1")
port = egress_config.get("port", 12000)
@ -167,7 +167,7 @@ def start_cli_agent(arch_config_file=None, settings_json="{}"):
env = os.environ.copy()
env.update(
{
"ANTHROPIC_AUTH_TOKEN": "test", # Use test token for arch
"ANTHROPIC_AUTH_TOKEN": "test", # Use test token for plano
"ANTHROPIC_API_KEY": "",
"ANTHROPIC_BASE_URL": f"http://{host}:{port}",
"NO_PROXY": host,
@ -184,7 +184,7 @@ def start_cli_agent(arch_config_file=None, settings_json="{}"):
]
else:
# Check if arch.claude.code.small.fast alias exists in model_aliases
model_aliases = arch_config_yaml.get("model_aliases", {})
model_aliases = plano_config_yaml.get("model_aliases", {})
if "arch.claude.code.small.fast" in model_aliases:
env["ANTHROPIC_SMALL_FAST_MODEL"] = "arch.claude.code.small.fast"
else:
@ -220,7 +220,7 @@ def start_cli_agent(arch_config_file=None, settings_json="{}"):
# Use claude from PATH
claude_path = "claude"
log.info(f"Connecting Claude Code Agent to Arch at {host}:{port}")
log.info(f"Connecting Claude Code Agent to Plano at {host}:{port}")
try:
subprocess.run([claude_path] + claude_args, env=env, check=True)

View file

@ -41,7 +41,7 @@ def docker_remove_container(container: str) -> str:
def docker_start_plano_detached(
arch_config_file: str,
plano_config_file: str,
env: dict,
gateway_ports: list[int],
) -> str:
@ -58,7 +58,7 @@ def docker_start_plano_detached(
port_mappings_args = [item for port in port_mappings for item in ("-p", port)]
volume_mappings = [
f"{arch_config_file}:/app/arch_config.yaml:ro",
f"{plano_config_file}:/app/plano_config.yaml:ro",
]
volume_mappings_args = [
item for volume in volume_mappings for item in ("-v", volume)
@ -115,7 +115,7 @@ def stream_gateway_logs(follow, service="plano"):
log.info(f"Failed to stream logs: {str(e)}")
def docker_validate_plano_schema(arch_config_file):
def docker_validate_plano_schema(plano_config_file):
import os
env = os.environ.copy()
@ -129,7 +129,7 @@ def docker_validate_plano_schema(arch_config_file):
"--rm",
*env_args,
"-v",
f"{arch_config_file}:/app/arch_config.yaml:ro",
f"{plano_config_file}:/app/plano_config.yaml:ro",
"--entrypoint",
"python",
PLANO_DOCKER_IMAGE,

View file

@ -22,7 +22,7 @@ from planoai.utils import (
find_repo_root,
)
from planoai.core import (
start_arch,
start_plano,
stop_docker_container,
start_cli_agent,
)
@ -45,7 +45,7 @@ def _is_port_in_use(port: int) -> bool:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
try:
s.bind(("0.0.0.0", port))
s.bind(("0.0.0.0", port)) # noqa: S104
return False
except OSError:
return True
@ -200,12 +200,12 @@ def up(file, path, foreground, with_tracing, tracing_port):
_print_cli_header(console)
# Use the utility function to find config file
arch_config_file = find_config_file(path, file)
plano_config_file = find_config_file(path, file)
# Check if the file exists
if not os.path.exists(arch_config_file):
if not os.path.exists(plano_config_file):
console.print(
f"[red]✗[/red] Config file not found: [dim]{arch_config_file}[/dim]"
f"[red]✗[/red] Config file not found: [dim]{plano_config_file}[/dim]"
)
sys.exit(1)
@ -216,7 +216,7 @@ def up(file, path, foreground, with_tracing, tracing_port):
validation_return_code,
_,
validation_stderr,
) = docker_validate_plano_schema(arch_config_file)
) = docker_validate_plano_schema(plano_config_file)
if validation_return_code != 0:
console.print(f"[red]✗[/red] Validation failed")
@ -234,7 +234,7 @@ def up(file, path, foreground, with_tracing, tracing_port):
env.pop("PATH", None)
# Check access keys
access_keys = get_llm_provider_access_keys(arch_config_file=arch_config_file)
access_keys = get_llm_provider_access_keys(plano_config_file=plano_config_file)
access_keys = set(access_keys)
access_keys = [item[1:] if item.startswith("$") else item for item in access_keys]
@ -302,7 +302,7 @@ def up(file, path, foreground, with_tracing, tracing_port):
env.update(env_stage)
try:
start_arch(arch_config_file, env, foreground=foreground)
start_plano(plano_config_file, env, foreground=foreground)
# When tracing is enabled but --foreground is not, keep the process
# alive so the OTLP collector continues to receive spans.
@ -363,35 +363,35 @@ def generate_prompt_targets(file):
def logs(debug, follow):
"""Stream logs from access logs services."""
archgw_process = None
plano_process = None
try:
if debug:
archgw_process = multiprocessing.Process(
plano_process = multiprocessing.Process(
target=stream_gateway_logs, args=(follow,)
)
archgw_process.start()
plano_process.start()
archgw_access_logs_process = multiprocessing.Process(
plano_access_logs_process = multiprocessing.Process(
target=stream_access_logs, args=(follow,)
)
archgw_access_logs_process.start()
archgw_access_logs_process.join()
plano_access_logs_process.start()
plano_access_logs_process.join()
if archgw_process:
archgw_process.join()
if plano_process:
plano_process.join()
except KeyboardInterrupt:
log.info("KeyboardInterrupt detected. Exiting.")
if archgw_access_logs_process.is_alive():
archgw_access_logs_process.terminate()
if archgw_process and archgw_process.is_alive():
archgw_process.terminate()
if plano_access_logs_process.is_alive():
plano_access_logs_process.terminate()
if plano_process and plano_process.is_alive():
plano_process.terminate()
@click.command()
@click.argument("type", type=click.Choice(["claude"]), required=True)
@click.argument("file", required=False) # Optional file argument
@click.option(
"--path", default=".", help="Path to the directory containing arch_config.yaml"
"--path", default=".", help="Path to the directory containing plano_config.yaml"
)
@click.option(
"--settings",
@ -405,20 +405,20 @@ def cli_agent(type, file, path, settings):
"""
# Check if plano docker container is running
archgw_status = docker_container_status(PLANO_DOCKER_NAME)
if archgw_status != "running":
log.error(f"plano docker container is not running (status: {archgw_status})")
plano_status = docker_container_status(PLANO_DOCKER_NAME)
if plano_status != "running":
log.error(f"plano docker container is not running (status: {plano_status})")
log.error("Please start plano using the 'planoai up' command.")
sys.exit(1)
# Determine arch_config.yaml path
arch_config_file = find_config_file(path, file)
if not os.path.exists(arch_config_file):
log.error(f"Config file not found: {arch_config_file}")
# Determine plano_config.yaml path
plano_config_file = find_config_file(path, file)
if not os.path.exists(plano_config_file):
log.error(f"Config file not found: {plano_config_file}")
sys.exit(1)
try:
start_cli_agent(arch_config_file, settings)
start_cli_agent(plano_config_file, settings)
except SystemExit:
# Re-raise SystemExit to preserve exit codes
raise

View file

@ -1,13 +1,6 @@
version: v0.1
version: v0.3.0
listeners:
egress_traffic:
address: 0.0.0.0
port: 12000
message_format: openai
timeout: 30s
llm_providers:
model_providers:
# OpenAI Models
- model: openai/gpt-5-2025-08-07
access_key: $OPENAI_API_KEY
@ -39,5 +32,10 @@ model_aliases:
arch.claude.code.small.fast:
target: claude-haiku-4-5
listeners:
- type: model
name: model_listener
port: 12000
tracing:
random_sampling: 100

View file

@ -1,25 +1,36 @@
version: v0.1
version: v0.3.0
listeners:
egress_traffic:
address: 0.0.0.0
port: 12000
message_format: openai
timeout: 30s
llm_providers:
agents:
- id: assistant
url: http://localhost:10510
model_providers:
# OpenAI Models
- model: openai/gpt-5-mini-2025-08-07
access_key: $OPENAI_API_KEY
default: true
# Anthropic Models
# Anthropic Models
- model: anthropic/claude-sonnet-4-20250514
access_key: $ANTHROPIC_API_KEY
listeners:
- type: agent
name: conversation_service
port: 8001
router: plano_orchestrator_v1
agents:
- id: assistant
description: |
A conversational assistant that maintains context across multi-turn
conversations. It can answer follow-up questions, remember previous
context, and provide coherent responses in ongoing dialogues.
# State storage configuration for v1/responses API
# Manages conversation state for multi-turn conversations
state_storage:
# Type: memory | postgres
type: memory
tracing:
random_sampling: 100

View file

@ -1,13 +1,6 @@
version: v0.1.0
version: v0.3.0
listeners:
egress_traffic:
address: 0.0.0.0
port: 12000
message_format: openai
timeout: 30s
llm_providers:
model_providers:
- model: openai/gpt-4o-mini
access_key: $OPENAI_API_KEY
@ -25,5 +18,10 @@ llm_providers:
- name: code generation
description: generating new code snippets, functions, or boilerplate based on user prompts or requirements
listeners:
- type: model
name: model_listener
port: 12000
tracing:
random_sampling: 100

View file

@ -31,6 +31,17 @@ MAX_TRACES = 50
MAX_SPANS_PER_TRACE = 500
class TraceListenerBindError(RuntimeError):
"""Raised when the OTLP/gRPC listener cannot bind to the requested address."""
def _trace_listener_bind_error_message(address: str) -> str:
return (
f"Failed to start OTLP listener on {address}: address is already in use.\n"
"Stop the process using that port or run `planoai trace listen --port <PORT>`."
)
@dataclass
class TraceSummary:
trace_id: str
@ -496,7 +507,15 @@ def _start_trace_server(host: str, grpc_port: int) -> grpc.Server:
trace_service_pb2_grpc.add_TraceServiceServicer_to_server(
_OTLPTraceServicer(), grpc_server
)
grpc_server.add_insecure_port(f"{host}:{grpc_port}")
address = f"{host}:{grpc_port}"
try:
bound_port = grpc_server.add_insecure_port(address)
except RuntimeError as exc:
raise TraceListenerBindError(
_trace_listener_bind_error_message(address)
) from exc
if bound_port == 0:
raise TraceListenerBindError(_trace_listener_bind_error_message(address))
grpc_server.start()
return grpc_server

View file

@ -68,19 +68,19 @@ def find_repo_root(start_path=None):
return None
def has_ingress_listener(arch_config_file):
"""Check if the arch config file has ingress_traffic listener configured."""
def has_ingress_listener(plano_config_file):
"""Check if the plano config file has ingress_traffic listener configured."""
try:
with open(arch_config_file) as f:
arch_config_dict = yaml.safe_load(f)
with open(plano_config_file) as f:
plano_config_dict = yaml.safe_load(f)
ingress_traffic = arch_config_dict.get("listeners", {}).get(
ingress_traffic = plano_config_dict.get("listeners", {}).get(
"ingress_traffic", {}
)
return bool(ingress_traffic)
except Exception as e:
log.error(f"Error reading config file {arch_config_file}: {e}")
log.error(f"Error reading config file {plano_config_file}: {e}")
return False
@ -154,34 +154,37 @@ def convert_legacy_listeners(
)
listener["model_providers"] = model_providers or []
model_provider_set = True
llm_gateway_listener = listener
# Merge user listener values into defaults for the Envoy template
llm_gateway_listener = {**llm_gateway_listener, **listener}
elif listener.get("type") == "prompt":
prompt_gateway_listener = {**prompt_gateway_listener, **listener}
if not model_provider_set:
listeners.append(llm_gateway_listener)
return listeners, llm_gateway_listener, prompt_gateway_listener
def get_llm_provider_access_keys(arch_config_file):
with open(arch_config_file, "r") as file:
arch_config = file.read()
arch_config_yaml = yaml.safe_load(arch_config)
def get_llm_provider_access_keys(plano_config_file):
with open(plano_config_file, "r") as file:
plano_config = file.read()
plano_config_yaml = yaml.safe_load(plano_config)
access_key_list = []
# Convert legacy llm_providers to model_providers
if "llm_providers" in arch_config_yaml:
if "model_providers" in arch_config_yaml:
if "llm_providers" in plano_config_yaml:
if "model_providers" in plano_config_yaml:
raise Exception(
"Please provide either llm_providers or model_providers, not both. llm_providers is deprecated, please use model_providers instead"
)
arch_config_yaml["model_providers"] = arch_config_yaml["llm_providers"]
del arch_config_yaml["llm_providers"]
plano_config_yaml["model_providers"] = plano_config_yaml["llm_providers"]
del plano_config_yaml["llm_providers"]
listeners, _, _ = convert_legacy_listeners(
arch_config_yaml.get("listeners"), arch_config_yaml.get("model_providers")
plano_config_yaml.get("listeners"), plano_config_yaml.get("model_providers")
)
for prompt_target in arch_config_yaml.get("prompt_targets", []):
for prompt_target in plano_config_yaml.get("prompt_targets", []):
for k, v in prompt_target.get("endpoint", {}).get("http_headers", {}).items():
if k.lower() == "authorization":
print(
@ -200,7 +203,7 @@ def get_llm_provider_access_keys(arch_config_file):
access_key_list.append(access_key)
# Extract environment variables from state_storage.connection_string
state_storage = arch_config_yaml.get("state_storage_v1_responses")
state_storage = plano_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):
@ -251,16 +254,16 @@ def find_config_file(path=".", file=None):
# If a file is provided, process that file
return os.path.abspath(file)
else:
# If no file is provided, use the path and look for arch_config.yaml first, then config.yaml for convenience
arch_config_file = os.path.abspath(os.path.join(path, "config.yaml"))
if not os.path.exists(arch_config_file):
arch_config_file = os.path.abspath(os.path.join(path, "arch_config.yaml"))
return arch_config_file
# If no file is provided, use the path and look for plano_config.yaml first, then config.yaml for convenience
plano_config_file = os.path.abspath(os.path.join(path, "config.yaml"))
if not os.path.exists(plano_config_file):
plano_config_file = os.path.abspath(os.path.join(path, "plano_config.yaml"))
return plano_config_file
def stream_access_logs(follow):
"""
Get the archgw access logs
Get the plano access logs
"""
follow_arg = "-f" if follow else ""

View file

@ -1,6 +1,6 @@
[project]
name = "planoai"
version = "0.4.6"
version = "0.4.7"
description = "Python-based CLI tool to manage Plano."
authors = [{name = "Katanemo Labs, Inc."}]
readme = "README.md"
@ -14,6 +14,7 @@ dependencies = [
"questionary>=2.1.1,<3.0.0",
"pyyaml>=6.0.2,<7.0.0",
"requests>=2.31.0,<3.0.0",
"urllib3>=2.6.3",
"rich>=14.2.0",
"rich-click>=1.9.5",
]

View file

@ -12,14 +12,14 @@ def cleanup_env(monkeypatch):
def test_validate_and_render_happy_path(monkeypatch):
monkeypatch.setenv("ARCH_CONFIG_FILE", "fake_arch_config.yaml")
monkeypatch.setenv("ARCH_CONFIG_SCHEMA_FILE", "fake_arch_config_schema.yaml")
monkeypatch.setenv("PLANO_CONFIG_FILE", "fake_plano_config.yaml")
monkeypatch.setenv("PLANO_CONFIG_SCHEMA_FILE", "fake_plano_config_schema.yaml")
monkeypatch.setenv("ENVOY_CONFIG_TEMPLATE_FILE", "./envoy.template.yaml")
monkeypatch.setenv("ARCH_CONFIG_FILE_RENDERED", "fake_arch_config_rendered.yaml")
monkeypatch.setenv("PLANO_CONFIG_FILE_RENDERED", "fake_plano_config_rendered.yaml")
monkeypatch.setenv("ENVOY_CONFIG_FILE_RENDERED", "fake_envoy.yaml")
monkeypatch.setenv("TEMPLATE_ROOT", "../")
arch_config = """
plano_config = """
version: v0.1.0
listeners:
@ -50,24 +50,24 @@ llm_providers:
tracing:
random_sampling: 100
"""
arch_config_schema = ""
with open("../config/arch_config_schema.yaml", "r") as file:
arch_config_schema = file.read()
plano_config_schema = ""
with open("../config/plano_config_schema.yaml", "r") as file:
plano_config_schema = file.read()
m_open = mock.mock_open()
# Provide enough file handles for all open() calls in validate_and_render_schema
m_open.side_effect = [
# Removed empty read - was causing validation failures
mock.mock_open(read_data=arch_config).return_value, # ARCH_CONFIG_FILE
mock.mock_open(read_data=plano_config).return_value, # PLANO_CONFIG_FILE
mock.mock_open(
read_data=arch_config_schema
).return_value, # ARCH_CONFIG_SCHEMA_FILE
mock.mock_open(read_data=arch_config).return_value, # ARCH_CONFIG_FILE
read_data=plano_config_schema
).return_value, # PLANO_CONFIG_SCHEMA_FILE
mock.mock_open(read_data=plano_config).return_value, # PLANO_CONFIG_FILE
mock.mock_open(
read_data=arch_config_schema
).return_value, # ARCH_CONFIG_SCHEMA_FILE
read_data=plano_config_schema
).return_value, # PLANO_CONFIG_SCHEMA_FILE
mock.mock_open().return_value, # ENVOY_CONFIG_FILE_RENDERED (write)
mock.mock_open().return_value, # ARCH_CONFIG_FILE_RENDERED (write)
mock.mock_open().return_value, # PLANO_CONFIG_FILE_RENDERED (write)
]
with mock.patch("builtins.open", m_open):
with mock.patch("planoai.config_generator.Environment"):
@ -75,14 +75,14 @@ tracing:
def test_validate_and_render_happy_path_agent_config(monkeypatch):
monkeypatch.setenv("ARCH_CONFIG_FILE", "fake_arch_config.yaml")
monkeypatch.setenv("ARCH_CONFIG_SCHEMA_FILE", "fake_arch_config_schema.yaml")
monkeypatch.setenv("PLANO_CONFIG_FILE", "fake_plano_config.yaml")
monkeypatch.setenv("PLANO_CONFIG_SCHEMA_FILE", "fake_plano_config_schema.yaml")
monkeypatch.setenv("ENVOY_CONFIG_TEMPLATE_FILE", "./envoy.template.yaml")
monkeypatch.setenv("ARCH_CONFIG_FILE_RENDERED", "fake_arch_config_rendered.yaml")
monkeypatch.setenv("PLANO_CONFIG_FILE_RENDERED", "fake_plano_config_rendered.yaml")
monkeypatch.setenv("ENVOY_CONFIG_FILE_RENDERED", "fake_envoy.yaml")
monkeypatch.setenv("TEMPLATE_ROOT", "../")
arch_config = """
plano_config = """
version: v0.3.0
agents:
@ -123,35 +123,35 @@ model_providers:
- access_key: ${OPENAI_API_KEY}
model: openai/gpt-4o
"""
arch_config_schema = ""
with open("../config/arch_config_schema.yaml", "r") as file:
arch_config_schema = file.read()
plano_config_schema = ""
with open("../config/plano_config_schema.yaml", "r") as file:
plano_config_schema = file.read()
m_open = mock.mock_open()
# Provide enough file handles for all open() calls in validate_and_render_schema
m_open.side_effect = [
# Removed empty read - was causing validation failures
mock.mock_open(read_data=arch_config).return_value, # ARCH_CONFIG_FILE
mock.mock_open(read_data=plano_config).return_value, # PLANO_CONFIG_FILE
mock.mock_open(
read_data=arch_config_schema
).return_value, # ARCH_CONFIG_SCHEMA_FILE
mock.mock_open(read_data=arch_config).return_value, # ARCH_CONFIG_FILE
read_data=plano_config_schema
).return_value, # PLANO_CONFIG_SCHEMA_FILE
mock.mock_open(read_data=plano_config).return_value, # PLANO_CONFIG_FILE
mock.mock_open(
read_data=arch_config_schema
).return_value, # ARCH_CONFIG_SCHEMA_FILE
read_data=plano_config_schema
).return_value, # PLANO_CONFIG_SCHEMA_FILE
mock.mock_open().return_value, # ENVOY_CONFIG_FILE_RENDERED (write)
mock.mock_open().return_value, # ARCH_CONFIG_FILE_RENDERED (write)
mock.mock_open().return_value, # PLANO_CONFIG_FILE_RENDERED (write)
]
with mock.patch("builtins.open", m_open):
with mock.patch("planoai.config_generator.Environment"):
validate_and_render_schema()
arch_config_test_cases = [
plano_config_test_cases = [
{
"id": "duplicate_provider_name",
"expected_error": "Duplicate model_provider name",
"arch_config": """
"plano_config": """
version: v0.1.0
listeners:
@ -176,7 +176,7 @@ llm_providers:
{
"id": "provider_interface_with_model_id",
"expected_error": "Please provide provider interface as part of model name",
"arch_config": """
"plano_config": """
version: v0.1.0
listeners:
@ -197,7 +197,7 @@ llm_providers:
{
"id": "duplicate_model_id",
"expected_error": "Duplicate model_id",
"arch_config": """
"plano_config": """
version: v0.1.0
listeners:
@ -219,7 +219,7 @@ llm_providers:
{
"id": "custom_provider_base_url",
"expected_error": "Must provide base_url and provider_interface",
"arch_config": """
"plano_config": """
version: v0.1.0
listeners:
@ -237,7 +237,7 @@ llm_providers:
{
"id": "base_url_with_path_prefix",
"expected_error": None,
"arch_config": """
"plano_config": """
version: v0.1.0
listeners:
@ -258,7 +258,7 @@ llm_providers:
{
"id": "duplicate_routeing_preference_name",
"expected_error": "Duplicate routing preference name",
"arch_config": """
"plano_config": """
version: v0.1.0
listeners:
@ -295,42 +295,42 @@ tracing:
@pytest.mark.parametrize(
"arch_config_test_case",
arch_config_test_cases,
ids=[case["id"] for case in arch_config_test_cases],
"plano_config_test_case",
plano_config_test_cases,
ids=[case["id"] for case in plano_config_test_cases],
)
def test_validate_and_render_schema_tests(monkeypatch, arch_config_test_case):
monkeypatch.setenv("ARCH_CONFIG_FILE", "fake_arch_config.yaml")
monkeypatch.setenv("ARCH_CONFIG_SCHEMA_FILE", "fake_arch_config_schema.yaml")
def test_validate_and_render_schema_tests(monkeypatch, plano_config_test_case):
monkeypatch.setenv("PLANO_CONFIG_FILE", "fake_plano_config.yaml")
monkeypatch.setenv("PLANO_CONFIG_SCHEMA_FILE", "fake_plano_config_schema.yaml")
monkeypatch.setenv("ENVOY_CONFIG_TEMPLATE_FILE", "./envoy.template.yaml")
monkeypatch.setenv("ARCH_CONFIG_FILE_RENDERED", "fake_arch_config_rendered.yaml")
monkeypatch.setenv("PLANO_CONFIG_FILE_RENDERED", "fake_plano_config_rendered.yaml")
monkeypatch.setenv("ENVOY_CONFIG_FILE_RENDERED", "fake_envoy.yaml")
monkeypatch.setenv("TEMPLATE_ROOT", "../")
arch_config = arch_config_test_case["arch_config"]
expected_error = arch_config_test_case.get("expected_error")
plano_config = plano_config_test_case["plano_config"]
expected_error = plano_config_test_case.get("expected_error")
arch_config_schema = ""
with open("../config/arch_config_schema.yaml", "r") as file:
arch_config_schema = file.read()
plano_config_schema = ""
with open("../config/plano_config_schema.yaml", "r") as file:
plano_config_schema = file.read()
m_open = mock.mock_open()
# Provide enough file handles for all open() calls in validate_and_render_schema
m_open.side_effect = [
mock.mock_open(
read_data=arch_config
).return_value, # validate_prompt_config: ARCH_CONFIG_FILE
read_data=plano_config
).return_value, # validate_prompt_config: PLANO_CONFIG_FILE
mock.mock_open(
read_data=arch_config_schema
).return_value, # validate_prompt_config: ARCH_CONFIG_SCHEMA_FILE
read_data=plano_config_schema
).return_value, # validate_prompt_config: PLANO_CONFIG_SCHEMA_FILE
mock.mock_open(
read_data=arch_config
).return_value, # validate_and_render_schema: ARCH_CONFIG_FILE
read_data=plano_config
).return_value, # validate_and_render_schema: PLANO_CONFIG_FILE
mock.mock_open(
read_data=arch_config_schema
).return_value, # validate_and_render_schema: ARCH_CONFIG_SCHEMA_FILE
read_data=plano_config_schema
).return_value, # validate_and_render_schema: PLANO_CONFIG_SCHEMA_FILE
mock.mock_open().return_value, # ENVOY_CONFIG_FILE_RENDERED (write)
mock.mock_open().return_value, # ARCH_CONFIG_FILE_RENDERED (write)
mock.mock_open().return_value, # PLANO_CONFIG_FILE_RENDERED (write)
]
with mock.patch("builtins.open", m_open):
with mock.patch("planoai.config_generator.Environment"):

View file

@ -26,7 +26,7 @@ def test_init_template_builtin_writes_config(tmp_path, monkeypatch):
config_path = tmp_path / "config.yaml"
assert config_path.exists()
config_text = config_path.read_text(encoding="utf-8")
assert "llm_providers:" in config_text
assert "model_providers:" in config_text
def test_init_refuses_overwrite_without_force(tmp_path, monkeypatch):

View file

@ -67,6 +67,31 @@ def failure_traces() -> list[dict]:
return copy.deepcopy(_load_failure_traces())
class _FakeGrpcServer:
def add_insecure_port(self, _address: str) -> int:
raise RuntimeError("bind failed")
def start(self) -> None:
return None
def test_start_trace_server_raises_bind_error(monkeypatch):
monkeypatch.setattr(
trace_cmd.grpc, "server", lambda *_args, **_kwargs: _FakeGrpcServer()
)
monkeypatch.setattr(
trace_cmd.trace_service_pb2_grpc,
"add_TraceServiceServicer_to_server",
lambda *_args, **_kwargs: None,
)
with pytest.raises(trace_cmd.TraceListenerBindError) as excinfo:
trace_cmd._start_trace_server("0.0.0.0", 4317)
assert "already in use" in str(excinfo.value)
assert "planoai trace listen --port" in str(excinfo.value)
def test_trace_listen_starts_listener_with_custom_bind_after_target(
runner, monkeypatch
):

8
cli/uv.lock generated
View file

@ -350,6 +350,7 @@ dependencies = [
{ name = "requests" },
{ name = "rich" },
{ name = "rich-click" },
{ name = "urllib3" },
]
[package.optional-dependencies]
@ -375,6 +376,7 @@ requires-dist = [
{ name = "requests", specifier = ">=2.31.0,<3.0.0" },
{ name = "rich", specifier = ">=14.2.0" },
{ name = "rich-click", specifier = ">=1.9.5" },
{ name = "urllib3", specifier = ">=2.6.3" },
]
provides-extras = ["dev"]
@ -742,11 +744,11 @@ wheels = [
[[package]]
name = "urllib3"
version = "2.5.0"
version = "2.6.3"
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, upload-time = "2025-06-18T14:07:41.644Z" }
sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" },
{ url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" },
]
[[package]]

View file

@ -8,14 +8,14 @@ services:
- "12000:12000"
- "19901:9901"
volumes:
- ${ARCH_CONFIG_FILE:-../demos/samples_python/weather_forecast/arch_config.yaml}:/app/arch_config.yaml
- ${PLANO_CONFIG_FILE:-../demos/getting_started/weather_forecast/plano_config.yaml}:/app/plano_config.yaml
- /etc/ssl/cert.pem:/etc/ssl/cert.pem
- ./envoy.template.yaml:/app/envoy.template.yaml
- ./arch_config_schema.yaml:/app/arch_config_schema.yaml
- ./plano_config_schema.yaml:/app/plano_config_schema.yaml
- ../cli/planoai/config_generator.py:/app/planoai/config_generator.py
- ../crates/target/wasm32-wasip1/release/llm_gateway.wasm:/etc/envoy/proxy-wasm-plugins/llm_gateway.wasm
- ../crates/target/wasm32-wasip1/release/prompt_gateway.wasm:/etc/envoy/proxy-wasm-plugins/prompt_gateway.wasm
- ~/archgw_logs:/var/log/
- ~/plano_logs:/var/log/
extra_hosts:
- "host.docker.internal:host-gateway"
environment:

View file

@ -40,7 +40,7 @@ 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 %}
{% if "random_sampling" in plano_tracing and plano_tracing["random_sampling"] > 0 %}
generate_request_id: true
tracing:
provider:
@ -53,7 +53,7 @@ static_resources:
timeout: 0.250s
service_name: plano(inbound)
random_sampling:
value: {{ arch_tracing.random_sampling }}
value: {{ plano_tracing.random_sampling }}
operation: "%REQ(:METHOD)% %REQ(X-ENVOY-ORIGINAL-PATH?:PATH)%"
{% endif %}
stat_prefix: plano(inbound)
@ -114,7 +114,7 @@ static_resources:
domains:
- "*"
routes:
{% for provider in arch_model_providers %}
{% for provider in plano_model_providers %}
# if endpoint is set then use custom cluster for upstream llm
{% if provider.endpoint %}
{% set llm_cluster_name = provider.cluster_name %}
@ -166,7 +166,7 @@ static_resources:
configuration:
"@type": "type.googleapis.com/google.protobuf.StringValue"
value: |
{{ arch_config | indent(32) }}
{{ plano_config | indent(32) }}
vm_config:
runtime: "envoy.wasm.runtime.v8"
code:
@ -183,7 +183,7 @@ static_resources:
configuration:
"@type": "type.googleapis.com/google.protobuf.StringValue"
value: |
{{ arch_llm_config | indent(32) }}
{{ plano_llm_config | indent(32) }}
vm_config:
runtime: "envoy.wasm.runtime.v8"
code:
@ -215,7 +215,7 @@ 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 %}
# {% if "random_sampling" in plano_tracing and plano_tracing["random_sampling"] > 0 %}
# generate_request_id: true
# tracing:
# provider:
@ -228,7 +228,7 @@ static_resources:
# timeout: 0.250s
# service_name: tools
# random_sampling:
# value: {{ arch_tracing.random_sampling }}
# value: {{ plano_tracing.random_sampling }}
# {% endif %}
stat_prefix: outbound_api_traffic
codec_type: AUTO
@ -258,7 +258,7 @@ static_resources:
auto_host_rewrite: true
cluster: bright_staff
timeout: 300s
{% for cluster_name, cluster in arch_clusters.items() %}
{% for cluster_name, cluster in plano_clusters.items() %}
- match:
prefix: "/"
headers:
@ -290,7 +290,7 @@ 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 %}
{% if "random_sampling" in plano_tracing and plano_tracing["random_sampling"] > 0 %}
generate_request_id: true
tracing:
provider:
@ -303,7 +303,7 @@ static_resources:
timeout: 0.250s
service_name: plano(inbound)
random_sampling:
value: {{ arch_tracing.random_sampling }}
value: {{ plano_tracing.random_sampling }}
operation: "%REQ(:METHOD)% %REQ(X-ENVOY-ORIGINAL-PATH?:PATH)%"
{% endif %}
stat_prefix: {{ listener.name | replace(" ", "_") }}_traffic
@ -467,7 +467,7 @@ 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 %}
{% if "random_sampling" in plano_tracing and plano_tracing["random_sampling"] > 0 %}
generate_request_id: true
tracing:
provider:
@ -480,7 +480,7 @@ static_resources:
timeout: 0.250s
service_name: plano(outbound)
random_sampling:
value: {{ arch_tracing.random_sampling }}
value: {{ plano_tracing.random_sampling }}
operation: "%REQ(:METHOD)% %REQ(:AUTHORITY)%%REQ(X-ENVOY-ORIGINAL-PATH?:PATH)%"
{% endif %}
stat_prefix: egress_traffic
@ -501,7 +501,7 @@ static_resources:
domains:
- "*"
routes:
{% for provider in arch_model_providers %}
{% for provider in plano_model_providers %}
# if endpoint is set then use custom cluster for upstream llm
{% if provider.endpoint %}
{% set llm_cluster_name = provider.cluster_name %}
@ -564,7 +564,7 @@ static_resources:
configuration:
"@type": "type.googleapis.com/google.protobuf.StringValue"
value: |
{{ arch_llm_config | indent(32) }}
{{ plano_llm_config | indent(32) }}
vm_config:
runtime: "envoy.wasm.runtime.v8"
code:
@ -879,7 +879,7 @@ static_resources:
address: mistral_7b_instruct
port_value: 10001
hostname: "mistral_7b_instruct"
{% for cluster_name, cluster in arch_clusters.items() %}
{% for cluster_name, cluster in plano_clusters.items() %}
- name: {{ cluster_name }}
{% if cluster.connect_timeout -%}
connect_timeout: {{ cluster.connect_timeout }}
@ -1013,7 +1013,7 @@ static_resources:
port_value: 12001
hostname: arch_listener_llm
{% if "random_sampling" in arch_tracing and arch_tracing["random_sampling"] > 0 %}
{% if "random_sampling" in plano_tracing and plano_tracing["random_sampling"] > 0 %}
- name: opentelemetry_collector
type: STRICT_DNS
dns_lookup_family: V4_ONLY
@ -1030,7 +1030,7 @@ static_resources:
- endpoint:
address:
socket_address:
{% set _otel_endpoint = arch_tracing.opentracing_grpc_endpoint | default('host.docker.internal:4317') | replace("http://", "") | replace("https://", "") %}
{% set _otel_endpoint = plano_tracing.opentracing_grpc_endpoint | default('host.docker.internal:4317') | replace("http://", "") | replace("https://", "") %}
address: {{ _otel_endpoint.split(":") | first }}
port_value: {{ _otel_endpoint.split(":") | last }}
{% endif %}

View file

@ -3,9 +3,9 @@ nodaemon=true
[program:brightstaff]
command=sh -c "\
envsubst < /app/arch_config_rendered.yaml > /app/arch_config_rendered.env_sub.yaml && \
envsubst < /app/plano_config_rendered.yaml > /app/plano_config_rendered.env_sub.yaml && \
RUST_LOG=${LOG_LEVEL:-info} \
ARCH_CONFIG_PATH_RENDERED=/app/arch_config_rendered.env_sub.yaml \
PLANO_CONFIG_PATH_RENDERED=/app/plano_config_rendered.env_sub.yaml \
/app/brightstaff 2>&1 | \
tee /var/log/brightstaff.log | \
while IFS= read -r line; do echo '[brightstaff]' \"$line\"; done"

View file

@ -7,7 +7,7 @@
#
# To test:
# docker build -t plano-passthrough-test .
# docker run -d -p 10000:10000 -v $(pwd)/config/test_passthrough.yaml:/app/arch_config.yaml plano-passthrough-test
# docker run -d -p 10000:10000 -v $(pwd)/config/test_passthrough.yaml:/app/plano_config.yaml plano-passthrough-test
#
# curl http://localhost:10000/v1/chat/completions \
# -H "Authorization: Bearer sk-your-virtual-key" \

View file

@ -2,10 +2,10 @@
failed_files=()
for file in $(find . -name config.yaml -o -name arch_config_full_reference.yaml); do
for file in $(find . -name config.yaml -o -name plano_config_full_reference.yaml); do
echo "Validating ${file}..."
touch $(pwd)/${file}_rendered
if ! docker run --rm -v "$(pwd)/${file}:/app/arch_config.yaml:ro" -v "$(pwd)/${file}_rendered:/app/arch_config_rendered.yaml:rw" --entrypoint /bin/sh katanemo/plano:0.4.6 -c "python -m planoai.config_generator" 2>&1 > /dev/null ; then
if ! docker run --rm -v "$(pwd)/${file}:/app/plano_config.yaml:ro" -v "$(pwd)/${file}_rendered:/app/plano_config_rendered.yaml:rw" --entrypoint /bin/sh ${PLANO_DOCKER_IMAGE:-katanemo/plano:0.4.7} -c "python -m planoai.config_generator" 2>&1 > /dev/null ; then
echo "Validation failed for $file"
failed_files+=("$file")
fi

54
crates/Cargo.lock generated
View file

@ -372,9 +372,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "bytes"
version = "1.10.1"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
[[package]]
name = "bytes-utils"
@ -428,7 +428,7 @@ version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e"
dependencies = [
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@ -577,9 +577,9 @@ dependencies = [
[[package]]
name = "deranged"
version = "0.4.0"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e"
checksum = "d630bccd429a5bb5a64b5e94f693bfc48c9f8566418fda4c494cc94f911f87cc"
dependencies = [
"powerfmt",
"serde",
@ -707,7 +707,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18"
dependencies = [
"libc",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@ -1720,9 +1720,9 @@ dependencies = [
[[package]]
name = "num-conv"
version = "0.1.0"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050"
[[package]]
name = "num-integer"
@ -2166,7 +2166,7 @@ dependencies = [
"once_cell",
"socket2",
"tracing",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@ -2410,7 +2410,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@ -2616,18 +2616,28 @@ dependencies = [
[[package]]
name = "serde"
version = "1.0.219"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
name = "serde_core"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.219"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
@ -2927,7 +2937,7 @@ dependencies = [
"getrandom 0.3.3",
"once_cell",
"rustix",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@ -2997,30 +3007,30 @@ dependencies = [
[[package]]
name = "time"
version = "0.3.41"
version = "0.3.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40"
checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c"
dependencies = [
"deranged",
"itoa",
"num-conv",
"powerfmt",
"serde",
"serde_core",
"time-core",
"time-macros",
]
[[package]]
name = "time-core"
version = "0.1.4"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c"
checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca"
[[package]]
name = "time-macros"
version = "0.2.22"
version = "0.2.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49"
checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215"
dependencies = [
"num-conv",
"time-core",

View file

@ -210,8 +210,8 @@ async fn llm_chat_inner(
// Set the model to just the model name (without provider prefix)
// This ensures upstream receives "gpt-4" not "openai/gpt-4"
client_request.set_model(model_name_only.clone());
if client_request.remove_metadata_key("archgw_preference_config") {
debug!("removed archgw_preference_config from metadata");
if client_request.remove_metadata_key("plano_preference_config") {
debug!("removed plano_preference_config from metadata");
}
// === v1/responses state management: Determine upstream API and combine input if needed ===

View file

@ -78,7 +78,7 @@ pub async fn router_chat_get_upstream_model(
// Extract usage preferences from metadata
let usage_preferences_str: Option<String> = routing_metadata.as_ref().and_then(|metadata| {
metadata
.get("archgw_preference_config")
.get("plano_preference_config")
.map(|value| value.to_string())
});

View file

@ -52,57 +52,57 @@ fn empty() -> BoxBody<Bytes, hyper::Error> {
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let bind_address = env::var("BIND_ADDRESS").unwrap_or_else(|_| BIND_ADDRESS.to_string());
// loading arch_config.yaml file (before tracing init so we can read tracing config)
let arch_config_path = env::var("ARCH_CONFIG_PATH_RENDERED")
.unwrap_or_else(|_| "./arch_config_rendered.yaml".to_string());
eprintln!("loading arch_config.yaml from {}", arch_config_path);
// loading plano_config.yaml file (before tracing init so we can read tracing config)
let plano_config_path = env::var("PLANO_CONFIG_PATH_RENDERED")
.unwrap_or_else(|_| "./plano_config_rendered.yaml".to_string());
eprintln!("loading plano_config.yaml from {}", plano_config_path);
let config_contents =
fs::read_to_string(&arch_config_path).expect("Failed to read arch_config.yaml");
fs::read_to_string(&plano_config_path).expect("Failed to read plano_config.yaml");
let config: Configuration =
serde_yaml::from_str(&config_contents).expect("Failed to parse arch_config.yaml");
serde_yaml::from_str(&config_contents).expect("Failed to parse plano_config.yaml");
// Initialize tracing using config.yaml tracing section
let _tracer_provider = init_tracer(config.tracing.as_ref());
info!(path = %arch_config_path, "loaded arch_config.yaml");
info!(path = %plano_config_path, "loaded plano_config.yaml");
let arch_config = Arc::new(config);
let plano_config = Arc::new(config);
// combine agents and filters into a single list of agents
let all_agents: Vec<Agent> = arch_config
let all_agents: Vec<Agent> = plano_config
.agents
.as_deref()
.unwrap_or_default()
.iter()
.chain(arch_config.filters.as_deref().unwrap_or_default())
.chain(plano_config.filters.as_deref().unwrap_or_default())
.cloned()
.collect();
// Create expanded provider list for /v1/models endpoint
let llm_providers = LlmProviders::try_from(arch_config.model_providers.clone())
let llm_providers = LlmProviders::try_from(plano_config.model_providers.clone())
.expect("Failed to create LlmProviders");
let llm_providers = Arc::new(RwLock::new(llm_providers));
let combined_agents_filters_list = Arc::new(RwLock::new(Some(all_agents)));
let listeners = Arc::new(RwLock::new(arch_config.listeners.clone()));
let listeners = Arc::new(RwLock::new(plano_config.listeners.clone()));
let llm_provider_url =
env::var("LLM_PROVIDER_ENDPOINT").unwrap_or_else(|_| "http://localhost:12001".to_string());
let listener = TcpListener::bind(bind_address).await?;
let routing_model_name: String = arch_config
let routing_model_name: String = plano_config
.routing
.as_ref()
.and_then(|r| r.model.clone())
.unwrap_or_else(|| DEFAULT_ROUTING_MODEL_NAME.to_string());
let routing_llm_provider = arch_config
let routing_llm_provider = plano_config
.routing
.as_ref()
.and_then(|r| r.model_provider.clone())
.unwrap_or_else(|| DEFAULT_ROUTING_LLM_PROVIDER.to_string());
let router_service: Arc<RouterService> = Arc::new(RouterService::new(
arch_config.model_providers.clone(),
plano_config.model_providers.clone(),
format!("{llm_provider_url}{CHAT_COMPLETIONS_PATH}"),
routing_model_name,
routing_llm_provider,
@ -113,19 +113,19 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
PLANO_ORCHESTRATOR_MODEL_NAME.to_string(),
));
let model_aliases = Arc::new(arch_config.model_aliases.clone());
let model_aliases = Arc::new(plano_config.model_aliases.clone());
// Initialize trace collector and start background flusher
// Tracing is enabled if the tracing config is present in arch_config.yaml
// Tracing is enabled if the tracing config is present in plano_config.yaml
// Pass Some(true/false) to override, or None to use env var OTEL_TRACING_ENABLED
// OpenTelemetry automatic instrumentation is configured in utils/tracing.rs
// Initialize conversation state storage for v1/responses
// Configurable via arch_config.yaml state_storage section
// Configurable via plano_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<Arc<dyn StateStorage>> =
if let Some(storage_config) = &arch_config.state_storage {
if let Some(storage_config) = &plano_config.state_storage {
let storage: Arc<dyn StateStorage> = match storage_config.storage_type {
common::configuration::StateStorageType::Memory => {
info!(

View file

@ -182,7 +182,7 @@ pub mod signals {
// Operation Names
// =============================================================================
/// Canonical operation name components for Arch Gateway
/// Canonical operation name components for Plano Gateway
pub mod operation_component {
/// Inbound request handling
pub const INBOUND: &str = "plano(inbound)";
@ -210,7 +210,7 @@ pub mod operation_component {
///
/// Format: `{method} {path} {target}`
///
/// The operation component (e.g., "archgw(llm)") is now part of the service name,
/// The operation component (e.g., "plano(llm)") is now part of the service name,
/// so the operation name focuses on the HTTP request details and target.
///
/// # Examples
@ -218,7 +218,7 @@ pub mod operation_component {
/// use brightstaff::tracing::OperationNameBuilder;
///
/// // LLM call operation: "POST /v1/chat/completions gpt-4"
/// // (service name will be "archgw(llm)")
/// // (service name will be "plano(llm)")
/// let op = OperationNameBuilder::new()
/// .with_method("POST")
/// .with_path("/v1/chat/completions")
@ -226,7 +226,7 @@ pub mod operation_component {
/// .build();
///
/// // Agent filter operation: "POST /agents/v1/chat/completions hallucination-detector"
/// // (service name will be "archgw(agent filter)")
/// // (service name will be "plano(agent filter)")
/// let op = OperationNameBuilder::new()
/// .with_method("POST")
/// .with_path("/agents/v1/chat/completions")
@ -234,7 +234,7 @@ pub mod operation_component {
/// .build();
///
/// // Routing operation: "POST /v1/chat/completions"
/// // (service name will be "archgw(routing)")
/// // (service name will be "plano(routing)")
/// let op = OperationNameBuilder::new()
/// .with_method("POST")
/// .with_path("/v1/chat/completions")

View file

@ -493,7 +493,7 @@ mod test {
#[test]
fn test_deserialize_configuration() {
let ref_config = fs::read_to_string(
"../../docs/source/resources/includes/arch_config_full_reference_rendered.yaml",
"../../docs/source/resources/includes/plano_config_full_reference_rendered.yaml",
)
.expect("reference config file not found");
@ -520,7 +520,7 @@ mod test {
#[test]
fn test_tool_conversion() {
let ref_config = fs::read_to_string(
"../../docs/source/resources/includes/arch_config_full_reference_rendered.yaml",
"../../docs/source/resources/includes/plano_config_full_reference_rendered.yaml",
)
.expect("reference config file not found");
let config: super::Configuration = serde_yaml::from_str(&ref_config).unwrap();

View file

@ -1,7 +1,219 @@
version: '1.0'
source: canonical-apis
providers:
mistralai:
- mistralai/mistral-medium-2505
- mistralai/mistral-medium-2508
- mistralai/mistral-medium-latest
- mistralai/mistral-medium
- mistralai/mistral-vibe-cli-with-tools
- mistralai/open-mistral-nemo
- mistralai/open-mistral-nemo-2407
- mistralai/mistral-tiny-2407
- mistralai/mistral-tiny-latest
- mistralai/mistral-large-2411
- mistralai/pixtral-large-2411
- mistralai/pixtral-large-latest
- mistralai/mistral-large-pixtral-2411
- mistralai/codestral-2508
- mistralai/codestral-latest
- mistralai/devstral-small-2507
- mistralai/devstral-medium-2507
- mistralai/devstral-2512
- mistralai/mistral-vibe-cli-latest
- mistralai/devstral-medium-latest
- mistralai/devstral-latest
- mistralai/labs-devstral-small-2512
- mistralai/devstral-small-latest
- mistralai/mistral-small-2506
- mistralai/mistral-small-latest
- mistralai/labs-mistral-small-creative
- mistralai/magistral-medium-2509
- mistralai/magistral-medium-latest
- mistralai/magistral-small-2509
- mistralai/magistral-small-latest
- mistralai/mistral-large-2512
- mistralai/mistral-large-latest
- mistralai/ministral-3b-2512
- mistralai/ministral-3b-latest
- mistralai/ministral-8b-2512
- mistralai/ministral-8b-latest
- mistralai/ministral-14b-2512
- mistralai/ministral-14b-latest
- mistralai/mistral-small-2501
- mistralai/mistral-embed-2312
- mistralai/mistral-embed
- mistralai/codestral-embed
- mistralai/codestral-embed-2505
openai:
- openai/gpt-4-0613
- openai/gpt-4
- openai/gpt-3.5-turbo
- openai/gpt-5.2-codex
- openai/gpt-3.5-turbo-instruct
- openai/gpt-3.5-turbo-instruct-0914
- openai/gpt-4-1106-preview
- openai/gpt-3.5-turbo-1106
- openai/gpt-4-0125-preview
- openai/gpt-4-turbo-preview
- openai/gpt-3.5-turbo-0125
- openai/gpt-4-turbo
- openai/gpt-4-turbo-2024-04-09
- openai/gpt-4o
- openai/gpt-4o-2024-05-13
- openai/gpt-4o-mini-2024-07-18
- openai/gpt-4o-mini
- openai/gpt-4o-2024-08-06
- openai/chatgpt-4o-latest
- openai/o1-2024-12-17
- openai/o1
- openai/computer-use-preview
- openai/o3-mini
- openai/o3-mini-2025-01-31
- openai/gpt-4o-2024-11-20
- openai/computer-use-preview-2025-03-11
- openai/gpt-4o-search-preview-2025-03-11
- openai/gpt-4o-search-preview
- openai/gpt-4o-mini-search-preview-2025-03-11
- openai/gpt-4o-mini-search-preview
- openai/o1-pro-2025-03-19
- openai/o1-pro
- openai/o3-2025-04-16
- openai/o4-mini-2025-04-16
- openai/o3
- openai/o4-mini
- openai/gpt-4.1-2025-04-14
- openai/gpt-4.1
- openai/gpt-4.1-mini-2025-04-14
- openai/gpt-4.1-mini
- openai/gpt-4.1-nano-2025-04-14
- openai/gpt-4.1-nano
- openai/o3-pro
- openai/o3-pro-2025-06-10
- openai/o4-mini-deep-research
- openai/o3-deep-research
- openai/o3-deep-research-2025-06-26
- openai/o4-mini-deep-research-2025-06-26
- openai/gpt-5-chat-latest
- openai/gpt-5-2025-08-07
- openai/gpt-5
- openai/gpt-5-mini-2025-08-07
- openai/gpt-5-mini
- openai/gpt-5-nano-2025-08-07
- openai/gpt-5-nano
- openai/gpt-5-codex
- openai/gpt-5-pro-2025-10-06
- openai/gpt-5-pro
- openai/gpt-5-search-api
- openai/gpt-5-search-api-2025-10-14
- openai/gpt-5.1-chat-latest
- openai/gpt-5.1-2025-11-13
- openai/gpt-5.1
- openai/gpt-5.1-codex
- openai/gpt-5.1-codex-mini
- openai/gpt-5.1-codex-max
- openai/gpt-5.2-2025-12-11
- openai/gpt-5.2
- openai/gpt-5.2-pro-2025-12-11
- openai/gpt-5.2-pro
- openai/gpt-5.2-chat-latest
- openai/gpt-3.5-turbo-16k
- openai/ft:gpt-3.5-turbo-0613:katanemo::8CMZbm0P
deepseek:
- deepseek/deepseek-chat
- deepseek/deepseek-reasoner
x-ai:
- x-ai/grok-2-vision-1212
- x-ai/grok-3
- x-ai/grok-3-mini
- x-ai/grok-4-0709
- x-ai/grok-4-1-fast-non-reasoning
- x-ai/grok-4-1-fast-reasoning
- x-ai/grok-4-fast-non-reasoning
- x-ai/grok-4-fast-reasoning
- x-ai/grok-code-fast-1
- x-ai/grok-imagine-image
- x-ai/grok-imagine-video
moonshotai:
- moonshotai/kimi-k2-thinking
- moonshotai/kimi-k2.5
- moonshotai/moonshot-v1-128k-vision-preview
- moonshotai/moonshot-v1-8k
- moonshotai/kimi-k2-turbo-preview
- moonshotai/moonshot-v1-128k
- moonshotai/moonshot-v1-32k-vision-preview
- moonshotai/kimi-k2-thinking-turbo
- moonshotai/kimi-latest
- moonshotai/moonshot-v1-32k
- moonshotai/moonshot-v1-auto
- moonshotai/kimi-k2-0711-preview
- moonshotai/kimi-k2-0905-preview
- moonshotai/moonshot-v1-8k-vision-preview
anthropic:
- anthropic/claude-opus-4-6
- anthropic/claude-opus-4-5-20251101
- anthropic/claude-opus-4-5
- anthropic/claude-haiku-4-5-20251001
- anthropic/claude-haiku-4-5
- anthropic/claude-sonnet-4-5-20250929
- anthropic/claude-sonnet-4-5
- anthropic/claude-opus-4-1-20250805
- anthropic/claude-opus-4-1
- anthropic/claude-opus-4-20250514
- anthropic/claude-opus-4
- anthropic/claude-sonnet-4-20250514
- anthropic/claude-sonnet-4
- anthropic/claude-3-7-sonnet-20250219
- anthropic/claude-3-7-sonnet
- anthropic/claude-3-5-haiku-20241022
- anthropic/claude-3-5-haiku
- anthropic/claude-3-haiku-20240307
- anthropic/claude-3-haiku
google:
- google/gemini-2.5-flash
- google/gemini-2.5-pro
- google/gemini-2.0-flash
- google/gemini-2.0-flash-001
- google/gemini-2.0-flash-exp-image-generation
- google/gemini-2.0-flash-lite-001
- google/gemini-2.0-flash-lite
- google/gemini-exp-1206
- google/gemini-2.5-flash-preview-tts
- google/gemini-2.5-pro-preview-tts
- google/gemma-3-1b-it
- google/gemma-3-4b-it
- google/gemma-3-12b-it
- google/gemma-3-27b-it
- google/gemma-3n-e4b-it
- google/gemma-3n-e2b-it
- google/gemini-flash-latest
- google/gemini-flash-lite-latest
- google/gemini-pro-latest
- google/gemini-2.5-flash-lite
- google/gemini-2.5-flash-image
- google/gemini-2.5-flash-preview-09-2025
- google/gemini-2.5-flash-lite-preview-09-2025
- google/gemini-3-pro-preview
- google/gemini-3-flash-preview
- google/gemini-3-pro-image-preview
- google/nano-banana-pro-preview
- google/gemini-robotics-er-1.5-preview
- google/gemini-2.5-computer-use-preview-10-2025
- google/deep-research-pro-preview-12-2025
amazon:
- amazon/amazon.nova-pro-v1:0
- amazon/amazon.nova-2-lite-v1:0
- amazon/amazon.nova-2-sonic-v1:0
- amazon/amazon.titan-tg1-large
- amazon/amazon.nova-premier-v1:0:8k
- amazon/amazon.nova-premier-v1:0:20k
- amazon/amazon.nova-premier-v1:0:1000k
- amazon/amazon.nova-premier-v1:0:mm
- amazon/amazon.nova-premier-v1:0
- amazon/amazon.nova-lite-v1:0
- amazon/amazon.nova-micro-v1:0
qwen:
- qwen/qwen3-vl-flash-2026-01-22
- qwen/qwen3-max-2026-01-23
- qwen/qwen-plus-character
- qwen/qwen-flash-character
@ -82,234 +294,13 @@ providers:
- qwen/qwen-max
- qwen/qwen-plus
- qwen/qwen-turbo
openai:
- openai/gpt-4-0613
- openai/gpt-4
- openai/gpt-3.5-turbo
- openai/gpt-5.2-codex
- openai/gpt-3.5-turbo-instruct
- openai/gpt-3.5-turbo-instruct-0914
- openai/gpt-4-1106-preview
- openai/gpt-3.5-turbo-1106
- openai/gpt-4-0125-preview
- openai/gpt-4-turbo-preview
- openai/gpt-3.5-turbo-0125
- openai/gpt-4-turbo
- openai/gpt-4-turbo-2024-04-09
- openai/gpt-4o
- openai/gpt-4o-2024-05-13
- openai/gpt-4o-mini-2024-07-18
- openai/gpt-4o-mini
- openai/gpt-4o-2024-08-06
- openai/chatgpt-4o-latest
- openai/o1-2024-12-17
- openai/o1
- openai/computer-use-preview
- openai/o3-mini
- openai/o3-mini-2025-01-31
- openai/gpt-4o-2024-11-20
- openai/computer-use-preview-2025-03-11
- openai/gpt-4o-search-preview-2025-03-11
- openai/gpt-4o-search-preview
- openai/gpt-4o-mini-search-preview-2025-03-11
- openai/gpt-4o-mini-search-preview
- openai/o1-pro-2025-03-19
- openai/o1-pro
- openai/o3-2025-04-16
- openai/o4-mini-2025-04-16
- openai/o3
- openai/o4-mini
- openai/gpt-4.1-2025-04-14
- openai/gpt-4.1
- openai/gpt-4.1-mini-2025-04-14
- openai/gpt-4.1-mini
- openai/gpt-4.1-nano-2025-04-14
- openai/gpt-4.1-nano
- openai/codex-mini-latest
- openai/o3-pro
- openai/o3-pro-2025-06-10
- openai/o4-mini-deep-research
- openai/o3-deep-research
- openai/o3-deep-research-2025-06-26
- openai/o4-mini-deep-research-2025-06-26
- openai/gpt-5-chat-latest
- openai/gpt-5-2025-08-07
- openai/gpt-5
- openai/gpt-5-mini-2025-08-07
- openai/gpt-5-mini
- openai/gpt-5-nano-2025-08-07
- openai/gpt-5-nano
- openai/gpt-5-codex
- openai/gpt-5-pro-2025-10-06
- openai/gpt-5-pro
- openai/gpt-5-search-api
- openai/gpt-5-search-api-2025-10-14
- openai/gpt-5.1-chat-latest
- openai/gpt-5.1-2025-11-13
- openai/gpt-5.1
- openai/gpt-5.1-codex
- openai/gpt-5.1-codex-mini
- openai/gpt-5.1-codex-max
- openai/gpt-5.2-2025-12-11
- openai/gpt-5.2
- openai/gpt-5.2-pro-2025-12-11
- openai/gpt-5.2-pro
- openai/gpt-5.2-chat-latest
- openai/gpt-3.5-turbo-16k
- openai/ft:gpt-3.5-turbo-0613:katanemo::8CMZbm0P
google:
- google/gemini-2.5-flash
- google/gemini-2.5-pro
- google/gemini-2.0-flash-exp
- google/gemini-2.0-flash
- google/gemini-2.0-flash-001
- google/gemini-2.0-flash-exp-image-generation
- google/gemini-2.0-flash-lite-001
- google/gemini-2.0-flash-lite
- google/gemini-2.0-flash-lite-preview-02-05
- google/gemini-2.0-flash-lite-preview
- google/gemini-exp-1206
- google/gemini-2.5-flash-preview-tts
- google/gemini-2.5-pro-preview-tts
- google/gemma-3-1b-it
- google/gemma-3-4b-it
- google/gemma-3-12b-it
- google/gemma-3-27b-it
- google/gemma-3n-e4b-it
- google/gemma-3n-e2b-it
- google/gemini-flash-latest
- google/gemini-flash-lite-latest
- google/gemini-pro-latest
- google/gemini-2.5-flash-lite
- google/gemini-2.5-flash-image
- google/gemini-2.5-flash-preview-09-2025
- google/gemini-2.5-flash-lite-preview-09-2025
- google/gemini-3-pro-preview
- google/gemini-3-flash-preview
- google/gemini-3-pro-image-preview
- google/nano-banana-pro-preview
- google/gemini-robotics-er-1.5-preview
- google/gemini-2.5-computer-use-preview-10-2025
- google/deep-research-pro-preview-12-2025
mistralai:
- mistralai/mistral-medium-2505
- mistralai/mistral-medium-2508
- mistralai/mistral-medium-latest
- mistralai/mistral-medium
- mistralai/open-mistral-nemo
- mistralai/open-mistral-nemo-2407
- mistralai/mistral-tiny-2407
- mistralai/mistral-tiny-latest
- mistralai/mistral-large-2411
- mistralai/pixtral-large-2411
- mistralai/pixtral-large-latest
- mistralai/mistral-large-pixtral-2411
- mistralai/codestral-2508
- mistralai/codestral-latest
- mistralai/devstral-small-2507
- mistralai/devstral-medium-2507
- mistralai/devstral-2512
- mistralai/mistral-vibe-cli-latest
- mistralai/devstral-medium-latest
- mistralai/devstral-latest
- mistralai/labs-devstral-small-2512
- mistralai/devstral-small-latest
- mistralai/mistral-small-2506
- mistralai/mistral-small-latest
- mistralai/labs-mistral-small-creative
- mistralai/magistral-medium-2509
- mistralai/magistral-medium-latest
- mistralai/magistral-small-2509
- mistralai/magistral-small-latest
- mistralai/mistral-large-2512
- mistralai/mistral-large-latest
- mistralai/ministral-3b-2512
- mistralai/ministral-3b-latest
- mistralai/ministral-8b-2512
- mistralai/ministral-8b-latest
- mistralai/ministral-14b-2512
- mistralai/ministral-14b-latest
- mistralai/open-mistral-7b
- mistralai/mistral-tiny
- mistralai/mistral-tiny-2312
- mistralai/pixtral-12b-2409
- mistralai/pixtral-12b
- mistralai/pixtral-12b-latest
- mistralai/ministral-3b-2410
- mistralai/ministral-8b-2410
- mistralai/codestral-2501
- mistralai/codestral-2412
- mistralai/codestral-2411-rc5
- mistralai/mistral-small-2501
- mistralai/mistral-embed-2312
- mistralai/mistral-embed
- mistralai/codestral-embed
- mistralai/codestral-embed-2505
z-ai:
- z-ai/glm-4.5
- z-ai/glm-4.5-air
- z-ai/glm-4.6
- z-ai/glm-4.7
amazon:
- amazon/amazon.nova-pro-v1:0
- amazon/amazon.nova-2-lite-v1:0
- amazon/amazon.nova-2-sonic-v1:0
- amazon/amazon.titan-tg1-large
- amazon/amazon.nova-premier-v1:0:8k
- amazon/amazon.nova-premier-v1:0:20k
- amazon/amazon.nova-premier-v1:0:1000k
- amazon/amazon.nova-premier-v1:0:mm
- amazon/amazon.nova-premier-v1:0
- amazon/amazon.nova-lite-v1:0
- amazon/amazon.nova-micro-v1:0
deepseek:
- deepseek/deepseek-chat
- deepseek/deepseek-reasoner
x-ai:
- x-ai/grok-2-vision-1212
- x-ai/grok-3
- x-ai/grok-3-mini
- x-ai/grok-4-0709
- x-ai/grok-4-1-fast-non-reasoning
- x-ai/grok-4-1-fast-reasoning
- x-ai/grok-4-fast-non-reasoning
- x-ai/grok-4-fast-reasoning
- x-ai/grok-code-fast-1
moonshotai:
- moonshotai/kimi-latest
- moonshotai/kimi-k2.5
- moonshotai/moonshot-v1-8k-vision-preview
- moonshotai/kimi-k2-thinking
- moonshotai/moonshot-v1-auto
- moonshotai/kimi-k2-0711-preview
- moonshotai/moonshot-v1-32k
- moonshotai/kimi-k2-thinking-turbo
- moonshotai/kimi-k2-0905-preview
- moonshotai/moonshot-v1-128k
- moonshotai/moonshot-v1-32k-vision-preview
- moonshotai/moonshot-v1-128k-vision-preview
- moonshotai/kimi-k2-turbo-preview
- moonshotai/moonshot-v1-8k
anthropic:
- anthropic/claude-opus-4-5-20251101
- anthropic/claude-opus-4-5
- anthropic/claude-haiku-4-5-20251001
- anthropic/claude-haiku-4-5
- anthropic/claude-sonnet-4-5-20250929
- anthropic/claude-sonnet-4-5
- anthropic/claude-opus-4-1-20250805
- anthropic/claude-opus-4-1
- anthropic/claude-opus-4-20250514
- anthropic/claude-opus-4
- anthropic/claude-sonnet-4-20250514
- anthropic/claude-sonnet-4
- anthropic/claude-3-7-sonnet-20250219
- anthropic/claude-3-7-sonnet
- anthropic/claude-3-5-haiku-20241022
- anthropic/claude-3-5-haiku
- anthropic/claude-3-haiku-20240307
- anthropic/claude-3-haiku
- z-ai/glm-5
metadata:
total_providers: 10
total_models: 298
last_updated: 2026-01-27T22:40:53.653700+00:00
total_models: 289
last_updated: 2026-02-13T22:44:30.413065+00:00

View file

@ -990,7 +990,7 @@ impl HttpContext for StreamContext {
self.send_server_error(
ServerError::BadRequest {
why: format!(
"No model specified in request and couldn't determine model name from arch_config. Model name in req: {}, arch_config, provider: {}, model: {:?}",
"No model specified in request and couldn't determine model name from plano_config. Model name in req: {}, plano_config, provider: {}, model: {:?}",
model_requested,
self.llm_provider().name,
self.llm_provider().model

View file

@ -419,7 +419,7 @@ impl HttpContext for StreamContext {
);
}
let data_serialized = serde_json::to_string(&data).unwrap();
info!("archgw <= developer: {}", data_serialized);
info!("plano <= developer: {}", data_serialized);
self.set_http_response_body(0, body_size, data_serialized.as_bytes());
};
}

View file

@ -246,7 +246,7 @@ impl StreamContext {
let chat_completion_request_json =
serde_json::to_string(&chat_completion_request).unwrap();
info!(
"archgw => upstream llm request: {}",
"plano => upstream llm request: {}",
chat_completion_request_json
);
self.set_http_request_body(
@ -799,7 +799,7 @@ impl StreamContext {
};
let json_resp = serde_json::to_string(&chat_completion_request).unwrap();
info!("archgw => (default target) llm request: {}", json_resp);
info!("plano => (default target) llm request: {}", json_resp);
self.set_http_request_body(0, self.request_body_size, json_resp.as_bytes());
self.resume_http_request();
}

48
demos/README.md Normal file
View file

@ -0,0 +1,48 @@
# Plano Demos
This directory contains demos showcasing Plano's capabilities as an AI-native proxy for agentic applications.
## Getting Started
| Demo | Description |
|------|-------------|
| [Weather Forecast](getting_started/weather_forecast/) | Core function calling with a weather query agent, interactive chat UI, and Jaeger tracing |
| [LLM Gateway](getting_started/llm_gateway/) | Key management and dynamic routing to multiple LLM providers with header-based model override |
## LLM Routing
| Demo | Description |
|------|-------------|
| [Preference-Based Routing](llm_routing/preference_based_routing/) | Routes prompts to LLMs based on user-defined preferences and task type (e.g. code generation vs. understanding) |
| [Model Alias Routing](llm_routing/model_alias_routing/) | Maps semantic aliases (`arch.summarize.v1`) to provider-specific models for centralized governance |
| [Claude Code Router](llm_routing/claude_code_router/) | Extends Claude Code with multi-provider access and preference-aligned routing for coding tasks |
## Agent Orchestration
| Demo | Description |
|------|-------------|
| [Travel Agents](agent_orchestration/travel_agents/) | Multi-agent travel booking with weather and flight agents, intelligent routing, and OpenTelemetry tracing |
| [Multi-Agent CrewAI & LangChain](agent_orchestration/multi_agent_crewai_langchain/) | Framework-agnostic orchestration combining CrewAI and LangChain agents in unified conversations |
## Filter Chains
| Demo | Description |
|------|-------------|
| [HTTP Filter](filter_chains/http_filter/) | RAG agent with filter chains for input validation, query rewriting, and context building |
| [MCP Filter](filter_chains/mcp_filter/) | RAG agent using MCP-based filters for domain validation, query optimization, and knowledge base retrieval |
## Integrations
| Demo | Description |
|------|-------------|
| [Ollama](integrations/ollama/) | Use Ollama as a local LLM provider through Plano |
| [Spotify Bearer Auth](integrations/spotify_bearer_auth/) | Bearer token authentication for third-party APIs (Spotify new releases and top tracks) |
## Advanced
| Demo | Description |
|------|-------------|
| [Currency Exchange](advanced/currency_exchange/) | Function calling with public REST APIs (Frankfurter currency exchange) |
| [Stock Quote](advanced/stock_quote/) | Protected REST API integration with access key management |
| [Multi-Turn RAG](advanced/multi_turn_rag/) | Multi-turn conversational RAG agent for answering questions about energy sources |
| [Model Choice Test Harness](advanced/model_choice_test_harness/) | Evaluation framework for safely testing and switching between models with benchmark fixtures |

View file

@ -1 +1 @@
This demo shows how you can use a publicly hosted rest api and interact it using arch gateway.
This demo shows how you can use a publicly hosted rest api and interact it using Plano gateway.

View file

@ -1,13 +1,11 @@
version: v0.1.0
version: v0.3.0
listeners:
ingress_traffic:
address: 0.0.0.0
- type: prompt
name: prompt_listener
port: 10000
message_format: openai
timeout: 30s
llm_providers:
model_providers:
- model: openai/gpt-4o-mini
access_key: $OPENAI_API_KEY
default: true

View file

@ -0,0 +1,25 @@
services:
anythingllm:
image: mintplexlabs/anythingllm
restart: always
ports:
- "3001:3001"
cap_add:
- SYS_ADMIN
environment:
- STORAGE_DIR=/app/server/storage
- LLM_PROVIDER=generic-openai
- GENERIC_OPEN_AI_BASE_PATH=http://host.docker.internal:10000/v1
- GENERIC_OPEN_AI_MODEL_PREF=gpt-4o-mini
- GENERIC_OPEN_AI_MODEL_TOKEN_LIMIT=128000
- GENERIC_OPEN_AI_API_KEY=sk-placeholder
extra_hosts:
- "host.docker.internal:host-gateway"
jaeger:
build:
context: ../../shared/jaeger
ports:
- "16686:16686"
- "4317:4317"
- "4318:4318"

View file

@ -1,6 +1,6 @@
# Model Choice Newsletter Demo
This folder demonstrates a practical workflow for rapid model adoption and safe model switching using Arch Gateway (`plano`). It includes both a minimal test harness and a sample proxy configuration.
This folder demonstrates a practical workflow for rapid model adoption and safe model switching using Plano (`plano`). It includes both a minimal test harness and a sample proxy configuration.
---
@ -85,13 +85,13 @@ See `config.yaml` for a sample configuration mapping aliases to provider models.
```
2. **Install dependencies:**
- Install all dependencies as described in the main Arch README ([link](https://github.com/katanemo/arch/?tab=readme-ov-file#prerequisites))
- Install all dependencies as described in the main Plano README ([link](https://github.com/katanemo/plano/?tab=readme-ov-file#prerequisites))
- Then run
```sh
uv sync
```
3. **Start Arch Gateway**
3. **Start Plano**
```sh
run_demo.sh
```

View file

@ -3,7 +3,7 @@ import json, time, yaml, statistics as stats
from pydantic import BaseModel, ValidationError
from openai import OpenAI
# archgw endpoint (keys are handled by archgw)
# Plano endpoint (keys are handled by Plano)
client = OpenAI(base_url="http://localhost:12000/v1", api_key="n/a")
MODELS = ["arch.summarize.v1", "arch.reason.v1"]
FIXTURES = "evals_summarize.yaml"

View file

@ -1,13 +1,11 @@
version: v0.1.0
version: v0.3.0
listeners:
egress_traffic:
address: 0.0.0.0
- type: model
name: model_listener
port: 12000
message_format: openai
timeout: 30s
llm_providers:
model_providers:
- model: openai/gpt-4o-mini
access_key: $OPENAI_API_KEY
default: true
@ -20,3 +18,6 @@ model_aliases:
target: gpt-4o-mini
arch.reason.v1:
target: o3
tracing:
random_sampling: 100

View file

@ -1,11 +1,11 @@
[project]
name = "model-choice-newsletter-code-snippets"
version = "0.1.0"
description = "Benchmarking model alias routing with Arch Gateway."
description = "Benchmarking model alias routing with Plano."
authors = [{name = "Your Name", email = "your@email.com"}]
license = {text = "Apache 2.0"}
readme = "README.md"
requires-python = ">=3.10,<3.13.3"
requires-python = ">=3.10"
dependencies = [
"pydantic>=2.0",
"openai>=1.0",

View file

@ -17,18 +17,18 @@ start_demo() {
echo ".env file created with API keys."
fi
# Step 3: Start Arch
echo "Starting Arch with arch_config_with_aliases.yaml..."
# Step 3: Start Plano
echo "Starting Plano with arch_config_with_aliases.yaml..."
planoai up arch_config_with_aliases.yaml
echo "\n\nArch started successfully."
echo "\n\nPlano started successfully."
echo "Please run the following command to test the setup: python bench.py\n"
}
# Function to stop the demo
stop_demo() {
# Step 2: Stop Arch
echo "Stopping Arch..."
# Step 2: Stop Plano
echo "Stopping Plano..."
planoai down
}

View file

@ -1,6 +1,6 @@
version = 1
revision = 3
requires-python = ">=3.10, <3.13.3"
requires-python = ">=3.10"
[[package]]
name = "annotated-types"
@ -113,6 +113,22 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" },
{ url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" },
{ url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" },
{ url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" },
{ url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" },
{ url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" },
{ url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" },
{ url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" },
{ url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" },
{ url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" },
{ url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" },
{ url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" },
{ url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" },
{ url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" },
{ url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" },
{ url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" },
{ url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" },
{ url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" },
{ url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" },
{ url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" },
]
@ -287,6 +303,31 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/05/17/710bf8472d1dff0d3caf4ced6031060091c1320f84ee7d5dcbed1f352417/jiter-0.12.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:351d54f2b09a41600ffea43d081522d792e81dcfb915f6d2d242744c1cc48beb", size = 361272, upload-time = "2025-11-09T20:48:06.951Z" },
{ url = "https://files.pythonhosted.org/packages/fb/f1/1dcc4618b59761fef92d10bcbb0b038b5160be653b003651566a185f1a5c/jiter-0.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2a5e90604620f94bf62264e7c2c038704d38217b7465b863896c6d7c902b06c7", size = 204604, upload-time = "2025-11-09T20:48:08.328Z" },
{ url = "https://files.pythonhosted.org/packages/d9/32/63cb1d9f1c5c6632a783c0052cde9ef7ba82688f7065e2f0d5f10a7e3edb/jiter-0.12.0-cp313-cp313t-win_arm64.whl", hash = "sha256:88ef757017e78d2860f96250f9393b7b577b06a956ad102c29c8237554380db3", size = 185628, upload-time = "2025-11-09T20:48:09.572Z" },
{ url = "https://files.pythonhosted.org/packages/a8/99/45c9f0dbe4a1416b2b9a8a6d1236459540f43d7fb8883cff769a8db0612d/jiter-0.12.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c46d927acd09c67a9fb1416df45c5a04c27e83aae969267e98fba35b74e99525", size = 312478, upload-time = "2025-11-09T20:48:10.898Z" },
{ url = "https://files.pythonhosted.org/packages/4c/a7/54ae75613ba9e0f55fcb0bc5d1f807823b5167cc944e9333ff322e9f07dd/jiter-0.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:774ff60b27a84a85b27b88cd5583899c59940bcc126caca97eb2a9df6aa00c49", size = 318706, upload-time = "2025-11-09T20:48:12.266Z" },
{ url = "https://files.pythonhosted.org/packages/59/31/2aa241ad2c10774baf6c37f8b8e1f39c07db358f1329f4eb40eba179c2a2/jiter-0.12.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5433fab222fb072237df3f637d01b81f040a07dcac1cb4a5c75c7aa9ed0bef1", size = 351894, upload-time = "2025-11-09T20:48:13.673Z" },
{ url = "https://files.pythonhosted.org/packages/54/4f/0f2759522719133a9042781b18cc94e335b6d290f5e2d3e6899d6af933e3/jiter-0.12.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f8c593c6e71c07866ec6bfb790e202a833eeec885022296aff6b9e0b92d6a70e", size = 365714, upload-time = "2025-11-09T20:48:15.083Z" },
{ url = "https://files.pythonhosted.org/packages/dc/6f/806b895f476582c62a2f52c453151edd8a0fde5411b0497baaa41018e878/jiter-0.12.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:90d32894d4c6877a87ae00c6b915b609406819dce8bc0d4e962e4de2784e567e", size = 478989, upload-time = "2025-11-09T20:48:16.706Z" },
{ url = "https://files.pythonhosted.org/packages/86/6c/012d894dc6e1033acd8db2b8346add33e413ec1c7c002598915278a37f79/jiter-0.12.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:798e46eed9eb10c3adbbacbd3bdb5ecd4cf7064e453d00dbef08802dae6937ff", size = 378615, upload-time = "2025-11-09T20:48:18.614Z" },
{ url = "https://files.pythonhosted.org/packages/87/30/d718d599f6700163e28e2c71c0bbaf6dace692e7df2592fd793ac9276717/jiter-0.12.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3f1368f0a6719ea80013a4eb90ba72e75d7ea67cfc7846db2ca504f3df0169a", size = 364745, upload-time = "2025-11-09T20:48:20.117Z" },
{ url = "https://files.pythonhosted.org/packages/8f/85/315b45ce4b6ddc7d7fceca24068543b02bdc8782942f4ee49d652e2cc89f/jiter-0.12.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:65f04a9d0b4406f7e51279710b27484af411896246200e461d80d3ba0caa901a", size = 386502, upload-time = "2025-11-09T20:48:21.543Z" },
{ url = "https://files.pythonhosted.org/packages/74/0b/ce0434fb40c5b24b368fe81b17074d2840748b4952256bab451b72290a49/jiter-0.12.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:fd990541982a24281d12b67a335e44f117e4c6cbad3c3b75c7dea68bf4ce3a67", size = 519845, upload-time = "2025-11-09T20:48:22.964Z" },
{ url = "https://files.pythonhosted.org/packages/e8/a3/7a7a4488ba052767846b9c916d208b3ed114e3eb670ee984e4c565b9cf0d/jiter-0.12.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:b111b0e9152fa7df870ecaebb0bd30240d9f7fff1f2003bcb4ed0f519941820b", size = 510701, upload-time = "2025-11-09T20:48:24.483Z" },
{ url = "https://files.pythonhosted.org/packages/c3/16/052ffbf9d0467b70af24e30f91e0579e13ded0c17bb4a8eb2aed3cb60131/jiter-0.12.0-cp314-cp314-win32.whl", hash = "sha256:a78befb9cc0a45b5a5a0d537b06f8544c2ebb60d19d02c41ff15da28a9e22d42", size = 205029, upload-time = "2025-11-09T20:48:25.749Z" },
{ url = "https://files.pythonhosted.org/packages/e4/18/3cf1f3f0ccc789f76b9a754bdb7a6977e5d1d671ee97a9e14f7eb728d80e/jiter-0.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:e1fe01c082f6aafbe5c8faf0ff074f38dfb911d53f07ec333ca03f8f6226debf", size = 204960, upload-time = "2025-11-09T20:48:27.415Z" },
{ url = "https://files.pythonhosted.org/packages/02/68/736821e52ecfdeeb0f024b8ab01b5a229f6b9293bbdb444c27efade50b0f/jiter-0.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:d72f3b5a432a4c546ea4bedc84cce0c3404874f1d1676260b9c7f048a9855451", size = 185529, upload-time = "2025-11-09T20:48:29.125Z" },
{ url = "https://files.pythonhosted.org/packages/30/61/12ed8ee7a643cce29ac97c2281f9ce3956eb76b037e88d290f4ed0d41480/jiter-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e6ded41aeba3603f9728ed2b6196e4df875348ab97b28fc8afff115ed42ba7a7", size = 318974, upload-time = "2025-11-09T20:48:30.87Z" },
{ url = "https://files.pythonhosted.org/packages/2d/c6/f3041ede6d0ed5e0e79ff0de4c8f14f401bbf196f2ef3971cdbe5fd08d1d/jiter-0.12.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a947920902420a6ada6ad51892082521978e9dd44a802663b001436e4b771684", size = 345932, upload-time = "2025-11-09T20:48:32.658Z" },
{ url = "https://files.pythonhosted.org/packages/d5/5d/4d94835889edd01ad0e2dbfc05f7bdfaed46292e7b504a6ac7839aa00edb/jiter-0.12.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:add5e227e0554d3a52cf390a7635edaffdf4f8fce4fdbcef3cc2055bb396a30c", size = 367243, upload-time = "2025-11-09T20:48:34.093Z" },
{ url = "https://files.pythonhosted.org/packages/fd/76/0051b0ac2816253a99d27baf3dda198663aff882fa6ea7deeb94046da24e/jiter-0.12.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f9b1cda8fcb736250d7e8711d4580ebf004a46771432be0ae4796944b5dfa5d", size = 479315, upload-time = "2025-11-09T20:48:35.507Z" },
{ url = "https://files.pythonhosted.org/packages/70/ae/83f793acd68e5cb24e483f44f482a1a15601848b9b6f199dacb970098f77/jiter-0.12.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:deeb12a2223fe0135c7ff1356a143d57f95bbf1f4a66584f1fc74df21d86b993", size = 380714, upload-time = "2025-11-09T20:48:40.014Z" },
{ url = "https://files.pythonhosted.org/packages/b1/5e/4808a88338ad2c228b1126b93fcd8ba145e919e886fe910d578230dabe3b/jiter-0.12.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c596cc0f4cb574877550ce4ecd51f8037469146addd676d7c1a30ebe6391923f", size = 365168, upload-time = "2025-11-09T20:48:41.462Z" },
{ url = "https://files.pythonhosted.org/packages/0c/d4/04619a9e8095b42aef436b5aeb4c0282b4ff1b27d1db1508df9f5dc82750/jiter-0.12.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ab4c823b216a4aeab3fdbf579c5843165756bd9ad87cc6b1c65919c4715f783", size = 387893, upload-time = "2025-11-09T20:48:42.921Z" },
{ url = "https://files.pythonhosted.org/packages/17/ea/d3c7e62e4546fdc39197fa4a4315a563a89b95b6d54c0d25373842a59cbe/jiter-0.12.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:e427eee51149edf962203ff8db75a7514ab89be5cb623fb9cea1f20b54f1107b", size = 520828, upload-time = "2025-11-09T20:48:44.278Z" },
{ url = "https://files.pythonhosted.org/packages/cc/0b/c6d3562a03fd767e31cb119d9041ea7958c3c80cb3d753eafb19b3b18349/jiter-0.12.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:edb868841f84c111255ba5e80339d386d937ec1fdce419518ce1bd9370fac5b6", size = 511009, upload-time = "2025-11-09T20:48:45.726Z" },
{ url = "https://files.pythonhosted.org/packages/aa/51/2cb4468b3448a8385ebcd15059d325c9ce67df4e2758d133ab9442b19834/jiter-0.12.0-cp314-cp314t-win32.whl", hash = "sha256:8bbcfe2791dfdb7c5e48baf646d37a6a3dcb5a97a032017741dea9f817dca183", size = 205110, upload-time = "2025-11-09T20:48:47.033Z" },
{ url = "https://files.pythonhosted.org/packages/b2/c5/ae5ec83dec9c2d1af805fd5fe8f74ebded9c8670c5210ec7820ce0dbeb1e/jiter-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2fa940963bf02e1d8226027ef461e36af472dea85d36054ff835aeed944dd873", size = 205223, upload-time = "2025-11-09T20:48:49.076Z" },
{ url = "https://files.pythonhosted.org/packages/97/9a/3c5391907277f0e55195550cf3fa8e293ae9ee0c00fb402fec1e38c0c82f/jiter-0.12.0-cp314-cp314t-win_arm64.whl", hash = "sha256:506c9708dd29b27288f9f8f1140c3cb0e3d8ddb045956d7757b1fa0e0f39a473", size = 185564, upload-time = "2025-11-09T20:48:50.376Z" },
{ url = "https://files.pythonhosted.org/packages/fe/54/5339ef1ecaa881c6948669956567a64d2670941925f245c434f494ffb0e5/jiter-0.12.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:4739a4657179ebf08f85914ce50332495811004cc1747852e8b2041ed2aab9b8", size = 311144, upload-time = "2025-11-09T20:49:10.503Z" },
{ url = "https://files.pythonhosted.org/packages/27/74/3446c652bffbd5e81ab354e388b1b5fc1d20daac34ee0ed11ff096b1b01a/jiter-0.12.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:41da8def934bf7bec16cb24bd33c0ca62126d2d45d81d17b864bd5ad721393c3", size = 305877, upload-time = "2025-11-09T20:49:12.269Z" },
{ url = "https://files.pythonhosted.org/packages/a1/f4/ed76ef9043450f57aac2d4fbeb27175aa0eb9c38f833be6ef6379b3b9a86/jiter-0.12.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c44ee814f499c082e69872d426b624987dbc5943ab06e9bbaa4f81989fdb79e", size = 340419, upload-time = "2025-11-09T20:49:13.803Z" },
@ -385,6 +426,28 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" },
{ url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" },
{ url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" },
{ url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" },
{ url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" },
{ url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" },
{ url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" },
{ url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" },
{ url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" },
{ url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" },
{ url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" },
{ url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" },
{ url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" },
{ url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" },
{ url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" },
{ url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" },
{ url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" },
{ url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" },
{ url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" },
{ url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" },
{ url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" },
{ url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" },
{ url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" },
{ url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" },
{ url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" },
]
[[package]]
@ -545,6 +608,34 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" },
{ url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" },
{ url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" },
{ url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" },
{ url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" },
{ url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" },
{ url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" },
{ url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" },
{ url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" },
{ url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" },
{ url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" },
{ url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" },
{ url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" },
{ url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" },
{ url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" },
{ url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" },
{ url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" },
{ url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" },
{ url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" },
{ url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" },
{ url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" },
{ url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" },
{ url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" },
{ url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" },
{ url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" },
{ url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" },
{ url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" },
{ url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" },
{ url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" },
{ url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" },
{ url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" },
{ url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" },
{ url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" },
{ url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" },
@ -642,6 +733,24 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" },
{ url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" },
{ url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" },
{ url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" },
{ url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" },
{ url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" },
{ url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" },
{ url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" },
{ url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" },
{ url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" },
{ url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" },
{ url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" },
{ url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" },
{ url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" },
{ url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" },
{ url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" },
{ url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" },
{ url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" },
{ url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" },
{ url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" },
{ url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
]
[[package]]
@ -752,6 +861,35 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" },
{ url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" },
{ url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" },
{ url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" },
{ url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" },
{ url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" },
{ url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" },
{ url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" },
{ url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" },
{ url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" },
{ url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" },
{ url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" },
{ url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" },
{ url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" },
{ url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" },
{ url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" },
{ url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" },
{ url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" },
{ url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" },
{ url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" },
{ url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" },
{ url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" },
{ url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" },
{ url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" },
{ url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" },
{ url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" },
{ url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" },
{ url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" },
{ url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" },
{ url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" },
{ url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" },
{ url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" },
{ url = "https://files.pythonhosted.org/packages/69/71/3f34339ee70521864411f8b6992e7ab13ac30d8e4e3309e07c7361767d91/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58", size = 372292, upload-time = "2025-11-30T20:24:16.537Z" },
{ url = "https://files.pythonhosted.org/packages/57/09/f183df9b8f2d66720d2ef71075c59f7e1b336bec7ee4c48f0a2b06857653/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a", size = 362128, upload-time = "2025-11-30T20:24:18.086Z" },
{ url = "https://files.pythonhosted.org/packages/7a/68/5c2594e937253457342e078f0cc1ded3dd7b2ad59afdbf2d354869110a02/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb", size = 391542, upload-time = "2025-11-30T20:24:20.092Z" },
@ -805,6 +943,22 @@ wheels = [
{ 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, upload-time = "2025-10-08T22:01:23.859Z" },
{ url = "https://files.pythonhosted.org/packages/f8/84/ef50c51b5a9472e7265ce1ffc7f24cd4023d289e109f669bdb1553f6a7c2/tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606", size = 96946, upload-time = "2025-10-08T22:01:24.893Z" },
{ url = "https://files.pythonhosted.org/packages/b2/b7/718cd1da0884f281f95ccfa3a6cc572d30053cba64603f79d431d3c9b61b/tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999", size = 107705, upload-time = "2025-10-08T22:01:26.153Z" },
{ 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, upload-time = "2025-10-08T22:01:27.06Z" },
{ 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, upload-time = "2025-10-08T22:01:28.059Z" },
{ 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, upload-time = "2025-10-08T22:01:29.066Z" },
{ 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, upload-time = "2025-10-08T22:01:31.98Z" },
{ 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, upload-time = "2025-10-08T22:01:32.989Z" },
{ 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, upload-time = "2025-10-08T22:01:34.052Z" },
{ url = "https://files.pythonhosted.org/packages/22/0c/b4da635000a71b5f80130937eeac12e686eefb376b8dee113b4a582bba42/tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463", size = 97930, upload-time = "2025-10-08T22:01:35.082Z" },
{ url = "https://files.pythonhosted.org/packages/b9/74/cb1abc870a418ae99cd5c9547d6bce30701a954e0e721821df483ef7223c/tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8", size = 107964, upload-time = "2025-10-08T22:01:36.057Z" },
{ 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, upload-time = "2025-10-08T22:01:37.27Z" },
{ 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, upload-time = "2025-10-08T22:01:38.235Z" },
{ 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, upload-time = "2025-10-08T22:01:39.712Z" },
{ 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, upload-time = "2025-10-08T22:01:40.773Z" },
{ 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, upload-time = "2025-10-08T22:01:41.824Z" },
{ 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, upload-time = "2025-10-08T22:01:43.177Z" },
{ url = "https://files.pythonhosted.org/packages/7e/46/cc36c679f09f27ded940281c38607716c86cf8ba4a518d524e349c8b4874/tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0", size = 107563, upload-time = "2025-10-08T22:01:44.233Z" },
{ url = "https://files.pythonhosted.org/packages/84/ff/426ca8683cf7b753614480484f6437f568fd2fda2edbdf57a2d3d8b27a0b/tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba", size = 119756, upload-time = "2025-10-08T22:01:45.234Z" },
{ url = "https://files.pythonhosted.org/packages/77/b8/0135fadc89e73be292b473cb820b4f5a08197779206b33191e801feeae40/tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", size = 14408, upload-time = "2025-10-08T22:01:46.04Z" },
]

View file

@ -1,4 +1,4 @@
FROM python:3.12 AS base
FROM python:3.14 AS base
FROM base AS builder
@ -9,7 +9,7 @@ RUN pip install --prefix=/runtime --force-reinstall -r requirements.txt
COPY . /src
FROM python:3.12-slim AS output
FROM python:3.14-slim AS output
COPY --from=builder /runtime /usr/local

View file

@ -1,6 +1,6 @@
# Multi-Turn Agentic Demo (RAG)
This demo showcases how the **Arch** can be used to build accurate multi-turn RAG agent by just writing simple APIs.
This demo showcases how **Plano** can be used to build accurate multi-turn RAG agent by just writing simple APIs.
![Example of Multi-turn Interaction](mutli-turn-example.png)
@ -14,7 +14,7 @@ Provides information about various energy sources and considerations.
# Starting the demo
1. Please make sure the [pre-requisites](https://github.com/katanemo/arch/?tab=readme-ov-file#prerequisites) are installed correctly
2. Start Arch
2. Start Plano
```sh
sh run_demo.sh
```

View file

@ -1,18 +1,16 @@
version: v0.1.0
version: v0.3.0
listeners:
ingress_traffic:
address: 0.0.0.0
- type: prompt
name: prompt_listener
port: 10000
message_format: openai
timeout: 30s
endpoints:
rag_energy_source_agent:
endpoint: host.docker.internal:18083
connect_timeout: 0.005s
llm_providers:
model_providers:
- access_key: $OPENAI_API_KEY
model: openai/gpt-4o-mini
default: true

View file

@ -0,0 +1,28 @@
services:
rag_energy_source_agent:
build:
context: .
dockerfile: Dockerfile
ports:
- "18083:80"
healthcheck:
test: ["CMD", "curl" ,"http://localhost:80/healthz"]
interval: 5s
retries: 20
anythingllm:
image: mintplexlabs/anythingllm
restart: always
ports:
- "3001:3001"
cap_add:
- SYS_ADMIN
environment:
- STORAGE_DIR=/app/server/storage
- LLM_PROVIDER=generic-openai
- GENERIC_OPEN_AI_BASE_PATH=http://host.docker.internal:10000/v1
- GENERIC_OPEN_AI_MODEL_PREF=gpt-4o-mini
- GENERIC_OPEN_AI_MODEL_TOKEN_LIMIT=128000
- GENERIC_OPEN_AI_API_KEY=sk-placeholder
extra_hosts:
- "host.docker.internal:host-gateway"

View file

Before

Width:  |  Height:  |  Size: 852 KiB

After

Width:  |  Height:  |  Size: 852 KiB

Before After
Before After

View file

@ -18,8 +18,8 @@ start_demo() {
echo ".env file created with OPENAI_API_KEY."
fi
# Step 3: Start Arch
echo "Starting Arch with config.yaml..."
# Step 3: Start Plano
echo "Starting Plano with config.yaml..."
planoai up config.yaml
# Step 4: Start Network Agent
@ -33,8 +33,8 @@ stop_demo() {
echo "Stopping HR Agent using Docker Compose..."
docker compose down -v
# Step 2: Stop Arch
echo "Stopping Arch..."
# Step 2: Stop Plano
echo "Stopping Plano..."
planoai down
}

View file

@ -1,13 +1,11 @@
version: v0.1.0
version: v0.3.0
listeners:
ingress_traffic:
address: 0.0.0.0
- type: prompt
name: prompt_listener
port: 10000
message_format: openai
timeout: 30s
llm_providers:
model_providers:
- access_key: $OPENAI_API_KEY
model: openai/gpt-4o

View file

@ -0,0 +1,25 @@
services:
anythingllm:
image: mintplexlabs/anythingllm
restart: always
ports:
- "3001:3001"
cap_add:
- SYS_ADMIN
environment:
- STORAGE_DIR=/app/server/storage
- LLM_PROVIDER=generic-openai
- GENERIC_OPEN_AI_BASE_PATH=http://host.docker.internal:10000/v1
- GENERIC_OPEN_AI_MODEL_PREF=gpt-4o-mini
- GENERIC_OPEN_AI_MODEL_TOKEN_LIMIT=128000
- GENERIC_OPEN_AI_API_KEY=sk-placeholder
extra_hosts:
- "host.docker.internal:host-gateway"
jaeger:
build:
context: ../../shared/jaeger
ports:
- "16686:16686"
- "4317:4317"
- "4318:4318"

View file

Before

Width:  |  Height:  |  Size: 673 KiB

After

Width:  |  Height:  |  Size: 673 KiB

Before After
Before After

View file

@ -1,4 +1,4 @@
FROM python:3.13-slim
FROM python:3.14-slim
WORKDIR /app

View file

@ -37,7 +37,7 @@ Plano acts as a **framework-agnostic proxy and data plane** that:
```bash
# From the demo directory
cd demos/use_cases/multi_agent_with_crewai_langchain
cd demos/agent_orchestration/multi_agent_crewai_langchain
# Build and start all services
docker-compose up -d

View file

@ -8,12 +8,12 @@ services:
- "8001:8001"
- "12000:12000"
environment:
- ARCH_CONFIG_PATH=/app/arch_config.yaml
- PLANO_CONFIG_PATH=/app/plano_config.yaml
- OPENAI_API_KEY=${OPENAI_API_KEY:?OPENAI_API_KEY environment variable is required but not set}
- OTEL_TRACING_GRPC_ENDPOINT=http://jaeger:4317
- LOG_LEVEL=${LOG_LEVEL:-info}
volumes:
- ./config.yaml:/app/arch_config.yaml:ro
- ./config.yaml:/app/plano_config.yaml:ro
- /etc/ssl/cert.pem:/etc/ssl/cert.pem
crewai-flight-agent:

View file

Before

Width:  |  Height:  |  Size: 331 KiB

After

Width:  |  Height:  |  Size: 331 KiB

Before After
Before After

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,4 @@
FROM python:3.11-slim
FROM python:3.14-slim
WORKDIR /app

View file

@ -8,10 +8,10 @@ services:
- "12000:12000"
- "8001:8001"
environment:
- ARCH_CONFIG_PATH=/config/config.yaml
- PLANO_CONFIG_PATH=/config/config.yaml
- OPENAI_API_KEY=${OPENAI_API_KEY:?OPENAI_API_KEY environment variable is required but not set}
volumes:
- ./config.yaml:/app/arch_config.yaml
- ./config.yaml:/app/plano_config.yaml
- /etc/ssl/cert.pem:/etc/ssl/cert.pem
weather-agent:
build:

Some files were not shown because too many files have changed in this diff Show more