logicgaze
v1.6.0
Published
LogicGaze SDK — AI-native observability for LLM applications
Maintainers
Readme
logicgaze
Node.js / TypeScript SDK for LogicGaze — AI-native observability for LLM applications.
Zero runtime dependencies. Works with OpenAI, Anthropic, and any provider.
Installation
npm install logicgaze
# or
pnpm add logicgaze
# or
yarn add logicgazeRequires Node.js ≥ 18. Current version: 1.6.0
Quick Start
import { init, traceable, wrapOpenAI } from 'logicgaze'
import OpenAI from 'openai'
// 1. Initialise once at app startup
init({
apiKey: 'lgz-...', // or set LOGICGAZE_API_KEY env var
baseUrl: 'https://your-app.com/api/v1', // or set LOGICGAZE_BASE_URL
})
// 2. Wrap your AI client
const openai = wrapOpenAI(new OpenAI())
// 3. Wrap your function — every call is automatically traced
const answer = traceable(async (question: string) => {
const res = await openai.chat.completions.create({
model: 'gpt-4o',
messages: [{ role: 'user', content: question }],
})
return res.choices[0].message.content
}, { serviceName: 'qa-service' })
// 4. Call it normally — tracing is invisible
const result = await answer('What is the capital of France?')Environment Variables
| Variable | Default | Description |
|---|---|---|
| LOGICGAZE_API_KEY | — | API key (required) |
| LOGICGAZE_BASE_URL | http://localhost:8000/api/v1 | Backend URL |
| LOGICGAZE_SAMPLE_RATE | 1.0 | Sampling rate 0.0–1.0 |
| LOGICGAZE_MAX_RETRIES | 3 | Retry attempts on 429/5xx |
| LOGICGAZE_BATCH_SIZE | 50 | Events per ingestion batch |
| LOGICGAZE_FLUSH_INTERVAL | 5000 | Flush interval (ms) |
| LOGICGAZE_DEBUG | — | Set to 1 for verbose logs |
API
init(options)
Initialise the global client. Call once at startup.
import { init } from 'logicgaze'
init({
apiKey: 'lgz-...',
baseUrl: 'https://your-app.com/api/v1',
sampleRate: 0.5, // trace 50% of calls
maxRetries: 3,
debug: true,
// Masking — control what LogicGaze stores for span inputs/outputs
hideInputs: false, // true → store [REDACTED] instead of input messages
hideOutputs: false, // true → store [REDACTED] instead of output content
mask: (data) => scrubPII(data), // custom transform; if it throws, [MASKING_ERROR] is stored
})traceable(fn, options?)
Wrap any async function. Creates a trace for each invocation, propagates context to all wrapped LLM calls inside.
import { traceable } from 'logicgaze'
const myPipeline = traceable(async (input: string) => {
// All wrapOpenAI / wrapAnthropic calls here auto-attach spans
return processInput(input)
}, {
serviceName: 'my-pipeline',
sessionId: 'session-abc',
userId: 'user-123',
tags: { env: 'production' },
})wrapOpenAI(client, options?)
Patch an OpenAI client to record LLM spans inside active traces.
import OpenAI from 'openai'
import { wrapOpenAI } from 'logicgaze'
const openai = wrapOpenAI(new OpenAI(), { serviceName: 'gpt-4-service' })
// openai.chat.completions.create() now auto-records spanswrapAnthropic(client, options?)
import Anthropic from '@anthropic-ai/sdk'
import { wrapAnthropic } from 'logicgaze'
const anthropic = wrapAnthropic(new Anthropic())
// anthropic.messages.create() now auto-records spansStreaming
stream: true calls through wrapped clients are fully instrumented — the wrapped
async iterable is a drop-in replacement:
const stream = await openai.chat.completions.create({
model: 'gpt-4o',
messages: [{ role: 'user', content: 'Write a haiku' }],
stream: true,
})
for await (const chunk of stream) {
process.stdout.write(chunk.choices[0]?.delta?.content ?? '')
}- Content deltas are accumulated into the span output
- Token usage is captured — for OpenAI,
stream_options: { include_usage: true }is auto-injected when absent; for Anthropic, usage is read frommessage_start/message_deltaevents time_to_first_token_msis recorded in span metadata- The span is recorded when the stream completes, errors, or is terminated early — bookkeeping can never break your stream loop
withTrace(fn, context)
Manually scope a block of code to a trace (useful when you can't use traceable).
import { withTrace } from 'logicgaze'
import { randomUUID } from 'crypto'
await withTrace(async () => {
// All wrapped LLM calls here attach to this trace
const res = await openai.chat.completions.create({ ... })
}, {
traceId: randomUUID(),
serviceName: 'my-service',
sessionId: req.session.id,
userId: req.user.id,
})Scores & Feedback
import { getClient } from 'logicgaze'
const client = getClient()
// Data type inferred: number → NUMERIC, boolean → BOOLEAN, stringValue → CATEGORICAL
await client.logScore(traceId, 'relevance', 0.92)
await client.logScore(traceId, 'is_correct', true)
await client.logScore(traceId, 'tone', undefined, { stringValue: 'formal' })
await client.logScore(traceId, 'faithfulness', 0.8, { spanId, comment: 'minor gap' })
// End-user feedback: 'thumbs_up' | 'thumbs_down' | 'rating' | 'free_form'
await client.logFeedback(traceId, { feedbackType: 'thumbs_up', score: 1 })
await client.logFeedback(traceId, { feedbackType: 'rating', score: 4, comment: 'Helpful' })Datasets
const dataset = await client.createDataset('qa-regression', {
description: 'Golden QA test cases',
tags: ['qa'],
})
await client.addDatasetItem(dataset.id, { question: 'What is RAG?' }, {
expectedOutput: { answer: '...' },
metadata: { source: 'manual' },
})
const items = await client.getDatasetItems(dataset.id, 1, 50)
const all = await client.listDatasets()Prompt Management
// Create (or version-bump — the server auto-increments versions)
await client.createPrompt('welcome-email', 'Hi {{name}}, welcome to {{product}}!', {
tags: ['onboarding'],
config: { model: 'gpt-4o' },
})
// Fetch with a client-side stale-while-revalidate TTL cache (default 60s)
const prompt = await client.getPrompt('welcome-email')
const pinned = await client.getPrompt('welcome-email', { version: 2, cacheTtlSeconds: 300 })
const text = prompt.compile({ name: 'Ada', product: 'LogicGaze' })
// "Hi Ada, welcome to LogicGaze!" — unknown {{placeholders}} are left intactFresh cache entries are served with no network call; stale entries are served immediately and revalidated in the background — network errors keep serving the stale prompt.
Manual Tracing
For full control:
import { getClient } from 'logicgaze'
const client = getClient()
// Create trace
const trace = await client.startTrace({ serviceName: 'my-service' })
// Record a span directly
await client.createSpan(trace.traceId, {
spanType: 'llm',
name: 'GPT-4 call',
model: 'gpt-4o',
provider: 'openai',
promptTokens: 100,
completionTokens: 50,
durationMs: 1234,
})
// Finish trace
await client.endTrace(trace.traceId)observe (alias)
observe is an alias for traceable for LangSmith / Laminar compatibility:
import { observe } from 'logicgaze'
const myFn = observe(async () => { ... })Context Accessors
Inside a traceable / withTrace context, you can read the current trace:
import { getCurrentTraceId, getCurrentSessionId } from 'logicgaze'
const traceId = getCurrentTraceId() // string | null
const sessionId = getCurrentSessionId() // string | undefinedError Handling
All errors are typed:
import {
LogicGazeError,
AuthenticationError,
RateLimitError,
ServerError,
NetworkError,
TimeoutError,
ConfigurationError,
} from 'logicgaze'
try {
await client.startTrace({ ... })
} catch (err) {
if (err instanceof RateLimitError) {
console.log(`Rate limited. Retry after: ${err.retryAfter}s`)
} else if (err instanceof AuthenticationError) {
console.error('Invalid API key')
}
}Shutdown & Flushing
The background queue is unref-ed so it won't prevent Node from exiting, and the
client automatically flushes queued spans on process beforeExit — short-lived
scripts and serverless functions lose nothing without extra code.
For explicit control (e.g. before process.exit(), which skips beforeExit):
import { getClient } from 'logicgaze'
await getClient().flush() // send everything queued right now
getClient().shutdown() // stop the background queueLicense
MIT
