-
Notifications
You must be signed in to change notification settings - Fork 242
Copilot/update parser log javascript #17605
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,7 +1,7 @@ | ||||||||||||||||
| // @ts-check | ||||||||||||||||
| /// <reference types="@actions/github-script" /> | ||||||||||||||||
|
|
||||||||||||||||
| const { createEngineLogParser } = require("./log_parser_shared.cjs"); | ||||||||||||||||
| const { createEngineLogParser, generateConversationMarkdown, generateInformationSection, formatInitializationSummary, formatToolUse } = require("./log_parser_shared.cjs"); | ||||||||||||||||
|
|
||||||||||||||||
| const main = createEngineLogParser({ | ||||||||||||||||
| parserName: "Gemini", | ||||||||||||||||
|
|
@@ -10,8 +10,13 @@ const main = createEngineLogParser({ | |||||||||||||||
| }); | ||||||||||||||||
|
|
||||||||||||||||
| /** | ||||||||||||||||
| * Parse Gemini CLI streaming JSON log output and format as markdown. | ||||||||||||||||
| * Gemini CLI outputs one JSON object per line when using --output-format stream-json (JSONL). | ||||||||||||||||
| * Parse Gemini CLI JSONL log output and format as markdown. | ||||||||||||||||
| * Gemini CLI outputs one JSON object per line (JSONL) with typed entries: | ||||||||||||||||
| * - type "init": session initialization with model and session_id | ||||||||||||||||
| * - type "message": user/assistant messages, assistant uses delta:true for streaming chunks | ||||||||||||||||
| * - type "tool_use": tool invocations with tool_name, tool_id, and parameters | ||||||||||||||||
| * - type "tool_result": tool responses with tool_id, status, and output | ||||||||||||||||
| * - type "result": final stats with token usage, duration, and tool call count | ||||||||||||||||
| * @param {string} logContent - The raw log content to parse | ||||||||||||||||
| * @returns {{markdown: string, logEntries: Array, mcpFailures: Array<string>, maxTurnsHit: boolean}} Parsed log data | ||||||||||||||||
| */ | ||||||||||||||||
|
|
@@ -25,74 +30,161 @@ function parseGeminiLog(logContent) { | |||||||||||||||
| }; | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| let markdown = ""; | ||||||||||||||||
| let totalInputTokens = 0; | ||||||||||||||||
| let totalOutputTokens = 0; | ||||||||||||||||
| let lastResponse = ""; | ||||||||||||||||
|
|
||||||||||||||||
| const lines = logContent.split("\n"); | ||||||||||||||||
| for (const line of lines) { | ||||||||||||||||
| // Parse JSONL lines | ||||||||||||||||
| /** @type {Array<any>} */ | ||||||||||||||||
| const rawEntries = []; | ||||||||||||||||
| for (const line of logContent.split("\n")) { | ||||||||||||||||
| const trimmed = line.trim(); | ||||||||||||||||
| if (!trimmed) { | ||||||||||||||||
| if (!trimmed || !trimmed.startsWith("{")) { | ||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice defensive check — filtering lines that don't start with |
||||||||||||||||
| continue; | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| // Try to parse each line as a JSON object (Gemini --output-format json output) | ||||||||||||||||
| try { | ||||||||||||||||
| const parsed = JSON.parse(trimmed); | ||||||||||||||||
|
|
||||||||||||||||
| if (parsed.response) { | ||||||||||||||||
| lastResponse = parsed.response; | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| // Aggregate token usage from stats | ||||||||||||||||
| if (parsed.stats && parsed.stats.models) { | ||||||||||||||||
| for (const modelStats of Object.values(parsed.stats.models)) { | ||||||||||||||||
| if (modelStats && typeof modelStats === "object") { | ||||||||||||||||
| if (typeof modelStats.input_tokens === "number") { | ||||||||||||||||
| totalInputTokens += modelStats.input_tokens; | ||||||||||||||||
| } | ||||||||||||||||
| if (typeof modelStats.output_tokens === "number") { | ||||||||||||||||
| totalOutputTokens += modelStats.output_tokens; | ||||||||||||||||
| } | ||||||||||||||||
| } | ||||||||||||||||
| } | ||||||||||||||||
| } | ||||||||||||||||
| rawEntries.push(JSON.parse(trimmed)); | ||||||||||||||||
| } catch (_e) { | ||||||||||||||||
| // Not JSON - skip non-JSON lines | ||||||||||||||||
| // Skip non-JSON lines | ||||||||||||||||
| } | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| // Build markdown output | ||||||||||||||||
| if (lastResponse) { | ||||||||||||||||
| markdown += "## 🤖 Reasoning\n\n"; | ||||||||||||||||
| markdown += lastResponse + "\n\n"; | ||||||||||||||||
| if (rawEntries.length === 0) { | ||||||||||||||||
| return { | ||||||||||||||||
| markdown: "## 🤖 Gemini\n\nLog format not recognized as Gemini JSONL.\n\n", | ||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Providing a friendly error message when the log format isn't recognized is a great UX touch. Makes debugging much easier when a user accidentally points this at a non-Gemini log file. ✨ |
||||||||||||||||
| logEntries: [], | ||||||||||||||||
| mcpFailures: [], | ||||||||||||||||
| maxTurnsHit: false, | ||||||||||||||||
| }; | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| markdown += "## 📊 Information\n\n"; | ||||||||||||||||
| const totalTokens = totalInputTokens + totalOutputTokens; | ||||||||||||||||
| if (totalTokens > 0) { | ||||||||||||||||
| markdown += `**Total Tokens Used:** ${totalTokens.toLocaleString()}\n\n`; | ||||||||||||||||
| if (totalInputTokens > 0) { | ||||||||||||||||
| markdown += `**Input Tokens:** ${totalInputTokens.toLocaleString()}\n\n`; | ||||||||||||||||
| } | ||||||||||||||||
| if (totalOutputTokens > 0) { | ||||||||||||||||
| markdown += `**Output Tokens:** ${totalOutputTokens.toLocaleString()}\n\n`; | ||||||||||||||||
| } | ||||||||||||||||
| // Transform Gemini JSONL entries into canonical logEntries format | ||||||||||||||||
| const logEntries = transformGeminiEntries(rawEntries); | ||||||||||||||||
|
|
||||||||||||||||
| // Extract the final result entry for stats | ||||||||||||||||
| const resultEntry = rawEntries.find(e => e.type === "result"); | ||||||||||||||||
|
|
||||||||||||||||
| // Generate conversation markdown using shared function | ||||||||||||||||
| const conversationResult = generateConversationMarkdown(logEntries, { | ||||||||||||||||
| formatToolCallback: (toolUse, toolResult) => formatToolUse(toolUse, toolResult, { includeDetailedParameters: false }), | ||||||||||||||||
| formatInitCallback: initEntry => formatInitializationSummary(initEntry, { includeSlashCommands: false }), | ||||||||||||||||
| }); | ||||||||||||||||
|
|
||||||||||||||||
| let markdown = conversationResult.markdown; | ||||||||||||||||
|
|
||||||||||||||||
| // Add Information section using Gemini-specific stats from the result entry | ||||||||||||||||
| if (resultEntry && resultEntry.stats) { | ||||||||||||||||
| const stats = resultEntry.stats; | ||||||||||||||||
| const syntheticEntry = { | ||||||||||||||||
| usage: { | ||||||||||||||||
| input_tokens: stats.input_tokens || 0, | ||||||||||||||||
| output_tokens: stats.output_tokens || 0, | ||||||||||||||||
| cache_read_input_tokens: stats.cached || 0, | ||||||||||||||||
| }, | ||||||||||||||||
| duration_ms: stats.duration_ms || 0, | ||||||||||||||||
| num_turns: stats.tool_calls || 0, | ||||||||||||||||
|
||||||||||||||||
| num_turns: stats.tool_calls || 0, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The streaming delta merging logic here correctly handles Gemini's chunked assistant messages. The guard on entry.message.content.length === 1 ensures only single-content entries are merged, preventing accidental data loss. Consider adding a comment clarifying why multi-content entries are excluded from merging.
Copilot
AI
Feb 21, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When raw.output is not a string, JSON.stringify(raw.output || "") will incorrectly coerce valid falsy outputs like 0, false, or "" into "", and undefined becomes "\"\"". Use a nullish check (e.g., raw.output == null ? "" : JSON.stringify(raw.output)) so primitives are preserved and missing output stays empty.
| const output = typeof raw.output === "string" ? raw.output : JSON.stringify(raw.output || ""); | |
| const output = | |
| typeof raw.output === "string" | |
| ? raw.output | |
| : raw.output == null | |
| ? "" | |
| : JSON.stringify(raw.output); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good refactoring — importing
generateConversationMarkdown,generateInformationSection,formatInitializationSummary, andformatToolUsefrom the shared module eliminates duplicated formatting logic and ensures consistent output across all engine parsers.