Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## [4.43.0]

- Add `client.llmGateway.decisions(request)` for the LLM Gateway's `/v1/decisions` endpoint: answers `predicate`, `choice`, and `score` questions about an input in one request. New types `DecisionRequest`, `DecisionQuestion`, `DecisionResponse`, `DecisionAnswer`, `DecisionProbability`, and `DecisionUsage`; untyped request fields are still sent by name

## [4.42.0]

- Add Text-to-Speech: `client.tts.synthesize(request)` returns one text's complete audio (`TextToSpeechResponse`: `audio` plus the response headers as `requestId`, `region`, `model`, `voiceId`, `encoding`, `sampleRate`, `billedCharacters`, `audioDurationMs`, ...), `client.tts.stream(request)` returns it as the API generates it (`TextToSpeechStream`, an async iterable over a web `ReadableStream`), and `client.tts.session(params)` opens a WebSocket `TextToSpeechSession` for voice agents: `connect()`, `generate()` / `flush()` / `audio()`, `speak()`, `cancel()`, `keepAlive()`, `close()`, with `on("audio" | "flushDone" | "wordBoundaries" | "cancelled" | "termination" | "error" | "close", ...)`. Requests take the API's fields (`text`, `voice_id`, `model_id`, `language`, `sample_rate`, `encoding`, `container`, `bit_rate`); failures throw `TextToSpeechError` (`status`, `errorCode`, `param`, `requestId`, `retryable`, `retryAfter`, `errors`, `closeCode`, `body`). New client option `ttsBaseUrl` (default `https://tts.us.assemblyai.com`); `client.tts.createTemporaryToken()` mints a token for browser sessions. Text sent while `connect()` is pending is held and sent the moment the socket opens, so it need not wait for `Begin`
Expand Down
33 changes: 31 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ const client = new AssemblyAI({
- `client.llmGateway.chatCompletions(request)` — OpenAI-compatible chat completions
- `client.llmGateway.listModels()` — List models available on the LLM Gateway
- `client.llmGateway.understanding(request)` / `client.llmGateway.validateUnderstanding(request)` — Run or validate a Speech Understanding request
- `client.llmGateway.decisions(request)` — Answer predicate, choice, or score questions about an input
- `client.tts.synthesize({ text, voice_id? })` — Text-to-speech in one request: the complete audio (`TextToSpeechResponse.audio`, a Uint8Array) plus the response header fields
- `client.tts.stream(request, { signal? })` — Text-to-speech audio as it is generated: a `TextToSpeechStream`, an async iterable of Uint8Array chunks with `info` (header fields), `body`, `bytes()` and `cancel()`
- `client.tts.session({ voiceId })` — WebSocket `TextToSpeechSession` that speaks text as it is written (voice agents): `connect({ token? })` (text sent before it resolves is held until the socket opens), `speak(text)` (async iterable), `generate()`/`flush()`/`audio(flushId?)`, `cancel()`, `keepAlive()`, `close()`, `on("begin"|"audio"|"flushDone"|"wordBoundaries"|"cancelled"|"termination"|"error"|"close", fn)`
Expand Down Expand Up @@ -403,8 +404,8 @@ upload, the transcription and the LLM pass; the service caps audio at 120 s. `si
## LLM Gateway

`client.llmGateway` targets the LLM Gateway host (`llm-gateway.assemblyai.com`, override with the
`llmGatewayBaseUrl` client option). It exposes an OpenAI-compatible chat completions API and
AssemblyAI's Speech Understanding endpoints.
`llmGatewayBaseUrl` client option). It exposes an OpenAI-compatible chat completions API,
AssemblyAI's Speech Understanding endpoints, and decisions.

```typescript
const completion = await client.llmGateway.chatCompletions({
Expand All @@ -427,6 +428,34 @@ const result = await client.llmGateway.understanding({
});
```

Decisions answers `predicate`, `choice`, and `score` questions about an input (a string or a list
of input items) in one request. Each answer carries its question's `name`; `probabilities[].value`
is the choice value (string) for a choice question and the level index (number) for a score question.
Only some models serve decisions, and `listModels()` does not show decisions-only models.

```typescript
const { answers } = await client.llmGateway.decisions({
model: "gpt-6-luna",
input: "Agent: Anything else? Caller: No, that fixed it, thanks!",
questions: [
{
type: "predicate",
name: "resolved",
instructions: "Was the issue resolved?",
},
{
type: "choice",
name: "sentiment",
instructions: "Caller sentiment at the end of the call",
choices: [
{ value: "positive", description: "Satisfied" },
{ value: "negative", description: "Unhappy" },
],
},
],
});
```

Failures throw `LlmGatewayError` with `.status`, `.requestId`, and `.errors` (validation detail
strings, when present).

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "assemblyai",
"version": "4.42.0",
"version": "4.43.0",
"description": "The AssemblyAI JavaScript SDK provides an easy-to-use interface for interacting with the AssemblyAI API, which supports async and real-time transcription and text-to-speech.",
"engines": {
"node": ">=18"
Expand Down
19 changes: 17 additions & 2 deletions src/services/llm-gateway/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,17 @@ import {
BaseServiceParams,
ChatCompletionRequest,
ChatCompletionResponse,
DecisionRequest,
DecisionResponse,
ListModelsResponse,
LlmGatewaySpeechUnderstandingRequest,
LlmGatewaySpeechUnderstandingResponse,
} from "../..";
import { LlmGatewayError } from "../../utils/errors/llm-gateway";

/**
* The LLM Gateway service: an OpenAI-compatible chat completions API plus
* AssemblyAI's Speech Understanding endpoints.
* The LLM Gateway service: an OpenAI-compatible chat completions API,
* AssemblyAI's Speech Understanding endpoints, and decisions.
*/
export class LlmGatewayService extends BaseService {
/**
Expand Down Expand Up @@ -84,6 +86,19 @@ export class LlmGatewayService extends BaseService {
});
if (!response.ok) throw await errorFromResponse(response);
}

/**
* Answer predicate, choice, or score questions about an input.
* @throws LlmGatewayError when the request fails.
*/
async decisions(request: DecisionRequest): Promise<DecisionResponse> {
const response = await this.fetchResponse("/v1/decisions", {
method: "POST",
body: JSON.stringify(request),
});
if (!response.ok) throw await errorFromResponse(response);
return (await response.json()) as DecisionResponse;
}
}

/**
Expand Down
75 changes: 75 additions & 0 deletions src/types/llm-gateway/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,3 +124,78 @@ export type LlmGatewaySpeechUnderstandingRequest = {
* transcript source, so it is left loosely typed.
*/
export type LlmGatewaySpeechUnderstandingResponse = Record<string, unknown>;

/**
* A question in a `/v1/decisions` request. `choices` is required for
* `choice` questions and `levels` for `score` questions.
*/
export type DecisionQuestion = {
type: "predicate" | "choice" | "score";
/** Unique within the request; echoed on the matching answer. */
name: string;
/** A string, or a list of `input_text` / `input_image` parts. */
instructions: string | Record<string, unknown>[];
choices?: { value: string; description: string }[];
levels?: { label: string; description: string }[];
};

/**
* A request to the LLM Gateway's `/v1/decisions` endpoint. Fields not typed
* here can still be passed by name.
*/
export type DecisionRequest = {
model: string;
/** A string, or a list of OpenAI input items. */
input: string | Record<string, unknown>[];
questions: DecisionQuestion[];
prompt_cache_key?: string;
prompt_cache_options?: { mode?: string; ttl?: string };
provider?: string;
fallbacks?: Record<string, unknown>[];
fallback_config?: {
depth?: number;
retry?: boolean;
circuit_breaker?: boolean;
};
model_region?: string;
[key: string]: unknown;
};

export type DecisionProbability = {
/** The choice value for a `choice` question, or the level index for a `score` question. */
value: string | number;
label?: string;
probability: number;
};

/**
* The answer to one question, matched to it by `name`. Which fields are set
* depends on the question `type`.
*/
export type DecisionAnswer = {
type?: string;
name?: string;
probability?: number;
choice?: string;
score?: number;
probabilities?: DecisionProbability[];
confidence?: number;
};

export type DecisionUsage = {
input_tokens: number;
input_tokens_details: { cached_tokens: number; cache_write_tokens: number };
output_tokens: number;
output_tokens_details: { reasoning_tokens: number };
total_tokens: number;
};

/**
* The response from the LLM Gateway's `/v1/decisions` endpoint.
*/
export type DecisionResponse = {
request_id: string;
model: string;
answers: DecisionAnswer[];
usage: DecisionUsage;
};
31 changes: 31 additions & 0 deletions tests/integration/llm-gateway.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,35 @@ describe("llmGateway", () => {
const result = await client.llmGateway.understanding(request);
expect(result).toBeTruthy();
});

it("should answer decisions questions", async () => {
const result = await client.llmGateway.decisions({
model: "gpt-6-luna",
input: "Agent: Anything else? Caller: No, that fixed it, thanks!",
questions: [
{
type: "predicate",
name: "resolved",
instructions: "Was the caller's issue resolved?",
},
{
type: "choice",
name: "sentiment",
instructions: "Caller sentiment at the end of the call",
choices: [
{ value: "positive", description: "Satisfied" },
{ value: "negative", description: "Unhappy" },
],
},
],
});

expect(result.request_id).toBeTruthy();
expect(result.answers.map((a) => a.name)).toEqual([
"resolved",
"sentiment",
]);
expect(["positive", "negative"]).toContain(result.answers[1].choice);
expect(result.usage.total_tokens).toBeGreaterThan(0);
});
});
55 changes: 55 additions & 0 deletions tests/unit/llm-gateway.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,61 @@ describe("llmGateway", () => {
).resolves.toBeUndefined();
});

it("should run a decisions request", async () => {
const response = {
request_id: "req_dec",
model: "gpt-6-luna",
answers: [
{ type: "predicate", name: "resolved", probability: 0.9 },
{
type: "score",
name: "politeness",
score: 0.8,
probabilities: [
{ value: 0, label: "rude", probability: 0.2 },
{ value: 1, label: "polite", probability: 0.8 },
],
},
],
usage: {
input_tokens: 120,
input_tokens_details: { cached_tokens: 0, cache_write_tokens: 0 },
output_tokens: 0,
output_tokens_details: { reasoning_tokens: 0 },
total_tokens: 120,
},
};
fetchMock.doMockOnceIf(
requestMatches({ url: "/v1/decisions", method: "POST" }),
JSON.stringify(response),
);
const request = {
model: "gpt-6-luna",
input: "Caller: thanks, that fixed it!",
questions: [
{
type: "predicate" as const,
name: "resolved",
instructions: "Was the issue resolved?",
},
{
type: "score" as const,
name: "politeness",
instructions: "How polite was the agent?",
levels: [
{ label: "rude", description: "Dismissive" },
{ label: "polite", description: "Courteous" },
],
},
],
prompt_cache_key: "call-123",
brand_new_param: true,
};
const result = await assembly.llmGateway.decisions(request);
expect(result).toEqual(response);
expect(requestBody()).toEqual(request);
});

it("should map the post-auth error envelope to an LlmGatewayError", async () => {
fetchMock.mockResponseOnce(
JSON.stringify({
Expand Down
Loading