Right, let’s talk about how you actually get these models to do your bidding. Forget the flashy demos for a second; we’re getting into the API trenches. Bedrock offers two primary ways to have a chat: the newer, more capable Converse API and the older, more granular InvokeModel (and InvokeModelWithResponseStream) API. One is for having a conversation, the other is for sending a precisely crafted note and hoping for the best. You can probably guess which one I prefer.

The Converse API: Your New Best Friend

Introduced later, the Converse API is Bedrock’s attempt to give you a sane, modern interface that abstracts away the messy, model-specific JSON formatting. This is the one you should use for probably 95% of your new projects. Its beauty is in its consistency. You talk to Claude, Command, and Llama the exact same way. Bedrock itself handles the translation of your standardized request into the weird, proprietary JSON structure each model secretly craves.

The core concepts are simple: you create a Message, put it in a Conversation, and send it off. Here’s what a basic call looks like using the JavaScript SDK (v3).

import { BedrockRuntimeClient, ConverseCommand } from "@aws-sdk/client-bedrock-runtime";

const client = new BedrockRuntimeClient({ region: "us-east-1" });

const command = new ConverseCommand({
  modelId: "anthropic.claude-3-sonnet-20240229-v1:0",
  messages: [
    {
      role: "user", // Can be 'user' or 'assistant'
      content: [{ text: "Explain quantum computing like I'm a seasoned software engineer who hates hype." }]
    }
  ],
  inferenceConfig: { maxTokens: 1024, temperature: 0.7 }
});

try {
  const response = await client.send(command);
  // The response is also standardized!
  console.log(response.output.message?.content[0].text);
} catch (error) {
  console.error("Failed to converse:", error);
}

See? No fumbling with "prompt": "\n\nHuman: ...\n\nAssistant:" nonsense. The API handles the conversation history, the roles, everything. It’s clean. The response isn’t just a blob of text; it’s a structured object that includes things like usage metrics, which is a lifesaver for cost tracking.

The InvokeModel API: The Legacy Power Tool

Now, let’s talk about InvokeModel. You’ll use this for two reasons: 1) you’re working with a model that doesn’t yet support Converse (like some of the older ones), or 2) you need absolute, low-level control over the exact prompt structure for some esoteric reason. This is where you have to do the work yourself.

The brutal part is that the body field in the request expects a raw, model-specific JSON string. Not an object. A string. And the structure of that JSON is different for every single model family. It’s a configuration nightmare.

Here’s an example for the same Claude 3 model, but using the InvokeModel approach. Look at the difference.

import { BedrockRuntimeClient, InvokeModelCommand } from "@aws-sdk/client-bedrock-runtime";

const client = new BedrockRuntimeClient({ region: "us-east-1" });

// Notice: This is the PROMPT FORMAT that Claude requires. You must build this string yourself.
const claudePrompt = `\n\nHuman: Explain quantum computing like I'm a seasoned software engineer who hates hype.\n\nAssistant:`;

const command = new InvokeModelCommand({
  modelId: "anthropic.claude-3-sonnet-20240229-v1:0",
  contentType: "application/json",
  accept: "application/json",
  body: JSON.stringify({
    anthropic_version: "bedrock-2023-05-31",
    max_tokens: 1024,
    temperature: 0.7,
    messages: [{ role: "user", content: [{ type: "text", text: claudePrompt }] }] // Wait, what? This is getting confusing.
    // Just kidding! For Claude 3, even the InvokeModel API now uses the messages format.
    // But for other models, like Jurassic-2, you'd use a completely different structure: {"prompt":"...","maxTokens":...}
  })
});

try {
  const response = await client.send(command);
  const responseBody = JSON.parse(new TextDecoder().decode(response.body));
  // The response structure is also model-specific! For Claude, it's:
  console.log(responseBody.content[0].text);
} catch (error) {
  console.error("Failed to invoke:", error);
}

I threw a curveball in there because this is the reality of InvokeModel. The ground is constantly shifting. The point is: you are responsible for knowing the exact incantation for each model. You must check the AWS documentation for each model’s specific input and output format. It’s a pain.

Streaming: When You Can’t Wait for the Whole Thing

For a better user experience, especially in chat applications, you’ll want to stream the response token by token instead of waiting for the entire thing to be generated. Converse has ConverseStreamCommand and InvokeModel has InvokeModelWithResponseStreamCommand. The streaming API for Converse is again more consistent and easier to work with, returning a series of events you can process as they arrive.

Best Practices and Pitfalls

  1. Always Use Converse First: Seriously. Default to it. Your code will be more readable, more maintainable, and more portable across models.
  2. Mind Your Region: Not all models are available in all regions. Your first AccessDeniedException will probably be because you’re trying to call a model in us-west-2 that’s only in us-east-1. Check the AWS console.
  3. Handle Errors Gracefully: Models can be throttled or hit internal errors. Your code needs to handle ThrottlingException, ModelTimeoutException, and ModelErrorException. Retry logic with exponential backoff is your friend here.
  4. Track Your Tokens: The response objects from Converse include usage metrics. Log them. This isn’t just about cost; it’s about understanding the performance profile of your application. A sudden spike in input tokens might mean a user is trying to inject a massive prompt.
  5. The InvokeModel Trap: If you must use InvokeModel, abstract the model-specific formatting away immediately. Create a wrapper function for each model family that takes a simple prompt and returns the correctly formatted JSON string. Don’t let that complexity leak into your core application logic. You’ll thank me later when Anthropic changes their format again.