Making a request

Behold. An HTTP endpoint that will answer any question you put to it.

You know how to talk to an HTTP endpoint. You have been doing it since you were a stripling. So let us do it the honest way, with fetch, and see how far honesty gets us.

const res = await fetch("https://api.anthropic.com/v1/messages", {
  method: "POST",
  headers: {
    "x-api-key": process.env.ANTHROPIC_API_KEY!,
    "anthropic-version": "2023-06-01",
    "content-type": "application/json",
  },
  body: JSON.stringify({
    model: "claude-sonnet-5",
    max_tokens: 1000,
    messages: [{ role: "user", content: "What is quantum computing?" }],
  }),
});

const data = await res.json();

This works.

Instantly immanentised synthetic wisdom.

You may now feel a small flush of pride.

There’s a link to a repo with runnable code at the end of this post, if you want to get this running before you proceed.

Please hover over data.

Our endpoint has imparted a great deal, yet fetch has jettisoned it into the unknown.

As TypeScript developers, we must now write one of our sacred interfaces.

We could try to reverse-engineer the payload.

{
  model: 'claude-sonnet-5',
  id: 'msg_011Ce3nv2H22zBLTiKU7b2ro',
  type: 'message',
  role: 'assistant',
  content: [
    {
      type: 'text',
      text: '# Quantum Computing\n' +
        '\n' +
        'Quantum computing is a type of computation that uses principles from quantum mechanics to process information in fundamentally different ways than classical computers.\n' +
        '...' +
        '\n' +
        "Would you like me to go deeper into any particular aspect—the physics, specific algorithms (like Shor's or Grover's), or potential applications?"
    }
  ],
  stop_reason: 'end_turn',
  stop_sequence: null,
  stop_details: null,
  usage: {
    input_tokens: 14,
    cache_creation_input_tokens: 0,
    cache_read_input_tokens: 0,
    cache_creation: { ephemeral_5m_input_tokens: 0, ephemeral_1h_input_tokens: 0 },
    output_tokens: 713,
    output_tokens_details: { thinking_tokens: 0 },
    service_tier: 'standard',
    inference_geo: 'global'
  }
}

Perhaps it would be better to stumble through the docs for a more official contract.

But what if there was a better way?

pnpm install @anthropic-ai/sdk

THE CLIENT #

import Anthropic from "@anthropic-ai/sdk";

const client = new Anthropic();
const model = "claude-sonnet-5";

THREE PARAMETERS #

Every request needs exactly three things.

const message = await client.messages.create({
  model,
  max_tokens: 1000,
  messages: [
    {
      role: "user",
      content: "What is quantum computing? Answer in one sentence",
    },
  ],
});

One object, where…

If the answer runs longer than our ceiling, generation halts. Mid-sentence. Mid-word, occasionally.

Which raises a question worth asking before it happens to you in production: how would you know?

if (message.stop_reason === "max_tokens") {
  // the answer is a fragment, and you should treat it as one
}

stop_reason tells you why the model stopped talking. "end_turn" means it finished. "max_tokens" means you interrupted it. Nothing in the response text will announce the difference.

MESSAGES #

A conversation is an array, and each element carries a role and some content.

There are two roles. "user" is what you send. "assistant" is what the model sends.

That is the whole conversation. There is no session, no thread, no handle on the server holding your history. Each request carries its entire past, and if you omit it, it’s gone.

The type is exported, which is convenient once the array stops being a literal:

const messages: Anthropic.MessageParam[] = [
  { role: "user", content: "What is quantum computing?" },
  { role: "assistant", content: "It is a form of computation that..." },
  { role: "user", content: "Explain superposition." },
];

Annotate it and TypeScript will catch the third role you invent at two in the morning.

THE JUICY RESULT #

We have a response. Let us read it.

console.log(message.content[0].text);

And the compiler refuses.

Property ‘text’ does not exist on type ‘ContentBlock’.

Ah, yes. Your first honest disagreement with the type checker, and the type checker is right.

content is not a string. It is an array of blocks, and ContentBlock is a union: a text block, a tool_use block, a thinking block, and others besides. Only one of those variants has anything called text on it. The compiler is declining to guess which one you got.

Now, there is a cast that makes this message disappear. I will not write it down, because it does not make the problem disappear — it only postpones it until the day you enable tools, receive a tool_use block in position zero, and read .text off something that has never had one. The compiler was the last thing standing between you and that afternoon, and you dismissed it.

Instead, narrow on the tag.

const block = message.content[0];
if (block.type === "text") {
  console.log(block.text);
}

Inside that branch, block is a TextBlock. Outside it, it is not. TypeScript worked this out from the type field alone, which is why the field is there.

For a response with several blocks, gather them:

const text = message.content
  .filter((block): block is Anthropic.TextBlock => block.type === "text")
  .map((block) => block.text)
  .join("\n");

The odd-looking return type in that predicate does all the work. Without it, filter hands back the full union again and .map is back where we started.

And that is your first request.

All this ceremony must feel like tax, for all you wanted was but one sentence about quantum computing. Yet the moment you turn tools on, it will all make more sense: that same array is where the tool calls arrive — and now you have the branch written out.

Repo #

Code exercises set up here

 
0
Kudos
 
0
Kudos

Now read this

Hot Modules In Create React App

Create React App automatically reloads css style changes but not other code changes. Adding hot modules to a create-react-app lets you change source code in your app and see the changes without the app reloading. Below the... Continue →