Agent frameworks
Point the Vercel AI SDK at TokenTriage
Both providers want the /v1 base.
Each provider factory takes its own baseURL, and both want /v1. tokentriage wrap sets a different root for Anthropic. Read the gotcha.
import { generateText } from 'ai';
import { createOpenAI } from '@ai-sdk/openai';
import { createAnthropic } from '@ai-sdk/anthropic';
const baseURL = 'http://127.0.0.1:8787/v1'; // both providers. Anthropic too.
const openai = createOpenAI({ baseURL });
const anthropic = createAnthropic({ baseURL });
// openai(id) is the Responses model, so name the chat endpoint yourself.
await generateText({ model: openai.chat('gpt-4o-mini'), prompt: 'One line.' });
await generateText({ model: anthropic('claude-3-5-haiku-20241022'), prompt: 'One line.' });- The setting
createOpenAI({ baseURL }) · createAnthropic({ baseURL })- The base URL
http://127.0.0.1:8787/v1 — the same /v1 for Anthropic, not the bare root
How to tell it worked
Send one call, then read the row it wrote. Three fields separate a parsed request from a relayed one: the path, the dialect and the pricing key. Empty means passthrough.
tail -1 ~/.tokentriage/decisions.jsonl |
jq -r '[.http.path, .http.dialect, .cost.pricing_key] | join(" · ")'What you should see
/v1/chat/completions · openai · openai/gpt-4o-mini
How sure we are
Reproduced
A person ran it by hand against a real gateway on a stated date, and the transcript is on the page.
Verified against ai 7.0.79 · @ai-sdk/openai 4.0.47 · @ai-sdk/anthropic 4.0.42 on
LAB RUN · MOCK UPSTREAM — tokentriage wrap child — 2026-08-25T21:02:31Z
tokentriage wrap: base=http://127.0.0.1:12164 trace=tt_nbh6gepdtz5kfpqx356z3yhxnq agent="vercel-ai-sdk-lab"
env ANTHROPIC_BASE_URL=http://127.0.0.1:12164
env OPENAI_BASE_URL=http://127.0.0.1:12164/v1
env ANTHROPIC_CUSTOM_HEADERS="x-tt-trace-id: tt_nbh6gepdtz5kfpqx356z3yhxnq\nx-tt-agent: vercel-ai-sdk-lab"
## wrap-child-openai — createOpenAI({ apiKey }) with NO baseURL — OPENAI_BASE_URL only
WIRE POST http://127.0.0.1:12164/v1/chat/completions -> status=200 req=tt_qlddxhnstli7ungt25s22adr7u
x-tt-mode=route x-tt-decision=fast x-tt-cache=miss
CLIENT OK {"text":"Hello from the mock upstream.","usage":{"inputTokens":100,"outputTokens":20,"totalTokens":120}}
LEDGER ROW path=/v1/chat/completions dialect=openai model=gpt-4o-mini tier=fast
trace_agent=(empty) trace_id=(empty)
status=200 cost.usd=0.000027 pricing_key=openai/gpt-4o-mini unpriced=null
## wrap-child-anthropic — createAnthropic({ apiKey }) with NO baseURL — ANTHROPIC_BASE_URL only
WIRE POST http://127.0.0.1:12164/messages -> status=404 req=tt_sxldnmcfws6hrx6wippys2ff2y
x-tt-mode=route x-tt-decision=- x-tt-cache=-
CLIENT ERROR AI_APICallError: Not Found
LEDGER ROW path=/messages dialect=(empty) model=(empty) tier=(empty)
trace_agent=(empty) trace_id=(empty)
status=404 cost.usd=0 pricing_key=(empty) unpriced=null
LAB RUN · MOCK UPSTREAM — no vendor API was called.The gotcha
tokentriage wrap points @ai-sdk/anthropic at the wrong root
tokentriage wrap sets ANTHROPIC_BASE_URL to the bare root. The provider appends /messages, which matches no dialect and takes the ledgered passthrough. Your client sees AI_APICallError: Not Found. The row carries no dialect, no model, no tier and no price. Pass baseURL yourself.
// An explicit baseURL beats ANTHROPIC_BASE_URL, so this survives tokentriage wrap.
const anthropic = createAnthropic({ baseURL: 'http://127.0.0.1:8787/v1' });“Use a different URL prefix for API calls, e.g. to use proxy servers. The default prefix is https://api.anthropic.com/v1.”
@ai-sdk/anthropic 4.0.42 · AnthropicProviderSettings.baseURL · read 2026-08-25
What you get now
What both providers get you once traffic flows.
Three things that switch on the first time either provider sends a request. Each links its evidence. Read those before you trust this page.
Token counts your stream never carried
includeUsage off costs your app the counts. TokenTriage injects stream_options.include_usage upstream and strips the frame from your copy, so the row is priced either way. The upstream sees a body you did not send.
What lands on a rowTwo dialects, one costed view
The OpenAI and Anthropic providers speak different wire dialects. TokenTriage parses both at /v1 and prices them from the same dated table. One ledger covers the pair. The snapshot id is on every row.
How the two dialects are matchedPer-agent grouping, passed in code
Both providers take a headers option. Set x-tt-agent and x-tt-trace-id there and every call in a run groups under one trace id in the ledger. The environment does not carry them for this SDK.
The headers the ledger reads
- 2 providers
- openai and anthropic, one baseURL each, both ending in /v1
- 1 string to change
- the base URL your app hands each factory
- 4 environment variables
- the whole env surface of both providers: two keys, two base URLs
- 10 scenarios
- the matrix this page was written from, run on 2026-08-25
What does not carry across
Six edges, and two of them are ours.
Four are facts about the SDK. Two are facts about our evidence. The last one is the limit of the chip above.
The chip does not cover a successful Anthropic call
The lab harness serves the OpenAI surface only. What was reproduced is that the provider builds /v1/messages and that TokenTriage matches the anthropic dialect and parses the model. Nothing here saw a reply.
The two dialect pathsopenai(id) is the Responses model, not chat
Call it without .chat() and the SDK posts to /v1/responses with a different body. TokenTriage registers that path, so the row reads /v1/responses and not the chat path you looked for. The anthropic factory has no such split.
Which paths are registeredA relayed call can return 200 and still cost nothing
A path TokenTriage does not register still reaches your upstream, with the path as written. An unregistered GET returned 200 in the same run and wrote a row with no dialect and no price. The two are separate claims.
Base URL and path, per clientFour environment variables, and none of them carries a header
Each provider reads its API key and its base URL. That is the whole list. wrap also exports a custom-header variable that neither provider reads, so both rows came back with an empty trace id and agent.
Where a trace id comes fromNo env fallback on openai-compatible
createOpenAICompatible reads nothing from the environment. Set baseURL in code. OPENAI_API_BASE is never read by any of these providers, so a base-URL warning written for another framework does not apply here.
Vercel AI Gateway is a different thing
TokenTriage carries a vercel_ai_gateway provider descriptor, which names Vercel AI Gateway as an upstream you point at. This page is about the client library. Nothing here exercised the descriptor.
What a provider descriptor is
Point both providers, then read one row.
The change is one string in two factory calls. Reading the row is the work. Send a call, check the dialect and the pricing key, then decide about the empty ones.
Names and trademarks
These are independent integrations. No product named here has endorsed, reviewed or sponsored TokenTriage; every name is its owner's trademark, used only to say what works with what.
Vercel, the Vercel design, Next.js and related marks, designs and logos are trademarks or registered trademarks of Vercel, Inc. or its affiliates in the US and other countries.