This is the multi-page printable view of this section. Click here to print.

Return to the regular view of this page.

Dapr Conversation JavaScript SDK

Converse with large language models using the Dapr JavaScript SDK

Use DaprClient.conversation to send messages to a large language model through a Dapr conversation component. The JavaScript SDK supports the Conversation Alpha2 API over HTTP and gRPC, including message history, function tool calls, structured output, PII scrubbing, prompt cache retention, and token usage statistics.

These guides target @dapr/dapr 3.19.0 or later. The underlying Conversation API is alpha. Basic Alpha2 requests require Dapr runtime 1.16 or later; some options require newer runtimes and support from the selected component and model.

GuideDescription
How to: Converse with an LLMInstall the package, configure a component, and send your first request.
Conversation client usageConfigure the client, work with messages and responses, and use advanced request options.
How to: Call toolsDefine functions, validate model-generated arguments, and return tool results to the model.

1 - How to: Converse with an LLM using the Dapr JavaScript SDK

Configure a conversation component and send messages using the Dapr JavaScript SDK

Use client.conversation.converse(request) to send role-based messages to a configured conversation component. You do not need a DaprServer or a provider-specific JavaScript SDK.

Prerequisites

Installation

In your application directory, initialize an npm project if you do not already have one, then install the SDK:

npm init -y
npm install @dapr/dapr@3.19.0

The examples use JavaScript ES modules and run as .mjs files without a TypeScript compiler. The same APIs are available in TypeScript, with request and response types exported from @dapr/dapr.

Configure a conversation component

Create a components directory and save the following as components\conversation.yaml:

apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: echo
spec:
  type: conversation.echo
  version: v1

The echo component returns the input text without calling an LLM. Use it to check local connectivity, not to test model reasoning, structured output, or tool calling.

The component’s metadata.name, echo, is the name passed in a conversation request. It is not the component type or the YAML file name.

Send a message

Save the following as app.mjs:

import { CommunicationProtocolEnum, DaprClient } from "@dapr/dapr";

const client = new DaprClient({
  daprHost: "127.0.0.1",
  daprPort: "3500",
  communicationProtocol: CommunicationProtocolEnum.HTTP,
});

try {
  const response = await client.conversation.converse({
    name: "echo",
    inputs: [
      {
        messages: [
          { ofUser: { content: [{ text: "What is Dapr?" }] } },
        ],
      },
    ],
  });

  for (const output of response.outputs) {
    for (const choice of output.choices) {
      console.log("Finish reason:", choice.finishReason);
      if (choice.message) {
        console.log("Conversation output:", choice.message.content);
      }
    }
  }
} finally {
  await client.stop();
}

Start the application with a Dapr sidecar:

dapr run --app-id conversation-example --dapr-http-port 3500 --dapr-grpc-port 50001 --resources-path ./components -- node app.mjs

The echoed message is available at response.outputs[0].choices[0].message.content. Unlike Alpha1 examples in other SDKs, the JavaScript Alpha2 response does not have an output.result property.

To use gRPC, change communicationProtocol to CommunicationProtocolEnum.GRPC and daprPort to "50001". The request and response objects are the same for both protocols.

Use an LLM provider

Replace the echo component with a supported conversation component. For example, use this OpenAI component with a local environment variable secret store:

apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: conversation-secrets
spec:
  type: secretstores.local.env
  version: v1
---
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: llm
spec:
  type: conversation.openai
  version: v1
  metadata:
    - name: key
      secretKeyRef:
        name: OPENAI_API_KEY
        key: OPENAI_API_KEY
    - name: model
      value: gpt-4o-mini
auth:
  secretStore: conversation-secrets

Set OPENAI_API_KEY in the environment inherited by dapr run, restart the sidecar, and change the request’s name to "llm". Do not put provider credentials in request metadata or commit them to a component file. See the OpenAI component reference for provider settings and model support.

Next steps

Read the client usage guidance for message history, structured output, caching, token limits, and errors. Follow the tool-calling guide to let a model request application-defined functions.

2 - Dapr Conversation JavaScript client usage

Configure conversation requests, message history, structured output, caching, and response handling in JavaScript

DaprClient.conversation exposes one asynchronous method:

converse(request: ConversationRequestAlpha2): Promise<ConversationResponseAlpha2>;

Import these types, and the other Conversation* types used below, from @dapr/dapr. This guidance targets 3.19.0 or later.

Runtime and provider support

CapabilityMinimum Dapr runtime
Alpha2 messages, tool calling, temperature, and PII scrubbing1.16
responseFormat, promptCacheRetention, and token usage statistics1.17
Model name in the response1.18
maxTokens1.19

Availability also depends on the conversation component and model. An SDK option does not make an unsupported provider feature available. See the Conversation API reference and your component reference.

Client configuration and lifetime

Create a long-lived DaprClient and reuse it across requests. Call await client.stop() when the application shuts down, not after each operation on a shared client.

import { CommunicationProtocolEnum, DaprClient } from "@dapr/dapr";

const client = new DaprClient({
  daprHost: "127.0.0.1",
  daprPort: "50001",
  communicationProtocol: CommunicationProtocolEnum.GRPC,
  daprApiToken: process.env.DAPR_API_TOKEN,
});

Use CommunicationProtocolEnum.HTTP with the sidecar’s HTTP port for HTTP requests. The SDK calls /v1.0-alpha2/conversation/{name}/converse over HTTP and ConverseAlpha2 over gRPC. You pass plain JavaScript objects, not protobuf messages or provider-native request objects.

The client also supports DAPR_HTTP_ENDPOINT and DAPR_GRPC_ENDPOINT for endpoint configuration. DAPR_API_TOKEN authenticates to the sidecar; it is separate from the provider API key configured on the component. See JavaScript client configuration for shared client options.

Messages and conversation history

A request contains one or more inputs, each with a nonempty messages array. Each message must contain exactly one role property:

Role propertyContent
ofDeveloperDeveloper instructions, with optional participant name.
ofSystemSystem instructions, with optional participant name.
ofUserUser input, with optional participant name.
ofAssistantPrevious assistant text and/or toolCalls, with optional participant name. Content can be omitted for a tool-only message.
ofToolFunction result with required nonempty name, text content, and toolId to correlate with the assistant’s tool call.

Message content is an array of { text: string } objects, not a single string. The SDK’s conversation message types support text, not image or audio content parts.

The following example reuses a configured client and an LLM component named llm:

const messages = [
  { ofSystem: { content: [{ text: "Give concise, factual answers." }] } },
  { ofUser: { name: "reader", content: [{ text: "What is Dapr?" }] } },
];

const response = await client.conversation.converse({
  name: "llm",
  contextId: "chat-123",
  inputs: [{ messages }],
});

const choice = response.outputs[0]?.choices[0];
if (!choice?.message || choice.finishReason !== "stop") {
  throw new Error("Expected a completed text response before continuing the chat");
}

messages.push(
  { ofAssistant: { content: [{ text: choice.message.content }] } },
  { ofUser: { content: [{ text: "How does it help with pub/sub?" }] } },
);

const followup = await client.conversation.converse({
  name: "llm",
  contextId: response.contextId || "chat-123",
  inputs: [{ messages }],
});

contextId is a correlation value, not server-side chat memory. Retain the history in your application and resend the messages needed for every turn. Choose one assistant choice when continuing a conversation; do not merge alternative choices into the same history.

Request options

PropertyDescription
nameRequired, nonempty component name.
inputsRequired, nonempty array of conversation inputs.
contextIdOptional correlation value.
temperatureOptional finite number controlling randomness. The component and model determine the accepted range. Explicit 0 is preserved.
scrubPiiOptional boolean enabling PII obfuscation in response text. Set inputs[].scrubPii independently for input text.
metadataOptional string-to-string conversation tags, not provider configuration or credentials.
tools, toolChoiceOptional function definitions and tool selection policy. See tool calling.
responseFormatOptional JSON Schema object for structured output.
promptCacheRetentionOptional nonnegative Temporal.Duration for prompt cache retention.
maxTokensOptional integer from 1 through 2147483647 limiting generated completion tokens.
parametersOptional JSON object reserved by Dapr. It is not a general-purpose provider options bag. The SDK encodes its values internally.

PII scrubbing and metadata

Input and output scrubbing are separate settings:

const response = await client.conversation.converse({
  name: "llm",
  inputs: [
    {
      scrubPii: true,
      messages: [
        { ofUser: { content: [{ text: "Contact me at reader@example.com." }] } },
      ],
    },
  ],
  scrubPii: true,
  temperature: 0,
  metadata: { user_id: "user-123", session_id: "chat-123" },
});

Metadata supports up to 16 pairs, with keys up to 64 characters and values up to 512 characters. Keep API keys and component connection settings in component configuration or referenced secrets.

Structured output

Pass the JSON Schema directly as responseFormat, not an OpenAI response_format wrapper or a JSON-encoded string:

const response = await client.conversation.converse({
  name: "llm",
  inputs: [
    {
      messages: [
        { ofUser: { content: [{ text: "Answer with the name of Dapr's pub/sub building block." }] } },
      ],
    },
  ],
  responseFormat: {
    type: "object",
    properties: { answer: { type: "string" } },
    required: ["answer"],
    additionalProperties: false,
  },
});

const choice = response.outputs[0]?.choices[0];
if (!choice?.message || choice.finishReason !== "stop") {
  throw new Error("Expected a completed structured response");
}
const value = JSON.parse(choice.message.content);
if (
  value === null ||
  typeof value !== "object" ||
  Array.isArray(value) ||
  typeof value.answer !== "string" ||
  Object.keys(value).length !== 1
) {
  throw new Error("Response does not match the expected answer schema");
}
console.log(value.answer);

The SDK returns content as a string; it does not parse or validate the generated JSON against your schema. Handle JSON parsing failures and validate the result before using it. A truncated or filtered response might not contain complete JSON.

Prompt cache retention and token limits

Import Temporal from the SDK to construct the retention duration:

import { Temporal } from "@dapr/dapr";

const response = await client.conversation.converse({
  name: "llm",
  inputs: [
    {
      messages: [
        { ofUser: { content: [{ text: "Explain Dapr in one paragraph." }] } },
      ],
    },
  ],
  promptCacheRetention: Temporal.Duration.from({ minutes: 30 }),
  maxTokens: 200,
});

promptCacheRetention must be a fixed, nonnegative duration. Days, hours, minutes, seconds, and subsecond fields are supported; calendar years, months, and weeks are not. Fields must be safe integers and the total must fit the protobuf duration range. Do not pass milliseconds as a number or a duration string instead of a Temporal.Duration.

Retention is a request to the component/provider, not a guarantee of a cache hit or stored conversation history. Provider-specific caching limits still apply.

Some components also expose responseCacheTTL in their YAML metadata to cache complete responses. This is component configuration, separate from promptCacheRetention; do not try to configure it through request metadata.

maxTokens requires Dapr 1.19 or later and overrides the component’s token cap where supported. Check finishReason for "length" rather than treating a capped answer as complete.

Responses and token usage

The SDK returns ConversationResponseAlpha2 with an optional contextId and an outputs array. Each output contains choices, an optional model, and optional usage. Each choice contains index, finishReason, and an optional message. A message has string content and a toolCalls array.

Do not assume there is only one output or choice, or that every choice has text. Common finish reasons are "stop", "length", "tool_calls", and "content_filter"; handle other provider values too.

for (const output of response.outputs) {
  console.log("Model:", output.model);
  if (output.usage) {
    console.log("Total tokens:", output.usage.totalTokens);
    console.log("Prompt tokens:", output.usage.promptTokens);
    console.log("Completion tokens:", output.usage.completionTokens);
    console.log("Cached tokens:", output.usage.promptTokensDetails?.cachedTokens);
    console.log("Reasoning tokens:", output.usage.completionTokensDetails?.reasoningTokens);
  }
}

Prompt token details also include audioTokens. Completion token details include acceptedPredictionTokens, rejectedPredictionTokens, audioTokens, and reasoningTokens. These are provider-reported statistics, not evidence that the SDK accepts audio input. Usage and detail objects can be absent; some components return zero counts when the provider omits usage.

Token counts and choice indexes are JavaScript numbers. The SDK rejects values outside the safe-integer range instead of silently losing precision.

Errors and API limits

converse rejects its promise for invalid local input, sidecar errors, provider failures, or response conversion failures. Invalid message roles or content and invalid JSON objects produce TypeError; invalid numeric limits and durations can produce RangeError. Schema and parameter objects must contain only finite JSON values, without cycles, undefined, BigInt, or class instances.

Catch errors at your application’s error boundary and report or propagate them. Do not turn a failed request into a successful empty answer. Provider errors can include rate limits, invalid tool histories, unsupported options, and authentication failures; their representation can differ between HTTP and gRPC.

The exposed method returns a complete response. It does not expose streaming completions, automatic tool execution, or a per-request AbortSignal, timeout, or cancellation parameter. Configure Dapr resiliency policies for sidecar-to-provider timeouts and retries. A JavaScript Promise.race timeout alone does not cancel the underlying request.

3 - How to: Call tools with the Dapr Conversation JavaScript SDK

Define conversation tools, validate function arguments, and send tool results back to an LLM in JavaScript

Tool calling lets a model request an application-defined function. Dapr and the JavaScript SDK transport tool definitions and calls; your application validates arguments, authorizes and executes the function, and sends the result back.

Prerequisites

Complete the conversation setup guide using @dapr/dapr 3.19.0 or later, Dapr runtime 1.16 or later, and a component/model that supports tool calling. The echo component is not sufficient.

Define a tool and handle its result

The following example uses the llm component configured in the setup guide. Save it as tools.mjs and run it with the same dapr run command, replacing app.mjs with tools.mjs.

import { DaprClient } from "@dapr/dapr";

const client = new DaprClient({ daprHost: "127.0.0.1", daprPort: "3500" });
const messages = [
  { ofUser: { content: [{ text: "Look up the description of Dapr." }] } },
];
const tools = [
  {
    function: {
      name: "lookupProject",
      description: "Look up a project description by its exact name.",
      parameters: {
        type: "object",
        properties: { name: { type: "string", enum: ["Dapr"] } },
        required: ["name"],
        additionalProperties: false,
      },
    },
  },
];

try {
  const first = await client.conversation.converse({
    name: "llm",
    inputs: [{ messages }],
    tools,
    toolChoice: "lookupProject",
  });

  const choice = first.outputs[0]?.choices[0];
  const calls = choice?.message?.toolCalls;
  if (choice?.finishReason !== "tool_calls" || !calls?.length) {
    throw new Error("Expected the model to request a tool");
  }

  messages.push({
    ofAssistant: {
      content: [{ text: choice.message.content }],
      toolCalls: calls,
    },
  });

  const seenIds = new Set();
  for (const call of calls) {
    if (!call.id || seenIds.has(call.id)) {
      throw new Error("Missing or duplicate tool call ID");
    }
    seenIds.add(call.id);
    if (call.function.name !== "lookupProject") {
      throw new Error("Unexpected tool function");
    }

    const args = JSON.parse(call.function.arguments);
    if (
      args === null ||
      typeof args !== "object" ||
      Array.isArray(args) ||
      args.name !== "Dapr" ||
      Object.keys(args).length !== 1
    ) {
      throw new Error("Invalid lookupProject arguments");
    }

    const result = {
      name: args.name,
      description: "Dapr provides APIs for building distributed applications.",
    };
    messages.push({
      ofTool: {
        toolId: call.id,
        name: call.function.name,
        content: [{ text: JSON.stringify(result) }],
      },
    });
  }

  const final = await client.conversation.converse({
    name: "llm",
    contextId: first.contextId,
    inputs: [{ messages }],
    tools,
    toolChoice: "none",
  });

  const answer = final.outputs[0]?.choices[0];
  if (!answer?.message || answer.finishReason !== "stop") {
    throw new Error("Expected a completed answer after the tool results");
  }
  console.log(answer.message.content);
} finally {
  await client.stop();
}

Tool definitions use { function: { name, description, parameters } }. Do not add a provider-native type: "function" wrapper. parameters is a JSON Schema object; the function’s name must be nonempty.

Returned call.function.arguments is model-generated string data, not a parsed object or trusted instruction. The example allowlists one read-only function and validates its exact arguments. For real tools, also enforce authorization, input limits, and any required confirmation before side effects. Never use eval or dispatch arbitrary function names supplied by the model.

Preserve the tool-call history

For each selected assistant choice, append the assistant message containing the original tool calls, followed by a tool result for every call. Preserve each call’s id, function name, and arguments. Set the tool result’s toolId to that call’s ID, not its function name.

Tool-only assistant messages can omit content. Tool result messages require a nonempty name and a content array. An assistant can request multiple calls in one turn, so do not process only toolCalls[0]. Alternative response choices are separate conversation branches.

Resend the original messages, assistant tool calls, and tool results in order on the next request. Passing only contextId does not preserve that history.

Choose a tool policy

toolChoiceBehavior
"auto"The model can answer directly or request tools.
"none"The model must answer without calling tools.
"required"The model must request one or more tools.
An exact function name, such as "lookupProject"Request that specific function.

The chosen component and model must support the policy. A named choice must match a declared tool; requiring tools without providing definitions is invalid.

The example forces a named tool on the first request and disables tools on the second, bounding the interaction to two requests. If you use "auto" for a repeated tool loop, handle direct text responses, limit the number of turns and calls, and surface failures rather than retrying indefinitely.