Karya Semi
HomeBlogSearchCategoriesAboutContact
Karya Semi

Less noise. More notes.

HomeBlogAboutContactPrivacy PolicyDisclaimer

© 2026 Karya Semi. All rights reserved.

XGitHubLinkedIn
  1. Home
  2. /Categories
  3. /Programming

Open Source Experiential Router Uses Request Data to Fine-Tune Models

New API router uses request data for openrouter model fine tuning. Turn inference routing patterns into training feedback for better LLMs.

Dian Rijal Asyrof/August 28, 2026/6 min read
Illustration for Open Source Experiential Router Uses Request Data to Fine-Tune Models

Most LLM routers static. Check latency, compare cost, follow basic fallback. GPT-4o hit rate limit, router switch to Claude 3.5 Sonnet. While platforms like OpenRouter manage LLM API costs globally, an in-house router can capture custom data. Keep application online, miss big opportunity. Every request through API gateway is potential training sample.

Open-source experiential router change this. Capture prompt-response pair, link downstream user feedback, package into training dataset. Over time, dataset train small, self-hosted model to match proprietary API performance. This mirrors the process of moving workflows to local small language models to cut costs. Start with expensive model, migrate to cheap custom model without change to application code.

Design, build, deploy open-source experiential router:

Core Architecture

System use four components in sequence:

[Client Application]
         │
         ▼ (Request with Trace ID)
[Experiential Router] ───► [Upstream LLM (e.g., GPT-4o)]
         │                         │
         │ (Log Prompt/Metadata)   ▼ (Response + Trace ID)
         ▼                         │
    [Data Store] ◄─────────────────┘
         ▲
         │ (User Feedback / Eval Score)
[Feedback Collector]
         │
         ▼
[Fine-Tuning Pipeline] ───► [Custom Local Model]
  1. Proxy Layer: Receive incoming OpenAI-compatible request, forward to target LLM, return response. Inject unique trace ID into response header.
  2. Data Store: Fast database link prompt, response, token usage, latency, trace ID. This data can also feed a token ledger for quota management.
  3. Feedback Collector: API endpoint receive human feedback (thumbs up/down, copy-paste) or programmatic feedback (code compile success), link to trace ID.
  4. Training Loop: Background process extract high-rate pair, format to template, run fine-tuning job.

PostgreSQL schema store transaction:

CREATE TABLE llm_traces (
    trace_id UUID PRIMARY KEY,
    prompt JSONB NOT NULL,
    response JSONB,
    model VARCHAR(50) NOT NULL,
    latency_ms INT,
    prompt_tokens INT,
    completion_tokens INT,
    feedback_score FLOAT,
    corrected_text TEXT,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
 
CREATE INDEX idx_traces_feedback ON llm_traces (feedback_score) WHERE feedback_score IS NOT NULL;

Index speed up dataset extraction. Filter query run fast.

Proxy Layer

Proxy must be fast, transparent. Mimic OpenAI API format to drop into existing SDK config by change of base_url.

FastAPI implementation handle standard request and Server-Sent Events (SSE) stream:

import uuid
import httpx
from fastapi import FastAPI, Request, BackgroundTasks
from fastapi.responses import StreamingResponse
from pydantic import BaseModel
import json
 
app = FastAPI()
UPSTREAM_URL = "https://api.openai.com/v1/chat/completions"
OPENAI_API_KEY = "your-api-key-here"
 
db = {}
 
def log_transaction(trace_id: str, request_body: dict, response_body: dict):
    db[trace_id] = {
        "prompt": request_body.get("messages", []),
        "response": response_body.get("choices", [{}])[0].get("message", {}),
        "model": request_body.get("model"),
        "feedback_score": None,
        "corrected_text": None
    }
    print(f"Logged transaction {trace_id}")
 
def log_stream_transaction(trace_id: str, request_body: dict, chunks: list):
    full_content = ""
    role = "assistant"
    for chunk in chunks:
        try:
            data = json.loads(chunk)
            delta = data.get("choices", [{}])[0].get("delta", {})
            if "content" in delta:
                full_content += delta["content"]
            if "role" in delta:
                role = delta["role"]
        except json.JSONDecodeError:
            continue
    
    db[trace_id] = {
        "prompt": request_body.get("messages", []),
        "response": {"role": role, "content": full_content},
        "model": request_body.get("model"),
        "feedback_score": None,
        "corrected_text": None
    }
    print(f"Logged stream transaction {trace_id}")
 
@app.post("/v1/chat/completions")
async def route_request(request: Request, background_tasks: BackgroundTasks):
    body = await request.json()
    trace_id = str(uuid.uuid4())
    
    headers = {
        "Authorization": f"Bearer {OPENAI_API_KEY}",
        "Content-Type": "application/json"
    }
    
    is_stream = body.get("stream", False)
    
    if not is_stream:
        async with httpx.AsyncClient() as client:
            response = await client.post(UPSTREAM_URL, json=body, headers=headers, timeout=60.0)
        response_data = response.json()
        background_tasks.add_task(log_transaction, trace_id, body, response_data)
        return StreamingResponse(
            content=iter([json.dumps(response_data)]),
            media_type="application/json",
            headers={"x-trace-id": trace_id}
        )
    
    async def stream_generator():
        chunks = []
        async with httpx.AsyncClient() as client:
            async with client.stream("POST", UPSTREAM_URL, json=body, headers=headers, timeout=60.0) as r:
                async for line in r.aiter_lines():
                    if not line.strip():
                        continue
                    yield line + "\n"
                    if line.startswith("data: "):
                        data_str = line[6:].strip()
                        if data_str == "[DONE]":
                            break
                        chunks.append(data_str)
        background_tasks.add_task(log_stream_transaction, trace_id, body, chunks)
 
    return StreamingResponse(
        stream_generator(),
        media_type="text/event-stream",
        headers={"x-trace-id": trace_id}
    )

Proxy intercept traffic, generate trace_id, hand off logging to background thread to prevent latency.

Production Feedback

Model need reward signal to learn. Router must know if output useful.

Explicit feedback is direct signal. User click thumbs-up, log positive score. User edit output, log negative score and edited text as target output.

Feedback endpoint:

class FeedbackPayload(BaseModel):
    trace_id: str
    score: float
    corrected_text: str | None = None
 
@app.post("/v1/feedback")
async def receive_feedback(payload: FeedbackPayload):
    if payload.trace_id not in db:
        return {"status": "error", "message": "Trace ID not found"}
        
    db[payload.trace_id]["feedback_score"] = payload.score
    if payload.corrected_text:
        db[payload.trace_id]["corrected_text"] = payload.corrected_text
        
    return {"status": "success", "trace_id": payload.trace_id}

Implicit feedback often more reliable. Monitor programmatic outcome:

  • Code Generation: Code run without syntax error.
  • Data Extraction: Output match JSON schema.
  • Chatbots: User close window or ask follow-up.

Evaluate code output programmatically to verify LLM code generation quality:

import sys
import io
 
def execute_and_evaluate(code: str) -> float:
    old_stdout = sys.stdout
    redirected_output = sys.stdout = io.StringIO()
    try:
        exec(code, {}, {})
        sys.stdout = old_stdout
        return 1.0
    except Exception:
        sys.stdout = old_stdout
        return 0.0

If code run, send score 1.0. If syntax error, send 0.0.

Data Cleaning and Filtering

Do not train model on raw production log. Bad input, incomplete response, PII ruin custom model. Clean dataset before training.

Extraction script:

import re
 
def clean_text(text: str) -> str:
    text = re.sub(r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b', '[EMAIL]', text)
    text = re.sub(r'sk-[a-zA-Z0-9]{32,}', '[API_KEY]', text)
    return text
 
def validate_chat_format(messages: list) -> bool:
    for msg in messages:
        if "role" not in msg or "content" not in msg:
            return False
        if msg["role"] not in ["system", "user", "assistant"]:
            return False
        if not isinstance(msg["content"], str) or len(msg["content"]) == 0:
            return False
    return True
 
def generate_training_data(database: dict):
    dataset = []
    for trace_id, entry in database.items():
        score = entry.get("feedback_score")
        if score is None or score < 0.8:
            continue
            
        target_output = entry["corrected_text"] if entry["corrected_text"] else entry["response"].get("content")
        if not target_output:
            continue
            
        system_msg = ""
        user_msg = ""
        
        for msg in entry["prompt"]:
            if msg["role"] == "system":
                system_msg = msg["content"]
            elif msg["role"] == "user":
                user_msg = msg["content"]
                
        messages = [
            {"role": "system", "content": clean_text(system_msg)},
            {"role": "user", "content": clean_text(user_msg)},
            {"role": "assistant", "content": clean_text(target_output)}
        ]
        
        if validate_chat_format(messages):
            dataset.append({"messages": messages})
            
    return dataset

Filter drop transaction with score below 0.8, replace bad output with human correction.

Fine-Tuning Run

Accumulate threshold (e.g., 1000 high-quality run), export to JSONL.

{"messages": [{"role": "system", "content": "You convert natural language to SQL."}, {"role": "user", "content": "Find users registered last week."}, {"role": "assistant", "content": "SELECT * FROM users WHERE created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY);"}]}

Run training with Axolotl or Unsloth on GPU. Config for Llama 3 8B QLoRA:

base_model: meta-llama/Meta-Llama-3-8B-Instruct
model_type: LlamaForCausalLM
tokenizer_type: AutoTokenizer
 
load_in_8bit: false
load_in_4bit: true
strict: false
 
datasets:
  - path: /data/processed_feedback.jsonl
    type: chat_template
    chat_template: llama3
 
dataset_prepared_path:
val_set_size: 0.05
output_dir: ./outputs/llama3-custom
 
adapter: qlora
lora_model_dir:
 
sequence_len: 2048
sample_packing: true
pad_to_sequence_len: true
 
lora_r: 16
lora_alpha: 32
lora_dropout: 0.05
lora_target_modules:
  - q_proj
  - v_proj
  - k_proj
  - o_proj
 
wandb_project: experiential-router-tuning
tf32: true
 
epochs: 3
micro_batch_size: 2
gradient_accumulation_steps: 4
optimizer: adamw_torch
learning_rate: 0.0002
lr_scheduler: cosine

Training output LoRA adapter. Deploy with vLLM or Ollama.

Progressive Routing

Do not switch 100% traffic immediately. Use progressive routing.

Router use weighted probability. Start 5% custom model, 95% commercial model.

import random
 
class RouterConfig:
    def __init__(self):
        self.weights = [0.0, 1.0] 
        self.custom_model_id = "llama3-custom"
        self.commercial_model_id = "gpt-4o"
 
    def select_model(self) -> str:
        choices = [self.custom_model_id, self.commercial_model_id]
        return random.choices(choices, weights=self.weights, k=1)[0]
 
router_config = RouterConfig()

Monitor feedback score. If custom model score close to commercial model, increase weight.

Calculate rolling average score:

def get_rolling_average(model_name: str, limit: int = 100) -> float:
    scores = [
        entry["feedback_score"]
        for entry in db.values()
        if entry.get("model") == model_name and entry.get("feedback_score") is not None
    ][-limit:]
    if not scores:
        return 0.0
    return sum(scores) / len(scores)
 
def adjust_routing_weights():
    custom_avg = get_rolling_average(router_config.custom_model_id)
    commercial_avg = get_rolling_average(router_config.commercial_model_id)
    
    if custom_avg >= (commercial_avg * 0.95):
        current_custom_weight = router_config.weights[0]
        new_custom_weight = min(current_custom_weight + 0.1, 1.0)
        router_config.weights = [new_custom_weight, 1.0 - new_custom_weight]
        print(f"Increased custom model weight to {new_custom_weight}")
    else:
        router_config.weights = [0.0, 1.0]
        print("Performance dropped. Rolled back to commercial model.")

Feedback loop automate model optimization. Router handle transition based on metric.

Edge Cases

Feedback Latency

Feedback arrive late. Can take second, minute, day. Storage layer must support upsert. Use PostgreSQL or Redis with TTL to hold log until feedback window close.

Cold Start

No task-specific data at start. Router default to commercial model to gather prompt, response. Once feedback loop log enough sample, train first adapter, enable progressive routing.

Data Drift

User behavior change. Model train on winter query fail on summer trend. Prevent decay with sliding window dataset. Keep data fresh: discard log older than 90 days, run weekly fine-tuning.

Context Window Mismatch

Custom model has smaller context window than commercial model (e.g., 8k vs 128k). Router must check prompt length before routing. If prompt exceeds custom model limit, route to commercial model.

def check_context_limit(prompt: list, limit: int = 8192) -> bool:
    total_chars = sum(len(msg.get("content", "")) for msg in prompt)
    estimated_tokens = total_chars // 4
    return estimated_tokens <= limit
DR

Dian Rijal Asyrof

Writes about useful AI tools, programming practice, and the craft of building reliable software.

Previous articleWhy Local LLM Execution Yields Subpar Reasoning OutputNext articleNvidia Agrees to Acquire Open Source AI Platform Hugging Face for 13B
LLMLLMsRouterFine TuneDeveloper Tools
On this page↓
  1. Core Architecture
  2. Proxy Layer
  3. Production Feedback
  4. Data Cleaning and Filtering
  5. Fine-Tuning Run
  6. Progressive Routing
  7. Edge Cases
  8. Feedback Latency
  9. Cold Start
  10. Data Drift
  11. Context Window Mismatch

On this page

  1. Core Architecture
  2. Proxy Layer
  3. Production Feedback
  4. Data Cleaning and Filtering
  5. Fine-Tuning Run
  6. Progressive Routing
  7. Edge Cases
  8. Feedback Latency
  9. Cold Start
  10. Data Drift
  11. Context Window Mismatch

See also

Illustration for Ramp Releases Unified API Router for Dynamic LLM Switching
AI/Aug 22, 2026

Ramp Releases Unified API Router for Dynamic LLM Switching

Integrate ramp ai model router to swap LLM providers dynamically. Optimize cost and latency via unified API. Switch models instantly in production.

7 min read
LLMsLLM
Illustration for Why Local LLM Execution Yields Subpar Reasoning Output
AI/Aug 28, 2026

Why Local LLM Execution Yields Subpar Reasoning Output

Aggressive quantization, small context windows, bad samplers explain why local llm dumber. Adjust parameters to restore reasoning.

6 min read
LLMLLMs
Illustration for Nvidia Research Shows Agent Harness Matters More Than Underlying AI Model
AI/Aug 28, 2026

Nvidia Research Shows Agent Harness Matters More Than Underlying AI Model

Fine-tuning nvidia ai agent harness stops execution drift. New research proves system design beats raw model power for complex task completion.

7 min read
AI AgentsNvidia