@aseity/lyra
Lyra is a TypeScript library for running tool-using agents across OpenAI, Anthropic, and compatible model providers. It includes streaming, sessions, handoffs, structured outputs, tracing, MCP clients, and sandbox capabilities.
Changelog
See the release history
for changes in each version. CHANGELOG.md is also included in the npm package.
Install
pnpm add @aseity/lyra zod
Quick start
import { Agent, OpenAIProvider, run } from '@aseity/lyra';
const provider = new OpenAIProvider({
apiKey: process.env.OPENAI_API_KEY,
});
const model = await provider.getModel('gpt-5.4-mini');
const agent = new Agent({
name: 'Assistant',
instructions: 'Answer clearly and concisely.',
model,
});
const result = await run(agent, 'Hello!');
console.log(result.finalOutput);
Pass concrete Model instances when model selection is owned by your
application. Use Runner when you need reusable workflow, tracing, or session
configuration.
See tool input schemas and question approval for strict optional parameters, filesystem defaults, and restoring user answers.
Xiaomi MiMo
import { MiMoProvider, MIMO_MODEL_NAMES } from '@aseity/lyra';
const provider = new MiMoProvider(); // reads MIMO_API_KEY
const pro = await provider.getModel(); // mimo-v2.6-pro
const flash = await provider.getModel('mimo-v2.6-flash');
// MIMO_MODEL_NAMES contains the two built-in model names.
MiMo uses the Anthropic Messages API
at https://api.xiaomimimo.com/anthropic. Pass apiKey, baseURL, or
defaultModel to override the defaults; MIMO_DEFAULT_MODEL also sets the
default model. Token Plan users can supply their plan's Anthropic base URL and key.
Both built-in models have a 1M-token context window and a 131,072-token output
limit, with image input, streaming, tool calls, and thinking replay.
Thinking follows the service default. Use modelSettings.reasoning.effort: 'none'
to disable it, or modelSettings.providerData.thinking: { type: 'enabled' }
to enable it explicitly. MiMo has no reasoning effort levels. In thinking mode,
the service fixes temperature and top-p to its own defaults.
See the model list
and thinking guide.
Command Code
import { CommandCodeProvider } from '@aseity/lyra';
const provider = new CommandCodeProvider(); // COMMAND_CODE_API_KEY
const model = await provider.getModel(); // gpt-6-luna
Claude uses Messages; GPT and google/gemini-3.8-flash use Responses.
Other Claude/GPT IDs can pass through without inheriting verified capabilities.
Other Gemini models are rejected. See configuration, parameter limits and live
acceptance, including Gemini's numeric enum Schema limit.
DeepSeek Search API
Use DeepSeekSearchClient to call the standalone Search API from a local function
tool. It works independently of the Agent's model provider. The application owns
credentials, shared rate limiting, retries, and cancellation deadlines. See the
client contract.
DeepSeek hosted web search
Add webSearch() from @aseity/lyra to an agent's tools when using
DeepSeekProvider. The model decides whether to search; no parameters or local
executor are needed. Only DeepSeek Messages supports this declaration.
See hosted web search for normalized search records,
citations, streaming events, and persistent Session replay.
DeepSeek Responses JSON output
DeepSeekProvider continues to use Anthropic Messages. For JSON Schema output,
pass the separately exported DeepSeekResponsesModel directly to an agent:
import { Agent, DeepSeekResponsesModel, run } from '@aseity/lyra';
import { z } from 'zod';
const agent = new Agent({
name: 'Extractor',
model: new DeepSeekResponsesModel({
model: 'deepseek-v4-pro',
apiKey: process.env.DEEPSEEK_API_KEY,
}),
instructions: 'Extract the name and age from the input.',
outputType: z.object({ name: z.string(), age: z.number() }),
});
const result = await run(agent, 'Alice is 30 years old.');
console.log(result.finalOutput);
The model sends outputType as text.format.type: 'json_schema'. It defaults to
https://api.deepseek.com and uses the configured DeepSeek key when apiKey is
omitted. Its baseURL is independent of DEEPSEEK_BASE_URL, which belongs to the
Anthropic provider. You can supply fetch, maxRetries, and metadata, or inject
an openAIClient instead of client configuration.
Streaming is supported. modelSettings.maxTokens overrides the default 32,768
output tokens; reasoning effort supports low, high, max, and none/null
to disable thinking. Omission preserves the server's thinking default.
Responses are stateless: send full history, not conversation or previous-response
IDs. See the DeepSeek Responses reference.
To run the non-streaming and streaming live checks with DEEPSEEK_API_KEY set:
bun packages/lyra/test/manual/deepseek-responses.ts (makes two paid API calls).
OpenRouter
OpenRouterProvider uses OpenRouter's Chat Completions endpoint while
preserving OpenRouter routing metadata, cost data, structured reasoning, and
in-band stream errors:
import { OpenRouterProvider } from '@aseity/lyra';
const provider = new OpenRouterProvider({
apiKey: process.env.OPENROUTER_API_KEY,
httpReferer: 'https://example.com',
appTitle: 'Example Agent',
defaultRequest: {
provider: { require_parameters: true },
},
});
const model = await provider.getModel('anthropic/claude-sonnet-4.6');
Per-request OpenRouter extensions such as provider, models, plugins,
reasoning, and session_id can also be passed through
modelSettings.providerData.
License
MIT
Model parameter compatibility
See model parameter transport for modern effort support, legacy thinking budgets, explicit output validation, and migration from aliased effort values. Provider/model support is distinct from transport support.
MCP
Connect stdio, Streamable HTTP or SSE MCP servers and expose their tools to any Lyra tool-capable model provider. See MCP clients for connection management, authentication, filtering, resources and upstream compatibility.
Image validation
view_image validates inline PNG/JPEG/WebP/GIF bytes before native image results
or vision fallback requests, even when compression is disabled. It performs a
strict Sharp decode, including all animation frames; GIF block boundaries are
also checked because the decoder tolerates some truncated GIFs. Invalid images
return toolError text without image content, so the model can regenerate the
file and retry within the same turn. Source files are never repaired or rewritten.
A provider's later HTTP 400 still ends the turn; local validation does not guarantee
provider acceptance and does not add model retries. Remote image references from
custom sandboxes cannot be locally validated and return a tool error.
Byte and dimension limits are checked before full decoding. Host ceilings are 32 MiB per input, 8000 pixels per side and 64 million pixels summed across all frames; stricter model limits also apply. At most two decodes run concurrently per process. Validation uses streaming pixel statistics instead of returning a full raw pixel buffer. Queued calls retain their bounded input bytes, so callers must still bound tool-call concurrency. Sharp loads lazily on the first image. Static compression doubles as validation instead of decoding the image twice.
Offline regression: pnpm --filter @aseity/lyra exec vitest run test/view-image-validation.test.ts.
Set LYRA_TEST_DOCKER_IMAGE to an existing image with bash to include real Docker
Runner recovery. bun packages/lyra/test/manual/image-validation.ts measures four
concurrent calls at roughly 1, 16 and 64 million pixels; optional file arguments
replay local failures without contacting a model. Local 64-million-pixel runs took
6.8 seconds for four calls with sampled process RSS peaking near 232 MiB; these
measurements depend on codecs, pixels, runtime, and machine, and are not a memory guarantee.
Optional view_image compression
view_image preserves original image bytes by default. To reduce inline image payloads:
filesystem({ viewImageCompression: true })
This applies to both native vision and viewImageCaptioner requests. Static inline
images are encoded as JPEG at quality 90 with 4:4:4 chroma, without downscaling;
EXIF orientation is applied and transparent backgrounds become white. If encoding
would increase the payload, the original is retained. Animated images retain their original bytes after validation. Source files are never overwritten; returned
MIME type, byte size and dimensions describe the actual transmitted image.
Validation and compression share the installed sharp dependency.
Existing input and model image limits still apply. This reduces individual image
payloads but does not limit accumulated conversation size or guarantee avoidance
of HTTP 413 responses. Lossy compression can affect fine text; retain original
files for detailed crops and verification.
Docker lifecycle
Docker startup failures are cleaned up before the error returns. If cleanup also fails, catch DockerSandboxStartupError, retain the execution-resource reservation, and retry error.cleanup() after Docker recovers. See Docker startup and cleanup for the error contract, cancellation, and shutdown behavior.
See explicit skill input for manual selection, discovery flags, original message display and frozen model history.
Inline file references preserve their position in the user's message:
await runner.run(agent, [{
type: 'message',
role: 'user',
content: [
{ type: 'input_text', text: 'Read ' },
{ type: 'input_file_ref', name: 'notes.md', path: '/workspace/notes.md' },
{ type: 'input_text', text: ' and summarize it.' },
],
}]);
The host must make the file available at path before running the agent. Lyra
projects the reference to @notes.md and appends a reminder with its workspace
path and reading guidance in model and compaction inputs. Ambiguous same-name
references keep their paths inline instead, without repeating them at the end.
Session items retain the original parts. See file references. userMessageText displays
@notes.md; the exported InputFileRef schema contains only type, name, and
path, without a host-specific library ID.
See workspace file edits for structured input_file_edits
notifications, optional diffs, and model projection with original history preserved.
See per-run ephemeral context for frozen context snapshots that appear in model requests without entering history or compaction.
See background incremental summaries for frozen request inputs, configurable summary templates, and caller-scheduled memory updates that preserve the model prefix without blocking subsequent turns.
See model input reminders for the public wrapping and escaping API, host ownership, and migration, and uploads for reference-only file and image projection.
Application operation tracing
Import withTrace and withCustomSpan from @aseity/lyra to trace application
work around model calls. They share the processors configured by
setTraceProcessors and respect setTracingDisabled. withTrace(name, work, { groupId, metadata }) starts a separate trace. Within its callback,
withCustomSpan(work, { data: { name, data } }) adds a timed child span, and
model calls with tracing: true or tracing: 'enabled_without_data' join that
trace. ModelTracing is also exported as a type.
Custom span data is exported as supplied. The application must omit private payloads when its sensitive-data setting is off. A custom span records errors thrown by its callback; catch inside the callback and set a redacted span error when the original exception contains private data.
Non-streaming generation errors and Runner Agent errors retain the exception name when sensitive-data tracing is disabled. The original exception still reaches the caller.
Anthropic and Anthropic-compatible streaming generation spans record the
assembled model output at response_done, including text, reasoning and tool
calls. They record the model and final usage even when payload capture is off.
The output uses Lyra's normalized model items, with private replay metadata
removed, rather than the empty initial message_start payload.
Product thinking effort
Set modelSettings: { thinkingEffort: 'low' } to opt into Lyra's four-level
none | low | high | max intent. Verified direct providers map it before requests;
unknown channels attempt standard protocol fields. Native reasoning.effort
remains available separately. Catch ThinkingEffortUnsupportedError (code
THINKING_EFFORT_UNSUPPORTED) for explicit thinking-parameter rejection.
See the contract, provider mappings and examples.