---
title: Provider calls
summary: OpenAI, Anthropic, Gemini, Bedrock, and Vertex through one client, with streaming.
canonical: https://docs.caveman.so/docs/sdk/providers
license: MIT
capability: sdk-ts
updated: 2026-09-16T21:32:40-07:00
basis: inferred
---

# Provider calls

> OpenAI, Anthropic, Gemini, Bedrock, and Vertex through one client, with streaming.
`cave.openai()`, `cave.anthropic()`, `cave.gemini()` and `cave.vertex()` return a client that posts your request body to that provider's route on the Caveman service you configured, and hands back the provider's parsed JSON. `cave.bedrock()` describes the Bedrock route instead and performs no request. The request body is yours, unchanged, so anything the provider accepts goes through.

Keeping your provider's own SDK? Swap its base URL instead, on [Providers and base URLs](/docs/proxy/providers).

### TypeScript

```ts
import { Cave } from "@caveman-ai/sdk";

const { CAVE_API_KEY = "", CAVE_BASE_URL = "", OPENAI_API_KEY } = process.env;
const cave = new Cave({ apiKey: CAVE_API_KEY, baseURL: CAVE_BASE_URL, agent: "support-agent" });

const openai = cave.openai({ upstreamKey: OPENAI_API_KEY });
const answer = await openai.responses.create({ model: "gpt-5", input: "What is the refund window?" });

const anthropic = cave.anthropic({ upstreamKey: process.env.ANTHROPIC_API_KEY });
const gemini = cave.gemini({ upstreamKey: process.env.GEMINI_API_KEY });
const vertex = cave.vertex({ upstreamKey: process.env.GOOGLE_ACCESS_TOKEN });
const bedrock = cave.bedrock({ region: "eu-west-1" });

console.log(answer, bedrock);
```

### Python

```python
import os
from caveman_cloud import Cave

cave = Cave(api_key=os.environ["CAVE_API_KEY"], base_url=os.environ["CAVE_BASE_URL"], agent="support-agent")

openai = cave.openai(os.environ.get("OPENAI_API_KEY"))
answer = openai.responses.create({"model": "gpt-5", "input": "What is the refund window?"})

anthropic = cave.anthropic(os.environ.get("ANTHROPIC_API_KEY"))
gemini = cave.gemini(os.environ.get("GEMINI_API_KEY"))
vertex = cave.vertex(os.environ.get("GOOGLE_ACCESS_TOKEN"))
bedrock = cave.bedrock("eu-west-1")

print(answer, bedrock)
```

## What each call returns

Four of the five return a provider client with the same three members: `responses.create(body)`, `chat.completions.create(body)`, and `raw()`. Each one posts to a fixed prefix on your service.

| Call | Prefix | Returns |
| --- | --- | --- |
| `openai()` | `/openai/v1` | Provider client |
| `anthropic()` | `/anthropic` | Provider client |
| `gemini()` | `/gemini` | Provider client |
| `vertex()` | `/vertex` | Provider client |
| `bedrock({ region })` | `/bedrock` | Descriptor object, no request |

`bedrock()` takes a required `region` and an optional `endpoint`, which is `"runtime"` by default. `endpoint: "mantle"` changes the prefix to `/bedrock/anthropic`. Anything else raises `bedrock endpoint must be runtime or mantle`.

This is `cave.bedrock({ region: "eu-west-1" })` in Node 22.22:

```json
{
  "region": "eu-west-1",
  "endpoint": "runtime",
  "gatewayPrefix": "/bedrock",
  "instrumented": true,
  "sdkOnly": false
}
```

Python returns the same fields under their snake_case names: `region`, `endpoint`, `gateway_prefix`, `instrumented`, `sdk_only`.

## The upstream key

`upstreamKey` in TypeScript, the first positional argument in Python, is the provider credential. It travels as the `x-cave-upstream-key` header, separate from the `authorization: Bearer` header that carries your Caveman API key. Leave it out when your service holds provider credentials itself.

For `vertex()` the upstream key is a Google OAuth2 access token, such as the output of `gcloud auth print-access-token`. The service forwards it as `Authorization: Bearer …` to Vertex.

Redirects are refused on every request in both languages, so both credentials stay on the origin you configured.

## Streaming

`raw()` is the streaming path. It returns the `fetch` `Response` untouched, so the body is still a stream you can read chunk by chunk. The typed helpers parse the whole JSON body and return an object, which is the wrong shape for a token stream.

```ts
const response = await cave.openai({ upstreamKey: OPENAI_API_KEY }).raw("/openai/v1/responses", {
  method: "POST",
  headers: { "content-type": "application/json" },
  body: JSON.stringify({ model: "gpt-5", input: "What is the refund window?", stream: true }),
});

for await (const chunk of response.body.pipeThrough(new TextDecoderStream())) process.stdout.write(chunk);
```

Against a local server that replies with `text/event-stream`, that prints the events as they arrive:

```console
data: {"delta":"Refunds "}

data: {"delta":"take 30 days."}

data: [DONE]
```

Set `content-type` yourself on a `raw()` call. Caveman's own headers are merged in, and `content-type` is the one header it leaves to you.

Python reads the whole response either way: `Provider.raw(path, body)` returns decoded JSON, the same as `create()`.

## Raw response access

`raw()` is also how you reach a provider path beyond the two typed helpers, such as Anthropic's native `/v1/messages`. The path is checked against the client's prefix before the request leaves, so an OpenAI client stays on `/openai/v1`.

```ts
const messages = await cave.anthropic({ upstreamKey: process.env.ANTHROPIC_API_KEY }).raw("/anthropic/v1/messages", {
  method: "POST",
  headers: { "content-type": "application/json" },
  body: JSON.stringify({ model: "claude-sonnet-4-5", max_tokens: 256, messages: [{ role: "user", content: "hi" }] }),
});
```

In Python the path is relative to the prefix and the body is a dict:

```python
messages = cave.anthropic(os.environ["ANTHROPIC_API_KEY"]).raw("/v1/messages", {
    "model": "claude-sonnet-4-5",
    "max_tokens": 256,
    "messages": [{"role": "user", "content": "hi"}],
})
```

A path outside the prefix throws `cave_provider_raw_path_not_allowed` in TypeScript before any socket opens.

## Limits

These clients are HTTP wrappers. Your code owns retries, the tool-call loop, and reading the provider's own response shape.

`responses.create()` and `chat.completions.create()` post to `{prefix}/responses` and `{prefix}/chat/completions`. For Anthropic and Gemini that means the service owns the translation from those paths; use `raw()` when you want the provider's native path exactly.

A failed provider call throws with the response body as its message, so the provider's own error text is what you read. [Troubleshooting](/docs/sdk/troubleshooting) maps the common ones.
