diff --git a/web/components/llm-request-detail.js b/web/components/llm-request-detail.js
index 644be91..8b90b80 100644
--- a/web/components/llm-request-detail.js
+++ b/web/components/llm-request-detail.js
@@ -73,6 +73,14 @@ function extractTools(req) {
return req?.tools ?? [];
}
+// DTL (Anthropic): the system blocks may carry `cache_control` (the prompt
+// cache breakpoint active exactly when DTL is on). Surfaced as a badge on the
+// System section.
+function systemHasCache(req) {
+ if (!req || !Array.isArray(req.system)) return false;
+ return req.system.some(b => b && b.cache_control != null);
+}
+
function extractRespBlocks(resp) {
if (!resp) return [];
if (Array.isArray(resp.content)) return resp.content; // Anthropic
@@ -110,30 +118,63 @@ function paramsPreview(input) {
}
function normalizeToolResultContent(block) {
- if (Array.isArray(block.content))
- return block.content.map(b => b.text ?? JSON.stringify(b)).join('\n');
+ if (Array.isArray(block.content)) {
+ // DTL: Anthropic `tool_reference` blocks are rendered separately as a
+ // "loads" list inside the tool_use block; keep only the textual parts here.
+ const parts = block.content
+ .filter(b => b.type !== 'tool_reference')
+ .map(b => b.text ?? JSON.stringify(b));
+ return parts.join('\n');
+ }
if (typeof block.content === 'string') return block.content;
return JSON.stringify(block.content ?? '');
}
+// DTL: tool names an Anthropic `tool_result` activates via `tool_reference`
+// content blocks (an `activate_tools` result in AnthropicToolReference mode).
+function extractToolReferences(block) {
+ if (!Array.isArray(block.content)) return [];
+ return block.content
+ .filter(b => b.type === 'tool_reference')
+ .map(b => b.tool_name)
+ .filter(Boolean);
+}
+
+// DTL flags carried on a tool definition. Anthropic-native objects put
+// `defer_loading` and `cache_control` at the top level; OpenAI-shaped objects
+// (pre-conversion) may carry `defer_loading` on the top-level tool object too.
+function extractToolFlags(td) {
+ return {
+ deferred: td.defer_loading === true,
+ cached: td.cache_control != null,
+ };
+}
+
function buildToolResultMap(msgs) {
- const map = new Map(); // tool_use_id → { content, is_error }
+ const map = new Map(); // tool_use_id → { content, is_error, references }
for (const msg of msgs) {
// Anthropic format: tool_result blocks inside user message content
for (const block of contentBlocks(msg)) {
if (block.type === 'tool_result') {
map.set(block.tool_use_id, {
- content: normalizeToolResultContent(block),
- is_error: !!block.is_error,
+ content: normalizeToolResultContent(block),
+ is_error: !!block.is_error,
+ references: extractToolReferences(block),
});
}
}
- // OpenAI format: role='tool' messages carry the result directly
+ // OpenAI format: role='tool' messages carry the result directly.
+ // The pre-conversion `_tool_references` marker (set by the message builder
+ // in AnthropicToolReference mode) is handled defensively — most captured
+ // Anthropic bodies are already converted to native tool_result blocks.
if (msg.role === 'tool' && msg.tool_call_id) {
+ const references = Array.isArray(msg._tool_references)
+ ? msg._tool_references.filter(r => typeof r === 'string')
+ : [];
const content = typeof msg.content === 'string'
? msg.content
: (Array.isArray(msg.content) ? msg.content.map(b => b.text ?? JSON.stringify(b)).join('\n') : JSON.stringify(msg.content ?? ''));
- map.set(msg.tool_call_id, { content, is_error: false });
+ map.set(msg.tool_call_id, { content, is_error: false, references });
}
}
return map;
@@ -346,11 +387,13 @@ export class LlmRequestDetail extends LightElement {
const args = block.input != null ? JSON.stringify(block.input, null, 2) : '{}';
const preview = paramsPreview(block.input);
const result = toolResultMap?.get(block.id);
+ const refs = result?.references ?? [];
return html`
` : nothing}
@@ -409,15 +457,31 @@ export class LlmRequestDetail extends LightElement {
return nothing;
}
- // mid-conversation system prompt: render with markdown and a distinct style
+ // mid-conversation system prompt: render with markdown and a distinct style.
+ // DTL (Kimi): a mid-conversation `system` message may carry a `tools` array
+ // (the activated tool defs) with no textual content — render it as a
+ // dedicated "tools activated" block instead of dropping it.
if (role === 'system') {
- const text = typeof msg.content === 'string' ? msg.content : '';
- if (!text) return nothing;
+ const text = typeof msg.content === 'string' ? msg.content : '';
+ const sysTools = Array.isArray(msg.tools) ? msg.tools : [];
+ if (!text && sysTools.length === 0) return nothing;
return html`
${t('llmr.detail.system_role')}
-
${unsafeHTML(renderMarkdown(text))}
+ ${text ? html`
${unsafeHTML(renderMarkdown(text))}
` : nothing}
+ ${sysTools.length ? html`
+
+ ` : nothing}
`;
@@ -439,6 +503,38 @@ export class LlmRequestDetail extends LightElement {
return this._renderContentBlock(block, `resp-${idx}`);
}
+ // A single tool definition, collapsible. Shared by the Tools section and the
+ // Kimi DTL system-tools block. Handles both shapes:
+ // Anthropic: { name, description, input_schema, defer_loading?, cache_control? }
+ // OpenAI: { type:'function', function:{ name, description, parameters }, defer_loading? }
+ // DTL flags (defer_loading / cache_control) render as small badges.
+ _renderToolDef(td, key) {
+ const name = td.name ?? td.function?.name ?? '(unknown)';
+ const desc = td.description ?? td.function?.description ?? '';
+ const schema = td.input_schema ?? td.function?.parameters ?? null;
+ const flags = extractToolFlags(td);
+ const open = this._expandedTools.has(key);
+ return html`
+
+ `;
+ }
+
// ── Main render ──────────────────────────────────────────────────────────────
render() {
@@ -481,6 +577,7 @@ export class LlmRequestDetail extends LightElement {
const msgs = extractMessages(req);
const params = extractParams(req);
const tools = extractTools(req);
+ const sysCached = systemHasCache(req);
const payloadMissing = !req && !resp;
const respBlocks = extractRespBlocks(resp);
@@ -520,7 +617,8 @@ export class LlmRequestDetail extends LightElement {
) : nothing}
${system ? this._renderSection('system', t('llmr.detail.section_system'),
- html`
${unsafeHTML(renderMarkdown(system))}
`
+ html`
${unsafeHTML(renderMarkdown(system))}
`,
+ sysCached ? t('llmr.detail.flag_cached') : null
) : nothing}
${msgs.length ? this._renderSection('conversation', t('llmr.detail.section_conversation'),
@@ -532,32 +630,7 @@ export class LlmRequestDetail extends LightElement {
${tools.length ? this._renderSection('tools', t('llmr.detail.section_tools'),
html`
`,
tools.length
) : nothing}
diff --git a/web/css/llm-requests.css b/web/css/llm-requests.css
index 3b25b4a..a0b5355 100644
--- a/web/css/llm-requests.css
+++ b/web/css/llm-requests.css
@@ -587,6 +587,83 @@ llm-requests-page {
overflow-y: auto;
}
+/* ── DTL (dynamic tool loading) ─────────────────────────────────────────────── */
+
+/* Kimi: a mid-conversation system message carrying an activated `tools` array */
+.llmr-dtl-tools {
+ border: 1px solid color-mix(in srgb, #db2777 22%, transparent);
+ border-left: 3px solid #db2777;
+ border-radius: 5px;
+ overflow: hidden;
+ font-size: 0.8rem;
+}
+
+.llmr-dtl-tools-header {
+ display: flex;
+ align-items: center;
+ gap: 0.4rem;
+ padding: 4px 8px;
+ background: color-mix(in srgb, #db2777 8%, var(--bs-tertiary-bg));
+ color: #db2777;
+ font-family: var(--bs-font-monospace);
+ font-size: 0.75rem;
+ font-weight: 600;
+}
+
+.llmr-dtl-tools-list {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ padding: 6px 8px;
+}
+
+/* Anthropic: a tool_reference result — the tool names an activate_tools call loads */
+.llmr-tool-refs {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+ padding: 4px 8px 6px;
+}
+
+.llmr-tool-ref {
+ font-family: var(--bs-font-monospace);
+ font-size: 0.72rem;
+ color: var(--bs-body-color);
+}
+
+.llmr-tool-ref i { color: #db2777; }
+
+.llmr-tool-dtl-badge {
+ font-family: var(--bs-font-monospace);
+ font-size: 0.66rem;
+ color: #db2777;
+ background: color-mix(in srgb, #db2777 10%, transparent);
+ padding: 1px 6px;
+ border-radius: 10px;
+ white-space: nowrap;
+}
+
+/* Per-tool DTL flags in the tools list */
+.llmr-tool-def-flag {
+ font-family: var(--bs-font-monospace);
+ font-size: 0.62rem;
+ text-transform: uppercase;
+ letter-spacing: 0.04em;
+ padding: 1px 5px;
+ border-radius: 3px;
+ white-space: nowrap;
+}
+
+.llmr-tool-def-flag--deferred {
+ color: #db2777;
+ background: color-mix(in srgb, #db2777 12%, transparent);
+}
+
+.llmr-tool-def-flag--cached {
+ color: #0891b2;
+ background: color-mix(in srgb, #0891b2 12%, transparent);
+}
+
/* ── Reasoning block ─────────────────────────────────────────────────────────── */
.llmr-reasoning-block {
diff --git a/web/i18n/en.js b/web/i18n/en.js
index 25d7f28..f69adc4 100644
--- a/web/i18n/en.js
+++ b/web/i18n/en.js
@@ -165,6 +165,10 @@ export default {
'llmr.detail.section_conversation':'Conversation',
'llmr.detail.section_tools': 'Tools Defined',
'llmr.detail.section_response': 'Response',
+ 'llmr.detail.tools_activated': 'Tools activated (DTL)',
+ 'llmr.detail.tool_reference_loads': 'loads {n}',
+ 'llmr.detail.flag_deferred': 'deferred',
+ 'llmr.detail.flag_cached': 'cached',
// ── Config ──────────────────────────────────────────────────────────────────
'config.title': 'Config',
diff --git a/web/i18n/fr.js b/web/i18n/fr.js
index 121ef78..25e2130 100644
--- a/web/i18n/fr.js
+++ b/web/i18n/fr.js
@@ -165,6 +165,10 @@ export default {
'llmr.detail.section_conversation':'Conversation',
'llmr.detail.section_tools': 'Outils définis',
'llmr.detail.section_response': 'Réponse',
+ 'llmr.detail.tools_activated': 'Outils activés (DTL)',
+ 'llmr.detail.tool_reference_loads': 'charge {n}',
+ 'llmr.detail.flag_deferred': 'différé',
+ 'llmr.detail.flag_cached': 'en cache',
// ── Config ──────────────────────────────────────────────────────────────────
'config.title': 'Configuration',
diff --git a/web/i18n/it.js b/web/i18n/it.js
index 8997d66..81b2e4d 100644
--- a/web/i18n/it.js
+++ b/web/i18n/it.js
@@ -165,6 +165,10 @@ export default {
'llmr.detail.section_conversation':'Conversazione',
'llmr.detail.section_tools': 'Strumenti definiti',
'llmr.detail.section_response': 'Risposta',
+ 'llmr.detail.tools_activated': 'Strumenti attivati (DTL)',
+ 'llmr.detail.tool_reference_loads': 'carica {n}',
+ 'llmr.detail.flag_deferred': 'differito',
+ 'llmr.detail.flag_cached': 'in cache',
// ── Session detail ──────────────────────────────────────────────────────────
'session.back': 'Indietro',