Skip to content

Instantly share code, notes, and snippets.

@simonw
Last active December 23, 2025 06:13
Show Gist options
  • Select an option

  • Save simonw/fb521cd7bbce3cb62bd0756037e7137b to your computer and use it in GitHub Desktop.

Select an option

Save simonw/fb521cd7bbce3cb62bd0756037e7137b to your computer and use it in GitHub Desktop.
1. Generate Content (non-streaming)
const response = await fetch(
`https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-goog-api-key': API_KEY
},
body: JSON.stringify({
contents: [{ role: 'user', parts: [{ text: 'Hello!' }] }]
})
}
);
const data = await response.json();
console.log(data.candidates[0].content.parts[0].text);
2. Generate Content (streaming)
const response = await fetch(
`https://generativelanguage.googleapis.com/v1beta/models/${model}:streamGenerateContent?alt=sse`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-goog-api-key': API_KEY
},
body: JSON.stringify({
contents: [{ role: 'user', parts: [{ text: 'Hello!' }] }]
})
}
);
// Response is SSE with lines like: data: {"candidates":[...]}
3. Count Tokens
await fetch(
`https://generativelanguage.googleapis.com/v1beta/models/${model}:countTokens`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json', 'x-goog-api-key': API_KEY },
body: JSON.stringify({
generateContentRequest: {
contents: [{ role: 'user', parts: [{ text: 'Hello!' }] }]
}
})
}
);
4. Embed Content
await fetch(
`https://generativelanguage.googleapis.com/v1beta/models/${model}:embedContent`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json', 'x-goog-api-key': API_KEY },
body: JSON.stringify({
content: { role: 'user', parts: [{ text: 'Hello!' }] }
})
}
);
5. Batch Embed Contents
await fetch(
`https://generativelanguage.googleapis.com/v1beta/models/${model}:batchEmbedContents`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json', 'x-goog-api-key': API_KEY },
body: JSON.stringify({
requests: [
{ model: `models/${model}`, content: { parts: [{ text: 'Text 1' }] } },
{ model: `models/${model}`, content: { parts: [{ text: 'Text 2' }] } }
]
})
}
);
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Jina Reader</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/4.0.2/marked.min.js"></script>
<style>
body {
font-family: Helvetica, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
box-sizing: border-box;
}
h1 {
font-size: 24px;
margin-bottom: 16px;
}
h1 + p {
margin-bottom: 20px;
color: #666;
}
#url-form {
display: flex;
flex-direction: column;
gap: 12px;
}
#url-input, #format-select, #submit-btn, #markdown-raw, .copy-btn, #prompt-textarea, #run-prompt-btn {
font-size: 16px;
padding: 10px 12px;
box-sizing: border-box;
border-radius: 6px;
}
#url-input, #format-select {
width: 100%;
border: 1px solid #ccc;
background: #fff;
}
#url-input:focus, #format-select:focus, #prompt-textarea:focus {
outline: none;
border-color: #4CAF50;
box-shadow: 0 0 0 2px rgba(76, 175, 80, 0.2);
}
#submit-btn, .copy-btn, #run-prompt-btn {
background-color: #4CAF50;
color: white;
border: none;
cursor: pointer;
font-weight: 500;
}
#submit-btn:hover, .copy-btn:hover, #run-prompt-btn:hover {
background-color: #45a049;
}
#submit-btn:active, .copy-btn:active, #run-prompt-btn:active {
transform: scale(0.98);
}
#markdown-raw, #prompt-textarea {
width: 100%;
height: 200px;
margin-top: 20px;
resize: vertical;
border: 1px solid #ccc;
}
#prompt-textarea {
height: 100px;
margin-top: 0;
}
#markdown-rendered {
margin-top: 20px;
border: 1px solid #ccc;
padding: 10px;
overflow-wrap: break-word;
border-radius: 6px;
}
#loading, #prompt-loading {
display: none;
margin-top: 20px;
color: #666;
}
#result, #prompt-result {
display: none;
line-height: 1.5;
font-size: 16px;
}
#prompt-rendered {
font-family: system-ui, -apple-system, sans-serif;
max-width: 65ch;
margin: 1.5em 0;
}
#prompt-rendered p {
margin: 1em 0;
line-height: 1.6;
}
#prompt-rendered ul {
margin: 1em 0;
padding-left: 2em;
}
#prompt-rendered li {
margin: 0.5em 0;
line-height: 1.4;
}
iframe {
width: 100%;
height: 500px;
box-sizing: border-box;
border: 1px solid #ccc;
border-radius: 6px;
}
@media (min-width: 540px) {
#url-form {
flex-direction: row;
flex-wrap: wrap;
align-items: center;
}
#url-input {
flex: 1;
min-width: 200px;
}
#format-select {
width: auto;
min-width: 140px;
}
#submit-btn {
width: auto;
}
}
</style>
</head>
<body>
<h1>Jina Reader</h1>
<p>An interface for the <a href="https://jina.ai/reader/">Jina Reader API</a>.</p>
<form id="url-form">
<input type="text" id="url-input" placeholder="Enter URL" required>
<select id="format-select">
<option value="markdown">Markdown</option>
<option value="html">HTML</option>
<option value="text">Text</option>
<option value="llm_markdown">LLM Markdown</option>
</select>
<button type="submit" id="submit-btn">Submit</button>
</form>
<div id="loading">Loading...</div>
<div id="result">
<textarea id="markdown-raw" readonly></textarea>
<button id="copy-btn" class="copy-btn">Copy to clipboard</button>
<iframe id="markdown-rendered" sandbox></iframe>
</div>
<div id="prompt-section" style="margin-top: 2em; padding-top: 2em; border-top: 1px solid #eee; display: none;">
<h2>Run a prompt with Claude</h2>
<textarea id="prompt-textarea">Respond in markdown. You summarize the pasted in text. Start with a overall summary in a single paragraph. Then show a bullet pointed list of the most interesting illustrative quotes from the piece. Then a bullet point list of the most unusual ideas. Finally provide a longer summary that covers points not included already</textarea>
<button id="run-prompt-btn">Run prompt</button>
<div id="prompt-loading" style="margin: 1em 0; font-style: italic;">Generating...</div>
<div id="prompt-result">
<div id="prompt-rendered"></div>
<button id="prompt-copy-btn" class="copy-btn" style="margin-top: 1em;">Copy to clipboard</button>
</div>
</div>
<script>
const urlForm = document.getElementById('url-form');
const urlInput = document.getElementById('url-input');
const formatSelect = document.getElementById('format-select');
const loading = document.getElementById('loading');
const result = document.getElementById('result');
const markdownRaw = document.getElementById('markdown-raw');
const copyBtn = document.getElementById('copy-btn');
const markdownRendered = document.getElementById('markdown-rendered');
// Claude elements
const promptTextarea = document.getElementById('prompt-textarea');
const runPromptBtn = document.getElementById('run-prompt-btn');
const promptLoading = document.getElementById('prompt-loading');
const promptResult = document.getElementById('prompt-result');
const promptRendered = document.getElementById('prompt-rendered');
const promptCopyBtn = document.getElementById('prompt-copy-btn');
urlForm.addEventListener('submit', async (e) => {
e.preventDefault();
const url = urlInput.value;
const format = formatSelect.value;
if (!url) return;
loading.style.display = 'block';
result.style.display = 'none';
try {
let fetchOptions = {
method: 'GET'
};
if (format !== 'llm_markdown') {
fetchOptions.headers = {
"X-Return-Format": format
};
}
const response = await fetch(`https://r.jina.ai/${url}`, fetchOptions);
const content = await response.text();
markdownRaw.value = content;
let htmlContent;
if (format === 'html') {
htmlContent = content;
} else {
htmlContent = `
<html>
<head>
<style>
body { font-family: Helvetica, sans-serif; line-height: 1.6; color: #333; }
img { max-width: 100%; height: auto; }
</style>
</head>
<body>
${format === 'text' ? `<pre>${content}</pre>` : marked.parse(content)}
</body>
</html>
`;
}
markdownRendered.srcdoc = htmlContent;
result.style.display = 'block';
document.getElementById('prompt-section').style.display = 'block';
} catch (error) {
console.error('Error fetching content:', error);
markdownRaw.value = 'Error fetching content. Please try again.';
markdownRendered.srcdoc = '<p>Error fetching content. Please try again.</p>';
result.style.display = 'block';
document.getElementById('prompt-section').style.display = 'none';
} finally {
loading.style.display = 'none';
}
});
copyBtn.addEventListener('click', () => {
markdownRaw.select();
document.execCommand('copy');
const originalText = copyBtn.textContent;
copyBtn.textContent = 'Copied';
setTimeout(() => {
copyBtn.textContent = originalText;
}, 1500);
});
// Get the API key from localStorage or prompt the user to enter it
function getApiKey() {
let apiKey = localStorage.getItem("ANTHROPIC_API_KEY");
if (!apiKey) {
apiKey = prompt("Please enter your Anthropic API key:");
if (apiKey) {
localStorage.setItem("ANTHROPIC_API_KEY", apiKey);
}
}
return apiKey;
}
// Handle Claude prompt
runPromptBtn.addEventListener('click', async () => {
const apiKey = getApiKey();
if (!apiKey) {
alert("API key not found. Please enter your Anthropic API key.");
return;
}
// Get the prompt and content
const systemPrompt = promptTextarea.value;
const content = markdownRaw.value;
if (!content) {
alert("No content to analyze. Please fetch a URL first.");
return;
}
promptLoading.style.display = 'block';
promptResult.style.display = 'none';
try {
const response = await fetch("https://api.anthropic.com/v1/messages", {
method: "POST",
headers: {
"x-api-key": apiKey,
"anthropic-version": "2023-06-01",
"content-type": "application/json",
"anthropic-dangerous-direct-browser-access": "true"
},
body: JSON.stringify({
model: "claude-3-5-haiku-latest",
max_tokens: 4096,
system: systemPrompt,
messages: [
{
role: "user",
content: content
}
]
})
});
const data = await response.json();
console.log(JSON.stringify(data, null, 2));
const markdown = data.content[0].text;
promptRendered.innerHTML = marked.parse(markdown);
promptResult.style.display = 'block';
let responseMarkdown = markdown;
promptRendered.innerHTML = marked.parse(responseMarkdown);
promptCopyBtn.onclick = () => {
navigator.clipboard.writeText(responseMarkdown);
const originalText = promptCopyBtn.textContent;
promptCopyBtn.textContent = 'Copied';
setTimeout(() => {
promptCopyBtn.textContent = originalText;
}, 1500);
};
} catch (error) {
console.error("Error calling Claude API:", error);
promptRendered.innerHTML = '<p class="error">Error generating summary. Please try again.</p>';
} finally {
promptLoading.style.display = 'none';
}
});
</script>
<script src="footer.js?8e6f72a9"></script>
</body>
</html>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>OpenAI Prompt Caching Playground</title>
<style>
:root {
--bg:#0d1117; --panel:#161b22; --ink:#e6edf3; --muted:#9da7b3; --accent:#3fb950; --warn:#f0883e; --border:#30363d;
}
html,body {background:var(--bg); color:var(--ink); font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji", "Segoe UI Emoji"; margin:0; padding:0;}
header {padding:16px 20px; border-bottom:1px solid var(--border); position:sticky; top:0; background:rgba(13,17,23,.9); backdrop-filter: blur(6px);}
h1 {font-size:18px; margin:0 0 6px;}
.sub {color:var(--muted); font-size:13px;}
.wrap {display:grid; grid-template-columns: 360px 1fr; gap:16px; padding:16px;}
.panel {background:var(--panel); border:1px solid var(--border); border-radius:10px; padding:14px;}
.panel h2 {font-size:14px; margin:0 0 10px; color:#c9d1d9; letter-spacing:.2px;}
textarea, input, select {background:#0b0f14; color:var(--ink); border:1px solid var(--border); border-radius:8px; padding:8px; width:100%; box-sizing:border-box; font: inherit;}
textarea {min-height:110px; resize:vertical; line-height:1.35;}
.row {display:flex; gap:8px; align-items:center;}
.row > * {flex:1;}
.row .shrink {flex:0 0 auto;}
.btn {background:#21262d; border:1px solid var(--border); padding:8px 10px; border-radius:8px; cursor:pointer; color:var(--ink);}
.btn:hover {border-color:#6e7681;}
.btn.primary {background:var(--accent); color:#031d0b; border-color:#2ea043; font-weight:600;}
.btn.ghost {background:transparent;}
.grid-2 {display:grid; grid-template-columns:1fr 1fr; gap:8px;}
.stats {display:grid; grid-template-columns: repeat(6, minmax(90px, 1fr)); gap:8px; margin-top:8px;}
.stat {background:#0b0f14; border:1px dashed var(--border); border-radius:8px; padding:8px;}
.stat .k {font-size:11px; color:var(--muted);}
.stat .v {font-size:16px; font-weight:700; margin-top:4px;}
.badge {display:inline-block; padding:2px 8px; border-radius:999px; font-size:12px; border:1px solid var(--border); background:#0b0f14; color:var(--muted);}
.badge.hit {background:rgba(63,185,80,.12); color:#56d364; border-color:#2ea043;}
.badge.miss {background:rgba(240,136,62,.12); color:#f0883e; border-color:#8a5700;}
.log {font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size:12px; white-space:pre-wrap; background:#0b0f14; border:1px solid var(--border); border-radius:8px; padding:10px; max-height:260px; overflow:auto;}
.out {background:#0b0f14; border:1px solid var(--border); border-radius:8px; padding:10px; min-height:140px; font-family: ui-monospace, Menlo, Consolas, monospace; white-space:pre-wrap;}
/* Raw JSON sections */
.code {
background:#0b0f14; border:1px solid var(--border);
border-radius:8px; padding:10px;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size:12px; line-height:1.5; white-space:pre-wrap;
max-height:260px; overflow:auto;
}
details summary { cursor:pointer; user-select:none; }
details { margin:8px 0; }
.footer-note {color:var(--muted); font-size:12px;}
.inline {display:inline-flex; align-items:center; gap:8px;}
.sep {height:1px; background:var(--border); margin:10px 0;}
@media (max-width: 980px){ .wrap{grid-template-columns:1fr;} }
</style>
</head>
<body>
<header>
<h1>Prompt Caching Playground</h1>
<div class="sub">
Optimize your prompt structure and observe cache hits. Cache engages at <strong>≥ 1024 tokens</strong>, with cacheable chunks in <strong>128-token</strong> increments (e.g., 1024, 1152, 1280…).
</div>
</header>
<div class="wrap">
<!-- LEFT: Controls -->
<div class="panel">
<h2>Setup</h2>
<div class="row">
<select id="endpoint">
<option value="chat">Chat Completions API</option>
<option value="responses">Responses API (supports prompt_cache_key)</option>
</select>
<select id="model">
<option value="gpt-4o-mini">gpt-4o-mini</option>
<option value="gpt-4o">gpt-4o</option>
<option value="gpt-4.1-mini">gpt-4.1-mini</option>
</select>
</div>
<div class="row" style="margin-top:8px;">
<input id="cacheKey" placeholder="prompt_cache_key (Responses API only)" />
</div>
<div class="row" style="margin-top:8px;">
<input id="scenarioName" placeholder="Scenario label (for the log)" />
<select id="scenarioPicker" class="shrink">
<option value="starter">Load demo scenario…</option>
<option value="A">A) Static instructions + Doc A + Q1</option>
<option value="A2">A2) Same instructions + Doc A + Q2</option>
<option value="B">B) Same instructions + Doc B + Q1</option>
</select>
<button class="btn" id="loadScenarioBtn">Load</button>
</div>
<div class="sep"></div>
<div class="row">
<label class="inline">
Repeat instructions
<input type="number" id="repeatInstructions" min="1" value="1" style="width:90px" />
</label>
<label class="inline">
Repeat doc
<input type="number" id="repeatDoc" min="1" value="1" style="width:90px" />
</label>
</div>
<div class="footer-note" style="margin-top:6px;">
Use repeats to push your prompt over 1024 tokens (and observe cached increments).
</div>
<div class="sep"></div>
<h2>Prompt Parts</h2>
<label class="footer-note">Initial Instructions (static; put these first for best cache hit rate)</label>
<textarea id="instructions"></textarea>
<label class="footer-note" style="margin-top:8px; display:block;">Document Context (semi-static; keep above user input)</label>
<textarea id="doc"></textarea>
<label class="footer-note" style="margin-top:8px; display:block;">User Question (dynamic; place last)</label>
<textarea id="question"></textarea>
<div class="sep"></div>
<div class="grid-2">
<button class="btn primary" id="sendBtn">Send</button>
<button class="btn" id="sendVariantBtn">Send Variant (new Q)</button>
</div>
<div class="row" style="margin-top:8px;">
<button class="btn ghost" id="clearLogBtn">Clear Log</button>
<button class="btn ghost" id="copyLogBtn">Copy Log</button>
</div>
<div class="footer-note" style="margin-top:8px;">
Tip: Try A → A2 (same prefix, new question) to see cache hits; then A → B to see instruction-only prefix hits.
</div>
</div>
<!-- RIGHT: Results -->
<div class="panel">
<h2>Result</h2>
<div class="row">
<span id="hitBadge" class="badge">cache status</span>
<span id="timeBadge" class="badge">latency</span>
<span id="tsBadge" class="badge">timestamp</span>
</div>
<div class="stats">
<div class="stat"><div class="k">Prompt Tokens</div><div class="v" id="promptTokens">–</div></div>
<div class="stat"><div class="k">Cached Tokens</div><div class="v" id="cachedTokens">–</div></div>
<div class="stat"><div class="k">Cache Hit %</div><div class="v" id="hitPct">–</div></div>
<div class="stat"><div class="k">Completion Tokens</div><div class="v" id="completionTokens">–</div></div>
<div class="stat"><div class="k">Total Tokens</div><div class="v" id="totalTokens">–</div></div>
<div class="stat"><div class="k">Model</div><div class="v" id="modelEcho">–</div></div>
</div>
<div class="sep"></div>
<div class="row">
<div class="stat" style="flex:1;">
<div class="k">Endpoint</div>
<div class="v" id="endpointEcho">–</div>
</div>
<div class="stat" style="flex:1;">
<div class="k">prompt_cache_key</div>
<div class="v" id="pckEcho" title="Only used on Responses API">–</div>
</div>
</div>
<div class="sep"></div>
<div class="out" id="output">Response will appear here…</div>
<div class="sep"></div>
<h2>Raw JSON</h2>
<details open id="reqDetails">
<summary>Most recent request JSON</summary>
<pre id="reqJson" class="code">—</pre>
</details>
<details id="resDetails">
<summary>Most recent response JSON</summary>
<pre id="resJson" class="code">—</pre>
</details>
<div class="sep"></div>
<h2>Run Log</h2>
<div class="log" id="runLog"></div>
</div>
</div>
<script>
/** Minimal, in-page "localStorage prompt" API key flow */
function getAPIKey() {
let apiKey = localStorage.getItem('openai_api_key');
if (!apiKey) {
apiKey = prompt('Please enter your OpenAI API Key:');
if (apiKey) {
localStorage.setItem('openai_api_key', apiKey);
}
}
return apiKey;
}
/** Helpers */
const $ = (id) => document.getElementById(id);
function nowISO() {
const d = new Date();
return d.toLocaleString(undefined, {hour12:false}) + ' (' + d.toISOString() + ')';
}
function logLine(txt) {
const el = $('runLog');
el.textContent += txt + '\n';
el.scrollTop = el.scrollHeight;
}
function setBadge(el, text, kind) {
el.textContent = text;
el.classList.remove('hit','miss');
if (kind) el.classList.add(kind);
}
function pretty(obj) {
try { return JSON.stringify(obj, null, 2); }
catch { return String(obj); }
}
function setReqJson(url, payload) {
// Do NOT include headers / API key. Show only URL + payload.
const snapshot = { url, payload };
$('reqJson').textContent = pretty(snapshot);
}
function setResJson(data) {
$('resJson').textContent = pretty(data);
}
/** Demo scenarios (2× length) */
const demo = {
instructions: `You are a highly concise assistant. Always answer in 1–3 sentences unless explicitly asked for more. Use plain language. If the user asks for code, include only code unless told otherwise.
General rules:
- If asked to compare, list key differences as short bullets.
- If a calculation is needed, show the equation then the result.
- If a definition is requested, give a crisp one-liner first.
- Prefer step-by-step logic but keep it terse; avoid filler.
- Cite assumptions when they influence the answer.
Style guide:
- Avoid hedging like “it seems.” Be direct when evidence supports it.
- Use simple words; avoid jargon unless the question is technical.
- Bullets over paragraphs when listing 3+ items.
- For numbers: include units and orders of magnitude when relevant.
Math & formatting:
- Show one compact formula, then compute.
- Round sensibly; default to 2–3 sig figs unless precision matters.
- Monospace code blocks for code only; no prose inside code fences.
Safety rails:
- Decline disallowed content with a brief reason and a safer alternative.
- Don’t fabricate citations, links, or data you can’t verify.
Edge cases:
- If the question is ambiguous, answer the most common interpretation and note the alternative in one line.
- If insufficient data, state what’s missing and provide a minimal actionable next step.
Examples (for length and reference):
Q: Define latency vs throughput.
A: Latency is per-request delay; throughput is requests/second. They trade off: batching raises throughput but can add latency.
Q: Summarize a spec into 3 bullets.
A: Goal, core objects, critical risks (1 line each).
Q: Show a quick ROI calc.
A: ROI = (benefit − cost)/cost. With $50k benefit and $20k cost, ROI = (50−20)/20 = 1.5 = 150%.
When code is requested:
- Provide a minimal runnable snippet.
- Include a 1-line comment on how to run or integrate.
Tools you could hypothetically use (for realism in token length): web.run (search, open, click), python (analysis), image_query (images for people/places). These tool names are illustrative for prompt length; they don’t execute here.
Glossary (compact):
- CRDT: conflict-free replicated data type.
- Event sourcing: store events then derive state from them.
Extended guidance filler to lift token count:
${'Guideline: Prefer clear, literal phrasing; keep answers scoped; surface assumptions explicitly; show one example when helpful.\n'.repeat(60)}
(End of static instructions.)`,
docA: `Product Spec: Nimbus Notes
- Goal: Lightning-fast note capture with offline-first sync.
- Core objects: Notebook, Note, Tag, Attachment.
- Sync model: CRDT-based; conflict-free merges.
- Pricing: Free; Pro at $4/month with 10GB attachments.
- Roadmap Q4: iOS widgets, Android share sheet revamp, web clipper.
Architecture notes:
- Local-first write path, background sync, deterministic conflict resolution.
- Indexing: inverted index on device; server compaction nightly.
- Export: Markdown + attachments in a flat bundle.
Risks:
- Complex merge semantics for rich text.
- Battery usage on large notebooks during background sync.
Extra long filler to increase tokens:
${'• Feature: ' + 'rich text, backlinks, slash commands, export to Markdown.\n'.repeat(80)}`,
docB: `Product Spec: Zephyr Tasks
- Goal: Kanban-like tasking for small teams, built on ActivityPub.
- Core objects: Board, Column, Card, Checklist, Comment.
- Sync model: Event-sourced with snapshotting.
- Pricing: Free; Team at $6/user/month, custom SSO.
- Roadmap Q4: Gantt view, email in, calendar sync.
Architecture notes:
- Federated updates via ActivityPub; per-tenant queues.
- Snapshot intervals tuned to active column size.
- Automation hooks for webhooks and email ingestion.
Risks:
- Federation consistency and moderation boundaries.
- Notification overload without good defaults.
Extra long filler to increase tokens:
${'• Capability: ' + 'labels, due dates, swimlanes, WIP limits, automations.\n'.repeat(80)}`,
q1A: `What are the minimum moving parts I need to build first MVP?`,
q2A: `What are the top three risks for this approach?`,
q1B: `What metrics should we track in the first month?`,
};
function loadScenario(which) {
if (which === 'A') {
$('instructions').value = demo.instructions;
$('doc').value = demo.docA;
$('question').value = demo.q1A;
$('scenarioName').value = 'A (Instr + Doc A + Q1)';
} else if (which === 'A2') {
$('instructions').value = demo.instructions;
$('doc').value = demo.docA;
$('question').value = demo.q2A;
$('scenarioName').value = 'A2 (Instr + Doc A + Q2)';
} else if (which === 'B') {
$('instructions').value = demo.instructions;
$('doc').value = demo.docB;
$('question').value = demo.q1B;
$('scenarioName').value = 'B (Instr + Doc B + Q1)';
} else {
$('instructions').value = demo.instructions;
$('doc').value = demo.docA;
$('question').value = demo.q1A;
$('scenarioName').value = 'Starter';
}
}
$('loadScenarioBtn').addEventListener('click', () => loadScenario($('scenarioPicker').value));
/** Build messages with static-first ordering (to maximize cache hits) */
function buildMessages() {
const instr = $('instructions').value;
const doc = $('doc').value;
const question = $('question').value;
const repI = Math.max(1, parseInt($('repeatInstructions').value || '1', 10));
const repD = Math.max(1, parseInt($('repeatDoc').value || '1', 10));
const instrRepeated = Array.from({length:repI}).map(() => instr).join('\n\n');
const docRepeated = Array.from({length:repD}).map((_,i) => `# Document Copy ${i+1}\n` + doc).join('\n\n');
const system = instrRepeated.trim();
const user = (`Document Context:\n${docRepeated}\n\nUser Question:\n${question}`).trim();
const messages = [
{role:'system', content: system},
{role:'user', content: user},
];
return {messages, system, user};
}
/** Core send function (supports Chat Completions and Responses API) */
async function send(kind) {
const apiKey = getAPIKey();
if (!apiKey) return;
// Optionally mutate the user question for the "variant" button
if (kind === 'variant') {
$('question').value = $('question').value + ' (Please keep it to 3 bullets.)';
}
const endpointSel = $('endpoint').value;
const model = $('model').value;
const pck = $('cacheKey').value.trim() || null;
const label = $('scenarioName').value.trim() || '(unnamed)';
const {messages} = buildMessages();
let url, payload, headers = {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`,
};
if (endpointSel === 'responses') {
url = 'https://api.openai.com/v1/responses';
payload = {
model,
messages,
prompt_cache_key: pck || undefined,
temperature: 0.2,
};
} else {
url = 'https://api.openai.com/v1/chat/completions';
payload = {
model,
messages,
temperature: 0.2,
};
}
// Show outgoing request snapshot (no headers / key) and reset response pane
setReqJson(url, payload);
$('resJson').textContent = '—';
// UI pre-state
setBadge($('hitBadge'), '…', null);
setBadge($('timeBadge'), '–', null);
setBadge($('tsBadge'), new Date().toLocaleTimeString(), null);
$('output').textContent = 'Waiting for response…';
$('promptTokens').textContent = '–';
$('cachedTokens').textContent = '–';
$('hitPct').textContent = '–';
$('completionTokens').textContent = '–';
$('totalTokens').textContent = '–';
$('modelEcho').textContent = model;
$('endpointEcho').textContent = endpointSel;
$('pckEcho').textContent = pck || '—';
const t0 = performance.now();
let resp, data, ok = false, textOut = '', usage = {}, cached = 0;
try {
resp = await fetch(url, {method:'POST', headers, body: JSON.stringify(payload)});
const t1 = performance.now();
const latencyMs = Math.round(t1 - t0);
ok = resp.ok;
data = await resp.json();
setResJson(data);
// Extract output text + usage across both APIs
if (endpointSel === 'responses') {
textOut = data.output_text ?? '';
if (!textOut && Array.isArray(data.output)) {
textOut = data.output.map(o => {
if (o?.content) {
return o.content.map(c => (c.type === 'output_text' && c.text) ? c.text : '').join('');
}
return '';
}).join('').trim();
}
usage = data.usage || {};
} else {
textOut = (data.choices && data.choices[0] && data.choices[0].message && data.choices[0].message.content) || '';
usage = data.usage || {};
}
// Pull cached_tokens if present
const promptDetails = usage.prompt_tokens_details || {};
cached = (typeof promptDetails.cached_tokens === 'number') ? promptDetails.cached_tokens : 0;
// Display stats
const pt = usage.prompt_tokens ?? '—';
const ct = usage.completion_tokens ?? '—';
const tt = usage.total_tokens ?? '—';
$('promptTokens').textContent = pt;
$('cachedTokens').textContent = cached;
$('completionTokens').textContent = ct;
$('totalTokens').textContent = tt;
// Cache hit badge logic
let hitBadgeText = (cached && cached > 0) ? `cache hit: ${cached}` : 'cache miss';
let hitKind = (cached && cached > 0) ? 'hit' : 'miss';
setBadge($('hitBadge'), hitBadgeText, hitKind);
// Cache hit %
let hitPct = (typeof pt === 'number' && pt > 0 && typeof cached === 'number') ? ((cached/pt)*100).toFixed(1) + '%' : '—';
$('hitPct').textContent = hitPct;
setBadge($('timeBadge'), `${latencyMs} ms`, null);
setBadge($('tsBadge'), new Date().toLocaleTimeString(), null);
// Show response text
$('output').textContent = textOut || JSON.stringify(data, null, 2);
// Log line (concise)
const ts = nowISO();
logLine(`[${ts}] label="${label}" endpoint=${endpointSel} model=${model}` +
(pck ? ` pck="${pck}"` : '') +
` | latency=${latencyMs}ms tokens: prompt=${pt} cached=${cached} completion=${ct} total=${tt} | ` +
`hit=${cached>0} | Q="${$('question').value.trim().slice(0,80)}"`);
} catch (e) {
$('output').textContent = 'Error: ' + (e?.message || e);
setResJson({ error: String(e) });
setBadge($('hitBadge'), 'error', 'miss');
logLine(`[${nowISO()}] ERROR ${e?.message || e}`);
}
}
/** UI wires */
$('sendBtn').addEventListener('click', () => send('normal'));
$('sendVariantBtn').addEventListener('click', () => send('variant'));
$('clearLogBtn').addEventListener('click', () => { $('runLog').textContent=''; });
$('copyLogBtn').addEventListener('click', async () => {
await navigator.clipboard.writeText($('runLog').textContent);
alert('Log copied to clipboard.');
});
/** Initialize with starter scenario + session log */
function init() {
loadScenario('starter');
logLine(`[${nowISO()}] Session started. Try A → A2 for a cache hit (same prefix, new question). Then A → B to see instruction-only prefix hits. Caches typically persist ~5–10 minutes idle (sometimes up to ~1 hour off-peak).`);
}
init();
</script>
<script src="footer.js?8e6f72a9"></script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment