lemmy LLM Client Library and Tools
repository·main·Indexed 23 days ago
https://github.com/badlogic/lemmyA TypeScript API wrapper for multiple LLM providers. The ecosystem includes lemmy-chat for interactive and one-shot messaging with a TUI, claude-bridge for using non-Anthropic models with Claude Code via API interception, and claude-trace for recording and indexing Claude Code sessions. It also provides @mariozechner/lemmy-tools for file system, shell, and Model Context Protocol (MCP) integration.
What's inside lemmy
- Lemmy is a TypeScript monorepo ecosystem designed for building AI applications. It provides a unified interface for multiple LLM providers (Anthropic Claude, OpenAI, and Google Gemini), a rich terminal UI framework with differential rendering, and development tools for monitoring AI interactions. It also supports MCP (Model Context Protocol) integration and provides a bridge to use alternative LLMs with Claude-specific tools.
Core Architecture and Design Principles
mainThe Lemmy architecture is built around several key abstractions and principles:
- Provider Abstraction: Uses a
ChatClientinterface to unify different LLM providers. - Tool Definitions: Tools are defined using Zod schemas for type-safe validation.
- Streaming Support: Supports streaming responses, including specialized handling for thinking/reasoning blocks.
- Context Management: Built-in mechanisms for managing conversation state.
- Type Safety: The project enforces strict typing throughout to avoid the use of
any.
- Provider Abstraction: Uses a
Use multiple LLM providers with the same context
mainBecause
Contextis provider-agnostic, you can switch between different LLM providers (Anthropic, OpenAI, Google) mid-conversation while maintaining the same conversation history and tool definitions.// Switch providers mid-conversation const openai = lemmy.openai({ apiKey: "sk-...", model: "gpt-4o", }); const google = lemmy.google({ apiKey: "...", model: "gemini-1.5-pro", }); // Same context works across all providers await claude.ask("Start a story", { context }); await openai.ask("Continue the story", { context }); await google.ask("End the story", { context });How differential rendering works
mainDifferential rendering optimizes performance by only updating parts of the screen that have changed.
Components return a result object:
{lines: string[], changed: boolean, keepLines?: number}.lines: The full set of lines the component should display.changed: A boolean indicating if the content has changed.keepLines: (For Containers) The number of lines from the top that remain unchanged.
The TUI calculates the total unchanged lines, moves the cursor up by the difference, clears the remaining area using
\x1b[0J, and prints only the new lines. Note: Do not add extra cursor positioning after printing, as it interferes with terminal scrolling.Package Structure and Modules
mainThe monorepo is organized into the following key packages and applications:
@packages/lemmy: The core LLM wrapper library.@packages/lemmy-tui: Terminal UI components.@packages/lemmy-tools: A collection of built-in and custom tools.@packages/lemmy-cli-args: CLI argument parsing utilities.apps/: Contains example applications such aschat,bridge, andtrace.
Understand claude-trace request filtering
mainBy default,
claude-tracefilters logs to focus on substantial conversations to reduce file size and noise:- Default behavior: Only logs requests to
/v1/messageswith more than 2 messages in the conversation. - With
--include-all-requests: Logs all requests made toapi.anthropic.com, including single-message requests and other endpoints.
- Default behavior: Only logs requests to
Manage conversation context with Context
mainUse the
Contextclass to maintain conversation history and system messages across multiple turns. TheContextobject also automatically tracks usage costs, which can be retrieved via.getTotalCost().// Maintain context across multiple messages const context = new Context(); context.setSystemMessage("You are a helpful coding assistant."); await claude.ask("My name is Alice", { context }); const result = await claude.ask("What's my name?", { context }); // "Your name is Alice" // Track costs automatically console.log(`Total cost: $${context.getTotalCost()}`);How claude-bridge works
mainClaude Code is designed to work only with Anthropic models.
claude-bridgeenables other models by performing the following steps:- Spawn: It starts Claude Code as a subprocess using a custom Node.js loader.
- Patch: It patches the global
fetch()function to intercept requests sent toapi.anthropic.com/v1/messages. - Transform: It converts Anthropic-formatted requests into a unified
lemmyformat, then into the specific provider's API format. - Stream: It converts the provider's response back into Anthropic's Server-Sent Events (SSE) format and streams it back to Claude Code.
Understand claude-bridge limitations
mainBecause
claude-bridgeintercepts and transforms requests, some features are not fully supported:Completely Broken:
- Token usage/cost reporting: Claude Code's displays will show incorrect data.
- Image uploads: Drag/drop, pasting, or file paths will not work as they rely on Anthropic's servers.
- Input caching: Prompt caching is not implemented.
- Web search/fetch tools: These rely on Anthropic-specific logic.
Somewhat Janky:
- Model-specific features: Features like Claude's 'artifacts' or GPT's reasoning modes may not translate.
- Thinking/reasoning output: Formatting may differ between providers.
- Error messages: Provider-specific auth failures might result in cryptic errors.
- Tool schemas: Conversion between JSON Schema and Zod is used and usually works, but can occasionally fail.
- Streaming behavior: Subtle differences may exist despite SSE conversion.
Spawning Claude via Node.js
mainWhen running Claude with interception or extracting tokens, the
claude-traceapp uses a specific spawning pattern. Instead of executing the path directly, it uses Node.js to execute the file, which allows it to handle symlinks that point to JavaScript files.Pattern:
spawn("node", ["--require", loaderPath, claudePath, ...claudeArgs])Warning: This pattern requires
claudePathto be a valid JavaScript file (or a symlink to one). IfclaudePathpoints to a bash wrapper script, thespawncommand will fail with error code-1because Node.js cannot execute bash scripts.Understand claude-trace log file naming and location
mainWhen running the
claude-traceapp, logs are stored in a.claude-trace/directory. The filenames are generated using a timestamp based on the current ISO date, where colons, dots, and the 'T' separator are replaced with hyphens.Log files follow the pattern:
log-YYYY-MM-DD-HH-MM-SS.Two types of log files are produced:
.jsonlfiles: Machine-readable JSON Lines format..htmlfiles: Human-readable HTML format.
Note: While the console output may display a placeholder pattern like
log-YYYY-MM-DD-HH-MM-SS.{jsonl,html}, the actual files will contain the specific timestamp of the session.// Example of how the timestamp is constructed internally const timestamp = new Date().toISOString().replace(/[:.]/g, "-").replace("T", "-").slice(0, -5); this.logFile = path.join(this.logDir, `log-${timestamp}.jsonl`); this.htmlFile = path.join(this.logDir, `log-${timestamp}.html`);Develop claude-bridge
mainTo develop
claude-bridge, clone thelemmyrepository and set up the monorepo environment.Setup
git clone https://github.com/badlogic/lemmy cd lemmy && npm install && npm run devRunning
npm run devstarts compilation in watch mode for all packages and apps.Testing
Use the following commands to run specific test suites:
npm run test:all: All tests.npm run test:unit: Unit tests.npm run test:core: CLI functionality.npm run test:tools: Tool integration.npm run test:providers: Multi-provider tests.
Debugging in VS Code
To debug using a JavaScript Debug Terminal, use the
--patch-claudeflag:npx tsx src/cli.ts <arguments> --patch-claude