From 2a590e74cd063e0aaf0156c5beca377039652b74 Mon Sep 17 00:00:00 2001 From: Kaiss Bouali Date: Thu, 9 Apr 2026 13:04:40 -0400 Subject: [PATCH 1/2] feat: implement model auto-downgrade chain - Rewrite model field in request body when threshold crossed - Walk downgrade chain to find cheaper alternative - Add X-Oberwatch-Downgraded response headers - Set Downgraded and OriginalModel on cost records --- internal/proxy/provider_routing.go | 4 ++++ internal/proxy/proxy.go | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/internal/proxy/provider_routing.go b/internal/proxy/provider_routing.go index ec3d14f..85525f8 100644 --- a/internal/proxy/provider_routing.go +++ b/internal/proxy/provider_routing.go @@ -266,6 +266,10 @@ func serveAnthropicOpenAICompat(w http.ResponseWriter, r *http.Request, route re CreatedAt: time.Now().UTC(), }) } + if meta.downgraded && meta.originalModel != "" { + w.Header().Set("X-Oberwatch-Downgraded", "true") + w.Header().Set("X-Oberwatch-Original-Model", meta.originalModel) + } } } diff --git a/internal/proxy/proxy.go b/internal/proxy/proxy.go index 35547a1..a5e14e2 100644 --- a/internal/proxy/proxy.go +++ b/internal/proxy/proxy.go @@ -97,6 +97,10 @@ func New(cfg config.Config, hooks Hooks) (*Server, error) { if !ok { return nil } + if meta.downgraded && meta.originalModel != "" { + response.Header.Set("X-Oberwatch-Downgraded", "true") + response.Header.Set("X-Oberwatch-Original-Model", meta.originalModel) + } response.Body = newBudgetTrackingBody( response.Body, response.StatusCode, From e453a36694b96a64865a2eae6d076525afa693ff Mon Sep 17 00:00:00 2001 From: Kaiss Bouali Date: Thu, 9 Apr 2026 13:05:44 -0400 Subject: [PATCH 2/2] test: add integration test script for model auto-downgrade - Build binary and start mock upstream server - Verify X-Oberwatch-Downgraded header present on downgraded request - Verify X-Oberwatch-Original-Model header contains original model name --- scripts/dev/test-downgrade.sh | 155 ++++++++++++++++++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 scripts/dev/test-downgrade.sh diff --git a/scripts/dev/test-downgrade.sh b/scripts/dev/test-downgrade.sh new file mode 100644 index 0000000..470febd --- /dev/null +++ b/scripts/dev/test-downgrade.sh @@ -0,0 +1,155 @@ +#!/usr/bin/env bash +set -euo pipefail + +export PATH=/usr/local/go/bin:$PATH +export GOCACHE=/tmp/go-build + +echo "==> Testing model auto-downgrade chain" + +TEMP_DIR=$(mktemp -d) +trap 'rm -rf "$TEMP_DIR"' EXIT + +CONFIG_FILE="$TEMP_DIR/oberwatch.toml" +DB_FILE="$TEMP_DIR/oberwatch.db" +BINARY="$TEMP_DIR/oberwatch" + +# Build binary first +echo "==> Building oberwatch binary..." +go build -o "$BINARY" ./cmd/oberwatch + +# Strategy: set gpt-4o pricing at $1,000,000/M so even 1 estimated token = $1.00. +# Budget limit = $10. Threshold = 50% = $5. +# First request body is ~50 bytes ≈ 12 tokens estimated → cost estimate ~$12 > 50% threshold. +# This means the FIRST real request will trigger downgrade immediately. +# We verify the downgrade headers are present. +cat > "$CONFIG_FILE" </dev/null || true; rm -rf "$TEMP_DIR"' EXIT + +# Wait for mock server +sleep 1 + +# Start oberwatch +"$BINARY" serve --config "$CONFIG_FILE" & +OBERWATCH_PID=$! +trap 'kill $OBERWATCH_PID 2>/dev/null || true; kill $MOCK_PID 2>/dev/null || true; rm -rf "$TEMP_DIR"' EXIT + +# Wait for oberwatch to start +sleep 2 + +# Verify oberwatch is up +if ! curl -sf http://localhost:18080/_oberwatch/api/v1/health > /dev/null 2>&1; then + echo "FAIL: Oberwatch did not start" + exit 1 +fi +echo "✓ Oberwatch is running" + +# Send request — pricing is $1M/M so even body byte estimate crosses 50% of $10 threshold. +# Gate checks estimated cost before forwarding → ActionDowngrade → rewrite model. +RESPONSE2=$(curl -s -i -X POST http://localhost:18080/v1/chat/completions \ + -H "Content-Type: application/json" \ + -H "X-Oberwatch-Agent: test-agent" \ + -H "Authorization: Bearer sk-test" \ + -d '{"model": "gpt-4o", "messages": [{"role": "user", "content": "hello"}]}') + +echo "--- Response headers ---" +echo "$RESPONSE2" | head -20 +echo "---" + +# Check for downgrade headers +if ! echo "$RESPONSE2" | grep -qi "X-Oberwatch-Downgraded: true"; then + echo "FAIL: Second request should have X-Oberwatch-Downgraded: true header" + echo "--- Response ---" + echo "$RESPONSE2" + exit 1 +fi + +if ! echo "$RESPONSE2" | grep -qi "X-Oberwatch-Original-Model: gpt-4o"; then + echo "FAIL: Second request should have X-Oberwatch-Original-Model: gpt-4o header" + echo "--- Response ---" + echo "$RESPONSE2" + exit 1 +fi + +echo "✓ Second request triggered downgrade with correct headers" +echo "==> All downgrade tests passed!"