Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/blaze-plugin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@livekit/agents-plugin-blaze": patch
---

Fix Blaze plugin review issues and align LLM request route/query params with the Blaze voicebot-call API.
43 changes: 43 additions & 0 deletions plugins/blaze/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# @livekit/agents-plugin-blaze

LiveKit Agent Framework plugin for Blaze AI services:

- **STT (Speech-to-Text)**: `POST /v1/stt/transcribe` (batch only)
- **TTS (Text-to-Speech)**: `POST /v1/tts/realtime` (streaming PCM)
- **LLM (Conversational AI)**: `POST /voicebot/{botId}/chat-conversion?stream=true` (SSE streaming)

## Install

```bash
npm i @livekit/agents-plugin-blaze
```

## Quick start

```ts
import { STT, TTS, LLM } from '@livekit/agents-plugin-blaze';

// Reads BLAZE_* env vars by default
const stt = new STT({ language: 'vi' });
const tts = new TTS({ speakerId: 'speaker-1' });
const llm = new LLM({ botId: 'my-chatbot-id' });
```

## Environment variables

```bash
# Required for authenticated deployments
export BLAZE_API_URL=https://api.blaze.vn
export BLAZE_API_TOKEN=your-bearer-token

# Optional timeouts
export BLAZE_STT_TIMEOUT=30000
export BLAZE_TTS_TIMEOUT=60000
export BLAZE_LLM_TIMEOUT=60000
```

## Notes

- STT streaming is **not** supported (the plugin throws if `stream()` is called).
- LLM supports SSE streaming; `system/developer` messages are converted into user context as `"[System]: ..."`.

5 changes: 5 additions & 0 deletions plugins/blaze/api-extractor.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json",
"extends": "../../api-extractor-shared.json",
"mainEntryPointFilePath": "./dist/index.d.ts"
}
49 changes: 49 additions & 0 deletions plugins/blaze/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
{
"name": "@livekit/agents-plugin-blaze",
"version": "0.1.0",
"description": "Blaze AI plugin for LiveKit Node Agents (STT, TTS, LLM)",
"main": "dist/index.js",
"require": "dist/index.cjs",
"types": "dist/index.d.ts",
"exports": {
"import": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"require": {
"types": "./dist/index.d.cts",
"default": "./dist/index.cjs"
}
},
"author": "LiveKit",
"type": "module",
"repository": "git@github.com:livekit/agents-js.git",
"license": "Apache-2.0",
"files": [
"dist",
"src",
"README.md"
],
"scripts": {
"build": "tsup --onSuccess \"pnpm build:types\"",
"build:types": "tsc --declaration --emitDeclarationOnly && node ../../scripts/copyDeclarationOutput.js",
"clean": "rm -rf dist",
"clean:build": "pnpm clean && pnpm build",
"lint": "eslint -f unix \"src/**/*.{ts,js}\"",
"api:check": "api-extractor run --typescript-compiler-folder ../../node_modules/typescript",
"api:update": "api-extractor run --local --typescript-compiler-folder ../../node_modules/typescript --verbose"
},
"devDependencies": {
"@livekit/agents": "workspace:*",
"@livekit/agents-plugins-test": "workspace:*",
"@livekit/rtc-node": "catalog:",
"@microsoft/api-extractor": "^7.35.0",
"tsup": "^8.3.5",
"typescript": "^5.0.0"
},
"dependencies": {},
"peerDependencies": {
"@livekit/agents": "workspace:*",
"@livekit/rtc-node": "catalog:"
}
}
81 changes: 81 additions & 0 deletions plugins/blaze/src/config.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// SPDX-FileCopyrightText: 2025 LiveKit, Inc.
//
// SPDX-License-Identifier: Apache-2.0

import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { buildAuthHeaders, resolveConfig } from './config.js';

describe('resolveConfig', () => {
beforeEach(() => {
// Clear env vars before each test
delete process.env.BLAZE_API_URL;
delete process.env.BLAZE_API_TOKEN;
delete process.env.BLAZE_STT_TIMEOUT;
delete process.env.BLAZE_TTS_TIMEOUT;
delete process.env.BLAZE_LLM_TIMEOUT;
});

afterEach(() => {
delete process.env.BLAZE_API_URL;
delete process.env.BLAZE_API_TOKEN;
});

it('uses defaults when no config or env vars provided', () => {
const cfg = resolveConfig();
expect(cfg.apiUrl).toBe('https://api.blaze.vn');
expect(cfg.authToken).toBe('');
expect(cfg.sttTimeout).toBe(30000);
expect(cfg.ttsTimeout).toBe(60000);
expect(cfg.llmTimeout).toBe(60000);
});

it('uses env vars when provided', () => {
process.env.BLAZE_API_URL = 'http://api.example.com';
process.env.BLAZE_API_TOKEN = 'test-token';
const cfg = resolveConfig();
expect(cfg.apiUrl).toBe('http://api.example.com');
expect(cfg.authToken).toBe('test-token');
});

it('config values override env vars', () => {
process.env.BLAZE_API_URL = 'http://env.example.com';
const cfg = resolveConfig({ apiUrl: 'http://config.example.com' });
expect(cfg.apiUrl).toBe('http://config.example.com');
});

it('timeout env vars are parsed as numbers', () => {
process.env.BLAZE_STT_TIMEOUT = '15000';
process.env.BLAZE_TTS_TIMEOUT = '45000';
const cfg = resolveConfig();
expect(cfg.sttTimeout).toBe(15000);
expect(cfg.ttsTimeout).toBe(45000);
});

it('falls back to default timeout when env var is not a valid number', () => {
process.env.BLAZE_STT_TIMEOUT = 'abc';
process.env.BLAZE_TTS_TIMEOUT = '';
process.env.BLAZE_LLM_TIMEOUT = '-500';
const cfg = resolveConfig();
expect(cfg.sttTimeout).toBe(30000); // fallback
expect(cfg.ttsTimeout).toBe(60000); // fallback (empty string)
expect(cfg.llmTimeout).toBe(60000); // fallback (negative value)
});

it('falls back to default timeout when env var is zero', () => {
process.env.BLAZE_STT_TIMEOUT = '0';
const cfg = resolveConfig();
expect(cfg.sttTimeout).toBe(30000); // 0 is not a valid timeout
});
});

describe('buildAuthHeaders', () => {
it('returns empty object when no token', () => {
const headers = buildAuthHeaders('');
expect(headers).toEqual({});
});

it('returns Authorization header when token provided', () => {
const headers = buildAuthHeaders('my-token');
expect(headers).toEqual({ Authorization: 'Bearer my-token' });
});
});
86 changes: 86 additions & 0 deletions plugins/blaze/src/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// SPDX-FileCopyrightText: 2025 LiveKit, Inc.
//
// SPDX-License-Identifier: Apache-2.0

/**
* Blaze Configuration Module
*
* Provides centralized configuration for Blaze AI services.
* All services (STT, TTS, LLM) route through a single gateway URL.
* Service-specific configuration (language, speaker, etc.) comes from the
* voicebot ID and is passed as constructor options to each plugin.
*
* Values are resolved in priority order:
* Explicit options > BlazeConfig > Environment variables > Defaults
*
* Environment Variables (prefix: BLAZE_):
* BLAZE_API_URL - Base URL for all Blaze services
* BLAZE_API_TOKEN - Bearer token for authentication
* BLAZE_STT_TIMEOUT - STT timeout in ms (default: 30000)
* BLAZE_TTS_TIMEOUT - TTS timeout in ms (default: 60000)
* BLAZE_LLM_TIMEOUT - LLM timeout in ms (default: 60000)
*/

/** Configuration for Blaze AI services. */
export interface BlazeConfig {
/** Base URL for all Blaze API services. Default: https://api.blaze.vn */
apiUrl?: string;
/** Bearer token for API authentication. */
authToken?: string;
/** STT request timeout in milliseconds. Default: 30000 */
sttTimeout?: number;
/** TTS request timeout in milliseconds. Default: 60000 */
ttsTimeout?: number;
/** LLM request timeout in milliseconds. Default: 60000 */
llmTimeout?: number;
}

/** Resolved configuration with all values populated. */
export interface ResolvedBlazeConfig {
apiUrl: string;
authToken: string;
sttTimeout: number;
ttsTimeout: number;
llmTimeout: number;
}

/** Parse a timeout env var, falling back to a default if the value is missing or non-numeric. */
function parseTimeoutEnv(envVal: string | undefined, defaultMs: number): number {
if (!envVal) return defaultMs;
const n = Number(envVal);
return Number.isFinite(n) && n > 0 ? n : defaultMs;
}

/** Resolve configuration from options, environment variables, and defaults. */
export function resolveConfig(config?: BlazeConfig): ResolvedBlazeConfig {
return {
apiUrl: config?.apiUrl ?? process.env['BLAZE_API_URL'] ?? 'https://api.blaze.vn',
authToken: config?.authToken ?? process.env['BLAZE_API_TOKEN'] ?? '',
sttTimeout: config?.sttTimeout ?? parseTimeoutEnv(process.env['BLAZE_STT_TIMEOUT'], 30000),
ttsTimeout: config?.ttsTimeout ?? parseTimeoutEnv(process.env['BLAZE_TTS_TIMEOUT'], 60000),
llmTimeout: config?.llmTimeout ?? parseTimeoutEnv(process.env['BLAZE_LLM_TIMEOUT'], 60000),
};
}

/** Build Authorization header value if token is provided. */
export function buildAuthHeaders(authToken: string): Record<string, string> {
if (!authToken) return {};
return { 'Authorization': `Bearer ${authToken}` };
}

/** Maximum number of retry attempts for transient failures. */
export const MAX_RETRY_COUNT = 3;

/** Base delay in milliseconds for exponential backoff. */
export const RETRY_BASE_DELAY_MS = 2000;

/** Sleep for the given number of milliseconds. */
export function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}

/** Check if an error is retryable (not an intentional abort). */
export function isRetryableError(err: unknown): boolean {
if (err instanceof DOMException && err.name === 'AbortError') return false;
return true;
}
60 changes: 60 additions & 0 deletions plugins/blaze/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// SPDX-FileCopyrightText: 2025 LiveKit, Inc.
//
// SPDX-License-Identifier: Apache-2.0

/**
* @livekit/agents-plugin-blaze
*
* LiveKit Agent Framework plugin for Blaze AI services (STT, TTS, LLM).
*
* @example
* ```typescript
* import { STT, TTS, LLM } from '@livekit/agents-plugin-blaze';
*
* // Create plugins (reads BLAZE_* env vars automatically)
* const stt = new STT({ language: 'vi' });
* const tts = new TTS({ speakerId: 'speaker-1' });
* const llm = new LLM({ botId: 'my-chatbot' });
*
* // Or with shared configuration
* import type { BlazeConfig } from '@livekit/agents-plugin-blaze';
* const config: BlazeConfig = { apiUrl: 'http://gateway:8080', authToken: 'tok' };
* const stt2 = new STT({ config, language: 'vi' });
* ```
*/

import { Plugin } from '@livekit/agents';

export { STT } from './stt.js';
export type { STTOptions } from './stt.js';

export { TTS, ChunkedStream, SynthesizeStream } from './tts.js';
export type { TTSOptions } from './tts.js';

export { LLM, LLMStream } from './llm.js';
export type { LLMOptions, BlazeDemographics } from './llm.js';

export type { BlazeConfig } from './config.js';

export type {
BlazeTTSModel,
BlazeLanguage,
BlazeAudioFormat,
BlazeGender,
BlazeDemographics as BlazeDemographicsModel,
BlazeSTTResponse,
BlazeChatMessage,
BlazeLLMData,
} from './models.js';

class BlazePlugin extends Plugin {
constructor() {
super({
title: 'Blaze',
version: '0.1.0',
package: '@livekit/agents-plugin-blaze',
});
}
}

Plugin.registerPlugin(new BlazePlugin());
Loading
Loading