@grafana/sigil-sdk-js
v0.5.0
Published
Sigil records normalized LLM generation and tool-execution telemetry using your OpenTelemetry tracer/meter setup.
Readme
Grafana Sigil TypeScript/JavaScript SDK
Sigil records normalized LLM generation and tool-execution telemetry using your OpenTelemetry tracer/meter setup.
Installation
pnpm add @grafana/sigil-sdk-jsFor a Grafana Cloud setup walkthrough (where to find the endpoint URL, instance ID, and API token), refer to the Grafana Cloud setup guide.
Validation
Run the shared core conformance suite for the JavaScript SDK from the repo root:
mise run test:ts:sdk-conformanceRun the cross-language aggregate core conformance suite from the repo root:
mise run sdk:conformanceQuick Start
The snippet below configures the SDK explicitly. As an alternative, set SIGIL_* environment variables and call new SigilClient() with no arguments — refer to the Grafana Cloud setup guide for the variable names.
import { SigilClient } from "@grafana/sigil-sdk-js";
const client = new SigilClient({
generationExport: {
protocol: "http",
endpoint: "http://localhost:8080",
auth: { mode: "tenant", tenantId: "dev-tenant" },
},
api: {
endpoint: "http://localhost:8080",
},
});
await client.startGeneration(
{
conversationId: "conv-1",
model: { provider: "openai", name: "gpt-5" },
},
async (recorder) => {
const outputText = "Hello from model";
recorder.setResult({
output: [{ role: "assistant", content: outputText }],
});
}
);
await client.shutdown();Pre-Ingest Redaction
Use generationSanitizer when you want to redact substrings from normalized generations before
validation, span sync, debug snapshots, and export.
import {
SigilClient,
createSecretRedactionSanitizer,
} from "@grafana/sigil-sdk-js";
const client = new SigilClient({
generationSanitizer: createSecretRedactionSanitizer({
redactInputMessages: false,
redactEmailAddresses: true,
}),
});The built-in sanitizer:
- redacts high-confidence secret formats in assistant text and thinking
- redacts secret formats plus env-style secret values in tool call inputs and tool results
- redacts email addresses by default
- leaves user input unchanged unless
redactInputMessages: trueis set
To preserve email addresses, opt out explicitly:
const client = new SigilClient({
generationSanitizer: createSecretRedactionSanitizer({
redactEmailAddresses: false,
}),
});Configure OTEL exporters (traces/metrics) in your application OTEL SDK setup. You can optionally pass tracer and meter directly to SigilClient.
Quick OTEL setup pattern before creating the Sigil client:
import { NodeSDK } from "@opentelemetry/sdk-node";
const otel = new NodeSDK();
await otel.start();Core API
startGeneration(...)andstartStreamingGeneration(...)startToolExecution(...)- Recorder methods:
setResult(...),setCallError(...),end(),getError() - Lifecycle:
flush(),shutdown()
Manual try/finally style
const recorder = client.startGeneration({
model: { provider: "anthropic", name: "claude-sonnet-4-5" },
});
try {
recorder.setResult({
output: [{ role: "assistant", content: "Done" }],
});
} catch (error) {
recorder.setCallError(error);
throw error;
} finally {
recorder.end();
}Embedding Observability
Use startEmbedding(...) for embedding API calls. Embedding recording creates OTel spans and SDK metrics only, and does not enqueue generation exports.
await client.startEmbedding(
{
agentName: "retrieval-worker",
agentVersion: "1.0.0",
model: { provider: "openai", name: "text-embedding-3-small" },
},
async (recorder) => {
const response = await openai.embeddings.create(request);
recorder.setResult({
inputCount: request.input.length,
inputTokens: response.usage?.prompt_tokens ?? 0,
inputTexts: request.input,
responseModel: response.model,
});
}
);Input text capture is opt-in:
const client = new SigilClient({
embeddingCapture: {
captureInput: true,
maxInputItems: 20,
maxTextLength: 1024,
},
});embeddingCapture.captureInput may expose PII/document content in spans. Keep it disabled by default and enable it only for scoped debugging.
TraceQL examples:
traces{gen_ai.operation.name="embeddings"}traces{gen_ai.operation.name="embeddings" && gen_ai.request.model="text-embedding-3-small"}traces{gen_ai.operation.name="embeddings" && error.type!=""}
Tool Execution Example
await client.startToolExecution(
{
toolName: "weather",
includeContent: true,
},
async (recorder) => {
recorder.setResult({
arguments: { city: "Paris" },
result: { temp_c: 18 },
});
}
);Provider Helpers
- OpenAI:
docs/providers/openai.md - Anthropic:
docs/providers/anthropic.md - Gemini:
docs/providers/gemini.md
Framework Handlers
Use module subpath exports for framework callback integrations:
- LangChain:
@grafana/sigil-sdk-js/langchain - LangGraph:
@grafana/sigil-sdk-js/langgraph - OpenAI Agents:
@grafana/sigil-sdk-js/openai-agents - LlamaIndex:
@grafana/sigil-sdk-js/llamaindex - Google ADK:
@grafana/sigil-sdk-js/google-adk - Vercel AI SDK:
@grafana/sigil-sdk-js/vercel-ai-sdk - Strands Agents:
@grafana/sigil-sdk-js/strands - LangChain guide:
docs/frameworks/langchain.md - LangGraph guide:
docs/frameworks/langgraph.md - OpenAI Agents guide:
docs/frameworks/openai-agents.md - LlamaIndex guide:
docs/frameworks/llamaindex.md - Google ADK guide:
docs/frameworks/google-adk.md - Vercel AI SDK guide:
docs/frameworks/vercel-ai-sdk.md - Strands Agents guide:
docs/frameworks/strands.md
import { SigilClient } from "@grafana/sigil-sdk-js";
import { withSigilLangChainCallbacks } from "@grafana/sigil-sdk-js/langchain";
import { withSigilLangGraphCallbacks } from "@grafana/sigil-sdk-js/langgraph";
import { withSigilOpenAIAgentsHooks } from "@grafana/sigil-sdk-js/openai-agents";
import { withSigilLlamaIndexCallbacks } from "@grafana/sigil-sdk-js/llamaindex";
import { withSigilGoogleAdkPlugins } from "@grafana/sigil-sdk-js/google-adk";
import { createSigilVercelAiSdk } from "@grafana/sigil-sdk-js/vercel-ai-sdk";
import { withSigilStrandsHooks } from "@grafana/sigil-sdk-js/strands";
import { Runner } from "@openai/agents";
import { CallbackManager } from "llamaindex";
const client = new SigilClient();
const langChainConfig = withSigilLangChainCallbacks(undefined, client, { providerResolver: "auto" });
const langGraphConfig = withSigilLangGraphCallbacks(undefined, client, { providerResolver: "auto" });
const runner = new Runner();
const openAIAgentsHooks = withSigilOpenAIAgentsHooks(runner, client, { providerResolver: "auto" });
const callbackManager = new CallbackManager();
const llamaIndexConfig = withSigilLlamaIndexCallbacks({ callbackManager }, client, { providerResolver: "auto" });
const googleAdkRunnerConfig = withSigilGoogleAdkPlugins(undefined, client, { providerResolver: "auto" });
const vercelAiSdk = createSigilVercelAiSdk(client, { agentName: "vercel-agent" });
const strandsConfig = withSigilStrandsHooks(undefined, client, { conversationId: "chat-123" });Framework handlers use the SigilClient instance you pass in. If that client is configured with
generationSanitizer, the same redaction policy applies automatically to generations recorded
through LangChain, LangGraph, OpenAI Agents, LlamaIndex, Google ADK, and Vercel AI SDK integrations.
The same redaction policy also applies to Strands Agents generations.
Each framework handler injects:
sigil.framework.name(langchain,langgraph,openai-agents,llamaindex,google-adk,vercel-ai-sdk, orstrands)sigil.framework.source(handlerfor existing callback handlers,frameworkfor Vercel AI SDK hooks,hooksfor Strands)sigil.framework.language(javascriptfor existing callback handlers,typescriptfor Vercel AI SDK and Strands hooks)metadata["sigil.framework.run_id"]metadata["sigil.framework.thread_id"](when present)metadata["sigil.framework.parent_run_id"](when available)metadata["sigil.framework.component_name"]metadata["sigil.framework.run_type"]metadata["sigil.framework.tags"]metadata["sigil.framework.retry_attempt"](when available)metadata["sigil.framework.event_id"](when available)metadata["sigil.framework.langgraph.node"](LangGraph when available)
Conversation mapping is conversation-first:
conversation_id/session_id/group_idfrom framework context first- then
thread_id - deterministic fallback
sigil:framework:<framework_name>:<run_id>
When present in generation metadata, low-cardinality framework keys are copied onto generation span attributes.
For LangGraph persistence, pass configurable.thread_id and reuse it across invocations:
const threadConfig = {
...withSigilLangGraphCallbacks(undefined, client, { providerResolver: "auto" }),
configurable: { thread_id: 'customer-42' },
};
await graph.invoke({ prompt: 'Remember my timezone is UTC+1.', answer: '' }, threadConfig);
await graph.invoke({ prompt: 'What timezone did I give you?', answer: '' }, threadConfig);Behavior
- Generation modes are explicit:
SYNCandSTREAM. - Generation export supports HTTP, gRPC, and
none(instrumentation-only). - Traces/metrics use
config.tracer/config.meterwhen provided, otherwise OTEL globals. - Exports are asynchronous with bounded queueing and retry/backoff.
flush()drains queued generations;shutdown()flushes and closes generation exporters.- Empty tool names produce a no-op tool recorder.
- Generation/tool spans always include SDK identity attributes:
sigil.sdk.name=sdk-js
- Normalized generation metadata always includes the same SDK identity key; conflicting caller values are overwritten.
- Raw provider artifacts are opt-in (
rawArtifacts: true).
Instrumentation-only mode (no generation send)
Set generationExport.protocol to "none" to keep generation/tool instrumentation and spans while disabling generation transport.
const client = new SigilClient({
generationExport: {
protocol: "none",
},
});SDK metrics
The SDK emits these OTel histograms through your configured OTEL meter provider:
gen_ai.client.operation.durationgen_ai.client.token.usagegen_ai.client.time_to_first_tokengen_ai.client.tool_calls_per_operation
Generation export auth modes
Auth is configured for generationExport.
mode: "none"mode: "tenant"(requirestenantId, injectsX-Scope-OrgID)mode: "bearer"(requiresbearerToken, injectsAuthorization: Bearer <token>)mode: "basic"(requiresbasicPassword+basicUserortenantId, injectsAuthorization: Basic <base64(user:password)>; also injectsX-Scope-OrgIDwhentenantIdis set — for multi-tenant deployments only, not needed for Grafana Cloud)
Invalid mode/field combinations throw during client config resolution.
If explicit headers already contain Authorization or X-Scope-OrgID, explicit headers take precedence.
const client = new SigilClient({
generationExport: {
protocol: "http",
endpoint: "http://localhost:8080",
auth: { mode: "tenant", tenantId: "prod-tenant" },
},
api: {
endpoint: "http://localhost:8080",
},
});Grafana Cloud auth (basic)
For Grafana Cloud, use basic auth mode. The username is your Grafana Cloud instance/tenant ID and the password is your Grafana Cloud API key. See the Grafana Cloud AI Observability getting started docs for full setup steps; for this SDK endpoint, copy the API URL from Observability → AI Observability → Configuration. It looks like https://sigil-prod-<region>.grafana.net.
const client = new SigilClient({
generationExport: {
protocol: "http",
endpoint: "https://sigil-prod-<region>.grafana.net",
auth: {
mode: "basic",
tenantId: process.env.SIGIL_AUTH_TENANT_ID,
basicPassword: process.env.SIGIL_AUTH_TOKEN,
},
},
});If your deployment requires a distinct username, set basicUser explicitly:
auth: {
mode: "basic",
tenantId: process.env.SIGIL_AUTH_TENANT_ID,
basicUser: process.env.SIGIL_AUTH_TENANT_ID,
basicPassword: process.env.SIGIL_AUTH_TOKEN,
},Env-secret wiring example
The SDK does not auto-load env vars. Resolve env secrets in your app and map them into config.
const generationBearerToken = (process.env.SIGIL_GEN_BEARER_TOKEN ?? "").trim();
const client = new SigilClient({
generationExport: {
protocol: "http",
endpoint: "http://localhost:8080",
auth:
generationBearerToken.length > 0
? { mode: "bearer", bearerToken: generationBearerToken }
: { mode: "tenant", tenantId: "dev-tenant" },
},
api: {
endpoint: "http://localhost:8080",
},
});Common topology:
- Grafana Cloud: generation
basicmode with instance ID and API key. - Self-hosted direct to Sigil: generation
tenantmode. - Traces/metrics via OTEL Collector/Alloy: configure exporters in your app OTEL SDK setup.
- Enterprise proxy: generation
bearermode to proxy; proxy authenticates and forwards tenant header upstream.
Conversation Ratings
Use the SDK helper to submit user-facing ratings:
const result = await client.submitConversationRating("conv-123", {
ratingId: "rat-123",
rating: "CONVERSATION_RATING_VALUE_BAD",
comment: "Answer ignored user context",
metadata: { channel: "assistant-ui" },
source: "sdk-js",
});
console.log(result.rating.rating, result.summary.hasBadRating);submitConversationRating sends requests to api.endpoint (default http://localhost:8080) and uses the same generation-export auth headers (tenant or bearer) already configured on the SDK client.
