Refactor config: providers.upstream → providers.openai, add wildcard proxy
authorStefan Gasser <redacted>
Fri, 16 Jan 2026 23:36:14 +0000 (00:36 +0100)
committerStefan Gasser <redacted>
Fri, 16 Jan 2026 23:36:48 +0000 (00:36 +0100)
Config changes:
- Rename providers.upstream to providers.openai for clarity
- Remove routing config section (simplified to: PII → local, no PII → openai)
- Move local provider to top-level config (not under providers)
- Change default secrets action from block to redact

Proxy changes:
- Replace specific /models route with wildcard /* proxy
- Supports all OpenAI endpoints: /models, /embeddings, /audio/*, etc.

Documentation:
- Update all docs to reflect new config structure
- Remove docs/api-reference/models.mdx (now covered by wildcard proxy)

23 files changed:
CLAUDE.md
README.md
config.example.yaml
docs/api-reference/chat-completions.mdx
docs/api-reference/dashboard-api.mdx
docs/api-reference/models.mdx [deleted file]
docs/api-reference/status.mdx
docs/concepts/mask-mode.mdx
docs/concepts/route-mode.mdx
docs/configuration/overview.mdx
docs/configuration/providers.mdx
docs/integrations.mdx
docs/introduction.mdx
docs/mint.json
src/config.ts
src/index.ts
src/routes/info.ts
src/routes/proxy.ts
src/services/decision.test.ts
src/services/decision.ts
src/services/llm-client.ts
src/services/logger.ts
src/views/dashboard/page.tsx

index 1cddc1b3766486958e008f866aa555e68cdda1c9..837c9ce72fe5e85c06cade313879da9182e8c2ee 100644 (file)
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -1,6 +1,6 @@
 # PasteGuard
 
-OpenAI-compatible proxy with two privacy modes: route to local LLM or mask PII for upstream.
+OpenAI-compatible proxy with two privacy modes: route to local LLM or mask PII for configured provider.
 
 ## Tech Stack
 
@@ -19,7 +19,7 @@ src/
 ├── index.ts                 # Hono server entry
 ├── config.ts                # YAML config + Zod validation
 ├── routes/
-│   ├── proxy.ts             # POST /openai/v1/chat/completions
+│   ├── proxy.ts             # /openai/v1/* (chat completions + wildcard proxy)
 │   ├── dashboard.tsx        # Dashboard routes + API
 │   ├── health.ts            # GET /health
 │   └── info.ts              # GET /info
@@ -42,8 +42,8 @@ Tests are colocated (`*.test.ts`).
 
 Two modes configured in `config.yaml`:
 
-- **Route**: Routes PII-containing requests to local LLM (requires `local` provider + `routing` config)
-- **Mask**: Masks PII before upstream, unmasks response (no local provider needed)
+- **Route**: Routes PII-containing requests to local LLM (requires `local` provider config)
+- **Mask**: Masks PII before sending to configured provider, unmasks response (no local provider needed)
 
 See @config.example.yaml for full configuration.
 
index 7ba53337de1b0cbb87d8724872ed8b510043eda3..a22be0d3eb87c877d43329d452945a493c81a285 100644 (file)
--- a/README.md
+++ b/README.md
@@ -66,7 +66,7 @@ LLM responds: "Dear [[PERSON_1]], Following up on our discussion..."
 You receive:  "Dear Dr. Sarah Chen, Following up on our discussion..."
 ```
 
-PasteGuard sits between your app and the LLM provider. It's OpenAI-compatible — just change the base URL.
+PasteGuard sits between your app and your provider. It's OpenAI-compatible — just change the base URL.
 
 ## Quick Start
 
index 72452468a74b00dbfc3d71400414eebaf7fe6fb6..cc923d81e5ba4e6aba3d7c13a75e03529e2be8e0 100644 (file)
@@ -3,8 +3,8 @@
 
 # Privacy mode: "mask" or "route"
 #
-# mask:  Masks PII before sending to upstream, unmasks in response (no local LLM needed)
-# route: Routes requests to local LLM when PII detected (requires local provider)
+# mask:  Masks PII before sending to provider, unmasks in response
+# route: Routes PII requests to local LLM, non-PII to configured provider
 mode: mask
 
 # Server settings
@@ -12,33 +12,27 @@ server:
   port: 3000
   host: "0.0.0.0"
 
-# LLM Provider configuration
+# Providers - OpenAI-compatible API endpoints
+# Can be cloud (OpenAI, Azure) or self-hosted (vLLM, LiteLLM proxy, etc.)
 providers:
-  # Upstream provider (required for both modes)
-  # The proxy forwards your client's Authorization header to the upstream provider
-  # You can optionally set api_key here as a fallback
-  upstream:
-    type: openai
+  # OpenAI-compatible endpoint (required)
+  # The proxy forwards your client's Authorization header
+  openai:
     base_url: https://api.openai.com/v1
     # api_key: ${OPENAI_API_KEY}  # Optional fallback if client doesn't send auth header
 
-  # Local provider (only for route mode - can be removed if using mask mode)
-  # Supports: ollama, openai (for OpenAI-compatible servers like LocalAI, LM Studio)
-  local:
-    type: ollama  # or "openai" for OpenAI-compatible servers
-    base_url: http://localhost:11434
-    model: llama3.2  # All PII requests use this model
-    # api_key: ${LOCAL_API_KEY}  # Only needed for OpenAI-compatible servers
-
-# Routing rules (only for route mode - can be removed if using mask mode)
-routing:
-  # Default provider when no PII is detected
-  default: upstream
-
-  # Provider to use when PII is detected
-  on_pii_detected: local
-
-# Masking settings (only for mask mode - can be removed if using route mode)
+# Local provider - only for route mode
+# PII requests are sent here instead of the configured provider
+# Supports: ollama (native), openai (for vLLM, LocalAI, LM Studio, etc.)
+#
+# Uncomment for route mode:
+# local:
+#   type: ollama  # or "openai" for OpenAI-compatible servers
+#   base_url: http://localhost:11434
+#   model: llama3.2
+#   # api_key: ${LOCAL_API_KEY}  # Only needed for OpenAI-compatible servers
+
+# Masking settings (only for mask mode)
 masking:
   # Add visual markers to unmasked values in response (for debugging/demos)
   # Interferes with copy/paste, so disabled by default
@@ -95,11 +89,10 @@ secrets_detection:
   enabled: true
 
   # Action to take when secrets are detected:
+  #   redact:       Replace secrets with placeholders, unmask in response (default)
   #   block:        Block the request with HTTP 400
-  #   redact:       Replace secrets with placeholders, unmask in response
   #   route_local:  Route to local provider (only works in route mode)
-  # Default: redact (if not specified)
-  action: block
+  action: redact
 
   # Secret types to detect
   # Private Keys (enabled by default):
@@ -155,7 +148,7 @@ logging:
   log_content: false
 
   # Log masked content for dashboard preview (default: true)
-  # Shows what was actually sent to upstream LLM with PII replaced by placeholders
+  # Shows what was actually sent to provider with PII replaced by placeholders
   # Disable if you don't want any content stored, even masked
   log_masked_content: true
 
@@ -166,4 +159,4 @@ dashboard:
   # Basic auth for dashboard (optional)
   # auth:
   #   username: admin
-  #   password: ${DASHBOARD_PASSWORD}
\ No newline at end of file
+  #   password: ${DASHBOARD_PASSWORD}
index df01dc7422d0a6d32b38148874d1ad4cd361bf03..cd922be3921a910b62bfb666517574428534e56d 100644 (file)
@@ -5,12 +5,16 @@ description: POST /openai/v1/chat/completions
 
 # Chat Completions
 
-Generate chat completions. Identical to OpenAI's endpoint.
+Generate chat completions with automatic PII and secrets protection.
 
 ```
 POST /openai/v1/chat/completions
 ```
 
+<Note>
+This is the only endpoint that receives PII detection and masking. All other OpenAI endpoints (`/models`, `/embeddings`, `/files`, etc.) are proxied directly to OpenAI without modification.
+</Note>
+
 ## Request
 
 ```bash
@@ -112,7 +116,7 @@ PasteGuard adds headers to indicate PII and secrets handling:
 | Header | Description |
 |--------|-------------|
 | `X-PasteGuard-Mode` | Current mode (`mask` or `route`) |
-| `X-PasteGuard-Provider` | Provider used (`upstream` or `local`) |
+| `X-PasteGuard-Provider` | Provider used (`openai` or `local`) |
 | `X-PasteGuard-PII-Detected` | `true` if PII was found |
 | `X-PasteGuard-PII-Masked` | `true` if PII was masked (mask mode only) |
 | `X-PasteGuard-Language` | Detected language code |
index af02035b29c5a907b2c355fd47b52490f9e1f027..b23f33f616b1293e758e3dbb639dbf1dfc405312 100644 (file)
@@ -37,7 +37,7 @@ curl "http://localhost:3000/dashboard/api/logs?limit=100&offset=0"
       "id": 1,
       "timestamp": "2026-01-15T10:30:00Z",
       "mode": "mask",
-      "provider": "upstream",
+      "provider": "openai",
       "model": "gpt-5.2",
       "pii_detected": true,
       "entities": "[\"EMAIL_ADDRESS\",\"PERSON\"]",
@@ -85,7 +85,7 @@ curl http://localhost:3000/dashboard/api/stats
   "total_requests": 1500,
   "pii_requests": 342,
   "pii_percentage": 22.8,
-  "upstream_requests": 1200,
+  "openai_requests": 1200,
   "local_requests": 300,
   "avg_scan_time_ms": 45,
   "total_tokens": 125000,
diff --git a/docs/api-reference/models.mdx b/docs/api-reference/models.mdx
deleted file mode 100644 (file)
index ca9fe05..0000000
+++ /dev/null
@@ -1,59 +0,0 @@
----
-title: Models
-description: GET /openai/v1/models
----
-
-# Models
-
-List available models from your configured provider.
-
-```
-GET /openai/v1/models
-```
-
-## Request
-
-```bash
-curl http://localhost:3000/openai/v1/models \
-  -H "Authorization: Bearer $OPENAI_API_KEY"
-```
-
-## Response
-
-```json
-{
-  "object": "list",
-  "data": [
-    {"id": "gpt-5.2", "object": "model", "owned_by": "openai"}
-  ]
-}
-```
-
-## SDK Usage
-
-<CodeGroup>
-
-```python Python
-from openai import OpenAI
-
-client = OpenAI(base_url="http://localhost:3000/openai/v1")
-
-models = client.models.list()
-for model in models:
-    print(model.id)
-```
-
-```javascript JavaScript
-import OpenAI from 'openai';
-
-const client = new OpenAI({
-  baseURL: 'http://localhost:3000/openai/v1'
-});
-
-const models = await client.models.list();
-for (const model of models.data) {
-  console.log(model.id);
-}
-```
-
-</CodeGroup>
index df338d44edb92f3e73787b4e03a672c4753a15d2..f46d87f44150313343de2eb9c7d2de842882a32c 100644 (file)
@@ -72,7 +72,9 @@ curl http://localhost:3000/info
   "description": "Privacy proxy for LLMs",
   "mode": "mask",
   "providers": {
-    "upstream": { "type": "openai" }
+    "openai": {
+      "base_url": "https://api.openai.com/v1"
+    }
   },
   "pii_detection": {
     "languages": ["en"],
@@ -100,4 +102,4 @@ When language validation is available, `languages` becomes an object:
 }
 ```
 
-In route mode, `routing` and `providers.local` are also included.
+In route mode, `local` provider info is also included.
index 3d1ed527d568e3e063600dd2bd5532e7ef5ed4f1..27c2af8b75ef2e28a229fcf5a13f995682987c8f 100644 (file)
@@ -5,7 +5,7 @@ description: Replace PII with placeholders before sending to your provider
 
 # Mask Mode
 
-Mask mode replaces PII with placeholders before sending to your LLM provider. The response is automatically unmasked before returning to you.
+Mask mode replaces PII with placeholders before sending to your configured provider. The response is automatically unmasked before returning to you.
 
 ## How It Works
 
@@ -30,7 +30,7 @@ Mask mode replaces PII with placeholders before sending to your LLM provider. Th
 ## When to Use
 
 - Simple setup without local infrastructure
-- Want to use external LLM providers while protecting PII
+- Want to use any OpenAI-compatible provider while protecting PII
 
 ## Configuration
 
@@ -38,8 +38,7 @@ Mask mode replaces PII with placeholders before sending to your LLM provider. Th
 mode: mask
 
 providers:
-  upstream:
-    type: openai
+  openai:
     base_url: https://api.openai.com/v1
 ```
 
@@ -62,7 +61,7 @@ Mask mode sets these headers on responses:
 
 ```
 X-PasteGuard-Mode: mask
-X-PasteGuard-Provider: upstream
+X-PasteGuard-Provider: openai
 X-PasteGuard-PII-Detected: true
 X-PasteGuard-PII-Masked: true
 X-PasteGuard-Language: en
index 5cafa771f8720b0823961a8cf068c1de17d5186b..f72ff12a98732aeae9f75fa2ec4cec846a90d0e5 100644 (file)
@@ -15,8 +15,8 @@ Route mode sends requests containing PII to a local LLM. Requests without PII go
 
     PII stays on your network.
   </Card>
-  <Card title="Request without PII" icon="cloud">
-    Routed to **Your Provider** (OpenAI, Azure, etc.)
+  <Card title="Request without PII" icon="server">
+    Routed to **Configured Provider** (OpenAI, Azure, self-hosted, etc.)
 
     Full provider performance.
   </Card>
@@ -34,67 +34,56 @@ Route mode sends requests containing PII to a local LLM. Requests without PII go
 mode: route
 
 providers:
-  upstream:
-    type: openai
+  openai:
     base_url: https://api.openai.com/v1
-  local:
-    type: ollama
-    base_url: http://localhost:11434
-    model: llama3.2
-
-routing:
-  default: upstream
-  on_pii_detected: local
-```
 
-### Routing Options
+local:
+  type: ollama
+  base_url: http://localhost:11434
+  model: llama3.2
+```
 
-| Option | Description |
-|--------|-------------|
-| `default` | Provider for requests without PII |
-| `on_pii_detected` | Provider for requests with PII |
+In route mode:
+- **No PII detected** → Request goes to configured provider (openai)
+- **PII detected** → Request goes to local provider
 
 ## Local Provider Setup
 
 ### Ollama
 
 ```yaml
-providers:
-  local:
-    type: ollama
-    base_url: http://localhost:11434
-    model: llama3.2
+local:
+  type: ollama
+  base_url: http://localhost:11434
+  model: llama3.2
 ```
 
 ### vLLM
 
 ```yaml
-providers:
-  local:
-    type: openai
-    base_url: http://localhost:8000/v1
-    model: meta-llama/Llama-2-7b-chat-hf
+local:
+  type: openai
+  base_url: http://localhost:8000/v1
+  model: meta-llama/Llama-2-7b-chat-hf
 ```
 
 ### llama.cpp
 
 ```yaml
-providers:
-  local:
-    type: openai
-    base_url: http://localhost:8080/v1
-    model: local
+local:
+  type: openai
+  base_url: http://localhost:8080/v1
+  model: local
 ```
 
 ### LocalAI
 
 ```yaml
-providers:
-  local:
-    type: openai
-    base_url: http://localhost:8080/v1
-    model: your-model-name
-    api_key: ${LOCAL_API_KEY}  # if required
+local:
+  type: openai
+  base_url: http://localhost:8080/v1
+  model: your-model-name
+  api_key: ${LOCAL_API_KEY}  # if required
 ```
 
 ## Response Headers
@@ -110,11 +99,11 @@ X-PasteGuard-PII-Detected: true
 X-PasteGuard-Language: en
 ```
 
-When routed to your provider:
+When routed to configured provider:
 
 ```
 X-PasteGuard-Mode: route
-X-PasteGuard-Provider: upstream
+X-PasteGuard-Provider: openai
 X-PasteGuard-PII-Detected: false
 X-PasteGuard-Language: en
 ```
index fbe2b7fc5389b5290af230d75094fc0dacd89de1..0ba4291e888f25ccb51ffd462cf419a08e859124 100644 (file)
@@ -61,7 +61,7 @@ Use `${VAR}` or `${VAR:-default}` syntax:
 
 ```yaml
 providers:
-  upstream:
+  openai:
     api_key: ${OPENAI_API_KEY}
 
 pii_detection:
index 38c1a015a54bf165d95cd8d7d913e157726344a9..c8060387073ca6b44b87e904a6f0d98e97dd7e8d 100644 (file)
@@ -5,79 +5,70 @@ description: Configure your LLM providers
 
 # Providers
 
-PasteGuard supports two provider types: your configured provider (upstream) and local.
+PasteGuard supports two provider types: configured providers (`providers`) and local provider (`local`).
 
-## Upstream Provider
+## Providers
 
-Required for both modes. Your LLM provider (OpenAI, Azure, etc.).
+Required for both modes. Any OpenAI-compatible endpoint works — cloud services (OpenAI, Azure, OpenRouter) or self-hosted (LiteLLM proxy, vLLM).
 
 ```yaml
 providers:
-  upstream:
-    type: openai
+  openai:
     base_url: https://api.openai.com/v1
     # api_key: ${OPENAI_API_KEY}  # Optional fallback
 ```
 
 | Option | Description |
 |--------|-------------|
-| `type` | `openai` |
-| `base_url` | API endpoint |
+| `base_url` | API endpoint (any OpenAI-compatible URL) |
 | `api_key` | Optional. Used if client doesn't send Authorization header |
 
-### Supported Providers
+### Supported Endpoints
 
 Any OpenAI-compatible API works:
 
 ```yaml
 # OpenAI
 providers:
-  upstream:
-    type: openai
+  openai:
     base_url: https://api.openai.com/v1
 
 # Azure OpenAI
 providers:
-  upstream:
-    type: openai
+  openai:
     base_url: https://your-resource.openai.azure.com/openai/v1
 
 # OpenRouter
 providers:
-  upstream:
-    type: openai
+  openai:
     base_url: https://openrouter.ai/api/v1
     api_key: ${OPENROUTER_API_KEY}
 
-# LiteLLM Proxy
+# LiteLLM Proxy (self-hosted)
 providers:
-  upstream:
-    type: openai
-    base_url: http://localhost:4000  # LiteLLM default port
+  openai:
+    base_url: http://localhost:4000
 
 # Together AI
 providers:
-  upstream:
-    type: openai
+  openai:
     base_url: https://api.together.xyz/v1
 
 # Groq
 providers:
-  upstream:
-    type: openai
+  openai:
     base_url: https://api.groq.com/openai/v1
 ```
 
 ## Local Provider
 
-Required for Route mode only. Your local LLM.
+Required for route mode only. Your local LLM for PII requests.
 
 ```yaml
-providers:
-  local:
-    type: ollama
-    base_url: http://localhost:11434
-    model: llama3.2
+local:
+  type: ollama
+  base_url: http://localhost:11434
+  model: llama3.2
 ```
 
 | Option | Description |
@@ -90,52 +81,47 @@ providers:
 ### Ollama
 
 ```yaml
-providers:
-  local:
-    type: ollama
-    base_url: http://localhost:11434
-    model: llama3.2
+local:
+  type: ollama
+  base_url: http://localhost:11434
+  model: llama3.2
 ```
 
 ### vLLM
 
 ```yaml
-providers:
-  local:
-    type: openai
-    base_url: http://localhost:8000/v1
-    model: meta-llama/Llama-2-7b-chat-hf
+local:
+  type: openai
+  base_url: http://localhost:8000/v1
+  model: meta-llama/Llama-2-7b-chat-hf
 ```
 
 ### llama.cpp
 
 ```yaml
-providers:
-  local:
-    type: openai
-    base_url: http://localhost:8080/v1
-    model: local
+local:
+  type: openai
+  base_url: http://localhost:8080/v1
+  model: local
 ```
 
 ### LocalAI
 
 ```yaml
-providers:
-  local:
-    type: openai
-    base_url: http://localhost:8080/v1
-    model: your-model
-    api_key: ${LOCAL_API_KEY}  # if required
+local:
+  type: openai
+  base_url: http://localhost:8080/v1
+  model: your-model
+  api_key: ${LOCAL_API_KEY}  # if required
 ```
 
 ## API Key Handling
 
-PasteGuard forwards your client's `Authorization` header to your provider. You can optionally set `api_key` in config as a fallback:
+PasteGuard forwards your client's `Authorization` header to the configured provider. You can optionally set `api_key` in config as a fallback:
 
 ```yaml
 providers:
-  upstream:
-    type: openai
+  openai:
     base_url: https://api.openai.com/v1
     api_key: ${OPENAI_API_KEY}  # Used if client doesn't send auth
 ```
index b1c671662e5faaeb0f735ee649dd40f6ffb32567..f9d67d0374372810193af978e26afe0c0ef46f78 100644 (file)
@@ -130,5 +130,5 @@ curl -i http://localhost:3000/openai/v1/chat/completions \
 Look for:
 ```
 X-PasteGuard-Mode: mask
-X-PasteGuard-Provider: upstream
+X-PasteGuard-Provider: openai
 ```
index d5afdb94bb933bec784c1a525188ea9d372e0a52..5383b5391424c621875dc78cd64a19d63a9ee5dc 100644 (file)
@@ -5,7 +5,7 @@ description: Privacy proxy for LLMs
 
 # What is PasteGuard?
 
-PasteGuard is an OpenAI-compatible proxy that protects personal data and secrets before sending to your LLM provider (OpenAI, Azure, etc.).
+PasteGuard is an OpenAI-compatible proxy that protects personal data and secrets before sending to your provider (OpenAI, Azure, self-hosted, etc.).
 
 ## The Problem
 
index f0b077bc497a3c15941f0201e264b343a98b0dce..a73e45cac086672a0ea7438757b29782b6abd2ad 100644 (file)
@@ -49,7 +49,6 @@
       "group": "API Reference",
       "pages": [
         "api-reference/chat-completions",
-        "api-reference/models",
         "api-reference/status",
         "api-reference/dashboard-api"
       ]
index 10bd6ad1361c9eceb223ae4efca3e8b59b6cfa9b..0cfe236e1ab6b7124acf78a35b851efe9e5ad454 100644 (file)
@@ -4,11 +4,18 @@ import { z } from "zod";
 
 // Schema definitions
 
+// Local provider - for route mode when PII is detected
 const LocalProviderSchema = z.object({
-  type: z.enum(["openai", "ollama"]),
+  type: z.enum(["openai", "ollama"]), // ollama native or openai-compatible (vLLM, LocalAI, etc.)
   api_key: z.string().optional(),
   base_url: z.string().url(),
-  model: z.string(), // Required: maps incoming model to local model
+  model: z.string(), // Required: all PII requests use this model
+});
+
+// Providers - OpenAI-compatible endpoints (cloud or self-hosted)
+const OpenAIProviderSchema = z.object({
+  base_url: z.string().url().default("https://api.openai.com/v1"),
+  api_key: z.string().optional(), // Optional fallback if client doesn't send auth header
 });
 
 const MaskingSchema = z.object({
@@ -16,11 +23,6 @@ const MaskingSchema = z.object({
   marker_text: z.string().default("[protected]"),
 });
 
-const RoutingSchema = z.object({
-  default: z.enum(["upstream", "local"]),
-  on_pii_detected: z.enum(["upstream", "local"]),
-});
-
 // All 25 spaCy languages with trained pipelines
 // See presidio/languages.yaml for full list
 const SupportedLanguages = [
@@ -114,21 +116,16 @@ const SecretsDetectionSchema = z.object({
   log_detected_types: z.boolean().default(true),
 });
 
-const UpstreamProviderSchema = z.object({
-  type: z.enum(["openai"]),
-  api_key: z.string().optional(),
-  base_url: z.string().url(),
-});
-
 const ConfigSchema = z
   .object({
     mode: z.enum(["route", "mask"]).default("route"),
     server: ServerSchema.default({}),
+    // Providers - OpenAI-compatible endpoints
     providers: z.object({
-      upstream: UpstreamProviderSchema,
-      local: LocalProviderSchema.optional(),
+      openai: OpenAIProviderSchema.default({}),
     }),
-    routing: RoutingSchema.optional(),
+    // Local provider - only for route mode
+    local: LocalProviderSchema.optional(),
     masking: MaskingSchema.default({}),
     pii_detection: PIIDetectionSchema,
     logging: LoggingSchema.default({}),
@@ -137,14 +134,14 @@ const ConfigSchema = z
   })
   .refine(
     (config) => {
-      // Route mode requires local provider and routing config
+      // Route mode requires local provider
       if (config.mode === "route") {
-        return config.providers.local !== undefined && config.routing !== undefined;
+        return config.local !== undefined;
       }
       return true;
     },
     {
-      message: "Route mode requires 'providers.local' and 'routing' configuration",
+      message: "Route mode requires 'local' provider configuration",
     },
   )
   .refine(
@@ -162,8 +159,8 @@ const ConfigSchema = z
   );
 
 export type Config = z.infer<typeof ConfigSchema>;
-export type UpstreamProvider = z.infer<typeof UpstreamProviderSchema>;
-export type LocalProvider = z.infer<typeof LocalProviderSchema>;
+export type OpenAIProviderConfig = z.infer<typeof OpenAIProviderSchema>;
+export type LocalProviderConfig = z.infer<typeof LocalProviderSchema>;
 export type MaskingConfig = z.infer<typeof MaskingSchema>;
 export type SecretsDetectionConfig = z.infer<typeof SecretsDetectionSchema>;
 
index d0731562e3b1e6ca43f7f6279f771742e2d15014..003ea381f6cea5c67cc270f51a65a6bc43fcbb6e 100644 (file)
@@ -159,18 +159,18 @@ function printStartupBanner(config: ReturnType<typeof getConfig>, host: string,
     config.mode === "route"
       ? `
 Routing:
-  Default: ${config.routing?.default || "upstream"}
-  On PII:  ${config.routing?.on_pii_detected || "local"}
+  No PII:  openai (configured)
+  On PII:  local
 
 Providers:
-  Upstream: ${config.providers.upstream.type}
-  Local:    ${config.providers.local?.type || "not configured"} → ${config.providers.local?.model || "n/a"}`
+  OpenAI: ${config.providers.openai.base_url}
+  Local:  ${config.local?.type || "not configured"} → ${config.local?.model || "n/a"}`
       : `
 Masking:
   Markers: ${config.masking.show_markers ? "enabled" : "disabled"}
 
 Provider:
-  Upstream: ${config.providers.upstream.type}`;
+  OpenAI: ${config.providers.openai.base_url}`;
 
   console.log(`
 ╔═══════════════════════════════════════════════════════════╗
index d8fbf9896e37dcbc1c4f2da83f61b7038d539b7c..7b3ab3ddaa320657fce39059a461f34d96095027 100644 (file)
@@ -19,8 +19,8 @@ infoRoutes.get("/info", (c) => {
     description: "Privacy proxy for LLMs",
     mode: config.mode,
     providers: {
-      upstream: {
-        type: providers.upstream.type,
+      openai: {
+        base_url: providers.openai.baseUrl,
       },
     },
     pii_detection: {
@@ -37,16 +37,11 @@ infoRoutes.get("/info", (c) => {
     },
   };
 
-  if (config.mode === "route" && config.routing) {
-    info.routing = {
-      default: config.routing.default,
-      on_pii_detected: config.routing.on_pii_detected,
+  if (config.mode === "route" && providers.local) {
+    info.local = {
+      type: providers.local.type,
+      base_url: providers.local.baseUrl,
     };
-    if (providers.local) {
-      (info.providers as Record<string, unknown>).local = {
-        type: providers.local.type,
-      };
-    }
   }
 
   if (config.mode === "mask") {
index 4700d2bda18f7f41c14444b741799417a6364cc1..915b7584e8456e8f7c34d5f465a1727520a34460 100644 (file)
@@ -64,7 +64,7 @@ function createErrorLogData(
   return {
     timestamp: new Date().toISOString(),
     mode: decision?.mode ?? config.mode,
-    provider: decision?.provider ?? "upstream",
+    provider: decision?.provider ?? "openai",
     model: body.model || "unknown",
     piiDetected: decision?.piiResult.hasPII ?? false,
     entities: decision
@@ -83,16 +83,6 @@ function createErrorLogData(
   };
 }
 
-proxyRoutes.get("/models", (c) => {
-  const { upstream } = getRouter().getProvidersInfo();
-
-  return proxy(`${upstream.baseUrl}/models`, {
-    headers: {
-      Authorization: c.req.header("Authorization"),
-    },
-  });
-});
-
 /**
  * POST /v1/chat/completions - OpenAI-compatible chat completion endpoint
  */
@@ -144,7 +134,7 @@ proxyRoutes.post(
             {
               timestamp: new Date().toISOString(),
               mode: config.mode,
-              provider: "upstream", // Note: Request never reached provider
+              provider: "openai", // Note: Request never reached provider
               model: body.model || "unknown",
               piiDetected: false,
               entities: [],
@@ -357,7 +347,7 @@ async function handleCompletion(
 ) {
   const client = router.getClient(decision.provider);
   const maskingConfig = router.getMaskingConfig();
-  const authHeader = decision.provider === "upstream" ? c.req.header("Authorization") : undefined;
+  const authHeader = decision.provider === "openai" ? c.req.header("Authorization") : undefined;
 
   // Prepare request and masked content for logging
   let request: ChatCompletionRequest = body;
@@ -608,3 +598,19 @@ function formatMessagesForLog(messages: ChatMessage[]): string {
     })
     .join("\n");
 }
+
+/**
+ * Wildcard proxy - forwards all other /v1/* requests to the configured provider
+ * Supports: /models, /embeddings, /audio/*, /images/*, /files/*, etc.
+ * Must be defined AFTER specific routes to avoid matching them first
+ */
+proxyRoutes.all("/*", (c) => {
+  const { openai } = getRouter().getProvidersInfo();
+  const path = c.req.path.replace(/^\/openai\/v1/, "");
+
+  return proxy(`${openai.baseUrl}${path}`, {
+    headers: {
+      Authorization: c.req.header("Authorization"),
+    },
+  });
+});
index d6e1b90c8bf7b2228c76fdb37d63d90995daa2f9..3d4985a8698bfc8c5e6a772a5fae9768c7efdd67 100644 (file)
@@ -8,10 +8,9 @@ import type { PIIDetectionResult } from "./pii-detector";
  */
 function decideRoute(
   piiResult: PIIDetectionResult,
-  routing: { default: "upstream" | "local"; on_pii_detected: "upstream" | "local" },
   secretsResult?: SecretsDetectionResult,
   secretsAction?: "block" | "redact" | "route_local",
-): { provider: "upstream" | "local"; reason: string } {
+): { provider: "openai" | "local"; reason: string } {
   // Check for secrets route_local action first (takes precedence)
   if (secretsResult?.detected && secretsAction === "route_local") {
     const secretTypes = secretsResult.matches.map((m) => m.type);
@@ -24,13 +23,13 @@ function decideRoute(
   if (piiResult.hasPII) {
     const entityTypes = [...new Set(piiResult.newEntities.map((e) => e.entity_type))];
     return {
-      provider: routing.on_pii_detected,
+      provider: "local",
       reason: `PII detected: ${entityTypes.join(", ")}`,
     };
   }
 
   return {
-    provider: routing.default,
+    provider: "openai",
     reason: "No PII detected",
   };
 }
@@ -60,85 +59,47 @@ function createPIIResult(
 }
 
 describe("decideRoute", () => {
-  describe("with default=upstream, on_pii_detected=local", () => {
-    const routing = { default: "upstream" as const, on_pii_detected: "local" as const };
-
-    test("routes to upstream when no PII detected", () => {
-      const result = decideRoute(createPIIResult(false), routing);
-
-      expect(result.provider).toBe("upstream");
-      expect(result.reason).toBe("No PII detected");
-    });
-
-    test("routes to local when PII detected", () => {
-      const result = decideRoute(createPIIResult(true, [{ entity_type: "PERSON" }]), routing);
-
-      expect(result.provider).toBe("local");
-      expect(result.reason).toContain("PII detected");
-      expect(result.reason).toContain("PERSON");
-    });
-
-    test("includes all entity types in reason", () => {
-      const result = decideRoute(
-        createPIIResult(true, [
-          { entity_type: "PERSON" },
-          { entity_type: "EMAIL_ADDRESS" },
-          { entity_type: "PHONE_NUMBER" },
-        ]),
-        routing,
-      );
-
-      expect(result.reason).toContain("PERSON");
-      expect(result.reason).toContain("EMAIL_ADDRESS");
-      expect(result.reason).toContain("PHONE_NUMBER");
-    });
+  test("routes to openai when no PII detected", () => {
+    const result = decideRoute(createPIIResult(false));
 
-    test("deduplicates entity types in reason", () => {
-      const result = decideRoute(
-        createPIIResult(true, [
-          { entity_type: "PERSON" },
-          { entity_type: "PERSON" },
-          { entity_type: "PERSON" },
-        ]),
-        routing,
-      );
-
-      // Should only contain PERSON once
-      const matches = result.reason.match(/PERSON/g);
-      expect(matches?.length).toBe(1);
-    });
+    expect(result.provider).toBe("openai");
+    expect(result.reason).toBe("No PII detected");
   });
 
-  describe("with default=local, on_pii_detected=upstream", () => {
-    const routing = { default: "local" as const, on_pii_detected: "upstream" as const };
-
-    test("routes to local when no PII detected", () => {
-      const result = decideRoute(createPIIResult(false), routing);
-
-      expect(result.provider).toBe("local");
-      expect(result.reason).toBe("No PII detected");
-    });
-
-    test("routes to upstream when PII detected", () => {
-      const result = decideRoute(
-        createPIIResult(true, [{ entity_type: "EMAIL_ADDRESS" }]),
-        routing,
-      );
+  test("routes to local when PII detected", () => {
+    const result = decideRoute(createPIIResult(true, [{ entity_type: "PERSON" }]));
 
-      expect(result.provider).toBe("upstream");
-      expect(result.reason).toContain("PII detected");
-    });
+    expect(result.provider).toBe("local");
+    expect(result.reason).toContain("PII detected");
+    expect(result.reason).toContain("PERSON");
   });
 
-  describe("with same provider for both cases", () => {
-    const routing = { default: "upstream" as const, on_pii_detected: "upstream" as const };
+  test("includes all entity types in reason", () => {
+    const result = decideRoute(
+      createPIIResult(true, [
+        { entity_type: "PERSON" },
+        { entity_type: "EMAIL_ADDRESS" },
+        { entity_type: "PHONE_NUMBER" },
+      ]),
+    );
+
+    expect(result.reason).toContain("PERSON");
+    expect(result.reason).toContain("EMAIL_ADDRESS");
+    expect(result.reason).toContain("PHONE_NUMBER");
+  });
 
-    test("always routes to upstream regardless of PII", () => {
-      expect(decideRoute(createPIIResult(false), routing).provider).toBe("upstream");
-      expect(
-        decideRoute(createPIIResult(true, [{ entity_type: "PERSON" }]), routing).provider,
-      ).toBe("upstream");
-    });
+  test("deduplicates entity types in reason", () => {
+    const result = decideRoute(
+      createPIIResult(true, [
+        { entity_type: "PERSON" },
+        { entity_type: "PERSON" },
+        { entity_type: "PERSON" },
+      ]),
+    );
+
+    // Should only contain PERSON once
+    const matches = result.reason.match(/PERSON/g);
+    expect(matches?.length).toBe(1);
   });
 });
 
@@ -157,14 +118,12 @@ function createSecretsResult(
 }
 
 describe("decideRoute with secrets", () => {
-  const routing = { default: "upstream" as const, on_pii_detected: "local" as const };
-
   describe("with route_local action", () => {
     test("routes to local when secrets detected", () => {
       const piiResult = createPIIResult(false);
       const secretsResult = createSecretsResult(true, [{ type: "API_KEY_OPENAI", count: 1 }]);
 
-      const result = decideRoute(piiResult, routing, secretsResult, "route_local");
+      const result = decideRoute(piiResult, secretsResult, "route_local");
 
       expect(result.provider).toBe("local");
       expect(result.reason).toContain("Secrets detected");
@@ -173,15 +132,10 @@ describe("decideRoute with secrets", () => {
     });
 
     test("secrets routing takes precedence over PII routing", () => {
-      // Even with on_pii_detected=upstream, secrets route_local should go to local
-      const routingUpstream = {
-        default: "upstream" as const,
-        on_pii_detected: "upstream" as const,
-      };
       const piiResult = createPIIResult(true, [{ entity_type: "PERSON" }]);
       const secretsResult = createSecretsResult(true, [{ type: "API_KEY_AWS", count: 1 }]);
 
-      const result = decideRoute(piiResult, routingUpstream, secretsResult, "route_local");
+      const result = decideRoute(piiResult, secretsResult, "route_local");
 
       expect(result.provider).toBe("local");
       expect(result.reason).toContain("Secrets detected");
@@ -191,19 +145,19 @@ describe("decideRoute with secrets", () => {
       const piiResult = createPIIResult(true, [{ entity_type: "EMAIL_ADDRESS" }]);
       const secretsResult = createSecretsResult(false);
 
-      const result = decideRoute(piiResult, routing, secretsResult, "route_local");
+      const result = decideRoute(piiResult, secretsResult, "route_local");
 
-      expect(result.provider).toBe("local"); // PII detected -> on_pii_detected=local
+      expect(result.provider).toBe("local"); // PII detected -> local
       expect(result.reason).toContain("PII detected");
     });
 
-    test("routes to default when no secrets and no PII detected", () => {
+    test("routes to openai when no secrets and no PII detected", () => {
       const piiResult = createPIIResult(false);
       const secretsResult = createSecretsResult(false);
 
-      const result = decideRoute(piiResult, routing, secretsResult, "route_local");
+      const result = decideRoute(piiResult, secretsResult, "route_local");
 
-      expect(result.provider).toBe("upstream");
+      expect(result.provider).toBe("openai");
       expect(result.reason).toBe("No PII detected");
     });
   });
@@ -213,10 +167,10 @@ describe("decideRoute with secrets", () => {
       const piiResult = createPIIResult(false);
       const secretsResult = createSecretsResult(true, [{ type: "JWT_TOKEN", count: 1 }]);
 
-      const result = decideRoute(piiResult, routing, secretsResult, "block");
+      const result = decideRoute(piiResult, secretsResult, "block");
 
       // With block action, we shouldn't route based on secrets
-      expect(result.provider).toBe("upstream");
+      expect(result.provider).toBe("openai");
       expect(result.reason).toBe("No PII detected");
     });
   });
@@ -226,10 +180,10 @@ describe("decideRoute with secrets", () => {
       const piiResult = createPIIResult(false);
       const secretsResult = createSecretsResult(true, [{ type: "BEARER_TOKEN", count: 1 }]);
 
-      const result = decideRoute(piiResult, routing, secretsResult, "redact");
+      const result = decideRoute(piiResult, secretsResult, "redact");
 
       // With redact action, we route based on PII, not secrets
-      expect(result.provider).toBe("upstream");
+      expect(result.provider).toBe("openai");
       expect(result.reason).toBe("No PII detected");
     });
   });
@@ -243,7 +197,7 @@ describe("decideRoute with secrets", () => {
         { type: "JWT_TOKEN", count: 1 },
       ]);
 
-      const result = decideRoute(piiResult, routing, secretsResult, "route_local");
+      const result = decideRoute(piiResult, secretsResult, "route_local");
 
       expect(result.reason).toContain("API_KEY_OPENAI");
       expect(result.reason).toContain("API_KEY_GITHUB");
index 9be2b5ae4e3f6fda82985ea37120fe7b24d22b92..da1bdbff60e1d178211b260c54b54d1c84b815ff 100644 (file)
@@ -9,7 +9,7 @@ import { getPIIDetector, type PIIDetectionResult } from "../services/pii-detecto
  */
 export interface RouteDecision {
   mode: "route";
-  provider: "upstream" | "local";
+  provider: "openai" | "local";
   reason: string;
   piiResult: PIIDetectionResult;
 }
@@ -19,7 +19,7 @@ export interface RouteDecision {
  */
 export interface MaskDecision {
   mode: "mask";
-  provider: "upstream";
+  provider: "openai";
   reason: string;
   piiResult: PIIDetectionResult;
   maskedMessages: ChatMessage[];
@@ -30,19 +30,19 @@ export type RoutingDecision = RouteDecision | MaskDecision;
 
 /**
  * Router that decides how to handle requests based on PII detection
- * Supports two modes: route (to local LLM) or mask (anonymize for upstream)
+ * Supports two modes: route (to local LLM) or mask (anonymize for provider)
  */
 export class Router {
-  private upstreamClient: LLMClient;
+  private openaiClient: LLMClient;
   private localClient: LLMClient | null;
   private config: Config;
 
   constructor() {
     this.config = getConfig();
 
-    this.upstreamClient = new LLMClient(this.config.providers.upstream, "upstream");
-    this.localClient = this.config.providers.local
-      ? new LLMClient(this.config.providers.local, "local", this.config.providers.local.model)
+    this.openaiClient = new LLMClient(this.config.providers.openai, "openai");
+    this.localClient = this.config.local
+      ? new LLMClient(this.config.local, "local", this.config.local.model)
       : null;
   }
 
@@ -76,17 +76,14 @@ export class Router {
   /**
    * Route mode: decides which provider to use
    *
-   * Secrets routing takes precedence over PII routing when action is route_local
+   * - No PII/Secrets → use configured provider (openai)
+   * - PII detected → use local provider
+   * - Secrets detected with route_local action → use local provider (takes precedence)
    */
   private decideRoute(
     piiResult: PIIDetectionResult,
     secretsResult?: SecretsDetectionResult,
   ): RouteDecision {
-    const routing = this.config.routing;
-    if (!routing) {
-      throw new Error("Route mode requires routing configuration");
-    }
-
     // Check for secrets route_local action first (takes precedence)
     if (secretsResult?.detected && this.config.secrets_detection.action === "route_local") {
       const secretTypes = secretsResult.matches.map((m) => m.type);
@@ -103,16 +100,16 @@ export class Router {
       const entityTypes = [...new Set(piiResult.newEntities.map((e) => e.entity_type))];
       return {
         mode: "route",
-        provider: routing.on_pii_detected,
+        provider: "local",
         reason: `PII detected: ${entityTypes.join(", ")}`,
         piiResult,
       };
     }
 
-    // No PII detected, use default provider
+    // No PII detected, use configured provider
     return {
       mode: "route",
-      provider: routing.default,
+      provider: "openai",
       reason: "No PII detected",
       piiResult,
     };
@@ -125,7 +122,7 @@ export class Router {
     if (!piiResult.hasPII) {
       return {
         mode: "mask",
-        provider: "upstream",
+        provider: "openai",
         reason: "No PII detected",
         piiResult,
         maskedMessages: messages,
@@ -139,7 +136,7 @@ export class Router {
 
     return {
       mode: "mask",
-      provider: "upstream",
+      provider: "openai",
       reason: `PII masked: ${entityTypes.join(", ")}`,
       piiResult,
       maskedMessages: masked,
@@ -147,14 +144,14 @@ export class Router {
     };
   }
 
-  getClient(provider: "upstream" | "local"): LLMClient {
+  getClient(provider: "openai" | "local"): LLMClient {
     if (provider === "local") {
       if (!this.localClient) {
         throw new Error("Local provider not configured");
       }
       return this.localClient;
     }
-    return this.upstreamClient;
+    return this.openaiClient;
   }
 
   /**
@@ -187,7 +184,7 @@ export class Router {
   getProvidersInfo() {
     return {
       mode: this.config.mode,
-      upstream: this.upstreamClient.getInfo(),
+      openai: this.openaiClient.getInfo(),
       local: this.localClient?.getInfo() ?? null,
     };
   }
index 4120addb4e38300d83ac701fa45a9bc7b1103046..a7467e467114f8768e6a1448aadc3b63fddfa3d4 100644 (file)
@@ -1,4 +1,4 @@
-import type { LocalProvider, UpstreamProvider } from "../config";
+import type { LocalProviderConfig, OpenAIProviderConfig } from "../config";
 import type { MessageContent } from "../utils/content";
 
 /**
@@ -49,13 +49,13 @@ export type LLMResult =
       isStreaming: true;
       response: ReadableStream<Uint8Array>;
       model: string;
-      provider: "upstream" | "local";
+      provider: "openai" | "local";
     }
   | {
       isStreaming: false;
       response: ChatCompletionResponse;
       model: string;
-      provider: "upstream" | "local";
+      provider: "openai" | "local";
     };
 
 /**
@@ -79,17 +79,19 @@ export class LLMClient {
   private baseUrl: string;
   private apiKey?: string;
   private providerType: "openai" | "ollama";
-  private providerName: "upstream" | "local";
+  private providerName: "openai" | "local";
   private defaultModel?: string;
 
   constructor(
-    provider: UpstreamProvider | LocalProvider,
-    providerName: "upstream" | "local",
+    provider: OpenAIProviderConfig | LocalProviderConfig,
+    providerName: "openai" | "local",
     defaultModel?: string,
   ) {
     this.baseUrl = provider.base_url.replace(/\/$/, "");
     this.apiKey = provider.api_key;
-    this.providerType = provider.type;
+    // Configured providers (openai) always use openai protocol
+    // Local providers specify their type (ollama or openai-compatible)
+    this.providerType = "type" in provider ? provider.type : "openai";
     this.providerName = providerName;
     this.defaultModel = defaultModel;
   }
@@ -97,10 +99,10 @@ export class LLMClient {
   /**
    * Sends a chat completion request
    * @param request The chat completion request
-   * @param authHeader Optional Authorization header from client (forwarded for upstream)
+   * @param authHeader Optional Authorization header from client (forwarded for openai provider)
    */
   async chatCompletion(request: ChatCompletionRequest, authHeader?: string): Promise<LLMResult> {
-    // Local uses configured model, upstream uses request model
+    // Local uses configured model, openai uses request model
     const model = this.defaultModel || request.model;
     const isStreaming = request.stream ?? false;
 
@@ -188,7 +190,7 @@ export class LLMClient {
     }
   }
 
-  getInfo(): { name: "upstream" | "local"; type: "openai" | "ollama"; baseUrl: string } {
+  getInfo(): { name: "openai" | "local"; type: "openai" | "ollama"; baseUrl: string } {
     return {
       name: this.providerName,
       type: this.providerType,
index 0d86880b5ad8a90b88acd349dcd8aba2be32975c..3b335bd62cfe53c4aa7475f272ca56d9ac211f88 100644 (file)
@@ -6,7 +6,7 @@ export interface RequestLog {
   id?: number;
   timestamp: string;
   mode: "route" | "mask";
-  provider: "upstream" | "local";
+  provider: "openai" | "local";
   model: string;
   pii_detected: boolean;
   entities: string;
@@ -32,7 +32,7 @@ export interface Stats {
   total_requests: number;
   pii_requests: number;
   pii_percentage: number;
-  upstream_requests: number;
+  openai_requests: number;
   local_requests: number;
   avg_scan_time_ms: number;
   total_tokens: number;
@@ -170,8 +170,8 @@ export class Logger {
       .get() as { count: number };
 
     // Upstream vs Local
-    const upstreamResult = this.db
-      .prepare(`SELECT COUNT(*) as count FROM request_logs WHERE provider = 'upstream'`)
+    const openaiResult = this.db
+      .prepare(`SELECT COUNT(*) as count FROM request_logs WHERE provider = 'openai'`)
       .get() as { count: number };
     const localResult = this.db
       .prepare(`SELECT COUNT(*) as count FROM request_logs WHERE provider = 'local'`)
@@ -206,7 +206,7 @@ export class Logger {
       total_requests: total,
       pii_requests: pii,
       pii_percentage: total > 0 ? Math.round((pii / total) * 100 * 10) / 10 : 0,
-      upstream_requests: upstreamResult.count,
+      openai_requests: openaiResult.count,
       local_requests: localResult.count,
       avg_scan_time_ms: Math.round(scanTimeResult.avg || 0),
       total_tokens: tokensResult.total,
@@ -282,7 +282,7 @@ export function getLogger(): Logger {
 export interface RequestLogData {
   timestamp: string;
   mode: "route" | "mask";
-  provider: "upstream" | "local";
+  provider: "openai" | "local";
   model: string;
   piiDetected: boolean;
   entities: string[];
index 931a444e70a8a4369bfba605011e0b724f5553b8..a6dec0621498f38e4304dcb37c2ba1553ef6fcfc 100644 (file)
@@ -253,9 +253,9 @@ const StatsGrid: FC = () => (
                <StatCard label="Avg PII Scan" valueId="avg-scan" accent="teal" />
                <StatCard label="Requests/Hour" valueId="requests-hour" />
                <StatCard
-                       id="upstream-card"
-                       label="Upstream"
-                       valueId="upstream-requests"
+                       id="openai-card"
+                       label="OpenAI"
+                       valueId="openai-requests"
                        accent="info"
                        routeOnly
                />
@@ -454,15 +454,15 @@ async function fetchStats() {
     }
 
     if (data.mode === 'route') {
-      document.getElementById('upstream-requests').textContent = data.upstream_requests.toLocaleString();
+      document.getElementById('openai-requests').textContent = data.openai_requests.toLocaleString();
       document.getElementById('local-requests').textContent = data.local_requests.toLocaleString();
 
-      const total = data.upstream_requests + data.local_requests;
-      const upstreamPct = total > 0 ? Math.round((data.upstream_requests / total) * 100) : 50;
-      const localPct = 100 - upstreamPct;
+      const total = data.openai_requests + data.local_requests;
+      const openaiPct = total > 0 ? Math.round((data.openai_requests / total) * 100) : 50;
+      const localPct = 100 - openaiPct;
 
       document.getElementById('provider-split').innerHTML =
-        '<div class="flex items-center justify-center font-mono text-[0.7rem] font-medium text-white bg-info min-w-[48px] transition-all" style="width:' + Math.max(upstreamPct, 10) + '%">' + upstreamPct + '%</div>' +
+        '<div class="flex items-center justify-center font-mono text-[0.7rem] font-medium text-white bg-info min-w-[48px] transition-all" style="width:' + Math.max(openaiPct, 10) + '%">' + openaiPct + '%</div>' +
         '<div class="flex items-center justify-center font-mono text-[0.7rem] font-medium text-white bg-success min-w-[48px] transition-all" style="width:' + Math.max(localPct, 10) + '%">' + localPct + '%</div>';
     }
 
@@ -592,7 +592,7 @@ async function fetchLogs() {
           '<td class="text-sm px-4 py-3 border-b border-border-subtle align-middle">' + statusBadge + '</td>' +
           '<td class="route-only text-sm px-4 py-3 border-b border-border-subtle align-middle">' +
             '<span class="inline-flex items-center px-2 py-1 rounded-sm font-mono text-[0.6rem] font-medium uppercase tracking-wide ' +
-              (log.provider === 'upstream' ? 'bg-info/10 text-info' : 'bg-success/10 text-success') + '">' + log.provider + '</span>' +
+              (log.provider === 'openai' ? 'bg-info/10 text-info' : 'bg-success/10 text-success') + '">' + log.provider + '</span>' +
           '</td>' +
           '<td class="font-mono text-[0.7rem] text-text-secondary px-4 py-3 border-b border-border-subtle align-middle">' + log.model + '</td>' +
           '<td class="font-mono text-[0.65rem] font-medium px-4 py-3 border-b border-border-subtle align-middle">' + langDisplay + '</td>' +
git clone https://git.99rst.org/PROJECT