Skip to main content
The @outputai/http package gives you an HTTP client that automatically shows up in your traces. Every request — URL, method, status code, timing — is recorded as a child node in the trace tree, so you can see exactly what API calls your steps made and how long they took. Under the hood, it wraps ky, a lightweight HTTP client built on fetch.

Creating a Client

The typical pattern is to create a client per external service in your clients directory, then import it in your steps:
clients/jina.ts
import { httpClient } from '@outputai/http';

export const jina = httpClient({
  prefixUrl: 'https://r.jina.ai',
  headers: {
    'Authorization': `Bearer ${process.env.JINA_API_KEY}`,
    'Accept': 'application/json'
  },
  timeout: 30000
});
You can extend clients to create specialized instances:
const authenticatedClient = jina.extend({
  headers: { 'X-Custom-Header': 'value' }
});

HTTP Methods

// GET
const response = await client.get('https://acme.com');
const data = await response.json();

// POST
const response = await client.post('companies', {
  json: { url: 'https://acme.com' }
});
const result = await response.json();

// PUT
await client.put('companies/123', {
  json: { industry: 'SaaS' }
});

// DELETE
await client.delete('companies/123');

Using in Steps

Wrap HTTP calls in steps for automatic retry and tracing:
steps.ts
import { step } from '@outputai/core';
import { jina } from '../../clients/jina.js';
import { ScrapePageInput, ScrapePageOutput } from './types.js';

export const scrapePage = step({
  name: 'scrapePage',
  description: 'Scrape a web page using Jina Reader API',
  inputSchema: ScrapePageInput,
  outputSchema: ScrapePageOutput,
  fn: async (url) => {
    const response = await jina.get(url);
    const data = await response.json();

    return {
      title: data.data?.title ?? '',
      content: data.data?.content ?? '',
      url: data.data?.url ?? url
    };
  }
});

// types.ts
// import { z } from '@outputai/core';
//
// export const ScrapePageInput = z.string();
//
// export const ScrapePageOutput = z.object({
//   title: z.string(),
//   content: z.string(),
//   url: z.string()
// });

Tracing

All requests made with @outputai/http are automatically traced — no configuration needed. In your trace files, HTTP calls appear as children of the step that made them:
{
  "kind": "http",
  "name": "request",
  "input": { "method": "GET", "url": "https://r.jina.ai/https://acme.com" },
  "output": { "status": 200, "statusText": "OK" }
}
See Tracing for details on the trace format.

Request Events

Every HTTP call made through @outputai/http’s fetch (and the higher-level httpClient) emits an http:request hook event — independent of whether you attach a cost. Subscribe to it with on for logging, alerting, or per-vendor metrics:
import { on } from '@outputai/core/hooks';
import type { HttpRequestEvent } from '@outputai/http';

on<HttpRequestEvent>( 'http:request', async ( payload ) => {
  // payload: { eventId, eventDate, requestId, method, url, status?, durationMs, outcome, activityInfo, workflowDetails, outputActivityKind }
  console.log( `[${payload.outcome}] ${payload.method} ${payload.url} ${payload.status ?? '-'} ${payload.durationMs}ms` );
} );
The payload includes:
FieldDescription
eventIdUUID v4 stamped per emit. Stable per-emit idempotency key — http:request and cost:http:request for the same fetch get distinct eventIds, so dedup keyed on eventId won’t collapse the two.
eventDateMillisecond epoch timestamp for when the event was emitted.
requestIdUUID generated per request inside @outputai/http. Shared between the matching http:request and cost:http:request events for cross-event correlation.
methodHTTP method (uppercase).
urlAbsolute request URL.
statusHTTP status code; undefined on network failure.
durationMsElapsed time from request issuance to response (or failure), in milliseconds.
outcomeOne of 'success' (2xx-3xx), 'error' (status >= 400), 'failure' (DNS / timeout / abort).
activityInfoTemporal activity.Info for the activity that made the request.
workflowDetailsOutput’s serializable subset of Temporal workflow.WorkflowInfo.
outputActivityKindOutput activity kind. Possible values are step, evaluator, and internal_step.
The event fires for every call — success, non-2xx responses, and network failures alike. The existing cost:http:request event is unchanged and continues to fire only when you call addRequestCost.

Attaching Request Cost

When you know the dollar cost of an HTTP request (for example from provider billing headers), you can attach it to the HTTP trace event with addRequestCost.
import { httpClient, addRequestCost } from '@outputai/http';

const client = httpClient( { prefixUrl: 'https://api.vendor.com' } );

const response = await client.post( 'search', {
  json: { query: 'acme' }
} );

const inputCost = Number( response.headers.get( 'x-cost-input-usd' ) ?? 0 );
const outputCost = Number( response.headers.get( 'x-cost-output-usd' ) ?? 0 );

addRequestCost( response, inputCost + outputCost );
addRequestCost accepts the total request cost as a number. addRequestCost only works with responses created by @outputai/http (or its exported fetch). If the response did not come from this package, the function safely no-ops and logs a warning. It also emits a cost:http:request hook event (same hooks system as cost:llm:request). For the payload and examples, see Cost Events — HTTP.

Error Handling

The client throws HTTPError for non-2xx responses and TimeoutError for timeouts:
import { HTTPError, TimeoutError } from '@outputai/http';

try {
  const response = await jina.get(url);
  return response.json();
} catch (error) {
  if (error instanceof HTTPError) {
    if (error.response.status === 404) {
      return null; // Page not found
    }
    // 429, 500, etc. — let the step retry
    throw error;
  }
  if (error instanceof TimeoutError) {
    throw error; // Step will retry
  }
  throw error;
}
Since steps retry automatically, you generally just need to handle cases where retrying won’t help (like a 404). For everything else, let the error propagate and the step’s retry policy will handle it.

Environment Variables

VariableDescription
OUTPUT_TRACE_HTTP_VERBOSESet to true or 1 to include request/response headers and bodies in trace files (otherwise only method, URL, and status are recorded)

API Reference

For complete TypeScript API documentation, see the HTTP Module API Reference.