Last active
April 19, 2026 00:01
-
-
Save alexjsteffen/4af438766bea350ed4a524e6f7cc1717 to your computer and use it in GitHub Desktop.
A vibecoded python script to convert OpenRouter json output into pdf, odt, and a html file
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import json | |
| import subprocess | |
| import os | |
| import glob | |
| import html | |
| def clean_text_for_markdown(text): | |
| """Cleans up literal escape characters so LaTeX doesn't crash.""" | |
| if isinstance(text, str): | |
| return text.replace('\\n', '\n') | |
| return str(text) | |
| def parse_json_to_markdown(json_filepath, md_filepath, data): | |
| """Reads the JSON log and formats it into a clean Markdown file.""" | |
| trace = data.get('trace', {}) | |
| output_data = trace.get('output', {}) | |
| observations = data.get('observations', [{}])[0] if data.get('observations') else {} | |
| md_content = f"# OpenRouter Trace Log\n\n" | |
| md_content += f"**Trace ID:** {trace.get('id', 'N/A')}\n\n" | |
| md_content += f"**Timestamp:** {trace.get('timestamp', 'N/A')}\n\n" | |
| md_content += f"**Model:** {observations.get('model', 'N/A')}\n\n" | |
| md_content += "---\n\n" | |
| md_content += "## Input Messages\n\n" | |
| for msg in trace.get('input', {}).get('messages', []): | |
| role = msg.get('role', 'unknown').capitalize() | |
| raw_content = msg.get('content') | |
| content_text = "" | |
| # Robustly handle None, strings, and lists | |
| if isinstance(raw_content, str): | |
| content_text = raw_content + "\n" | |
| elif isinstance(raw_content, list): | |
| for block in raw_content: | |
| if isinstance(block, dict) and 'text' in block: | |
| content_text += block.get('text', '') + "\n" | |
| elif isinstance(block, str): | |
| content_text += block + "\n" | |
| md_content += f"### {role} Prompt\n\n" | |
| md_content += f"{clean_text_for_markdown(content_text.strip())}\n\n" | |
| md_content += "---\n\n" | |
| md_content += "## Model Output\n\n" | |
| if 'reasoning' in output_data: | |
| reasoning_clean = clean_text_for_markdown(output_data['reasoning']) | |
| md_content += f"### Reasoning\n\n" | |
| md_content += f"{reasoning_clean}\n\n" | |
| completion = output_data.get('completion', 'No completion data') | |
| if not isinstance(completion, str): | |
| completion = json.dumps(completion, indent=2) | |
| md_content += f"### Tool Call / Completion\n\n" | |
| md_content += f"```json\n{clean_text_for_markdown(completion)}\n```\n\n" | |
| md_content += "---\n\n" | |
| md_content += "## Usage Metrics\n\n" | |
| md_content += f"- **Total Tokens:** {observations.get('totalTokens', 'N/A')}\n" | |
| md_content += f"- **Total Cost:** ${observations.get('totalCost', 'N/A')}\n" | |
| with open(md_filepath, 'w', encoding='utf-8') as md_file: | |
| md_file.write(md_content) | |
| return True | |
| def generate_html_block(filename, data): | |
| """Generates the side-by-side HTML block for a single JSON file.""" | |
| trace = data.get('trace', {}) | |
| output_data = trace.get('output', {}) | |
| observations = data.get('observations', [{}])[0] if data.get('observations') else {} | |
| # Left Side: Raw JSON | |
| raw_json = html.escape(json.dumps(data, indent=2)) | |
| # Right Side: Formatted Content | |
| right_html = f"<h2>{html.escape(filename)}</h2>" | |
| right_html += f"<p><strong>Trace ID:</strong> {html.escape(str(trace.get('id', 'N/A')))}<br>" | |
| right_html += f"<strong>Model:</strong> {html.escape(str(observations.get('model', 'N/A')))}</p><hr>" | |
| right_html += "<h3>Input Messages</h3>" | |
| for msg in trace.get('input', {}).get('messages', []): | |
| role = msg.get('role', 'unknown').capitalize() | |
| raw_content = msg.get('content') | |
| content_text = "" | |
| # Robustly handle None, strings, and lists for HTML output too | |
| if isinstance(raw_content, str): | |
| content_text = raw_content + "\n" | |
| elif isinstance(raw_content, list): | |
| for block in raw_content: | |
| if isinstance(block, dict) and 'text' in block: | |
| content_text += block.get('text', '') + "\n" | |
| elif isinstance(block, str): | |
| content_text += block + "\n" | |
| right_html += f"<h4>{html.escape(role)}</h4>" | |
| right_html += f"<pre class='content-block'>{html.escape(content_text.strip())}</pre>" | |
| right_html += "<hr><h3>Model Output</h3>" | |
| if 'reasoning' in output_data: | |
| right_html += "<h4>Reasoning</h4>" | |
| right_html += f"<pre class='content-block'>{html.escape(str(output_data['reasoning']))}</pre>" | |
| completion = output_data.get('completion', 'No completion data') | |
| if not isinstance(completion, str): | |
| completion = json.dumps(completion, indent=2) | |
| right_html += "<h4>Completion</h4>" | |
| right_html += f"<pre class='content-block'>{html.escape(completion)}</pre>" | |
| return f""" | |
| <div class="log-entry"> | |
| <div class="left-pane"> | |
| <pre><code>{raw_json}</code></pre> | |
| </div> | |
| <div class="right-pane"> | |
| {right_html} | |
| </div> | |
| </div> | |
| """ | |
| def run_pandoc(md_filepath, output_pdf, output_odt): | |
| """Uses subprocess to call Pandoc and convert the Markdown file.""" | |
| print(f" -> Generating {output_odt}...") | |
| try: | |
| subprocess.run(['pandoc', md_filepath, '-o', output_odt, '--quiet'], check=True) | |
| except subprocess.CalledProcessError as e: | |
| print(f" -> Error creating ODT: {e}") | |
| print(f" -> Generating {output_pdf}...") | |
| try: | |
| subprocess.run([ | |
| 'pandoc', md_filepath, | |
| '-o', output_pdf, | |
| '--pdf-engine=xelatex', | |
| '--quiet', | |
| '-V', 'geometry:margin=1in', | |
| '-V', 'papersize:letter' | |
| ], check=True) | |
| except subprocess.CalledProcessError as e: | |
| print(f" -> Error creating PDF. (XeLaTeX might have hit another unescaped character): {e}") | |
| if __name__ == "__main__": | |
| target_folder = "." | |
| search_pattern = os.path.join(target_folder, '*.json') | |
| json_files = glob.glob(search_pattern) | |
| html_entries = [] | |
| if not json_files: | |
| print(f"No JSON files found in the directory: '{target_folder}'") | |
| else: | |
| print(f"Found {len(json_files)} JSON file(s). Starting conversion...\n") | |
| for json_path in json_files: | |
| base_filename = os.path.splitext(os.path.basename(json_path))[0] | |
| filename_only = os.path.basename(json_path) | |
| temp_markdown_file = os.path.join(target_folder, f"{base_filename}_temp.md") | |
| final_pdf = os.path.join(target_folder, f"{base_filename}.pdf") | |
| final_odt = os.path.join(target_folder, f"{base_filename}.odt") | |
| print(f"Processing: {filename_only}") | |
| try: | |
| with open(json_path, 'r', encoding='utf-8') as file: | |
| data = json.load(file) | |
| except Exception as e: | |
| print(f" -> Error loading {filename_only}: {e}") | |
| continue | |
| if parse_json_to_markdown(json_path, temp_markdown_file, data): | |
| run_pandoc(temp_markdown_file, final_pdf, final_odt) | |
| if os.path.exists(temp_markdown_file): | |
| os.remove(temp_markdown_file) | |
| html_entries.append(generate_html_block(filename_only, data)) | |
| print("-" * 30) | |
| dashboard_html = f""" | |
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>JSON Trace Dashboard</title> | |
| <style> | |
| body {{ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background-color: #f9fafb; margin: 0; padding: 20px; }} | |
| h1 {{ text-align: center; color: #333; }} | |
| .search-container {{ display: flex; justify-content: center; margin-bottom: 30px; position: sticky; top: 10px; z-index: 100; }} | |
| #searchInput {{ width: 60%; padding: 15px; font-size: 16px; border: 2px solid #cbd5e1; border-radius: 8px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); outline: none; }} | |
| #searchInput:focus {{ border-color: #3b82f6; }} | |
| .log-entry {{ display: flex; background: white; margin-bottom: 30px; border-radius: 10px; box-shadow: 0 4px 15px rgba(0,0,0,0.05); overflow: hidden; height: 80vh; }} | |
| .left-pane, .right-pane {{ width: 50%; padding: 20px; overflow-y: auto; box-sizing: border-box; }} | |
| .left-pane {{ background-color: #1e1e1e; color: #d4d4d4; border-right: 2px solid #e2e8f0; }} | |
| .right-pane {{ background-color: #ffffff; color: #333; }} | |
| pre {{ white-space: pre-wrap; word-wrap: break-word; font-family: 'Courier New', Courier, monospace; font-size: 13px; }} | |
| .content-block {{ background-color: #f1f5f9; padding: 15px; border-radius: 6px; border: 1px solid #e2e8f0; }} | |
| h2, h3, h4 {{ margin-top: 0; color: #1e293b; }} | |
| </style> | |
| </head> | |
| <body> | |
| <h1>Log Trace Dashboard</h1> | |
| <div class="search-container"> | |
| <input type="text" id="searchInput" onkeyup="filterEntries()" placeholder="Search through logs, traces, or reasoning..."> | |
| </div> | |
| <div id="logContainer"> | |
| {"".join(html_entries)} | |
| </div> | |
| <script> | |
| function filterEntries() {{ | |
| let input = document.getElementById('searchInput').value.toLowerCase(); | |
| let entries = document.getElementsByClassName('log-entry'); | |
| for (let i = 0; i < entries.length; i++) {{ | |
| let text = entries[i].innerText.toLowerCase(); | |
| entries[i].style.display = text.includes(input) ? "flex" : "none"; | |
| }} | |
| }} | |
| </script> | |
| </body> | |
| </html> | |
| """ | |
| html_filename = "all_traces_dashboard.html" | |
| with open(html_filename, 'w', encoding='utf-8') as f: | |
| f.write(dashboard_html) | |
| print(f"All conversions complete! Dashboard generated at: {html_filename}") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment