llmsim 0.4.0

LLM Traffic Simulator - A lightweight, high-performance LLM API simulator
Documentation
name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

env:
  CARGO_TERM_COLOR: always
  RUST_BACKTRACE: 1

jobs:
  check:
    name: Check
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install Rust toolchain
        uses: dtolnay/rust-toolchain@1.95.0

      - name: Cache cargo registry
        uses: actions/cache@v4
        with:
          path: |
            ~/.cargo/registry
            ~/.cargo/git
            target
          key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}

      - name: Run cargo check
        run: cargo check --all-targets

  fmt:
    name: Format
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install Rust toolchain
        uses: dtolnay/rust-toolchain@1.95.0
        with:
          components: rustfmt

      - name: Check formatting
        run: cargo fmt --all -- --check

  clippy:
    name: Clippy
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install Rust toolchain
        uses: dtolnay/rust-toolchain@1.95.0
        with:
          components: clippy

      - name: Cache cargo registry
        uses: actions/cache@v4
        with:
          path: |
            ~/.cargo/registry
            ~/.cargo/git
            target
          key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}

      - name: Run clippy
        run: cargo clippy --all-targets -- -D warnings

  test:
    name: Test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install Rust toolchain
        uses: dtolnay/rust-toolchain@1.95.0

      - name: Cache cargo registry
        uses: actions/cache@v4
        with:
          path: |
            ~/.cargo/registry
            ~/.cargo/git
            target
          key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}

      - name: Run tests
        run: cargo test

  audit:
    name: Security Audit
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install Rust toolchain
        uses: dtolnay/rust-toolchain@1.95.0

      - name: Install cargo-audit
        run: cargo install cargo-audit

      - name: Run cargo-audit
        run: cargo audit

  deny:
    name: License & Dependency Check
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install Rust toolchain
        uses: dtolnay/rust-toolchain@1.95.0

      - name: Install cargo-deny
        run: cargo install cargo-deny

      - name: Run cargo-deny
        run: cargo deny check

  doc:
    name: Documentation
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install Rust toolchain
        uses: dtolnay/rust-toolchain@1.95.0

      - name: Cache cargo registry
        uses: actions/cache@v4
        with:
          path: |
            ~/.cargo/registry
            ~/.cargo/git
            target
          key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}

      - name: Build documentation
        run: cargo doc --no-deps
        env:
          RUSTDOCFLAGS: "-D warnings"

  build:
    name: Build
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ubuntu-latest, macos-latest, windows-latest]
    steps:
      - uses: actions/checkout@v4

      - name: Install Rust toolchain
        uses: dtolnay/rust-toolchain@1.95.0

      - name: Cache cargo registry
        uses: actions/cache@v4
        with:
          path: |
            ~/.cargo/registry
            ~/.cargo/git
            target
          key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}

      - name: Build release
        run: cargo build --release

  examples:
    name: Examples
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install Rust toolchain
        uses: dtolnay/rust-toolchain@1.95.0

      - name: Cache cargo registry
        uses: actions/cache@v4
        with:
          path: |
            ~/.cargo/registry
            ~/.cargo/git
            target
          key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'

      - name: Install Node dependencies
        run: npm install
        working-directory: examples/node

      - name: Run Rust example (basic)
        run: cargo run --example basic_usage

      - name: Run Rust example (responses)
        run: cargo run --example responses_usage

      - name: Run Rust example (scripted)
        run: cargo run --example scripted_demo

      - name: Build server
        run: cargo build --release

      - name: Start server
        run: |
          ./target/release/llmsim serve --port 8080 &
          sleep 2
          curl -sf http://localhost:8080/health

      - name: Run TypeScript example
        run: npx tsx openai_client.ts
        working-directory: examples/node

      - name: Smoke-test scripted server (examples/scripted_demo.json)
        run: |
          set -euo pipefail
          ./target/release/llmsim serve \
            --config examples/scripted_demo.toml \
            --port 8090 &
          SCRIPTED_PID=$!
          trap "kill $SCRIPTED_PID 2>/dev/null || true" EXIT
          sleep 2
          curl -sf http://localhost:8090/health > /dev/null

          # Turn 0: tool_calls(bash echo hello)
          R0=$(curl -sf -X POST http://localhost:8090/openai/v1/chat/completions \
            -H 'Content-Type: application/json' \
            -d '{"model":"gpt-5","messages":[{"role":"user","content":"go"}]}')
          echo "turn 0: $R0"
          echo "$R0" | grep -q '"finish_reason":"tool_calls"'
          echo "$R0" | grep -q '"name":"bash"'

          # Turn 1: tool_calls(bash sed)
          R1=$(curl -sf -X POST http://localhost:8090/openai/v1/chat/completions \
            -H 'Content-Type: application/json' \
            -d '{"model":"gpt-5","messages":[{"role":"user","content":"go"}]}')
          echo "$R1" | grep -q '"finish_reason":"tool_calls"'

          # Turn 2: mixed (streaming, should include tool_calls delta + DONE)
          R2=$(curl -sN -X POST http://localhost:8090/openai/v1/chat/completions \
            -H 'Content-Type: application/json' \
            -d '{"model":"gpt-5","messages":[{"role":"user","content":"go"}],"stream":true}')
          echo "$R2" | grep -q '"finish_reason":"tool_calls"'
          echo "$R2" | grep -q '\[DONE\]'

          # Turn 3: plain assistant "done"
          R3=$(curl -sf -X POST http://localhost:8090/openai/v1/chat/completions \
            -H 'Content-Type: application/json' \
            -d '{"model":"gpt-5","messages":[{"role":"user","content":"go"}]}')
          echo "$R3" | grep -q '"content":"done"'

          # Turn 4: on_exhausted=error → HTTP 500
          STATUS=$(curl -s -o /dev/null -w '%{http_code}' -X POST \
            http://localhost:8090/openai/v1/chat/completions \
            -H 'Content-Type: application/json' \
            -d '{"model":"gpt-5","messages":[{"role":"user","content":"go"}]}')
          test "$STATUS" = "500"
          echo "Scripted server smoke test OK"