fuzzyworld

dm / commit

fix: remove anthropic-beta header, add comprehensive logging

bdebbc321fecfc52541912cd73abd6a998d0ab39 · Pavel Safronov · 2026-05-27 13:25:53 -0700 · parents 1890c88994af

fix: remove anthropic-beta header, add comprehensive logging

The prompt-caching-2024-07-31 beta header is for Claude 3 only; sending
it with claude-opus-4-7 caused a 400 response. CORS then prevented
reading the error body, producing a misleading generic NetworkError.

Fixes:
- Remove anthropic-beta header entirely (caching is GA on claude-opus-4-7)
- Simplify system prompt from array-with-cache_control to plain string

Logging added:
- console.group per API call: key hint, message count, secure context
- HTTP status logged on every response
- block_start logged per content block (text vs tool_use)
- tool_use inputs logged when detected
- stop_reason and event count logged on message_delta
- stream error events logged
- Descriptive multi-line error messages distinguishing CORS/network
  failures, API errors with body, and API errors CORS hides the body of

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
 public/index.html | 87 +++++++++++++++++++++++++++++++++++++++++--------------
 1 file changed, 66 insertions(+), 21 deletions(-)

diff --git a/public/index.html b/public/index.html
index fb75dc0..27169c9 100644
--- a/public/index.html
+++ b/public/index.html
@@ -1654,30 +1654,64 @@ async function sendMessage(text, showAsPlayer = true) {
     let apiMessages = [...conversationHistory];
     let statUpdate = null;
     let firstChunk = true;
+    let loopIteration = 0;
 
     while (true) {
-      const response = await fetch('https://api.anthropic.com/v1/messages', {
-        method: 'POST',
-        headers: {
-          'Content-Type': 'application/json',
-          'x-api-key': gameState.apiKey,
-          'anthropic-version': '2023-06-01',
-          'anthropic-beta': 'prompt-caching-2024-07-31',
-          'anthropic-dangerous-allow-browser': 'true',
-        },
-        body: JSON.stringify({
-          model: 'claude-opus-4-7',
-          max_tokens: 1500,
-          stream: true,
-          system: [{ type: 'text', text: buildSystemPrompt(gameState.character, gameState.theme), cache_control: { type: 'ephemeral' } }],
-          tools: [UPDATE_TOOL],
-          messages: apiMessages,
-        }),
-      });
+      loopIteration++;
+      const keyHint = gameState.apiKey.slice(0, 10) + '…' + gameState.apiKey.slice(-4);
+      const requestBody = {
+        model: 'claude-opus-4-7',
+        max_tokens: 1500,
+        stream: true,
+        system: buildSystemPrompt(gameState.character, gameState.theme),
+        tools: [UPDATE_TOOL],
+        messages: apiMessages,
+      };
+
+      console.group(`🎲 DM API call #${loopIteration} (turn ${conversationHistory.length})`);
+      console.log('key:', keyHint);
+      console.log('messages:', apiMessages.length);
+      console.log('model:', requestBody.model);
+      console.log('secure context:', window.isSecureContext);
+      console.groupEnd();
+
+      let response;
+      try {
+        response = await fetch('https://api.anthropic.com/v1/messages', {
+          method: 'POST',
+          headers: {
+            'Content-Type': 'application/json',
+            'x-api-key': gameState.apiKey,
+            'anthropic-version': '2023-06-01',
+            'anthropic-dangerous-allow-browser': 'true',
+          },
+          body: JSON.stringify(requestBody),
+        });
+      } catch (fetchErr) {
+        // fetch() itself threw — almost always a CORS preflight failure or network issue
+        console.error('🔴 fetch() threw (CORS / network):', fetchErr);
+        throw new Error(
+          `Network error — could not reach Anthropic. Likely causes:\n` +
+          `• Invalid or revoked API key\n` +
+          `• No internet connection\n` +
+          `• CORS preflight blocked (check console)\n` +
+          `(${fetchErr.message})`
+        );
+      }
+
+      console.log(`↩ Response: HTTP ${response.status} ${response.statusText}`);
 
       if (!response.ok) {
-        const err = await response.json().catch(() => ({}));
-        throw new Error(err.error?.message || `API error ${response.status}`);
+        let errBody = {};
+        try { errBody = await response.json(); } catch { /* CORS may block reading error body */ }
+        console.error('🔴 API error body:', errBody);
+        const msg = errBody.error?.message || errBody.message;
+        throw new Error(
+          msg
+            ? `Anthropic API error (${response.status}): ${msg}`
+            : `Anthropic returned HTTP ${response.status}. ` +
+              `Check your API key and account credits. (CORS may be hiding the full error — open Network tab)`
+        );
       }
 
       const reader = response.body.getReader();
@@ -1685,6 +1719,7 @@ async function sendMessage(text, showAsPlayer = true) {
       let buf = '';
       let blocks = {};
       let stopReason = null;
+      let eventCount = 0;
 
       while (true) {
         const { done, value } = await reader.read();
@@ -1698,9 +1733,11 @@ async function sendMessage(text, showAsPlayer = true) {
           if (!line.startsWith('data: ')) continue;
           let data;
           try { data = JSON.parse(line.slice(6)); } catch { continue; }
+          eventCount++;
 
           if (data.type === 'content_block_start') {
             blocks[data.index] = { ...data.content_block, _buf: '' };
+            console.log(`  ▶ block_start [${data.index}] type=${data.content_block.type}`);
           } else if (data.type === 'content_block_delta') {
             const block = blocks[data.index];
             if (!block) continue;
@@ -1716,9 +1753,13 @@ async function sendMessage(text, showAsPlayer = true) {
             const block = blocks[data.index];
             if (block?.type === 'tool_use') {
               try { block.input = JSON.parse(block._buf || '{}'); } catch { block.input = {}; }
+              console.log(`  🛠 tool_use: ${block.name}`, block.input);
             }
           } else if (data.type === 'message_delta') {
             stopReason = data.delta.stop_reason;
+            console.log(`  ✓ message_delta stop_reason=${stopReason} events=${eventCount}`);
+          } else if (data.type === 'error') {
+            console.error('  🔴 stream error event:', data);
           }
         }
       }
@@ -1732,6 +1773,7 @@ async function sendMessage(text, showAsPlayer = true) {
 
       if (stopReason !== 'tool_use' || toolBlocks.length === 0) break;
 
+      console.log(`  ↺ tool_use loop: adding tool_result and continuing`);
       const assistantContent = allBlocks.map(b =>
         b.type === 'text'
           ? { type: 'text', text: b._buf }
@@ -1755,9 +1797,12 @@ async function sendMessage(text, showAsPlayer = true) {
     finalizeDMMessage(dmEl, rawText);
     if (statUpdate) applyStatUpdate(statUpdate);
     conversationHistory.push({ role: 'assistant', content: rawText });
+    console.log(`✅ Turn complete. History length: ${conversationHistory.length}`);
 
   } catch (err) {
-    dmEl.innerHTML = `<span style="color:#e74c3c">⚠️ ${err.message}</span>`;
+    console.error('🔴 sendMessage error:', err);
+    const lines = err.message.split('\n');
+    dmEl.innerHTML = `<span style="color:#e74c3c">⚠️ ${lines.map(escHtml).join('<br>')}</span>`;
     conversationHistory.pop();
   }