From f884766445bbf1fbce11f1db4bc6174e72d9baa5 Mon Sep 17 00:00:00 2001 From: Dax Date: Mon, 7 Jul 2025 15:53:43 -0400 Subject: v2 message format and upgrade to ai sdk v5 (#743) Co-authored-by: GitHub Action Co-authored-by: Liang-Shih Lin Co-authored-by: Dominik Engelhardt Co-authored-by: Jay V Co-authored-by: adamdottv <2363879+adamdottv@users.noreply.github.com> --- packages/tui/internal/components/chat/editor.go | 8 +- packages/tui/internal/components/chat/message.go | 324 ++++++++++------------ packages/tui/internal/components/chat/messages.go | 66 ++--- packages/tui/internal/components/status/status.go | 25 +- 4 files changed, 203 insertions(+), 220 deletions(-) (limited to 'packages/tui/internal/components') diff --git a/packages/tui/internal/components/chat/editor.go b/packages/tui/internal/components/chat/editor.go index 9466d541c..5aa05bd1c 100644 --- a/packages/tui/internal/components/chat/editor.go +++ b/packages/tui/internal/components/chat/editor.go @@ -248,10 +248,10 @@ func (m *editorComponent) Submit() (tea.Model, tea.Cmd) { fileParts := make([]opencode.FilePartParam, 0) for _, attachment := range attachments { fileParts = append(fileParts, opencode.FilePartParam{ - Type: opencode.F(opencode.FilePartTypeFile), - MediaType: opencode.F(attachment.MediaType), - URL: opencode.F(attachment.URL), - Filename: opencode.F(attachment.Filename), + Type: opencode.F(opencode.FilePartTypeFile), + Mime: opencode.F(attachment.MediaType), + URL: opencode.F(attachment.URL), + Filename: opencode.F(attachment.Filename), }) } diff --git a/packages/tui/internal/components/chat/message.go b/packages/tui/internal/components/chat/message.go index 9e245c8b2..3414a77d5 100644 --- a/packages/tui/internal/components/chat/message.go +++ b/packages/tui/internal/components/chat/message.go @@ -17,7 +17,6 @@ import ( "github.com/sst/opencode/internal/styles" "github.com/sst/opencode/internal/theme" "github.com/sst/opencode/internal/util" - "github.com/tidwall/gjson" "golang.org/x/text/cases" "golang.org/x/text/language" ) @@ -217,51 +216,50 @@ func renderContentBlock( func renderText( app *app.App, - message opencode.Message, + message opencode.MessageUnion, text string, author string, showToolDetails bool, highlight bool, width int, extra string, - toolCalls ...opencode.ToolInvocationPart, + toolCalls ...opencode.ToolPart, ) string { t := theme.CurrentTheme() - timestamp := time.UnixMilli(int64(message.Metadata.Time.Created)). - Local(). - Format("02 Jan 2006 03:04 PM") - if time.Now().Format("02 Jan 2006") == timestamp[:11] { - // don't show the date if it's today - timestamp = timestamp[12:] - } - info := fmt.Sprintf("%s (%s)", author, timestamp) - info = styles.NewStyle().Foreground(t.TextMuted()).Render(info) - + var ts time.Time backgroundColor := t.BackgroundPanel() if highlight { backgroundColor = t.BackgroundElement() } messageStyle := styles.NewStyle().Background(backgroundColor) - if message.Role == opencode.MessageRoleUser { + content := messageStyle.Render(text) + + switch casted := message.(type) { + case opencode.AssistantMessage: + ts = time.UnixMilli(int64(casted.Time.Created)) + content = util.ToMarkdown(text, width, backgroundColor) + case opencode.UserMessage: + ts = time.UnixMilli(int64(casted.Time.Created)) messageStyle = messageStyle.Width(width - 6) } - content := messageStyle.Render(text) - if message.Role == opencode.MessageRoleAssistant { - content = util.ToMarkdown(text, width, backgroundColor) + timestamp := ts. + Local(). + Format("02 Jan 2006 03:04 PM") + if time.Now().Format("02 Jan 2006") == timestamp[:11] { + // don't show the date if it's today + timestamp = timestamp[12:] } + info := fmt.Sprintf("%s (%s)", author, timestamp) + info = styles.NewStyle().Foreground(t.TextMuted()).Render(info) if !showToolDetails && toolCalls != nil && len(toolCalls) > 0 { content = content + "\n\n" for _, toolCall := range toolCalls { - title := renderToolTitle(toolCall, message.Metadata, width) - metadata := opencode.MessageMetadataTool{} - if _, ok := message.Metadata.Tool[toolCall.ToolInvocation.ToolCallID]; ok { - metadata = message.Metadata.Tool[toolCall.ToolInvocation.ToolCallID] - } + title := renderToolTitle(toolCall, width) style := styles.NewStyle() - if _, ok := metadata.ExtraFields["error"]; ok { + if toolCall.State.Status == opencode.ToolPartStateStatusError { style = style.Foreground(t.Error()) } title = style.Render(title) @@ -276,8 +274,8 @@ func renderText( } content = strings.Join(sections, "\n") - switch message.Role { - case opencode.MessageRoleUser: + switch message.(type) { + case opencode.UserMessage: return renderContentBlock( app, content, @@ -286,7 +284,7 @@ func renderText( WithTextColor(t.Text()), WithBorderColorRight(t.Secondary()), ) - case opencode.MessageRoleAssistant: + case opencode.AssistantMessage: return renderContentBlock( app, content, @@ -300,39 +298,32 @@ func renderText( func renderToolDetails( app *app.App, - toolCall opencode.ToolInvocationPart, - messageMetadata opencode.MessageMetadata, + toolCall opencode.ToolPart, highlight bool, width int, ) string { ignoredTools := []string{"todoread"} - if slices.Contains(ignoredTools, toolCall.ToolInvocation.ToolName) { + if slices.Contains(ignoredTools, toolCall.Tool) { return "" } - toolCallID := toolCall.ToolInvocation.ToolCallID - metadata := opencode.MessageMetadataTool{} - if _, ok := messageMetadata.Tool[toolCallID]; ok { - metadata = messageMetadata.Tool[toolCallID] + if toolCall.State.Status == opencode.ToolPartStateStatusPending || toolCall.State.Status == opencode.ToolPartStateStatusRunning { + title := renderToolTitle(toolCall, width) + return renderContentBlock(app, title, highlight, width) } var result *string - if toolCall.ToolInvocation.Result != "" { - result = &toolCall.ToolInvocation.Result - } - - if toolCall.ToolInvocation.State == "partial-call" { - title := renderToolTitle(toolCall, messageMetadata, width) - return renderContentBlock(app, title, highlight, width) + if toolCall.State.Output != "" { + result = &toolCall.State.Output } - toolArgsMap := make(map[string]any) - if toolCall.ToolInvocation.Args != nil { - value := toolCall.ToolInvocation.Args + toolInputMap := make(map[string]any) + if toolCall.State.Input != nil { + value := toolCall.State.Input if m, ok := value.(map[string]any); ok { - toolArgsMap = m - keys := make([]string, 0, len(toolArgsMap)) - for key := range toolArgsMap { + toolInputMap = m + keys := make([]string, 0, len(toolInputMap)) + for key := range toolInputMap { keys = append(keys, key) } slices.Sort(keys) @@ -340,7 +331,6 @@ func renderToolDetails( } body := "" - finished := result != nil && *result != "" t := theme.CurrentTheme() backgroundColor := t.BackgroundPanel() borderColor := t.BackgroundPanel() @@ -349,137 +339,128 @@ func renderToolDetails( borderColor = t.BorderActive() } - switch toolCall.ToolInvocation.ToolName { - case "read": - preview := metadata.ExtraFields["preview"] - if preview != nil && toolArgsMap["filePath"] != nil { - filename := toolArgsMap["filePath"].(string) - body = preview.(string) - body = util.RenderFile(filename, body, width, util.WithTruncate(6)) - } - case "edit": - if filename, ok := toolArgsMap["filePath"].(string); ok { - diffField := metadata.ExtraFields["diff"] - if diffField != nil { - patch := diffField.(string) - var formattedDiff string - formattedDiff, _ = diff.FormatUnifiedDiff( - filename, - patch, - diff.WithWidth(width-2), - ) - body = strings.TrimSpace(formattedDiff) - style := styles.NewStyle(). - Background(backgroundColor). - Foreground(t.TextMuted()). - Padding(1, 2). - Width(width - 4) - if highlight { - style = style.Foreground(t.Text()).Bold(true) - } + if toolCall.State.Status == opencode.ToolPartStateStatusCompleted { + metadata := toolCall.State.Metadata.(map[string]any) + switch toolCall.Tool { + case "read": + preview := metadata["preview"] + if preview != nil && toolInputMap["filePath"] != nil { + filename := toolInputMap["filePath"].(string) + body = preview.(string) + body = util.RenderFile(filename, body, width, util.WithTruncate(6)) + } + case "edit": + if filename, ok := toolInputMap["filePath"].(string); ok { + diffField := metadata["diff"] + if diffField != nil { + patch := diffField.(string) + var formattedDiff string + formattedDiff, _ = diff.FormatUnifiedDiff( + filename, + patch, + diff.WithWidth(width-2), + ) + body = strings.TrimSpace(formattedDiff) + style := styles.NewStyle(). + Background(backgroundColor). + Foreground(t.TextMuted()). + Padding(1, 2). + Width(width - 4) + if highlight { + style = style.Foreground(t.Text()).Bold(true) + } - if diagnostics := renderDiagnostics(metadata, filename); diagnostics != "" { - diagnostics = style.Render(diagnostics) - body += "\n" + diagnostics - } + if diagnostics := renderDiagnostics(metadata, filename); diagnostics != "" { + diagnostics = style.Render(diagnostics) + body += "\n" + diagnostics + } - title := renderToolTitle(toolCall, messageMetadata, width) - title = style.Render(title) - content := title + "\n" + body - content = renderContentBlock( - app, - content, - highlight, - width, - WithPadding(0), - WithBorderColor(borderColor), - ) - return content + title := renderToolTitle(toolCall, width) + title = style.Render(title) + content := title + "\n" + body + content = renderContentBlock( + app, + content, + highlight, + width, + WithPadding(0), + WithBorderColor(borderColor), + ) + return content + } } - } - case "write": - if filename, ok := toolArgsMap["filePath"].(string); ok { - if content, ok := toolArgsMap["content"].(string); ok { - body = util.RenderFile(filename, content, width) - if diagnostics := renderDiagnostics(metadata, filename); diagnostics != "" { - body += "\n\n" + diagnostics + case "write": + if filename, ok := toolInputMap["filePath"].(string); ok { + if content, ok := toolInputMap["content"].(string); ok { + body = util.RenderFile(filename, content, width) + if diagnostics := renderDiagnostics(metadata, filename); diagnostics != "" { + body += "\n\n" + diagnostics + } } } - } - case "bash": - stdout := metadata.ExtraFields["stdout"] - if stdout != nil { - command := toolArgsMap["command"].(string) - body = fmt.Sprintf("```console\n> %s\n%s```", command, stdout) - body = util.ToMarkdown(body, width, backgroundColor) - } - case "webfetch": - if format, ok := toolArgsMap["format"].(string); ok && result != nil { - body = *result - body = util.TruncateHeight(body, 10) - if format == "html" || format == "markdown" { + case "bash": + stdout := metadata["stdout"] + if stdout != nil { + command := toolInputMap["command"].(string) + body = fmt.Sprintf("```console\n> %s\n%s```", command, stdout) body = util.ToMarkdown(body, width, backgroundColor) } - } - case "todowrite": - todos := metadata.JSON.ExtraFields["todos"] - if !todos.IsNull() && finished { - strTodos := todos.Raw() - todos := gjson.Parse(strTodos) - for _, todo := range todos.Array() { - content := todo.Get("content").String() - switch todo.Get("status").String() { - case "completed": - body += fmt.Sprintf("- [x] %s\n", content) - // case "in-progress": - // body += fmt.Sprintf("- [ ] %s\n", content) - default: - body += fmt.Sprintf("- [ ] %s\n", content) + case "webfetch": + if format, ok := toolInputMap["format"].(string); ok && result != nil { + body = *result + body = util.TruncateHeight(body, 10) + if format == "html" || format == "markdown" { + body = util.ToMarkdown(body, width, backgroundColor) } } - body = util.ToMarkdown(body, width, backgroundColor) - } - case "task": - summary := metadata.JSON.ExtraFields["summary"] - if !summary.IsNull() { - strValue := summary.Raw() - toolcalls := gjson.Parse(strValue).Array() - - steps := []string{} - for _, toolcall := range toolcalls { - call := toolcall.Value().(map[string]any) - if toolInvocation, ok := call["toolInvocation"].(map[string]any); ok { - data, _ := json.Marshal(toolInvocation) - var toolCall opencode.ToolInvocationPart - _ = json.Unmarshal(data, &toolCall) - - if metadata, ok := call["metadata"].(map[string]any); ok { - data, _ = json.Marshal(metadata) - var toolMetadata opencode.MessageMetadataTool - _ = json.Unmarshal(data, &toolMetadata) - - step := renderToolTitle(toolCall, messageMetadata, width) + case "todowrite": + todos := metadata["todos"] + if todos != nil { + for _, item := range todos.([]any) { + todo := item.(map[string]any) + content := todo["content"].(string) + switch todo["status"] { + case "completed": + body += fmt.Sprintf("- [x] %s\n", content) + // case "in-progress": + // body += fmt.Sprintf("- [ ] %s\n", content) + default: + body += fmt.Sprintf("- [ ] %s\n", content) + } + } + body = util.ToMarkdown(body, width, backgroundColor) + } + case "task": + summary := metadata["summary"] + if summary != nil { + toolcalls := summary.([]any) + steps := []string{} + for _, toolcall := range toolcalls { + call := toolcall.(map[string]any) + if toolInvocation, ok := call["toolInvocation"].(map[string]any); ok { + data, _ := json.Marshal(toolInvocation) + var toolCall opencode.ToolPart + _ = json.Unmarshal(data, &toolCall) + step := renderToolTitle(toolCall, width) step = "∟ " + step steps = append(steps, step) } } + body = strings.Join(steps, "\n") } - body = strings.Join(steps, "\n") - } - default: - if result == nil { - empty := "" - result = &empty + default: + if result == nil { + empty := "" + result = &empty + } + body = *result + body = util.TruncateHeight(body, 10) } - body = *result - body = util.TruncateHeight(body, 10) } error := "" - if err, ok := metadata.ExtraFields["error"].(bool); ok && err { - if message, ok := metadata.ExtraFields["message"].(string); ok { - error = message - } + if toolCall.State.Status == opencode.ToolPartStateStatusError { + error = toolCall.State.Error } if error != "" { @@ -494,7 +475,7 @@ func renderToolDetails( body = util.TruncateHeight(body, 10) } - title := renderToolTitle(toolCall, messageMetadata, width) + title := renderToolTitle(toolCall, width) content := title + "\n\n" + body return renderContentBlock(app, content, highlight, width, WithBorderColor(borderColor)) } @@ -515,20 +496,19 @@ func renderToolName(name string) string { } func renderToolTitle( - toolCall opencode.ToolInvocationPart, - messageMetadata opencode.MessageMetadata, + toolCall opencode.ToolPart, width int, ) string { // TODO: handle truncate to width - if toolCall.ToolInvocation.State == "partial-call" { - return renderToolAction(toolCall.ToolInvocation.ToolName) + if toolCall.State.Status == opencode.ToolPartStateStatusPending { + return renderToolAction(toolCall.Tool) } toolArgs := "" toolArgsMap := make(map[string]any) - if toolCall.ToolInvocation.Args != nil { - value := toolCall.ToolInvocation.Args + if toolCall.State.Input != nil { + value := toolCall.State.Input if m, ok := value.(map[string]any); ok { toolArgsMap = m @@ -546,8 +526,8 @@ func renderToolTitle( } } - title := renderToolName(toolCall.ToolInvocation.ToolName) - switch toolCall.ToolInvocation.ToolName { + title := renderToolName(toolCall.Tool) + switch toolCall.Tool { case "read": toolArgs = renderArgs(&toolArgsMap, "filePath") title = fmt.Sprintf("%s %s", title, toolArgs) @@ -565,7 +545,7 @@ func renderToolTitle( case "todowrite", "todoread": // title is just the tool name default: - toolName := renderToolName(toolCall.ToolInvocation.ToolName) + toolName := renderToolName(toolCall.Tool) title = fmt.Sprintf("%s %s", toolName, toolArgs) } return title @@ -645,8 +625,8 @@ type Diagnostic struct { } // renderDiagnostics formats LSP diagnostics for display in the TUI -func renderDiagnostics(metadata opencode.MessageMetadataTool, filePath string) string { - if diagnosticsData, ok := metadata.ExtraFields["diagnostics"].(map[string]any); ok { +func renderDiagnostics(metadata map[string]any, filePath string) string { + if diagnosticsData, ok := metadata["diagnostics"].(map[string]any); ok { if fileDiagnostics, ok := diagnosticsData[filePath].([]any); ok { var errorDiagnostics []string for _, diagInterface := range fileDiagnostics { diff --git a/packages/tui/internal/components/chat/messages.go b/packages/tui/internal/components/chat/messages.go index a59b5d79e..26bee8714 100644 --- a/packages/tui/internal/components/chat/messages.go +++ b/packages/tui/internal/components/chat/messages.go @@ -99,7 +99,7 @@ func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } } case opencode.EventListResponseEventMessageUpdated: - if msg.Properties.Info.Metadata.SessionID == m.app.Session.ID { + if msg.Properties.Info.SessionID == m.app.Session.ID { m.renderView(m.width) if m.tail { m.viewport.GotoBottom() @@ -124,19 +124,19 @@ func (m *messagesComponent) renderView(width int) { m.partCount = 0 m.lineCount = 0 - orphanedToolCalls := make([]opencode.ToolInvocationPart, 0) + orphanedToolCalls := make([]opencode.ToolPart, 0) for _, message := range m.app.Messages { var content string var cached bool - switch message.Role { - case opencode.MessageRoleUser: + switch casted := message.(type) { + case opencode.UserMessage: userLoop: - for partIndex, part := range message.Parts { + for partIndex, part := range casted.Parts { switch part := part.AsUnion().(type) { case opencode.TextPart: - remainingParts := message.Parts[partIndex+1:] + remainingParts := casted.Parts[partIndex+1:] fileParts := make([]opencode.FilePart, 0) for _, part := range remainingParts { switch part := part.AsUnion().(type) { @@ -150,7 +150,7 @@ func (m *messagesComponent) renderView(width int) { mediaTypeStyle := styles.NewStyle().Background(t.Secondary()).Foreground(t.BackgroundPanel()).Padding(0, 1) for _, filePart := range fileParts { mediaType := "" - switch filePart.MediaType { + switch filePart.Mime { case "text/plain": mediaType = "txt" case "image/png", "image/jpeg", "image/gif", "image/webp": @@ -175,7 +175,7 @@ func (m *messagesComponent) renderView(width int) { flexItems..., ) - key := m.cache.GenerateKey(message.ID, part.Text, width, m.selectedPart == m.partCount, files) + key := m.cache.GenerateKey(casted.ID, part.Text, width, m.selectedPart == m.partCount, files) content, cached = m.cache.Get(key) if !cached { content = renderText( @@ -199,21 +199,21 @@ func (m *messagesComponent) renderView(width int) { } } - case opencode.MessageRoleAssistant: + case opencode.AssistantMessage: hasTextPart := false - for partIndex, p := range message.Parts { + for partIndex, p := range casted.Parts { switch part := p.AsUnion().(type) { case opencode.TextPart: hasTextPart = true - finished := message.Metadata.Time.Completed > 0 - remainingParts := message.Parts[partIndex+1:] - toolCallParts := make([]opencode.ToolInvocationPart, 0) + finished := casted.Time.Completed > 0 + remainingParts := casted.Parts[partIndex+1:] + toolCallParts := make([]opencode.ToolPart, 0) // sometimes tool calls happen without an assistant message // these should be included in this assistant message as well if len(orphanedToolCalls) > 0 { toolCallParts = append(toolCallParts, orphanedToolCalls...) - orphanedToolCalls = make([]opencode.ToolInvocationPart, 0) + orphanedToolCalls = make([]opencode.ToolPart, 0) } remaining := true @@ -226,9 +226,9 @@ func (m *messagesComponent) renderView(width int) { // we only want tool calls associated with the current text part. // if we hit another text part, we're done. remaining = false - case opencode.ToolInvocationPart: + case opencode.ToolPart: toolCallParts = append(toolCallParts, part) - if part.ToolInvocation.State != "result" { + if part.State.Status != opencode.ToolPartStateStatusCompleted || part.State.Status != opencode.ToolPartStateStatusError { // i don't think there's a case where a tool call isn't in result state // and the message time is 0, but just in case finished = false @@ -237,14 +237,14 @@ func (m *messagesComponent) renderView(width int) { } if finished { - key := m.cache.GenerateKey(message.ID, p.Text, width, m.showToolDetails, m.selectedPart == m.partCount) + key := m.cache.GenerateKey(casted.ID, p.Text, width, m.showToolDetails, m.selectedPart == m.partCount) content, cached = m.cache.Get(key) if !cached { content = renderText( m.app, message, p.Text, - message.Metadata.Assistant.ModelID, + casted.ModelID, m.showToolDetails, m.partCount == m.selectedPart, width, @@ -258,7 +258,7 @@ func (m *messagesComponent) renderView(width int) { m.app, message, p.Text, - message.Metadata.Assistant.ModelID, + casted.ModelID, m.showToolDetails, m.partCount == m.selectedPart, width, @@ -270,7 +270,7 @@ func (m *messagesComponent) renderView(width int) { m = m.updateSelected(content, p.Text) blocks = append(blocks, content) } - case opencode.ToolInvocationPart: + case opencode.ToolPart: if !m.showToolDetails { if !hasTextPart { orphanedToolCalls = append(orphanedToolCalls, part) @@ -278,9 +278,9 @@ func (m *messagesComponent) renderView(width int) { continue } - if part.ToolInvocation.State == "result" { - key := m.cache.GenerateKey(message.ID, - part.ToolInvocation.ToolCallID, + if part.State.Status == opencode.ToolPartStateStatusCompleted || part.State.Status == opencode.ToolPartStateStatusError { + key := m.cache.GenerateKey(casted.ID, + part.ID, m.showToolDetails, width, m.partCount == m.selectedPart, @@ -290,7 +290,6 @@ func (m *messagesComponent) renderView(width int) { content = renderToolDetails( m.app, part, - message.Metadata, m.partCount == m.selectedPart, width, ) @@ -301,7 +300,6 @@ func (m *messagesComponent) renderView(width int) { content = renderToolDetails( m.app, part, - message.Metadata, m.partCount == m.selectedPart, width, ) @@ -315,14 +313,16 @@ func (m *messagesComponent) renderView(width int) { } error := "" - switch err := message.Metadata.Error.AsUnion().(type) { - case nil: - case opencode.MessageMetadataErrorMessageOutputLengthError: - error = "Message output length exceeded" - case opencode.ProviderAuthError: - error = err.Data.Message - case opencode.UnknownError: - error = err.Data.Message + if assistant, ok := message.(opencode.AssistantMessage); ok { + switch err := assistant.Error.AsUnion().(type) { + case nil: + case opencode.AssistantMessageErrorMessageOutputLengthError: + error = "Message output length exceeded" + case opencode.ProviderAuthError: + error = err.Data.Message + case opencode.UnknownError: + error = err.Data.Message + } } if error != "" { diff --git a/packages/tui/internal/components/status/status.go b/packages/tui/internal/components/status/status.go index 791267e0a..7d5820de3 100644 --- a/packages/tui/internal/components/status/status.go +++ b/packages/tui/internal/components/status/status.go @@ -6,6 +6,7 @@ import ( tea "github.com/charmbracelet/bubbletea/v2" "github.com/charmbracelet/lipgloss/v2" + "github.com/sst/opencode-sdk-go" "github.com/sst/opencode/internal/app" "github.com/sst/opencode/internal/styles" "github.com/sst/opencode/internal/theme" @@ -101,18 +102,20 @@ func (m statusComponent) View() string { contextWindow := m.app.Model.Limit.Context for _, message := range m.app.Messages { - cost += message.Metadata.Assistant.Cost - usage := message.Metadata.Assistant.Tokens - if usage.Output > 0 { - if message.Metadata.Assistant.Summary { - tokens = usage.Output - continue + if assistant, ok := message.(opencode.AssistantMessage); ok { + cost += assistant.Cost + usage := assistant.Tokens + if usage.Output > 0 { + if assistant.Summary { + tokens = usage.Output + continue + } + tokens = (usage.Input + + usage.Cache.Write + + usage.Cache.Read + + usage.Output + + usage.Reasoning) } - tokens = (usage.Input + - usage.Cache.Write + - usage.Cache.Read + - usage.Output + - usage.Reasoning) } } -- cgit v1.2.3