Local fork: hardening + ops improvements (timeout knob, demotion, /livez, drain)
This commit captures both the prior accumulated work-in-progress
(framework migration web/→svelte/, postgres storage, conversation
viewer, dashboard auth, OpenAPI spec, integration tests) AND today's
operational improvements layered on top. History wasn't checkpointed
incrementally; happy to split it via interactive rebase if a reviewer
wants smaller commits.
Today's changes (in addition to the older WIP):
1. Configurable upstream response-header timeout
- ANTHROPIC_RESPONSE_HEADER_TIMEOUT env (default 300s)
- Replaces hardcoded 300s in provider/anthropic.go that was firing
on opus + 1M-context + extended thinking non-streaming requests
- Files: internal/config/config.go, internal/provider/anthropic.go
2. Structured forward-error diagnostic logging
- When a forward to Anthropic fails, log a single key=value line
with request_id, model, stream, body_bytes, has_thinking,
anthropic_beta, query, elapsed, ctx_err — alongside the existing
human-readable error line for back-compat
- Files: internal/handler/handlers.go (logForwardFailure)
3. Full SSE protocol passthrough + Flusher fix
- handler/handlers.go: forward all SSE lines verbatim (event:, id:,
retry:, : comments, blank-line terminators), not only data:.
Previous code produced malformed SSE for strict parsers.
- middleware/logging.go: explicit Flush() method on responseWriter.
Embedding http.ResponseWriter (interface) does not auto-promote
Flush(), so every w.(http.Flusher) check in the streaming
handler was returning ok=false and SSE writes buffered in net/http
until the body closed.
4. Non-streaming → streaming demotion (feature-flagged)
- ANTHROPIC_DEMOTE_NONSTREAMING env (default false)
- When enabled and the routed provider is anthropic, force stream=true
upstream for clients that asked for stream=false. Receive SSE,
accumulate via accumulateSSEToMessage (handles text, tool_use with
partial_json reassembly, thinking, signature, citations_delta,
usage merge), and synthesize a single non-streaming JSON response.
- Eliminates the ResponseHeaderTimeout class of failure entirely.
- Body rewrite uses json.Decoder + UseNumber() to preserve integer
precision in unknown nested fields (tool inputs from prior turns).
- Files: internal/config/config.go, internal/handler/handlers.go,
cmd/proxy/main.go, cmd/proxy/main_test.go
5. Live operational state: /livez gauge + graceful drain
- New internal/runtime package: atomic in-flight counter + draining flag
- New middleware/inflight.go: increments runtime gauge, applied to
/v1/* subrouter so Messages, ChatCompletions, and ProxyPassthrough
are all counted
- /v1/* moved to a gorilla/mux subrouter so the InFlight middleware
applies surgically; /health, /livez, /openapi.* remain on parent
router (unauthenticated, uncounted)
- Health handler returns 503 draining when runtime.IsDraining() is
true, so Traefik stops routing to a slot before drain begins
- New /livez handler returns {status, in_flight, draining, timestamp}
- SIGTERM handler in main.go: SetDraining(true), poll for in_flight==0
with 32-min ceiling and 1s tick (logs every 10s), then srv.Shutdown
- Auth bypass list extended with /livez
- Files: internal/runtime/runtime.go (new),
internal/middleware/inflight.go (new),
internal/middleware/auth.go,
internal/handler/handlers.go (Health, Livez, runtime import),
cmd/proxy/main.go (subrouter, drain loop)
6. OpenAPI spec updates
- Document Health 503 response and new DrainingResponse schema
- Add /livez path with LivezResponse schema
- Files: internal/handler/openapi.go
Verified: go build ./... clean, go test ./... all pass, go vet clean.
Three rounds of codex peer review across changes 1-5; all feedback
addressed (citations_delta, json.Number precision, drain-loop logging
via lastLog timestamp, PathPrefix tightened to "/v1/").
This commit is contained in:
parent
b9da198e1f
commit
8e550b9785
152 changed files with 19227 additions and 19463 deletions
152
Dockerfile
152
Dockerfile
|
|
@ -1,64 +1,132 @@
|
|||
# syntax=docker/dockerfile:1
|
||||
# Multi-stage Dockerfile for Claude Code Proxy
|
||||
# Builds both Go proxy server and Remix frontend in a single container
|
||||
# Builds both Go proxy server and SvelteKit frontend in a single container
|
||||
#
|
||||
# Targets:
|
||||
# - (default): Production runtime image
|
||||
# - dev: Development image with hot-reload tooling
|
||||
|
||||
# Stage 1: Build Go Backend
|
||||
FROM golang:1.21-alpine AS go-builder
|
||||
# ============================================================================
|
||||
# Stage: go-builder — compile Go proxy binary
|
||||
# ============================================================================
|
||||
FROM golang:1.26-alpine AS go-builder
|
||||
|
||||
WORKDIR /app
|
||||
WORKDIR /app/proxy
|
||||
|
||||
# Install build dependencies including gcc for CGO
|
||||
# Install build dependencies including gcc for CGO (sqlite)
|
||||
RUN apk add --no-cache git gcc musl-dev sqlite-dev
|
||||
|
||||
# Copy Go modules
|
||||
COPY proxy/go.mod proxy/go.sum ./proxy/
|
||||
WORKDIR /app/proxy
|
||||
RUN go mod download
|
||||
# Copy Go modules first (cache layer)
|
||||
COPY proxy/go.mod proxy/go.sum ./
|
||||
RUN --mount=type=cache,target=/go/pkg/mod \
|
||||
go mod download
|
||||
|
||||
# Copy Go source code
|
||||
# Copy Go source code and build
|
||||
COPY proxy/ ./
|
||||
# Build with CGO enabled for SQLite support
|
||||
RUN CGO_ENABLED=1 GOOS=linux go build -a -installsuffix cgo -o /app/bin/proxy cmd/proxy/main.go
|
||||
RUN --mount=type=cache,target=/go/pkg/mod \
|
||||
--mount=type=cache,target=/root/.cache/go-build \
|
||||
CGO_ENABLED=1 GOOS=linux go build -a -installsuffix cgo -o /app/bin/proxy cmd/proxy/main.go
|
||||
|
||||
# Stage 2: Build Node.js Frontend
|
||||
FROM node:20-alpine AS node-builder
|
||||
# ============================================================================
|
||||
# Stage: svelte-deps — install SvelteKit dependencies (cached)
|
||||
# ============================================================================
|
||||
FROM node:20-alpine AS svelte-deps
|
||||
|
||||
WORKDIR /app/svelte
|
||||
COPY svelte/package*.json ./
|
||||
RUN --mount=type=cache,target=/root/.npm \
|
||||
npm ci
|
||||
|
||||
# ============================================================================
|
||||
# Stage: svelte-builder — build SvelteKit frontend
|
||||
# ============================================================================
|
||||
FROM svelte-deps AS svelte-builder
|
||||
|
||||
COPY svelte/ ./
|
||||
# shared/ is referenced by vite.config.ts (../shared/frontend/backend)
|
||||
COPY shared/ /app/shared/
|
||||
RUN npm run build
|
||||
|
||||
# ============================================================================
|
||||
# Stage: svelte-prod — production SvelteKit deps only
|
||||
# ============================================================================
|
||||
FROM node:20-alpine AS svelte-prod
|
||||
|
||||
WORKDIR /app/svelte
|
||||
COPY svelte/package*.json ./
|
||||
RUN --mount=type=cache,target=/root/.npm \
|
||||
npm ci --omit=dev
|
||||
|
||||
# ============================================================================
|
||||
# Stage: dev — development image with hot-reload
|
||||
# ============================================================================
|
||||
# CGO is required for mattn/go-sqlite3. To avoid slow first-build times
|
||||
# at container start, we pre-build the binary (and warm the build cache)
|
||||
# during image build. CompileDaemon then does fast incremental rebuilds.
|
||||
FROM golang:1.26-alpine AS dev
|
||||
|
||||
# Copy Node.js 20 from official image (Alpine's repos ship Node 24 which has
|
||||
# breaking module-resolution changes that break SvelteKit's virtual modules)
|
||||
COPY --from=node:20-alpine /usr/local/bin/node /usr/local/bin/node
|
||||
COPY --from=node:20-alpine /usr/local/lib/node_modules /usr/local/lib/node_modules
|
||||
RUN ln -sf /usr/local/lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm \
|
||||
&& ln -sf /usr/local/lib/node_modules/npm/bin/npx-cli.js /usr/local/bin/npx
|
||||
|
||||
# Install build deps and runtime tools
|
||||
RUN apk add --no-cache \
|
||||
libstdc++ \
|
||||
git gcc musl-dev sqlite-dev \
|
||||
wget su-exec postgresql-client
|
||||
|
||||
# Install CompileDaemon for Go hot-reload
|
||||
RUN go install github.com/githubnemo/CompileDaemon@latest
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package files
|
||||
COPY web/package*.json ./web/
|
||||
WORKDIR /app/web
|
||||
RUN npm ci
|
||||
# Pre-install Go dependencies and do initial build to warm cgo cache
|
||||
COPY proxy/go.mod proxy/go.sum ./proxy/
|
||||
RUN cd proxy && go mod download
|
||||
|
||||
# Copy web source code and build
|
||||
COPY web/ ./
|
||||
RUN npm run build
|
||||
COPY proxy/ ./proxy/
|
||||
RUN cd proxy && CGO_ENABLED=1 go build -o /tmp/proxy-bin/proxy cmd/proxy/main.go
|
||||
|
||||
# Clean up dev dependencies after build
|
||||
RUN npm ci --only=production && npm cache clean --force
|
||||
# Pre-install Node dependencies for svelte (layer cache)
|
||||
COPY svelte/package*.json ./svelte/
|
||||
RUN cd svelte && npm install
|
||||
|
||||
# Stage 3: Production Runtime
|
||||
# Copy the dev entrypoint
|
||||
COPY docker-entrypoint.dev.sh ./
|
||||
RUN chmod +x docker-entrypoint.dev.sh
|
||||
|
||||
ENV PORT=3001
|
||||
ENV SVELTE_PORT=5174
|
||||
ENV CGO_ENABLED=1
|
||||
|
||||
EXPOSE 3001 5174
|
||||
|
||||
ENTRYPOINT ["./docker-entrypoint.dev.sh"]
|
||||
|
||||
# ============================================================================
|
||||
# Stage: (default) — production runtime
|
||||
# ============================================================================
|
||||
FROM node:20-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install runtime dependencies
|
||||
RUN apk add --no-cache sqlite wget
|
||||
|
||||
# Create app user for security
|
||||
RUN addgroup -g 1001 -S appgroup && \
|
||||
adduser -S appuser -u 1001 -G appgroup
|
||||
# Install runtime dependencies (sqlite for legacy, postgresql-client for healthcheck)
|
||||
RUN apk add --no-cache sqlite wget su-exec postgresql-client
|
||||
|
||||
# Copy built Go binary
|
||||
COPY --from=go-builder /app/bin/proxy ./bin/proxy
|
||||
RUN chmod +x ./bin/proxy
|
||||
|
||||
# Copy built Remix application
|
||||
COPY --from=node-builder /app/web/build ./web/build
|
||||
COPY --from=node-builder /app/web/package*.json ./web/
|
||||
COPY --from=node-builder /app/web/node_modules ./web/node_modules
|
||||
# Copy built SvelteKit application with production deps
|
||||
COPY --from=svelte-builder /app/svelte/build ./svelte/build
|
||||
COPY --from=svelte-prod /app/svelte/package*.json ./svelte/
|
||||
COPY --from=svelte-prod /app/svelte/node_modules ./svelte/node_modules
|
||||
|
||||
# Create data directory for SQLite database
|
||||
RUN mkdir -p /app/data && chown -R appuser:appgroup /app
|
||||
RUN mkdir -p /app/data && chown -R node:node /app
|
||||
|
||||
# Copy startup script
|
||||
COPY docker-entrypoint.sh ./
|
||||
|
|
@ -66,7 +134,7 @@ RUN chmod +x docker-entrypoint.sh
|
|||
|
||||
# Environment variables with defaults
|
||||
ENV PORT=3001
|
||||
ENV WEB_PORT=5173
|
||||
ENV SVELTE_PORT=5174
|
||||
ENV READ_TIMEOUT=600
|
||||
ENV WRITE_TIMEOUT=600
|
||||
ENV IDLE_TIMEOUT=600
|
||||
|
|
@ -75,15 +143,11 @@ ENV ANTHROPIC_VERSION=2023-06-01
|
|||
ENV ANTHROPIC_MAX_RETRIES=3
|
||||
ENV DB_PATH=/app/data/requests.db
|
||||
|
||||
# Expose ports
|
||||
EXPOSE 3001 5173
|
||||
EXPOSE 3001 5174
|
||||
|
||||
# Switch to app user
|
||||
USER appuser
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||
CMD wget -qO- http://localhost:3001/health > /dev/null || exit 1
|
||||
|
||||
# Start both services
|
||||
CMD ["./docker-entrypoint.sh"]
|
||||
# Entrypoint handles privilege drop — compose overrides user to root at start
|
||||
ENTRYPOINT ["./docker-entrypoint.sh"]
|
||||
CMD []
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue