diff --git a/server/debug_handlers.go b/server/debug_handlers.go index 00dceea25f620bc0b273804fd281af5548d387b6..e8a7d464a85aa093a54966779ca0644a2e10ed41 100644 --- a/server/debug_handlers.go +++ b/server/debug_handlers.go @@ -90,6 +90,36 @@ func (s *Server) handleDebugLLMResponseBody(w http.ResponseWriter, r *http.Reque w.Write([]byte(*body)) } +// handleDebugLLMRequestBodyFull returns the full reconstructed request body, +// including prefix data from the prefix chain. +func (s *Server) handleDebugLLMRequestBodyFull(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + idStr := r.PathValue("id") + id, err := strconv.ParseInt(idStr, 10, 64) + if err != nil { + http.Error(w, "Invalid ID", http.StatusBadRequest) + return + } + + // Use the existing DB method to reconstruct the full body + fullBody, err := s.db.GetFullLLMRequestBody(ctx, id) + if err != nil { + s.logger.Error("Failed to get full LLM request body", "error", err, "id", id) + http.Error(w, "Not found", http.StatusNotFound) + return + } + + if fullBody == "" { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte("null")) + return + } + + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(fullBody)) +} + const debugLLMRequestsHTML = ` @@ -102,10 +132,10 @@ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 0; padding: 20px; - background: #1a1a1a; - color: #e0e0e0; + background: #fff; + color: #1a1a1a; } -h1 { margin: 0 0 20px 0; font-size: 24px; color: #fff; } +h1 { margin: 0 0 20px 0; font-size: 24px; color: #000; } table { width: 100%; border-collapse: collapse; @@ -114,38 +144,40 @@ table { th, td { padding: 8px 12px; text-align: left; - border-bottom: 1px solid #333; + border-bottom: 1px solid #e0e0e0; } th { - background: #252525; + background: #f5f5f5; font-weight: 600; position: sticky; top: 0; } -tr:hover { background: #252525; } +tr:hover { background: #f8f8f8; } .mono { font-family: 'SF Mono', Monaco, monospace; font-size: 12px; } -.error { color: #ff6b6b; } -.success { color: #69db7c; } +.error { color: #d32f2f; } +.success { color: #2e7d32; } .btn { - background: #333; - border: 1px solid #444; - color: #e0e0e0; + background: #f5f5f5; + border: 1px solid #ccc; + color: #333; padding: 4px 8px; border-radius: 4px; cursor: pointer; font-size: 12px; } -.btn:hover { background: #444; } +.btn:hover { background: #e8e8e8; } .btn:disabled { opacity: 0.5; cursor: not-allowed; } +.btn.active { background: #1976d2; color: #fff; border-color: #1565c0; } .json-viewer { - background: #1e1e1e; - border: 1px solid #333; + background: #fafafa; + border: 1px solid #e0e0e0; border-radius: 4px; padding: 12px; - margin-top: 8px; overflow-x: auto; - max-height: 400px; + max-height: 600px; overflow-y: auto; + flex: 1; + min-width: 0; } .json-viewer pre { margin: 0; @@ -155,35 +187,42 @@ tr:hover { background: #252525; } word-wrap: break-word; } .collapsed { display: none; } -.size { color: #888; font-size: 11px; } -.prefix { color: #ffd43b; } -.dedup-info { color: #74c0fc; font-size: 11px; } -.loading { color: #888; font-style: italic; } -.expand-row { background: #1e1e1e; } +.size { color: #666; font-size: 11px; } +.prefix { color: #f57c00; } +.dedup-info { color: #1976d2; font-size: 11px; } +.loading { color: #666; font-style: italic; } +.expand-row { background: #fafafa; } .expand-row td { padding: 0; } .expand-content { padding: 12px; } -.expand-tabs { +.panels { display: flex; - gap: 8px; - margin-bottom: 12px; + gap: 16px; } -.tab-btn { - background: transparent; - border: 1px solid #444; - color: #888; - padding: 6px 12px; - border-radius: 4px; - cursor: pointer; +.panel { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; } -.tab-btn.active { - background: #333; - color: #fff; - border-color: #555; +.panel-header { + font-weight: 600; + margin-bottom: 8px; + color: #333; + display: flex; + align-items: center; + gap: 8px; } -.tab-content { display: none; } -.tab-content.active { display: block; } -.model-display { color: #a5d6ff; } -.model-id { color: #888; font-size: 11px; } +.panel-header .btn { + font-size: 11px; + padding: 2px 6px; +} +.model-display { color: #1976d2; } +.model-id { color: #666; font-size: 11px; } +.string { color: #2e7d32; } +.number { color: #e65100; } +.boolean { color: #0097a7; } +.null { color: #7b1fa2; } +.key { color: #c62828; } @@ -298,13 +337,13 @@ function renderTable(requests) { ${formatSize(req.request_body_length)} ${formatSize(req.response_body_length)} ${prefixInfo} - + ` + "`" + `; tbody.appendChild(tr); } } -async function toggleExpand(id) { +async function toggleExpand(id, hasPrefix) { const existingExpand = document.getElementById('expand-' + id); if (existingExpand) { existingExpand.remove(); @@ -320,23 +359,27 @@ async function toggleExpand(id) { expandRow.innerHTML = ` + "`" + `
-
- - -
-
-
Loading request...
-
-
-
Loading response...
+
+
+
+ Request + ${hasPrefix ? '' : ''} +
+
Loading request...
+
+
+
Response
+
Loading response...
+
` + "`" + `; row.after(expandRow); - // Load request body + // Load both request and response loadBody(id, 'request'); + loadBody(id, 'response'); } async function loadBody(id, type) { @@ -361,7 +404,8 @@ async function loadBody(id, type) { loadedData[key] = data; renderBody(id, type, data); } catch (e) { - const container = document.querySelector('#tab-' + type + '-' + id + ' pre'); + const panelId = type === 'request' ? 'request-panel-' + id : 'response-panel-' + id; + const container = document.querySelector('#' + panelId + ' pre'); if (container) { container.className = 'error'; container.textContent = 'Error loading: ' + e.message; @@ -369,8 +413,57 @@ async function loadBody(id, type) { } } +async function loadFullBody(id) { + const key = id + '-request-full'; + if (loadedData[key]) { + return loadedData[key]; + } + + try { + const resp = await fetch('/debug/llm_requests/' + id + '/request_full'); + const text = await resp.text(); + let data; + try { + data = JSON.parse(text); + } catch { + data = text; + } + loadedData[key] = data; + return data; + } catch (e) { + throw e; + } +} + +async function toggleFullRequest(id) { + const btn = document.getElementById('toggle-full-' + id); + if (!btn) return; + + const isShowingFull = btn.classList.contains('active'); + + if (isShowingFull) { + // Switch back to suffix-only + btn.classList.remove('active'); + btn.textContent = 'Show Full'; + renderBody(id, 'request', loadedData[id + '-request']); + } else { + // Load and show full request + btn.textContent = 'Loading...'; + try { + const fullData = await loadFullBody(id); + btn.classList.add('active'); + btn.textContent = 'Show Suffix Only'; + renderBody(id, 'request', fullData); + } catch (e) { + btn.textContent = 'Error'; + setTimeout(() => { btn.textContent = 'Show Full'; }, 2000); + } + } +} + function renderBody(id, type, data) { - const container = document.querySelector('#tab-' + type + '-' + id + ' pre'); + const panelId = type === 'request' ? 'request-panel-' + id : 'response-panel-' + id; + const container = document.querySelector('#' + panelId + ' pre'); if (!container) return; if (data === null) { @@ -387,40 +480,6 @@ function renderBody(id, type, data) { } } -function showTab(id, tab) { - // Update tab buttons - const expandRow = document.getElementById('expand-' + id); - if (!expandRow) return; - - expandRow.querySelectorAll('.tab-btn').forEach(btn => { - btn.classList.remove('active'); - if (btn.textContent.toLowerCase() === tab) { - btn.classList.add('active'); - } - }); - - // Update tab content - expandRow.querySelectorAll('.tab-content').forEach(content => { - content.classList.remove('active'); - }); - const activeTab = document.getElementById('tab-' + tab + '-' + id); - if (activeTab) { - activeTab.classList.add('active'); - loadBody(id, tab); - } -} - -// Add syntax highlighting styles -const style = document.createElement('style'); -style.textContent = ` + "`" + ` - .string { color: #98c379; } - .number { color: #d19a66; } - .boolean { color: #56b6c2; } - .null { color: #c678dd; } - .key { color: #e06c75; } -` + "`" + `; -document.head.appendChild(style); - loadRequests(); diff --git a/server/server.go b/server/server.go index 01ae012cb30fd13da8f525fca2181f9a32b050f7..15e3d4ee9036e9c56f377641c44f770608c85b4f 100644 --- a/server/server.go +++ b/server/server.go @@ -283,6 +283,7 @@ func (s *Server) RegisterRoutes(mux *http.ServeMux) { mux.Handle("GET /debug/llm_requests", http.HandlerFunc(s.handleDebugLLMRequests)) mux.Handle("GET /debug/llm_requests/api", http.HandlerFunc(s.handleDebugLLMRequestsAPI)) mux.Handle("GET /debug/llm_requests/{id}/request", http.HandlerFunc(s.handleDebugLLMRequestBody)) + mux.Handle("GET /debug/llm_requests/{id}/request_full", http.HandlerFunc(s.handleDebugLLMRequestBodyFull)) mux.Handle("GET /debug/llm_requests/{id}/response", http.HandlerFunc(s.handleDebugLLMResponseBody)) // Serve embedded UI assets