Skip to content

Commit 18a2ca7

Browse files
committed
blog post drafts, future date filtering, and ai blog posts
1 parent b5379db commit 18a2ca7

File tree

5 files changed

+451
-0
lines changed

5 files changed

+451
-0
lines changed

content-collections.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const posts = defineCollection({
88
schema: (z) => ({
99
title: z.string(),
1010
published: z.string().date(),
11+
draft: z.boolean().optional(),
1112
authors: z.string().array(),
1213
}),
1314
transform: ({ content, ...post }) => {

src/blog/tanstack-ai-alpha-2.md

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
---
2+
title: 'TanStack AI Alpha 2: Every Modality, Better APIs, Smaller Bundles'
3+
published: 2025-12-19
4+
draft: true
5+
authors:
6+
- Alem Tuzlak
7+
- Jack Herrington
8+
- Tanner Linsley
9+
---
10+
11+
It's been two weeks since we released the first alpha of TanStack AI. To us, it feels like decades ago. We've prototyped through 5-6 different internal architectures to bring you the best experience possible.
12+
13+
Our goals were simple: move away from monolithic adapters and their complexity, while expanding the flexibility and power of our public APIs. This release delivers on both.
14+
15+
## New Adapter Architecture
16+
17+
We wanted to support everything AI providers offer—image generation, video, audio, text-to-speech, transcription—without updating every adapter simultaneously.
18+
19+
We're a small team. Adding image support shouldn't mean extending `BaseAdapter`, updating 5+ provider implementations, ensuring per-model type safety for each, and combing through docs manually. That's a week per provider. Multiply that by 20 providers and 6 modalities.
20+
21+
So we split the monolith.
22+
23+
Instead of:
24+
25+
```ts
26+
import { openai } from "@tanstack/ai-openai"
27+
```
28+
29+
You now have:
30+
31+
```ts
32+
import { openaiText, openaiImage, openaiVideo } from "@tanstack/ai-openai"
33+
```
34+
35+
### Why This Matters
36+
37+
**Incremental feature support.** Add image generation to OpenAI this week, Gemini next week, video for a third provider the week after. Smaller releases, same pace.
38+
39+
**Easier maintenance.** Our adapter abstraction had grown to 7 type generics with only text, summarization, and embeddings. Adding 6 more modalities would have exploded complexity. Now each adapter is focused—3 generics max.
40+
41+
**Better bundle size.** You control what you pull in. Want only text? Import `openaiText`. Want text and images? Import both. Your bundle, your choice.
42+
43+
**Faster contributions.** Add support for your favorite provider with a few hundred lines. We can review and merge it quickly.
44+
45+
## New Modalities
46+
47+
What do we support now?
48+
49+
- Structured outputs
50+
- Image generation
51+
- Video generation
52+
- Audio generation
53+
- Transcription
54+
- Text-to-speech
55+
56+
You have a use-case with AI? We support it.
57+
58+
## API Changes
59+
60+
We made breaking changes. Here's what and why.
61+
62+
### Model Moved Into the Adapter
63+
64+
Before:
65+
66+
```ts
67+
chat({
68+
adapter: openai(),
69+
model: "gpt-4",
70+
// now you get typesafety...
71+
})
72+
```
73+
74+
After:
75+
76+
```ts
77+
chat({
78+
adapter: openaiText("gpt-4"),
79+
// immediately get typesafety
80+
})
81+
```
82+
83+
Fewer steps to autocomplete. No more type errors from forgetting to define the model.
84+
85+
### providerOptions → modelOptions
86+
87+
Quick terminology:
88+
89+
- **Provider**: Your LLM provider (OpenAI, Anthropic, Gemini)
90+
- **Adapter**: TanStack AI's interface to that provider
91+
- **Model**: The specific model (GPT-4, Claude, etc.)
92+
93+
The old `providerOptions` were tied to the *model*, not the provider. Changing from `gpt-4` to `gpt-3.5-turbo` changes those options. So we renamed them:
94+
95+
```ts
96+
chat({
97+
adapter: openaiText("gpt-4"),
98+
modelOptions: {
99+
text: {}
100+
}
101+
})
102+
```
103+
104+
### Options Flattened to Root
105+
106+
Settings like `temperature` work across providers. Our other modalities already put config at the root:
107+
108+
```ts
109+
image({
110+
adapter,
111+
numberOfImages: 3
112+
})
113+
```
114+
115+
So we brought chat in line:
116+
117+
```ts
118+
chat({
119+
adapter: openaiText("gpt-4"),
120+
modelOptions: {
121+
text: {}
122+
},
123+
temperature: 0.6
124+
})
125+
```
126+
127+
Start typing to see what's available.
128+
129+
### The Full Diff
130+
131+
```diff
132+
chat({
133+
- adapter: openai(),
134+
+ adapter: openaiText("gpt-4"),
135+
- model: "gpt-4",
136+
- providerOptions: {
137+
+ modelOptions: {
138+
text: {}
139+
},
140+
- options: {
141+
- temperature: 0.6
142+
- },
143+
+ temperature: 0.6
144+
})
145+
```
146+
147+
## What's Next
148+
149+
**Standard Schema support.** We're dropping the Zod constraint for tools and structured outputs. Bring your own schema validation library.
150+
151+
**On the roadmap:**
152+
- Middleware
153+
- Tool hardening
154+
- Headless UI library for AI components
155+
- Context-aware tools
156+
- Better devtools and usage reporting
157+
- More adapters: AWS Bedrock, OpenRouter, and more
158+
159+
Community contributions welcome.
160+
161+
## Wrapping Up
162+
163+
We've shipped a major architectural overhaul, new modalities across the board, and a cleaner API. The adapters are easy to make, easy to maintain, and easy to reason about. Your bundle stays minimal.
164+
165+
We're confident in this direction. We think you'll like it too.
166+
167+
---
168+
169+
*Curious how we got here? Read [The `ai()` Function That Almost Was](/blog/tanstack-ai-the-ai-function-postmortem)—a post-mortem on the API we loved, built, and had to kill.*
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
---
2+
title: "The ai() Function That Almost Was"
3+
published: 2025-12-26
4+
authors:
5+
- Alem Tuzlak
6+
---
7+
8+
We spent eight days building an API we had to kill. Here's what happened.
9+
10+
## The Dream
11+
12+
One function to rule them all. One function to control all adapters. One function to make it all typesafe.
13+
14+
```ts
15+
import { ai } from "@tanstack/ai"
16+
import { openaiText, openaiImage, openaiSummarize } from "@tanstack/ai-openai"
17+
18+
// text generation
19+
ai({
20+
adapter: openaiText("gpt-4"),
21+
// ... text options
22+
})
23+
24+
// image generation
25+
ai({
26+
adapter: openaiImage("dall-e-3"),
27+
// ... image options
28+
})
29+
30+
// summarization
31+
ai({
32+
adapter: openaiSummarize("gpt-4"),
33+
// ... summary options
34+
})
35+
```
36+
37+
Simple. Single function. Powers everything AI-related. Clear naming—you're using AI. Types constrained to each adapter's capabilities. Pass image options to an image adapter, text options to a text adapter.
38+
39+
Change models? Type errors if something's not supported. Change adapters? Type errors if something's not supported.
40+
41+
It felt powerful. Switching between adapters was fast. We were excited.
42+
43+
It was a failure.
44+
45+
## Why It Failed
46+
47+
Two things killed it: complexity and tree-shaking.
48+
49+
### The Complexity Trap
50+
51+
The simplicity of `ai()` for end users hid enormous implementation complexity.
52+
53+
**Attempt 1: Function Overloads**
54+
55+
We tried using function overloads to constrain each adapter's options. Too many scenarios. The overloads resolved to wrong signatures—you could end up providing video options instead of image options. We got it to 99% working, but the 1% felt wrong and was a bigger hurdle than you'd think.
56+
57+
Having 10+ overloads is cumbersome. Get the order wrong and it all falls apart. This would exponentially increase the difficulty of contributions and lowered our confidence in shipping stable releases.
58+
59+
**Attempt 2: Pure Inference**
60+
61+
We tried TypeScript inference instead. It actually worked. Everything inferred perfectly. Types constrained to models. Life was good. Coconuts were rolling on the beach.
62+
63+
But the inference code was 50-100 lines just to cover text, image, and audio. It would grow with more modalities and grow again with type safety improvements. After thorough analysis it was almost impossible to reason about. A single glance and understanding was out the window.
64+
65+
We'll take complexity on our side over forcing you to use `as` casts or `any` types. But where this API completely failed was in our options factories.
66+
67+
### The aiOptions Nightmare
68+
69+
We added a `createXXXOptions` API—`createTextOptions`, `createImageOptions`, etc. You can construct options as ready-made agents and pass them into functions, overriding what you need.
70+
71+
To match the theme, we called it `aiOptions`. It would constrain everything to the modality and provider:
72+
73+
```ts
74+
const opts = aiOptions({
75+
adapter: openaiText("gpt-4")
76+
})
77+
78+
ai(opts)
79+
```
80+
81+
Here's where we hit the wall.
82+
83+
When `aiOptions` returned readonly values, spreading into `ai()` worked. But `aiOptions` was loosely typed—you could pass anything in.
84+
85+
When we fixed `aiOptions` to accept only valid properties, the spread would cast the `ai()` function to `any`. Then it would accept anything.
86+
87+
We went in circles. Get one part working, break another. Fix that, break the first thing.
88+
89+
I believe it could have been done. Our approach was probably just wrong—some subtle bug in the system causing everything to break. But that proves the point: it was too complex to wrap your head around and find the root cause. Any fix would have to propagate through all the adapters. Very costly.
90+
91+
We spent almost a week trying to get this API to work perfectly. We couldn't. Maybe another week would have done it. But then what? How would we fix bugs in this brittle type system? How would we find root causes?
92+
93+
Even if we'd gotten it working, there was another problem.
94+
95+
### Tree-Shaking
96+
97+
We'd just split our adapters into smaller pieces so bundlers could tree-shake what you don't use. Then we put all that complexity right back into `ai()`.
98+
99+
We don't want to be the lodash of AI libraries—bundling everything you don't use and calling it a day. If a huge adapter that bundles everything is not okay, a single function that does the same thing is definitely not okay.
100+
101+
## The Warnings We Missed
102+
103+
Here's the part that stings.
104+
105+
### LLMs Couldn't Write It
106+
107+
We wrestled with the API for six days before reverting, then two more days to unwind it. Eight days total.
108+
109+
The warning sign we missed? LLMs couldn't reliably generate code for this API.
110+
111+
Think about that. We're building tools for AI, and AI couldn't figure out how to use them. That should have been a massive clue that humans wouldn't reliably write to this API unaided either.
112+
113+
LLMs like function names that indicate what the thing does. `ai()`—who knows? `generateImage()`—crystal clear.
114+
115+
When we finally asked the LLMs directly what they thought of the API, they were 4-0 against `ai()` and for the more descriptive approach we ended up with.
116+
117+
### Agents Hid the Pain
118+
119+
We used agents to do the implementation work. That hid the struggle from us.
120+
121+
If we'd been writing the code by hand, we would have *felt* the challenge of wrestling with the types. That probably would have stopped the idea early.
122+
123+
LLMs won't bark when you tell them to do crazy stuff. They won't criticize your designs unless you ask them to. They just try. And try. And eventually produce something that technically works but shouldn't exist.
124+
125+
### We Skipped the Vetting
126+
127+
We were so confident in the design that we didn't make an RFC. Didn't get external feedback. Didn't run it by the LLMs themselves.
128+
129+
This is the classic trap. Smart people in a room, design something cool, pat each other on the backs, not realizing they left off a key detail or two. Go build the simple new thing, and it turns into a nightmare.
130+
131+
These situations are almost unavoidable. The only optimization is to cut them off early. Which we could have done if we'd:
132+
133+
1. Written code by hand before automating it
134+
2. Asked the LLMs what they thought of the API
135+
3. Made an RFC and gotten feedback
136+
4. Noticed that the agents were struggling
137+
138+
## What We Explored Instead
139+
140+
Before landing on separate functions, we tried one more thing: an adapter with sub-properties.
141+
142+
```ts
143+
const adapter = openai()
144+
adapter.image("model")
145+
adapter.text("model")
146+
```
147+
148+
Looks nicer. Feels more unified. Same problem—still bundles everything.
149+
150+
We could have done custom bundling in TanStack Start to strip unused parts, but we don't want to force you to use our framework for the best experience. This library is for the web ecosystem, not just TanStack users.
151+
152+
## Where We Landed
153+
154+
Separate functions. `chat()`, `image()`, `audio()`, `transcribe()`.
155+
156+
```ts
157+
import { chat } from "@tanstack/ai"
158+
import { openaiText } from "@tanstack/ai-openai"
159+
160+
chat({
161+
adapter: openaiText("gpt-4"),
162+
temperature: 0.6
163+
})
164+
```
165+
166+
It's not as clever. That's the point.
167+
168+
You know what `chat()` does. You know what `image()` does. LLMs know what they do. Your bundle only includes what you import. The types are simple enough to reason about.
169+
170+
Like a lot of things in life, there has to be compromise between complexity, DX, and UX. We decided to keep the core simple, split features into separate bundles, and make modalities easy to pull in or ignore.
171+
172+
## Lessons
173+
174+
1. **If LLMs can't write to your API, reconsider.** It's a signal that humans will struggle too.
175+
176+
2. **Don't let agents hide the pain.** Write code by hand before automating. Feel the friction yourself.
177+
178+
3. **Vet designs externally.** RFC it. Get feedback. Ask the LLMs what they think.
179+
180+
4. **Simple and clear beats clever.** APIs shouldn't surprise you. Function names should say what they do.
181+
182+
5. **Cut early.** These traps are almost unavoidable. The win is recognizing them fast.
183+
184+
We loved the `ai()` API. We built it. We had to kill it. That's how it goes sometimes.
185+
186+
---
187+
188+
*Ready to try what we shipped instead? Read [TanStack AI Alpha 2: Every Modality, Better APIs, Smaller Bundles](/blog/tanstack-ai-alpha-2).*

0 commit comments

Comments
 (0)