-
-
Notifications
You must be signed in to change notification settings - Fork 153
Expand file tree
/
Copy pathprompt-caching.html
More file actions
518 lines (448 loc) · 20.3 KB
/
prompt-caching.html
File metadata and controls
518 lines (448 loc) · 20.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
<!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>
</body>
</html>