Bot API 9.4, Views System, OpenTelemetry & streamMessage
February 8 – 15, 2026
An absolutely packed week. GramIO catches up to Bot API 9.4 with 8 new contexts, ships a brand-new views/template system, launches OpenTelemetry and Sentry plugins, adds an onApiCall hook for API instrumentation, introduces streamMessage for live-typing drafts, brings button styling to keyboards, and delivers Node.js support for SQLite storage plus Bun-native Redis. Let's go.
gramio v0.4.14 — onApiCall Hook & Better Stack Traces
New hook: onApiCall for API call instrumentation
The 7th hook joins the family. onApiCall wraps the entire API call lifecycle, enabling tracing, logging, metrics — anything you want around every outgoing Telegram request. It works like middleware with next():
bot.onApiCall(async (context, next) => {
console.log(`→ ${context.method}`);
const start = Date.now();
const result = await next();
console.log(`← ${context.method} (${Date.now() - start}ms)`);
return result;
});You can scope it to specific methods:
bot.onApiCall("sendMessage", async (context, next) => {
// only fires for sendMessage calls
return next();
});Multiple hooks compose like middleware — the first registered wraps everything. Works in both Bot and Plugin. This is what powers the new OpenTelemetry plugin under the hood.
Error stack traces now point to your code
TelegramError now captures the call site where you made the API call. When bot.api.sendMessage(...) fails, the stack trace points to your line of code, not framework internals. No config needed — it just works.
Dependencies
@gramio/contextsbumped to^0.4.0@gramio/keyboardsbumped to^1.3.0@gramio/typesbumped to^9.4.0
@gramio/contexts v0.4.0 — Bot API 9.2 / 9.3 / 9.4
ea3049f aa3ff6d 3df81eb 524ba8b 13564e6
streamMessage — live-typing message drafts
Stream text chunks to the chat with live typing previews. Each chunk updates a draft in real-time via sendMessageDraft, and the message auto-finalizes via sendMessage when a 4096-character segment completes. Perfect for AI/LLM streaming responses:
bot.command("stream", async (context) => {
const chunks = generateTextChunks(); // Iterable or AsyncIterable
const messages = await context.streamMessage(chunks);
});Accepts Iterable<MessageDraftPiece> or AsyncIterable<MessageDraftPiece> where each piece is a string or { text, entities?, draft_id? }. Supports AbortSignal for cancellation.
Bot API 9.2 — Suggested posts & direct messages
8 new contexts and structures for the suggested posts lifecycle:
SuggestedPostApprovedContext,SuggestedPostApprovalFailedContext,SuggestedPostDeclinedContext,SuggestedPostPaidContext,SuggestedPostRefundedContextDirectMessagesTopicstructureChat.isDirectMessages,ChatFullInfo.parentChat,ChatAdministratorRights.canManageDirectMessagesGift.publisherChat,UniqueGift.publisherChatMessage.suggestedPostInfo,Message.directMessagesTopic,Message.isPaidPost
Bot API 9.3 — Gift upgrades, new structures
GiftUpgradeSentContext— service message about gift upgradessendMessageDraft()method on SendMixin- New structures:
GiftBackground,UniqueGiftColors,UserRating - Extended
GiftwithisPremium,background,uniqueGiftVariantCount - Extended
ChatFullInfowithrating,uniqueGiftColors,paidMessageStarCount
Bot API 9.4 — Chat ownership, video quality, profile audios
ChatOwnerLeftContext,ChatOwnerChangedContext— track chat ownership changesVideoQualitystructure withcodec("h265"|"av01")UserProfileAudiosstructure- Extended
VideoAttachmentwithqualitiesgetter - Extended
UniqueGiftModelwithrarity("uncommon"|"rare"|"epic"|"legendary") - Extended
UniqueGiftwithisBurned - Extended
UserwithallowsUsersToCreateTopics()
@gramio/views — Template System for Reusable Message Views (NEW)
4de789b cd3b934 944a5fb c7521ae b2f917b
A brand-new package for building reusable message templates with automatic send/edit strategy detection. Define views once, render them anywhere — the library figures out whether to send a new message or edit the existing one based on the context type.
Programmatic views
import { initViewsBuilder } from "@gramio/views";
import { defineAdapter } from "@gramio/views/define";
const adapter = defineAdapter({
welcome(name: string) {
return this.response
.text(`Hello, ${name}!`)
.keyboard([[{ text: "Start", callback_data: "start" }]]);
},
});
const defineView = initViewsBuilder().from(adapter);
bot.derive(["message", "callback_query"], (context) => ({
render: defineView.buildRender(context, {}),
}));
bot.command("start", (context) => context.render("welcome", "Alice"));JSON-driven views
Define views as JSON with interpolation for text, keyboards, and media:
import { createJsonAdapter } from "@gramio/views/json";
const adapter = createJsonAdapter({
views: {
welcome: {
text: "Hello, {{name}}!",
reply_markup: {
inline_keyboard: [
[{ text: "Profile {{name}}", callback_data: "profile_{{id}}" }],
],
},
},
},
});Filesystem loading
import { loadJsonViewsDir } from "@gramio/views/fs";
// views/messages.json → "messages.welcome", "messages.goodbye"
// views/goods/products.json → "goods.products.list"
const views = await loadJsonViewsDir("./views");i18n support
Two approaches: adapter factory for per-locale JSON files, or custom resolve callback for translation keys:
// Per-locale adapter selection
const defineView = initViewsBuilder<{ locale: string }>()
.from((globals) => adapters[globals.locale]);
// Or custom resolve for translation keys
const adapter = createJsonAdapter({
views: { greet: { text: "{{t:hello}}, {{name}}!" } },
resolve: (key, globals) => {
if (key.startsWith("t:")) return globals.t(key.slice(2));
},
});Supports all keyboard types (inline, reply, remove, force reply), single and grouped media with URL interpolation, and globals access via syntax.
@gramio/opentelemetry — Distributed Tracing (NEW)
Vendor-neutral distributed tracing for GramIO using OpenTelemetry API. Every update becomes a root span, every API call becomes a child span — zero config, works with any OTEL backend (Jaeger, Grafana, Axiom, etc.).
import { opentelemetryPlugin } from "@gramio/opentelemetry";
bot.extend(opentelemetryPlugin({
recordApiParams: true, // record API params as span attributes
}));Trace hierarchy:
gramio.update.message (CONSUMER)
├── telegram.api/sendMessage (CLIENT)
├── telegram.api/deleteMessage (CLIENT)
└── custom spans via record()Exported utilities: record(name, fn) for custom child spans, getCurrentSpan(), setAttributes(). Integrates seamlessly with Elysia webhooks — GramIO spans automatically nest under HTTP request spans.
@gramio/sentry — Error Tracking (NEW)
Sentry integration with automatic error capture, user identification, breadcrumbs, and optional tracing:
import { sentryPlugin } from "@gramio/sentry";
bot.extend(sentryPlugin({
setUser: true, // auto-set user from context.from
breadcrumbs: true, // breadcrumb per update + API call
tracing: false, // per-update isolation scopes + spans
}));
// Derived context methods:
bot.command("test", (context) => {
context.sentry.captureMessage("Something happened");
context.sentry.setTag("custom", "value");
});Uses @sentry/core for runtime-agnostic support (works in both Bun and Node.js).
@gramio/keyboards v1.3.0 — Button Styling
All button methods now accept an optional options parameter for visual styling:
new InlineKeyboard()
.text("Delete", "delete", { style: "danger" })
.text("Confirm", "confirm", {
style: "success",
icon_custom_emoji_id: "5368324170671202286",
});Three styles: "danger" (red), "primary" (blue), "success" (green). Plus icon_custom_emoji_id for custom emoji icons next to button text. Works on both InlineKeyboard and Keyboard.
@gramio/types v9.4.0
New types for Bot API 9.4: VideoQuality, UserProfileAudios, ChatOwnerLeft, ChatOwnerChanged, UniqueGiftModelRarity. Button styling types (KeyboardButtonStyle, InlineKeyboardButtonStyle) are now official. New API methods: getUserProfileAudios, setMyProfilePhoto, removeMyProfilePhoto.
@gramio/storage-sqlite v1.0.0 — Now Works on Node.js!
The SQLite adapter is no longer Bun-only! It now has dual runtime exports:
- Bun: uses
bun:sqlite(unchanged) - Node.js: uses
node:sqlitewithDatabaseSync
The correct implementation is auto-selected based on your runtime — no code changes needed.
@gramio/storage-redis — Bun Native Redis
The Redis adapter now supports Bun's built-in RedisClient alongside ioredis:
// Auto-selected on Bun — no ioredis needed
import { redisStorage } from "@gramio/storage-redis";
const storage = redisStorage({ url: "redis://localhost:6379" });Dual exports: @gramio/storage-redis (auto-detects runtime), @gramio/storage-redis/ioredis (explicit), @gramio/storage-redis/bun (explicit). The ioredis peer dependency is now optional.
@gramio/test — API Mocking & Chat Simulation
onApi / offApi for mocking API responses
import { apiError } from "@gramio/test";
// Static response
env.onApi("sendMessage", { message_id: 1, chat: { id: 1 }, ... });
// Dynamic handler
env.onApi("getChat", (params) => {
if (params.chat_id === 123) return chatData;
return apiError(400, "Chat not found");
});
// Simulate errors with retry_after
env.onApi("sendMessage", apiError(429, "Too Many Requests", { retry_after: 30 }));
env.offApi("sendMessage"); // reset single
env.offApi(); // reset allChat objects and user interactions
const chat = env.createChat();
const user = env.createUser({ first_name: "Alice" });
await user.join(chat); // emits chat_member + new_chat_members
await user.sendMessage(chat, "Hello!"); // emits message in chat
await user.click("button_data"); // emits callback_query
await user.leave(chat); // emits chat_member + left_chat_memberAll API calls are recorded in env.apiCalls for assertions.