zai-proxy/DEVELOPMENT.md
2026-06-21 10:05:18 -04:00

19 KiB

Developer Guide

Comprehensive onboarding guide for zai-proxy developers.

Table of Contents

Prerequisites

Required Tools

  • Go 1.23+ - Proxy and dashboard backend
  • Node.js 18+ and npm - Dashboard frontend
  • Docker - Container builds and testing
  • Git - Version control

Optional Tools

  • Make - Convenience targets (if available)
  • kubectl - Kubernetes access for deployment testing
  • Visual Studio Code or GoLand - Recommended IDEs

Verify Installation

# Check Go version
go version
# Expected: go version go1.23.x or later

# Check Node.js version
node --version
# Expected: v18.x.x or later

# Check npm version
npm --version

# Check Docker
docker --version

Project Structure

zai-proxy/
├── proxy/                    # Go reverse proxy
│   ├── cmd/                  # Command-line entry points
│   ├── evaluation/           # Token counting evaluation tools
│   ├── scripts/              # Utility scripts
│   ├── tests/                # Integration test fixtures
│   ├── main.go               # Proxy server implementation
│   ├── tokenizer.go          # Token counting logic
│   ├── translator.go         # Provider format translation
│   ├── metrics.go            # Prometheus metrics
│   ├── bodyparser.go         # Request/response parsing
│   ├── *_test.go             # Test files
│   ├── go.mod                # Go dependencies
│   ├── Dockerfile            # Container image
│   └── README.md             # Proxy-specific docs
│
├── dashboard/                # Monitoring dashboard
│   ├── api/                  # HTTP handlers and SSE
│   ├── collector/            # Prometheus scraper
│   ├── storage/              # SQLite database layer
│   ├── logger/               # Structured logging
│   ├── model/                # Data structures
│   ├── frontend/             # React UI
│   │   ├── src/              # TypeScript source
│   │   │   ├── components/   # React components
│   │   │   ├── hooks/        # Custom React hooks
│   │   │   ├── utils/        # Utilities
│   │   │   └── main.tsx      # Entry point
│   │   ├── package.json      # Node dependencies
│   │   └── vite.config.ts    # Vite build config
│   ├── main.go               # Dashboard server
│   ├── go.mod                # Go dependencies
│   ├── Dockerfile            # Multi-stage build
│   └── README.md             # Dashboard-specific docs
│
├── docs/                     # Documentation
│   ├── plan/                 # Architecture and roadmap
│   ├── notes/                # Operational guides
│   └── research/             # External research
│
├── CONTRIBUTING.md           # Contribution guidelines
├── DEVELOPMENT.md            # This file
└── README.md                 # Project overview

Local Development Setup

Quick Start (Full Stack)

# Clone repository
git clone https://git.ardenone.com/jedarden/zai-proxy.git
cd zai-proxy

# Setup proxy
cd proxy
go mod download
export ZAI_API_KEY="your-api-key-here"

# Setup dashboard (new terminal)
cd ../dashboard/frontend
npm install

# Run proxy (terminal 1)
cd ../proxy
go run .

# Run dashboard backend (terminal 2)
cd ../dashboard
export SCRAPE_TARGETS="http://localhost:8080/metrics"
go run .

# Run dashboard frontend (terminal 3)
cd frontend
npm run dev

Proxy Development

The proxy is a Go HTTP reverse proxy that fronts the Z.AI API.

Setup

cd /home/coding/zai-proxy/proxy

# Download dependencies
go mod download

# Set required environment variables
export ZAI_API_KEY="your-zai-api-key"

# Optional: Configure tokenizer model
export TOKENIZER_MODEL="glm-4"  # Default

# Optional: Adjust rate limits
export RATE_LIMIT_INITIAL="10.0"
export RATE_LIMIT_MAX="50.0"
export MAX_WORKERS="50"

Run Locally

# Simple run
go run .

# Run with specific listen address
export LISTEN_ADDR=":8080"
go run .

# Run with debug logging
export LOG_LEVEL="debug"
go run .

The proxy listens on :8080 by default:

  • Proxy API: http://localhost:8080/v1/messages
  • Metrics: http://localhost:8080/metrics

Test Proxy

# Make a test request
curl -X POST http://localhost:8080/v1/messages \
  -H "Content-Type: application/json" \
  -H "x-api-key: $ZAI_API_KEY" \
  -H "anthropic-version: 2023-06-01" \
  -d '{
    "model": "claude-3-sonnet-4-20250514",
    "max_tokens": 100,
    "messages": [{"role": "user", "content": "Hello!"}]
  }'

# Check metrics
curl http://localhost:8080/metrics | grep zai_proxy

Build Binary

# Build for current platform
go build -o zai-proxy .

# Build for Linux
GOOS=linux GOARCH=amd64 go build -o zai-proxy-linux-amd64 .

# Build with version info
VERSION=$(cat VERSION) go build -ldflags="-X main.Version=$VERSION" -o zai-proxy .

Dashboard Development

The dashboard has a Go backend and React + TypeScript frontend.

Backend Setup

cd /home/coding/zai-proxy/dashboard

# Download dependencies
go mod download

# Set environment variables (optional, defaults shown)
export LISTEN_ADDR=":8081"           # Dashboard API port
export SCRAPE_TARGETS="http://localhost:8080/metrics"
export SCRAPE_INTERVAL="5s"
export DB_PATH="/tmp/dashboard.db"

# Run backend
go run .

The dashboard backend listens on :8081 by default:

  • Frontend: http://localhost:8081/
  • API: http://localhost:8081/api/
  • SSE: http://localhost:8081/api/events
  • Health: http://localhost:8081/healthz

Frontend Development

cd /home/coding/zai-proxy/dashboard/frontend

# Install dependencies
npm install

# Run dev server (with hot reload)
npm run dev

# Build for production
npm run build

# Preview production build
npm run preview

# Run linter
npm run lint

# Run tests
npm run test
npm run test:watch  # Watch mode

The frontend dev server runs on :5173 and proxies API requests to :8081.

Frontend Project Structure

frontend/src/
├── main.tsx              # React entry point
├── App.tsx               # Root component
├── components/           # React components
│   ├── MetricCard.tsx    # Metric display cards
│   ├── StatusHeader.tsx  # Status bar
│   ├── TokenPanel.tsx    # Token usage panel
│   ├── RequestPanel.tsx  # Request metrics
│   ├── RateLimitPanel.tsx # Rate limiting panel
│   └── ...
├── hooks/                # Custom React hooks
│   ├── useMetrics.ts     # Metrics data fetching
│   ├── useSSE.ts         # SSE connection
│   └── ...
└── utils/                # Utilities
    ├── formatters.ts     # Number/time formatting
    └── constants.ts      # Constants

Running Tests

Proxy Tests

cd /home/coding/zai-proxy/proxy

# Run all tests
go test -v ./...

# Run specific package tests
go test -v ./tokenizer
go test -v ./translator

# Run specific test
go test -v -run TestTokenCountingBasicRequest

# Run with coverage
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out

# Run benchmarks
go test -bench=. -benchmem

# Run regression tests (token counting accuracy)
go test -v -run Regression

Dashboard Tests

Backend Tests

cd /home/coding/zai-proxy/dashboard

# Run all backend tests
go test -v ./...

# Run specific package tests
go test -v ./collector
go test -v ./storage
go test -v ./api

# Run with coverage
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out

Frontend Tests

cd /home/coding/zai-proxy/dashboard/frontend

# Run tests once
npm run test

# Run in watch mode
npm run test:watch

# Run with coverage
npm run test -- --coverage

# Run specific test file
npm run test -- src/hooks/useMetrics.test.ts

Running All Tests

# From project root
cd /home/coding/zai-proxy

# Run all Go tests
go test -v ./proxy/... ./dashboard/...

# Run all frontend tests
cd dashboard/frontend && npm run test

Debugging

Proxy Debugging

Enable Debug Logging

export LOG_LEVEL="debug"
go run .

Use Delve (Go Debugger)

# Install delve
go install github.com/go-delve/delve/cmd/dlv@latest

# Debug with delve
dlv debug

# Common delve commands:
# (dlv) break main.go:125    # Set breakpoint
# (dlv) continue             # Continue execution
# (dlv) next                 # Next line
# (dlv) print variableName   # Print variable
# (dlv) goroutines           # List goroutines
# (dlv) goroutine 5          # Switch to goroutine 5

VS Code Debug Configuration

Create .vscode/launch.json:

{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Debug Proxy",
      "type": "go",
      "request": "launch",
      "mode": "auto",
      "program": "${workspaceFolder}/proxy",
      "env": {
        "ZAI_API_KEY": "your-api-key",
        "LOG_LEVEL": "debug"
      }
    },
    {
      "name": "Debug Dashboard Backend",
      "type": "go",
      "request": "launch",
      "mode": "auto",
      "program": "${workspaceFolder}/dashboard",
      "env": {
        "SCRAPE_TARGETS": "http://localhost:8080/metrics",
        "LOG_LEVEL": "debug"
      }
    }
  ]
}

Dashboard Debugging

Backend Debugging

Use the same delve approach as the proxy:

cd /home/coding/zai-proxy/dashboard
dlv debug

Frontend Debugging

The Vite dev server includes hot module replacement (HMR). Use browser DevTools:

cd /home/coding/zai-proxy/dashboard/frontend
npm run dev

Chrome DevTools shortcuts:

  • F12 or Ctrl+Shift+I - Open DevTools
  • Ctrl+Shift+J - Jump to console
  • React DevTools extension recommended

SSE Connection Debugging

# Test SSE endpoint directly
curl -N http://localhost:8081/api/events

# With verbose output
curl -v -N http://localhost:8081/api/events

Common Debugging Scenarios

Token Counting Issues

# Check if tokenizer initialized correctly
kubectl logs deployment/zai-proxy -n mcp | grep -i "token counting"

# Expected: "Token counting enabled (tiktoken cl100k_base encoding, model: glm-4)"

# Check if fallback is active
kubectl logs deployment/zai-proxy -n mcp | grep -i "fallback"

Dashboard Not Showing Data

# Check if collector is scraping
kubectl logs deployment/zai-proxy-dashboard -n mcp | grep "scrape"

# Check proxy metrics endpoint
curl http://zai-proxy.mcp.svc.cluster.local:8080/metrics

# Test direct connection from dashboard pod
kubectl exec -n mcp deployment/zai-proxy-dashboard -- \
  wget -O- http://zai-proxy.mcp.svc.cluster.local:8080/metrics

High Memory/CPU Usage

# Check container metrics
kubectl top pod -n mcp -l app=zai-proxy

# Check Go runtime metrics
curl http://localhost:8080/metrics | grep go_

Common Development Workflows

Adding a New Feature

  1. Create feature branch

    git checkout -b feat/add-new-feature
    
  2. Write tests first

    # Create test file
    touch proxy/myfeature_test.go
    
  3. Implement feature

    # Edit source files
    vim proxy/myfeature.go
    
  4. Run tests

    go test -v ./proxy
    npm run test  # If frontend changes
    
  5. Format code

    gofmt -w ./proxy ./dashboard
    npm run lint  # Frontend
    
  6. Commit changes

    git add .
    git commit -m "feat(proxy): add new feature"
    
  7. Push and create PR

    git push origin feat/add-new-feature
    

Adding a New Metric

  1. Define metric in metrics.go

    var myNewMetric = prometheus.NewCounterVec(
        prometheus.CounterOpts{
            Name: "zai_proxy_my_new_metric",
            Help: "Description of what this metric tracks",
        },
        []string{"label1", "label2"},
    )
    
  2. Register in MetricsRegistry

    var MetricsRegistry = []*prometheus.CounterVec{
        // ... existing metrics
        myNewMetric,
    }
    
  3. Use in code

    myNewMetric.WithLabelValues("value1", "value2").Inc()
    
  4. Add test

    func TestMyNewMetric(t *testing.T) {
        // Test that metric is registered
        // Test that metric increments correctly
    }
    
  5. Test locally

    curl http://localhost:8080/metrics | grep my_new_metric
    

Adding a New Frontend Component

  1. Create component file

    touch dashboard/frontend/src/components/MyComponent.tsx
    
  2. Implement component

    interface MyComponentProps {
      title: string;
      value: number;
    }
    
    export function MyComponent({ title, value }: MyComponentProps) {
      return (
        <div className="p-4 bg-white rounded shadow">
          <h3 className="text-lg font-semibold">{title}</h3>
          <p className="text-2xl">{value}</p>
        </div>
      );
    }
    
  3. Add tests

    // MyComponent.test.tsx
    import { describe, it, expect } from 'vitest';
    import { render, screen } from '@testing-library/react';
    import { MyComponent } from './MyComponent';
    
    describe('MyComponent', () => {
      it('renders title and value', () => {
        render(<MyComponent title="Test" value={42} />);
        expect(screen.getByText('Test')).toBeTruthy();
        expect(screen.getByText('42')).toBeTruthy();
      });
    });
    
  4. Use in App

    import { MyComponent } from './components/MyComponent';
    
    // In your component
    <MyComponent title="My Metric" value={123} />
    
  5. Test

    npm run test
    npm run lint
    

Updating Dependencies

Go Dependencies

cd /home/coding/zai-proxy/proxy  # or dashboard/

# List dependencies
go list -m all

# Update all dependencies
go get -u ./...
go mod tidy

# Update specific dependency
go get -u github.com/prometheus/client_golang@latest
go mod tidy

# Verify tests still pass
go test -v ./...

Node Dependencies

cd /home/coding/zai-proxy/dashboard/frontend

# Check for updates
npm outdated

# Update all dependencies
npm update

# Update specific dependency
npm install package@latest

# Run tests to verify
npm run test

CI/CD Process

The project uses GitHub Actions for CI/CD. See .github/workflows/ for workflow definitions.

Workflow Types

  1. On Push - Runs tests and builds on every push
  2. On PR - Runs full test suite including regression tests
  3. Manual - Docker image builds for specific branches

Build Process

Docker Image Build

The project uses GitHub Actions for Docker builds in devpod environments due to overlayfs limitations.

# .github/workflows/build.yml
name: Build Docker Image

on:
  push:
    branches: [main]
  workflow_dispatch:

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Build and push
        run: |
          docker build -t ghcr.io/ardenone/zai-proxy:${{ github.sha }} .
          docker push ghcr.io/ardenone/zai-proxy:${{ github.sha }}

Deployment

Deployments are managed via GitOps through ArgoCD:

  1. Update manifests in jedarden/declarative-config repository
  2. Commit and push changes
  3. ArgoCD automatically syncs changes to clusters

See docs/notes/DEPLOYMENT.md for detailed deployment procedures.

Running CI Locally

# Run Go tests with race detector
go test -race -v ./...

# Run with coverage (required for CI)
go test -coverprofile=coverage.out ./...
go tool cover -func=coverage.out

# Check coverage threshold (typically 80%)
go test -coverprofile=coverage.out ./...
go tool cover -func=coverage.out | grep total

Code Style and Conventions

Go Code Style

  • Follow Effective Go
  • Run gofmt -w . before committing
  • Use meaningful variable names
  • Add doc comments for exported functions
  • Handle errors explicitly
  • Keep functions focused and small
// Good
func countTokens(text string) (int, error) {
    if text == "" {
        return 0, fmt.Errorf("empty text")
    }
    // Implementation
    return count, nil
}

// Bad
func cnt(t string) int {
    // Too terse
}

TypeScript/React Style

  • Use functional components with hooks
  • Prefer const over let
  • Avoid any type
  • Use TypeScript interfaces for props
  • Follow existing component patterns
// Good
interface Props {
  title: string;
  value: number;
}

export function MetricCard({ title, value }: Props) {
  return <div>{title}: {value}</div>;
}

// Bad
export function MetricCard(props: any) {
  return <div>{props.title}: {props.value}</div>;
}

Commit Message Format

Follow conventional commits:

<type>(<scope>): <description>

[optional body]

[optional footer]

Types:

  • feat - New feature
  • fix - Bug fix
  • docs - Documentation changes
  • chore - Maintenance tasks
  • refactor - Code refactoring
  • test - Adding or updating tests
  • perf - Performance improvements

Scopes:

  • proxy - Go proxy backend
  • dashboard - Dashboard (backend + frontend)
  • frontend - React frontend specifically

Examples:

feat(proxy): add GLM-4 tokenizer support
fix(dashboard): correct token count display
docs(proxy): update environment variables reference
chore: bump VERSION to 1.1.0

Troubleshooting

Port Already in Use

# Find process using port
lsof -i :8080

# Kill process
kill -9 <PID>

# Or use different port
export LISTEN_ADDR=":8081"

Module Download Failed

# Clear Go module cache
go clean -modcache

# Re-download dependencies
go mod download

# Verify checksums
go mod verify

Frontend Build Errors

# Clear node_modules and reinstall
rm -rf node_modules package-lock.json
npm install

# Clear Vite cache
rm -rf .vite dist
npm run build

Test Failures

# Run tests with verbose output
go test -v ./...

# Run specific test with debug output
go test -v -run TestName ./...

# Check for race conditions
go test -race ./...

Docker Build Issues

If Docker builds fail in devpod environments:

# Use GitHub Actions instead
# See .github/workflows/ for build workflows
# Or use a different environment with full Docker support

Getting Help

  • Documentation: Check docs/notes/ for operational guides
  • Issues: File in repository
  • Questions: Contact jedarden@jedarden.com
  • Logs: kubectl logs -f deployment/zai-proxy -n mcp
  • Metrics: http://zai-proxy.mcp.svc.cluster.local:8080/metrics

Additional Resources