Python#
Python 3.10 or newer. Calls through the official OpenAI, Anthropic and Google GenAI clients are recorded automatically, sync and async, streaming included.
pip install alye
export ALYE_API_KEY=alye_live_...import alye
alye.init(service="support-reply", env="production")Node.js and TypeScript#
Node 20 or newer, ESM or CommonJS, types included. Wrap each client once where you create it.
npm install alyeimport * as alye from "alye";
import OpenAI from "openai";
alye.init({ service: "support-reply", env: "production" });
const openai = alye.wrap(new OpenAI());alye.wrap() works with openai (including Azure and OpenAI-compatible providers), @anthropic-ai/sdk and @google/genai.
Java#
Java 11 or newer, from Maven Central, with no other dependencies. Sync, async and streaming calls are recorded.
<dependency>
<groupId>ai.alye</groupId>
<artifactId>alye</artifactId>
<version>0.1.0</version>
</dependency>implementation("ai.alye:alye:0.1.0")implementation 'ai.alye:alye:0.1.0'Alye.init(AlyeOptions.builder()
.service("support-reply")
.env("production")
.build());
OpenAIClient openai = Alye.wrap(OpenAIOkHttpClient.fromEnv());
AnthropicClient claude = Alye.wrap(AnthropicOkHttpClient.fromEnv());Alye.wrap(client, "azure.ai.openai").LangChain#
Every call a LangChain chat model makes is recorded: chains, streaming, batches, tools, agents (LangGraph) and structured output. That covers every provider, including Bedrock, Gemini, Mistral, Groq and Ollama, and a call is never counted twice.
# Nothing extra: alye.init() covers every LangChain chat model.
alye.init(service="support-reply")
llm = ChatOpenAI(model="gpt-5-mini") # or ChatBedrockConverse, ChatMistralAI, ...import { ChatOpenAI } from "@langchain/openai";
// Wrap each model: ChatOpenAI, ChatAnthropic, ChatBedrockConverse, ChatGoogleGenerativeAI, ...
const llm = alye.wrapLangChain(new ChatOpenAI({ model: "gpt-5-mini" }));// LangChain4j: add the listener to any chat model
ChatModel model = OpenAiChatModel.builder()
.modelName("gpt-5-mini")
.listeners(List.of(new AlyeChatModelListener()))
.build();Python and Node.js: SDK 0.1.2 or newer for every provider (0.1.1 covers OpenAI, Azure OpenAI and Anthropic models).
Express#
Each request becomes one trace with its model calls inside. Requests that made no model calls aren't sent.
import { alyeExpress } from "alye/express";
app.use(alyeExpress({ tags: (req) => ({ customerId: req.header("x-customer-id") }) }));NestJS#
The module starts Alye and adds an interceptor for HTTP, GraphQL and microservice handlers. Call app.enableShutdownHooks() so queued calls are sent on shutdown.
import { AlyeModule } from "alye/nestjs";
@Module({ imports: [AlyeModule.forRoot({ service: "orders-api" })] })
export class AppModule {}Tag customers#
Optional. If your app serves many customers, tag each request with your own customer ID: an account or tenant ID, not a name or email.
with alye.context(customer_id="northwind", session_id=ticket.id):
answer = run_agent(ticket)await alye.context({ customerId: "northwind" }, () => runAgent(ticket));try (Scope s = Alye.context(Tags.customer("northwind").session(ticket.id()))) {
answer(ticket);
}Tags carry through every call made inside, so cost and errors show per customer.
Group steps into traces#
Wrap an agent run or a job in a span, and the model calls inside become its children, so one run shows as a single trace with its steps.
@alye.trace()
def answer_ticket(ticket): ...
with alye.span("retrieve_context", data_source="support-kb"):
docs = search(query)const answerTicket = alye.trace(async (ticket) => { /* ... */ }, "answer_ticket");
const docs = await alye.span("retrieve_context", () => search(query));String reply = Alye.span("answer_ticket", () -> agent.run(ticket));Other providers#
For a provider without a wrapper, record the call yourself with the token counts from its response.
with alye.llm_call("voyage", "voyage-3", operation="embeddings") as call:
r = vo.embed(texts, model="voyage-3")
call.set_usage(input_tokens=r.total_tokens, output_tokens=0)await alye.llmCall({ provider: "voyage", model: "voyage-3", operation: "embeddings" }, async (call) => {
const r = await vo.embed({ input: texts, model: "voyage-3" });
call.setUsage({ inputTokens: r.usage.total_tokens, outputTokens: 0 });
});try (LlmCall call = Alye.llmCall("voyage", "voyage-3", "embeddings")) {
EmbedResult r = voyage.embed(texts);
call.setUsage(r.totalTokens(), 0L);
}Alerts#
Alerts are set in the app and checked every minute. Watch spend, the monthly forecast, error rate, p95 latency, number of calls, or a service that stops reporting, for all services, each service, one model or one customer.
OpenTelemetry#
Already sending GenAI spans? Point your exporter at Alye. Use the JSON protocol.
OTEL_EXPORTER_OTLP_ENDPOINT=https://api.alye.ai/ingest
OTEL_EXPORTER_OTLP_PROTOCOL=http/json
OTEL_EXPORTER_OTLP_HEADERS="x-api-key=alye_live_..."Settings#
Every option can be passed to init() or set as an environment variable.
| Environment variable | What it does | Default |
|---|---|---|
| ALYE_API_KEY | Your SDK key | None: calls print to the console |
| ALYE_SERVICE | One name per microservice | unknown_service |
| ALYE_ENV | Environment | production |
| ALYE_CAPTURE_CONTENT | Send prompts and responses, masked (Python and Node.js) | Off |
| ALYE_REDACT_PII | Mask emails, phones, cards and keys | On |
| ALYE_SAMPLE_RATE | Share of traces to keep; failures always kept (Python and Node.js) | 1 |
| ALYE_REGION | Where calls run, for regional prices | Detected for Bedrock and Vertex AI |
| ALYE_DISABLED | Turn the SDK off | Off |
Privacy#
- Prompts and responses are not sent unless you turn on content capture. The Java SDK never sends them.
- When they are, emails, phone numbers, card numbers and API keys are masked inside your service before anything is sent. Add your own patterns if you need more.
- Content is cut to a maximum length, 8,000 characters by default.
Next steps
Using an AI coding assistant? Point it at alye.ai/llms.txt for setup steps in Python, Node.js and Java.