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`
this._toggleToolExpand(key)}> ${block.name} + ${refs.length ? html` ${t('llmr.detail.tool_reference_loads', { n: refs.length })}` : nothing} ${preview ? html`${preview}` : nothing} @@ -364,7 +407,12 @@ export class LlmRequestDetail extends LightElement { -
${result.content}
+ ${result.content ? html`
${result.content}
` : nothing} + ${refs.length ? html` +
+ ${refs.map(r => html` ${r}`)} +
+ ` : nothing} ` : nothing}
` : 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` +
+
+ + ${t('llmr.detail.tools_activated')} + ${sysTools.length} +
+
+ ${sysTools.map((td, i) => this._renderToolDef(td, `sys-${idx}-${i}`))} +
+
+ ` : 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` +
+
this._toggleToolExpand(key)}> + + ${name} + ${flags.deferred ? html`${t('llmr.detail.flag_deferred')}` : nothing} + ${flags.cached ? html`${t('llmr.detail.flag_cached')}` : nothing} + ${desc} + ${schema ? html` + + + + ` : nothing} +
+ ${open && schema ? html` +
${JSON.stringify(schema, null, 2)}
+ ` : nothing} +
+ `; + } + // ── 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.map((t, i) => { - // Anthropic: { name, description, input_schema } - // OpenAI: { type: 'function', function: { name, description, parameters } } - const name = t.name ?? t.function?.name ?? '(unknown)'; - const desc = t.description ?? t.function?.description ?? ''; - const schema = t.input_schema ?? t.function?.parameters ?? null; - const key = `tooldef-${i}-${name}`; - const open = this._expandedTools.has(key); - return html` -
-
this._toggleToolExpand(key)}> - - ${name} - ${desc} - ${schema ? html` - - - - ` : nothing} -
- ${open && schema ? html` -
${JSON.stringify(schema, null, 2)}
- ` : nothing} -
- `; - })} + ${tools.map((td, i) => this._renderToolDef(td, `tooldef-${i}`))}
`, 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',