Eric Xu commited on
Security hardening: rate limiting, no creds in URLs, path traversal fix
Browse filesCritical fixes from security audit:
- Move API key from query params to Authorization header (never logged)
- Add per-IP rate limiting (20 LLM requests/hour)
- Cap parallel workers at 10 to prevent abuse
- Fix Nemotron path traversal (must be within project or /tmp)
- Disable uvicorn access log to prevent credential leakage
- Remove filesystem paths from /api/config response
- Fix Dockerfile: add git for HF Spaces build
- Update privacy claim to accurately describe key handling
- Dockerfile +2 -1
- web/app.py +62 -17
- web/static/index.html +18 -24
Dockerfile
CHANGED
|
@@ -1,9 +1,10 @@
|
|
| 1 |
FROM python:3.11-slim
|
| 2 |
|
|
|
|
|
|
|
| 3 |
WORKDIR /app
|
| 4 |
|
| 5 |
# Install dependencies
|
| 6 |
-
COPY pyproject.toml .
|
| 7 |
RUN pip install --no-cache-dir \
|
| 8 |
"datasets>=4.0.0" \
|
| 9 |
"huggingface_hub>=0.20.0" \
|
|
|
|
| 1 |
FROM python:3.11-slim
|
| 2 |
|
| 3 |
+
RUN apt-get update && apt-get install -y --no-install-recommends git && rm -rf /var/lib/apt/lists/*
|
| 4 |
+
|
| 5 |
WORKDIR /app
|
| 6 |
|
| 7 |
# Install dependencies
|
|
|
|
| 8 |
RUN pip install --no-cache-dir \
|
| 9 |
"datasets>=4.0.0" \
|
| 10 |
"huggingface_hub>=0.20.0" \
|
web/app.py
CHANGED
|
@@ -138,17 +138,43 @@ def _llm_from_params(api_key: str = "", base_url: str = "", model: str = ""):
|
|
| 138 |
)
|
| 139 |
|
| 140 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 141 |
@app.middleware("http")
|
| 142 |
async def inject_llm_config(request: Request, call_next):
|
| 143 |
-
"""
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 147 |
return await call_next(request)
|
| 148 |
|
| 149 |
|
| 150 |
def llm_from_request(request: Request):
|
| 151 |
-
"""Get LLM client+model from the current request
|
| 152 |
return _llm_from_params(
|
| 153 |
request.state.api_key, request.state.base_url, request.state.model
|
| 154 |
)
|
|
@@ -210,7 +236,6 @@ async def get_config():
|
|
| 210 |
"model": get_model(),
|
| 211 |
"has_api_key": has_key,
|
| 212 |
"base_url": os.getenv("LLM_BASE_URL", ""),
|
| 213 |
-
"nemotron_path": str(nem_path) if nem_path else None,
|
| 214 |
"nemotron_available": nem_path is not None,
|
| 215 |
"is_spaces": IS_SPACES,
|
| 216 |
"persona_datasets": list(NEMOTRON_DATASETS.keys()),
|
|
@@ -233,6 +258,9 @@ class NemotronPathInput(BaseModel):
|
|
| 233 |
async def setup_nemotron(input: NemotronPathInput):
|
| 234 |
"""Point to existing data, or download a Nemotron dataset to the given path."""
|
| 235 |
p = Path(input.path).expanduser().resolve()
|
|
|
|
|
|
|
|
|
|
| 236 |
hf_name = NEMOTRON_DATASETS.get(input.dataset, NEMOTRON_DATASETS["USA"])
|
| 237 |
|
| 238 |
if (p / "dataset_info.json").exists():
|
|
@@ -528,17 +556,25 @@ async def upload_cohort(sid: str, cohort: list[dict]):
|
|
| 528 |
# ── SSE streaming endpoints ──────────────────────────────────────────────
|
| 529 |
|
| 530 |
@app.get("/api/evaluate/stream/{sid}")
|
| 531 |
-
async def evaluate_stream(sid: str,
|
| 532 |
-
|
| 533 |
"""Run evaluation with Server-Sent Events for real-time progress."""
|
|
|
|
| 534 |
if sid not in sessions:
|
| 535 |
raise HTTPException(404, "Session not found")
|
| 536 |
session = sessions[sid]
|
| 537 |
if not session["cohort"]:
|
| 538 |
raise HTTPException(400, "No cohort — generate or upload one first")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 539 |
|
| 540 |
async def event_generator():
|
| 541 |
-
client, mdl = _llm_from_params(
|
| 542 |
cohort = session["cohort"]
|
| 543 |
entity_text = session["entity_text"]
|
| 544 |
total = len(cohort)
|
|
@@ -630,9 +666,9 @@ async def prepare_counterfactual(sid: str, req: CounterfactualRequest):
|
|
| 630 |
|
| 631 |
|
| 632 |
@app.get("/api/counterfactual/stream/{sid}")
|
| 633 |
-
async def counterfactual_stream(sid: str, ticket: str,
|
| 634 |
-
api_key: str = "", base_url: str = "", model: str = ""):
|
| 635 |
"""Run counterfactual probes with SSE progress."""
|
|
|
|
| 636 |
if sid not in sessions:
|
| 637 |
raise HTTPException(404, "Session not found")
|
| 638 |
session = sessions[sid]
|
|
@@ -651,8 +687,13 @@ async def counterfactual_stream(sid: str, ticket: str,
|
|
| 651 |
max_score = req.max_score
|
| 652 |
parallel = req.parallel
|
| 653 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 654 |
async def event_generator():
|
| 655 |
-
client, mdl = _llm_from_params(
|
| 656 |
cohort = session["cohort"]
|
| 657 |
eval_results = session["eval_results"]
|
| 658 |
cohort_map = {f"{p.get('name','')}_{p.get('user_id','')}": p for p in cohort}
|
|
@@ -742,11 +783,12 @@ async def counterfactual_stream(sid: str, ticket: str,
|
|
| 742 |
|
| 743 |
@app.get("/api/bias-audit/stream/{sid}")
|
| 744 |
async def bias_audit_stream(
|
| 745 |
-
sid: str, probes: str = "framing,authority,order",
|
| 746 |
-
sample: int = 10, parallel: int = 5
|
| 747 |
-
api_key: str = "", base_url: str = "", model: str = ""
|
| 748 |
):
|
| 749 |
"""Run bias audit probes with SSE progress."""
|
|
|
|
|
|
|
| 750 |
if sid not in sessions:
|
| 751 |
raise HTTPException(404, "Session not found")
|
| 752 |
session = sessions[sid]
|
|
@@ -757,7 +799,10 @@ async def bias_audit_stream(
|
|
| 757 |
|
| 758 |
async def event_generator():
|
| 759 |
import random
|
| 760 |
-
|
|
|
|
|
|
|
|
|
|
| 761 |
cohort = session["cohort"]
|
| 762 |
entity_text = session["entity_text"]
|
| 763 |
|
|
@@ -880,4 +925,4 @@ if __name__ == "__main__":
|
|
| 880 |
host = "0.0.0.0" if IS_SPACES else "127.0.0.1"
|
| 881 |
print(f"\n SGO Web Interface")
|
| 882 |
print(f" http://{host}:{port}\n")
|
| 883 |
-
uvicorn.run(app, host=host, port=port)
|
|
|
|
| 138 |
)
|
| 139 |
|
| 140 |
|
| 141 |
+
# Rate limiting — per-IP request counter
|
| 142 |
+
_rate_limits: dict = {} # ip -> {"count": N, "reset": timestamp}
|
| 143 |
+
RATE_LIMIT_MAX = 20 # LLM requests per window
|
| 144 |
+
RATE_LIMIT_WINDOW = 3600 # 1 hour
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
def _check_rate_limit(ip: str):
|
| 148 |
+
"""Raise 429 if IP has exceeded rate limit."""
|
| 149 |
+
now = time.time()
|
| 150 |
+
entry = _rate_limits.get(ip)
|
| 151 |
+
if not entry or now > entry["reset"]:
|
| 152 |
+
_rate_limits[ip] = {"count": 1, "reset": now + RATE_LIMIT_WINDOW}
|
| 153 |
+
return
|
| 154 |
+
if entry["count"] >= RATE_LIMIT_MAX:
|
| 155 |
+
raise HTTPException(429, f"Rate limit exceeded. Try again in {int(entry['reset'] - now)}s.")
|
| 156 |
+
entry["count"] += 1
|
| 157 |
+
|
| 158 |
+
|
| 159 |
@app.middleware("http")
|
| 160 |
async def inject_llm_config(request: Request, call_next):
|
| 161 |
+
"""Read LLM creds from Authorization header (not query params — those get logged)."""
|
| 162 |
+
# Authorization: Bearer <api_key>|<base_url>|<model> (pipe-separated)
|
| 163 |
+
auth = request.headers.get("authorization", "")
|
| 164 |
+
if auth.startswith("Bearer "):
|
| 165 |
+
parts = auth[7:].split("|", 2)
|
| 166 |
+
request.state.api_key = parts[0] if len(parts) > 0 else ""
|
| 167 |
+
request.state.base_url = parts[1] if len(parts) > 1 else ""
|
| 168 |
+
request.state.model = parts[2] if len(parts) > 2 else ""
|
| 169 |
+
else:
|
| 170 |
+
request.state.api_key = ""
|
| 171 |
+
request.state.base_url = ""
|
| 172 |
+
request.state.model = ""
|
| 173 |
return await call_next(request)
|
| 174 |
|
| 175 |
|
| 176 |
def llm_from_request(request: Request):
|
| 177 |
+
"""Get LLM client+model from the current request. Never logs credentials."""
|
| 178 |
return _llm_from_params(
|
| 179 |
request.state.api_key, request.state.base_url, request.state.model
|
| 180 |
)
|
|
|
|
| 236 |
"model": get_model(),
|
| 237 |
"has_api_key": has_key,
|
| 238 |
"base_url": os.getenv("LLM_BASE_URL", ""),
|
|
|
|
| 239 |
"nemotron_available": nem_path is not None,
|
| 240 |
"is_spaces": IS_SPACES,
|
| 241 |
"persona_datasets": list(NEMOTRON_DATASETS.keys()),
|
|
|
|
| 258 |
async def setup_nemotron(input: NemotronPathInput):
|
| 259 |
"""Point to existing data, or download a Nemotron dataset to the given path."""
|
| 260 |
p = Path(input.path).expanduser().resolve()
|
| 261 |
+
# Prevent path traversal — must be within project or /tmp
|
| 262 |
+
if not (p.is_relative_to(PROJECT_ROOT) or p.is_relative_to(Path("/tmp"))):
|
| 263 |
+
raise HTTPException(403, "Path must be within the project directory")
|
| 264 |
hf_name = NEMOTRON_DATASETS.get(input.dataset, NEMOTRON_DATASETS["USA"])
|
| 265 |
|
| 266 |
if (p / "dataset_info.json").exists():
|
|
|
|
| 556 |
# ── SSE streaming endpoints ──────────────────────────────────────────────
|
| 557 |
|
| 558 |
@app.get("/api/evaluate/stream/{sid}")
|
| 559 |
+
async def evaluate_stream(sid: str, request: Request, parallel: int = 5,
|
| 560 |
+
bias_calibration: bool = False):
|
| 561 |
"""Run evaluation with Server-Sent Events for real-time progress."""
|
| 562 |
+
_check_rate_limit(request.client.host)
|
| 563 |
if sid not in sessions:
|
| 564 |
raise HTTPException(404, "Session not found")
|
| 565 |
session = sessions[sid]
|
| 566 |
if not session["cohort"]:
|
| 567 |
raise HTTPException(400, "No cohort — generate or upload one first")
|
| 568 |
+
# Cap parallel to prevent abuse
|
| 569 |
+
parallel = min(parallel, 10)
|
| 570 |
+
|
| 571 |
+
# Capture LLM config from request headers before entering async generator
|
| 572 |
+
_api_key = request.state.api_key
|
| 573 |
+
_base_url = request.state.base_url
|
| 574 |
+
_model = request.state.model
|
| 575 |
|
| 576 |
async def event_generator():
|
| 577 |
+
client, mdl = _llm_from_params(_api_key, _base_url, _model)
|
| 578 |
cohort = session["cohort"]
|
| 579 |
entity_text = session["entity_text"]
|
| 580 |
total = len(cohort)
|
|
|
|
| 666 |
|
| 667 |
|
| 668 |
@app.get("/api/counterfactual/stream/{sid}")
|
| 669 |
+
async def counterfactual_stream(sid: str, ticket: str, request: Request):
|
|
|
|
| 670 |
"""Run counterfactual probes with SSE progress."""
|
| 671 |
+
_check_rate_limit(request.client.host)
|
| 672 |
if sid not in sessions:
|
| 673 |
raise HTTPException(404, "Session not found")
|
| 674 |
session = sessions[sid]
|
|
|
|
| 687 |
max_score = req.max_score
|
| 688 |
parallel = req.parallel
|
| 689 |
|
| 690 |
+
_api_key = request.state.api_key
|
| 691 |
+
_base_url = request.state.base_url
|
| 692 |
+
_model = request.state.model
|
| 693 |
+
parallel = min(req.parallel, 10)
|
| 694 |
+
|
| 695 |
async def event_generator():
|
| 696 |
+
client, mdl = _llm_from_params(_api_key, _base_url, _model)
|
| 697 |
cohort = session["cohort"]
|
| 698 |
eval_results = session["eval_results"]
|
| 699 |
cohort_map = {f"{p.get('name','')}_{p.get('user_id','')}": p for p in cohort}
|
|
|
|
| 783 |
|
| 784 |
@app.get("/api/bias-audit/stream/{sid}")
|
| 785 |
async def bias_audit_stream(
|
| 786 |
+
sid: str, request: Request, probes: str = "framing,authority,order",
|
| 787 |
+
sample: int = 10, parallel: int = 5
|
|
|
|
| 788 |
):
|
| 789 |
"""Run bias audit probes with SSE progress."""
|
| 790 |
+
_check_rate_limit(request.client.host)
|
| 791 |
+
parallel = min(parallel, 10)
|
| 792 |
if sid not in sessions:
|
| 793 |
raise HTTPException(404, "Session not found")
|
| 794 |
session = sessions[sid]
|
|
|
|
| 799 |
|
| 800 |
async def event_generator():
|
| 801 |
import random
|
| 802 |
+
_api_key = request.state.api_key
|
| 803 |
+
_base_url = request.state.base_url
|
| 804 |
+
_model = request.state.model
|
| 805 |
+
client, mdl = _llm_from_params(_api_key, _base_url, _model)
|
| 806 |
cohort = session["cohort"]
|
| 807 |
entity_text = session["entity_text"]
|
| 808 |
|
|
|
|
| 925 |
host = "0.0.0.0" if IS_SPACES else "127.0.0.1"
|
| 926 |
print(f"\n SGO Web Interface")
|
| 927 |
print(f" http://{host}:{port}\n")
|
| 928 |
+
uvicorn.run(app, host=host, port=port, access_log=False)
|
web/static/index.html
CHANGED
|
@@ -317,7 +317,7 @@
|
|
| 317 |
<div class="step-num" style="border-color:var(--yellow);color:var(--yellow)">!</div>
|
| 318 |
<div class="step-title">Connect your LLM</div>
|
| 319 |
</div>
|
| 320 |
-
<p class="step-desc">SGO works with any OpenAI-compatible API. Your key
|
| 321 |
<div class="field">
|
| 322 |
<label>API key</label>
|
| 323 |
<input type="password" id="apiKeyInput" placeholder="sk-...">
|
|
@@ -600,20 +600,17 @@ let llmBaseUrl = '';
|
|
| 600 |
let llmModel = '';
|
| 601 |
|
| 602 |
function llmHeaders() {
|
| 603 |
-
|
| 604 |
-
}
|
| 605 |
-
|
| 606 |
-
|
| 607 |
-
|
| 608 |
-
|
| 609 |
-
if (llmBaseUrl) p.set('base_url', llmBaseUrl);
|
| 610 |
-
if (llmModel) p.set('model', llmModel);
|
| 611 |
-
return p;
|
| 612 |
}
|
| 613 |
|
| 614 |
function apiUrl(path) {
|
| 615 |
-
|
| 616 |
-
return
|
| 617 |
}
|
| 618 |
|
| 619 |
// XSS sanitization helper
|
|
@@ -699,7 +696,7 @@ async function setupNemotron() {
|
|
| 699 |
try {
|
| 700 |
const resp = await fetch('/api/nemotron/setup', {
|
| 701 |
method: 'POST',
|
| 702 |
-
headers:
|
| 703 |
body: JSON.stringify({path, dataset}),
|
| 704 |
});
|
| 705 |
clearInterval(progressInterval);
|
|
@@ -776,7 +773,7 @@ async function inferSpec() {
|
|
| 776 |
try {
|
| 777 |
const resp = await fetch(apiUrl('/api/infer-spec'), {
|
| 778 |
method: 'POST',
|
| 779 |
-
headers:
|
| 780 |
body: JSON.stringify({entity_text: text}),
|
| 781 |
});
|
| 782 |
const data = await resp.json();
|
|
@@ -820,7 +817,7 @@ async function runFullPipeline() {
|
|
| 820 |
document.getElementById('pipelineProgressText').textContent = 'Setting up...';
|
| 821 |
const sessResp = await fetch('/api/session', {
|
| 822 |
method: 'POST',
|
| 823 |
-
headers:
|
| 824 |
body: JSON.stringify({entity_text: text}),
|
| 825 |
});
|
| 826 |
const sessData = await sessResp.json();
|
|
@@ -834,7 +831,7 @@ async function runFullPipeline() {
|
|
| 834 |
|
| 835 |
const segResp = await fetch(apiUrl('/api/suggest-segments'), {
|
| 836 |
method: 'POST',
|
| 837 |
-
headers:
|
| 838 |
body: JSON.stringify({
|
| 839 |
entity_text: text,
|
| 840 |
audience_context: audienceCtx || `People who would evaluate: ${text.substring(0, 200)}`,
|
|
@@ -859,7 +856,7 @@ async function runFullPipeline() {
|
|
| 859 |
const desc = audienceCtx || `People evaluating: ${text.substring(0, 200)}`;
|
| 860 |
const cohortResp = await fetch(apiUrl('/api/cohort/generate'), {
|
| 861 |
method: 'POST',
|
| 862 |
-
headers:
|
| 863 |
body: JSON.stringify({description: desc, audience_context: audienceCtx, segments, parallel: 3}),
|
| 864 |
});
|
| 865 |
const cohortData = await cohortResp.json();
|
|
@@ -867,7 +864,7 @@ async function runFullPipeline() {
|
|
| 867 |
// Upload cohort to our session
|
| 868 |
await fetch(`/api/cohort/upload/${sessionId}`, {
|
| 869 |
method: 'POST',
|
| 870 |
-
headers:
|
| 871 |
body: JSON.stringify(cohortData.cohort),
|
| 872 |
});
|
| 873 |
|
|
@@ -894,7 +891,6 @@ async function runFullPipeline() {
|
|
| 894 |
|
| 895 |
await new Promise((resolve, reject) => {
|
| 896 |
const params = new URLSearchParams({parallel: 5, bias_calibration: biasCal});
|
| 897 |
-
llmQueryParams().forEach((v, k) => params.set(k, v));
|
| 898 |
const es = new EventSource(`/api/evaluate/stream/${sessionId}?${params}`);
|
| 899 |
|
| 900 |
es.addEventListener('start', (e) => {
|
|
@@ -989,7 +985,7 @@ async function runDirections() {
|
|
| 989 |
document.getElementById('cfProgressText').textContent = 'Proposing changes to test...';
|
| 990 |
const suggestResp = await fetch(apiUrl('/api/suggest-changes'), {
|
| 991 |
method: 'POST',
|
| 992 |
-
headers:
|
| 993 |
body: JSON.stringify({entity_text: entityText, goal, concerns}),
|
| 994 |
});
|
| 995 |
const suggestData = await suggestResp.json();
|
|
@@ -1007,7 +1003,7 @@ async function runDirections() {
|
|
| 1007 |
// POST config first, get a ticket, then SSE with just the ticket
|
| 1008 |
const prepResp = await fetch(`/api/counterfactual/prepare/${sessionId}`, {
|
| 1009 |
method: 'POST',
|
| 1010 |
-
headers:
|
| 1011 |
body: JSON.stringify({
|
| 1012 |
changes: suggestedChanges,
|
| 1013 |
goal: goal,
|
|
@@ -1019,9 +1015,7 @@ async function runDirections() {
|
|
| 1019 |
const {ticket} = await prepResp.json();
|
| 1020 |
|
| 1021 |
await new Promise((resolve, reject) => {
|
| 1022 |
-
const
|
| 1023 |
-
llmQueryParams().forEach((v, k) => cfParams.set(k, v));
|
| 1024 |
-
const es = new EventSource(`/api/counterfactual/stream/${sessionId}?${cfParams}`);
|
| 1025 |
|
| 1026 |
es.addEventListener('start', (e) => {
|
| 1027 |
const d = JSON.parse(e.data);
|
|
|
|
| 317 |
<div class="step-num" style="border-color:var(--yellow);color:var(--yellow)">!</div>
|
| 318 |
<div class="step-title">Connect your LLM</div>
|
| 319 |
</div>
|
| 320 |
+
<p class="step-desc">SGO works with any OpenAI-compatible API. Your key stays in your browser and is sent to the server only via encrypted headers — never logged, stored, or visible in URLs.</p>
|
| 321 |
<div class="field">
|
| 322 |
<label>API key</label>
|
| 323 |
<input type="password" id="apiKeyInput" placeholder="sk-...">
|
|
|
|
| 600 |
let llmModel = '';
|
| 601 |
|
| 602 |
function llmHeaders() {
|
| 603 |
+
// Send credentials via Authorization header (never in URL/query params)
|
| 604 |
+
const h = {'Content-Type': 'application/json'};
|
| 605 |
+
if (llmApiKey) {
|
| 606 |
+
h['Authorization'] = `Bearer ${llmApiKey}|${llmBaseUrl}|${llmModel}`;
|
| 607 |
+
}
|
| 608 |
+
return h;
|
|
|
|
|
|
|
|
|
|
| 609 |
}
|
| 610 |
|
| 611 |
function apiUrl(path) {
|
| 612 |
+
// No credentials in URLs — they get logged
|
| 613 |
+
return path;
|
| 614 |
}
|
| 615 |
|
| 616 |
// XSS sanitization helper
|
|
|
|
| 696 |
try {
|
| 697 |
const resp = await fetch('/api/nemotron/setup', {
|
| 698 |
method: 'POST',
|
| 699 |
+
headers: llmHeaders(),
|
| 700 |
body: JSON.stringify({path, dataset}),
|
| 701 |
});
|
| 702 |
clearInterval(progressInterval);
|
|
|
|
| 773 |
try {
|
| 774 |
const resp = await fetch(apiUrl('/api/infer-spec'), {
|
| 775 |
method: 'POST',
|
| 776 |
+
headers: llmHeaders(),
|
| 777 |
body: JSON.stringify({entity_text: text}),
|
| 778 |
});
|
| 779 |
const data = await resp.json();
|
|
|
|
| 817 |
document.getElementById('pipelineProgressText').textContent = 'Setting up...';
|
| 818 |
const sessResp = await fetch('/api/session', {
|
| 819 |
method: 'POST',
|
| 820 |
+
headers: llmHeaders(),
|
| 821 |
body: JSON.stringify({entity_text: text}),
|
| 822 |
});
|
| 823 |
const sessData = await sessResp.json();
|
|
|
|
| 831 |
|
| 832 |
const segResp = await fetch(apiUrl('/api/suggest-segments'), {
|
| 833 |
method: 'POST',
|
| 834 |
+
headers: llmHeaders(),
|
| 835 |
body: JSON.stringify({
|
| 836 |
entity_text: text,
|
| 837 |
audience_context: audienceCtx || `People who would evaluate: ${text.substring(0, 200)}`,
|
|
|
|
| 856 |
const desc = audienceCtx || `People evaluating: ${text.substring(0, 200)}`;
|
| 857 |
const cohortResp = await fetch(apiUrl('/api/cohort/generate'), {
|
| 858 |
method: 'POST',
|
| 859 |
+
headers: llmHeaders(),
|
| 860 |
body: JSON.stringify({description: desc, audience_context: audienceCtx, segments, parallel: 3}),
|
| 861 |
});
|
| 862 |
const cohortData = await cohortResp.json();
|
|
|
|
| 864 |
// Upload cohort to our session
|
| 865 |
await fetch(`/api/cohort/upload/${sessionId}`, {
|
| 866 |
method: 'POST',
|
| 867 |
+
headers: llmHeaders(),
|
| 868 |
body: JSON.stringify(cohortData.cohort),
|
| 869 |
});
|
| 870 |
|
|
|
|
| 891 |
|
| 892 |
await new Promise((resolve, reject) => {
|
| 893 |
const params = new URLSearchParams({parallel: 5, bias_calibration: biasCal});
|
|
|
|
| 894 |
const es = new EventSource(`/api/evaluate/stream/${sessionId}?${params}`);
|
| 895 |
|
| 896 |
es.addEventListener('start', (e) => {
|
|
|
|
| 985 |
document.getElementById('cfProgressText').textContent = 'Proposing changes to test...';
|
| 986 |
const suggestResp = await fetch(apiUrl('/api/suggest-changes'), {
|
| 987 |
method: 'POST',
|
| 988 |
+
headers: llmHeaders(),
|
| 989 |
body: JSON.stringify({entity_text: entityText, goal, concerns}),
|
| 990 |
});
|
| 991 |
const suggestData = await suggestResp.json();
|
|
|
|
| 1003 |
// POST config first, get a ticket, then SSE with just the ticket
|
| 1004 |
const prepResp = await fetch(`/api/counterfactual/prepare/${sessionId}`, {
|
| 1005 |
method: 'POST',
|
| 1006 |
+
headers: llmHeaders(),
|
| 1007 |
body: JSON.stringify({
|
| 1008 |
changes: suggestedChanges,
|
| 1009 |
goal: goal,
|
|
|
|
| 1015 |
const {ticket} = await prepResp.json();
|
| 1016 |
|
| 1017 |
await new Promise((resolve, reject) => {
|
| 1018 |
+
const es = new EventSource(`/api/counterfactual/stream/${sessionId}?ticket=${ticket}`);
|
|
|
|
|
|
|
| 1019 |
|
| 1020 |
es.addEventListener('start', (e) => {
|
| 1021 |
const d = JSON.parse(e.data);
|