Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Provider adapter selection and configuration: openaiText, anthropicText, geminiText, ollamaText, grokText, groqText, openRouterText, bedrockText, byteplusText, openaiCompatible. Per-model type safety with modelOptions, reasoning/thinking configuration, runtime adapter switching, extendAdapter() for custom models, createModel(). Generic OpenAI-compatible providers (DeepSeek, Together, Fireworks, etc.) via openaiCompatible({ baseURL, apiKey, models }) from @tanstack/ai-openai/compatible. API key e
SkillScore95/100
Test results 95
3/3 safety checks passed
18%95%of test cases passed +77 pts
Passed 21 of 22 test cases with the skill, and 4 of 22 without it.
18 fixed3 passed either way0 still failing1 broke
This benchmark measured an earlier version of the skill.
A newer attempt on gemini-3.8-flash on 10/23/2026 did not produce a publishable comparison (graded differently). The snapshot above is the previously measured run.
One test taskWrite a TanStack AI chat request using Groq with llama-3.3-70b-versatile that limits output generation to 1500 tokens.
Adds every file the skill has, pinned so you can update it. No account needed.
pip install "decimalai>=0.15"
decimalai skills add tanstack-ai-core-adapter-configuration
claude --plugin-dir "$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.agents"The same install from Python, for a notebook or a setup script.
pip install "decimalai>=0.15"from decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("tanstack-ai-core-adapter-configuration")
if result.exit_code:
raise SystemExit(result.problems)
print("Added. Start Claude Code with the command below.")claude --plugin-dir "$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.agents"Type these in a Claude Code session. No account or API key. It writes the skill and any files it bundles, and asks first when its safety rating isn't Passed.
/plugin marketplace add decimal-labs/decimalai-claude-plugins
/plugin install decimalai@decimalai/decimalai:install tanstack-ai-core-adapter-configurationThe plugin needs python3 and curl on your machine.
No Python or account. Claude Code picks it up in its next session.
curl -fsSL --create-dirs -o .claude/skills/tanstack-ai-core-adapter-configuration/SKILL.md https://app.decimal.ai/s/tanstack-ai-core-adapter-configuration/SKILL.md.claude/skills/tanstack-ai-core-adapter-configuration/SKILL.mdThis gets SKILL.md only. The skill's 8 other files are not included.
agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[claude-agent-sdk]"
decimalai skills add tanstack-ai-core-adapter-configurationimport asyncio
from claude_agent_sdk import ClaudeAgentOptions
from decimalai.claude_agent_sdk import agent_config
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
async def main():
question = "Hello!"
async with support.run(question) as run: # one user message, one run
async for message in run.query(options=ClaudeAgentOptions(max_turns=6)):
print(message)
# In a notebook or async code: await main()
asyncio.run(main())Prefer its own loader? ClaudeAgentOptions(plugins=[skills_plugin()], skills="all") reads the same folder. Use one or the other: a skill delivered both ways reaches the prompt twice.
The same install from Python, for a notebook or a setup script. agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[claude-agent-sdk]"from decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("tanstack-ai-core-adapter-configuration")
if result.exit_code:
raise SystemExit(result.problems)
import asyncio
from claude_agent_sdk import ClaudeAgentOptions
from decimalai.claude_agent_sdk import agent_config
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
async def main():
question = "Hello!"
async with support.run(question) as run: # one user message, one run
async for message in run.query(options=ClaudeAgentOptions(max_turns=6)):
print(message)
# In a notebook or async code: await main()
asyncio.run(main())Prefer its own loader? ClaudeAgentOptions(plugins=[skills_plugin()], skills="all") reads the same folder. Use one or the other: a skill delivered both ways reaches the prompt twice.
No DecimalAI package. The whole skill goes in every prompt, and nothing pins or updates it.
.agents/skills/tanstack-ai-core-adapter-configuration/SKILL.mdimport asyncio
from pathlib import Path
from claude_agent_sdk import ClaudeAgentOptions, query
skill = Path(".agents/skills/tanstack-ai-core-adapter-configuration/SKILL.md").read_text()
options = ClaudeAgentOptions(system_prompt=f"You are a helpful agent.\n\n{skill}", max_turns=6)
async def main():
async for message in query(prompt="Hello!", options=options):
print(message)
# In a notebook or async code: await main()
asyncio.run(main())This gets SKILL.md only. The skill's 8 other files are not included.
agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[openai-agents]"
decimalai skills add tanstack-ai-core-adapter-configurationfrom agents import Agent, Runner
from decimalai.openai_agents import agent_config
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
agent = Agent(
name=support.name,
instructions=support.instructions, # your prompt, then the skill list
tools=[*support.tools], # your tools, then load_skill
)
# In a notebook or async code: result = await Runner.run(agent, "Hello!")
result = Runner.run_sync(agent, "Hello!")Using SandboxAgent? capabilities=[Shell(), decimalai.openai_agents.sandbox_skills()] points its own loader at the same folder.
The same install from Python, for a notebook or a setup script. agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[openai-agents]"from decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("tanstack-ai-core-adapter-configuration")
if result.exit_code:
raise SystemExit(result.problems)
from agents import Agent, Runner
from decimalai.openai_agents import agent_config
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
agent = Agent(
name=support.name,
instructions=support.instructions, # your prompt, then the skill list
tools=[*support.tools], # your tools, then load_skill
)
# In a notebook or async code: result = await Runner.run(agent, "Hello!")
result = Runner.run_sync(agent, "Hello!")Using SandboxAgent? capabilities=[Shell(), decimalai.openai_agents.sandbox_skills()] points its own loader at the same folder.
No DecimalAI package. The whole skill goes in every prompt, and nothing pins or updates it.
.agents/skills/tanstack-ai-core-adapter-configuration/SKILL.mdfrom pathlib import Path
from agents import Agent, Runner
skill = Path(".agents/skills/tanstack-ai-core-adapter-configuration/SKILL.md").read_text()
agent = Agent(name="support", instructions=f"You are a helpful agent.\n\n{skill}")
# In a notebook or async code: result = await Runner.run(agent, "Hello!")
result = Runner.run_sync(agent, "Hello!")This gets SKILL.md only. The skill's 8 other files are not included.
agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[openai]"
decimalai skills add tanstack-ai-core-adapter-configurationimport json
from openai import OpenAI
from decimalai.openai import agent_config
client = OpenAI()
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
def lookup_order(order_id: str) -> str:
return f"Order {order_id} shipped yesterday."
# Your tools, as Chat Completions takes them, and what answers each.
TOOLS = [{"type": "function", "function": {
"name": "lookup_order",
"description": "Look up the status of an order.",
"parameters": {"type": "object", "properties": {"order_id": {"type": "string"}},
"required": ["order_id"]},
}}]
FUNCTIONS = {"lookup_order": lookup_order}
question = "Where is order A1001?"
with support.run(question) as run: # one user message, one run
# run.system is your prompt, then the skill list.
messages = [{"role": "system", "content": run.system}, run.message]
while True:
reply = client.chat.completions.create(
model="gpt-5.4-mini",
messages=messages,
tools=[*TOOLS, *run.tools], # your tools, then load_skill
).choices[0].message
messages.append(reply)
if not reply.tool_calls:
break
for call in reply.tool_calls:
# load_skill is answered by the run; your own tools by FUNCTIONS.
function = FUNCTIONS.get(call.function.name)
messages.append(run.tool_result(call) or {
"role": "tool", "tool_call_id": call.id,
"content": function(**json.loads(call.function.arguments)) if function
else f"There is no tool named {call.function.name!r}.",
})
print(reply.content)
# The Responses API takes run.response_tools instead of run.tools.The same install from Python, for a notebook or a setup script. agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[openai]"from decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("tanstack-ai-core-adapter-configuration")
if result.exit_code:
raise SystemExit(result.problems)
import json
from openai import OpenAI
from decimalai.openai import agent_config
client = OpenAI()
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
def lookup_order(order_id: str) -> str:
return f"Order {order_id} shipped yesterday."
# Your tools, as Chat Completions takes them, and what answers each.
TOOLS = [{"type": "function", "function": {
"name": "lookup_order",
"description": "Look up the status of an order.",
"parameters": {"type": "object", "properties": {"order_id": {"type": "string"}},
"required": ["order_id"]},
}}]
FUNCTIONS = {"lookup_order": lookup_order}
question = "Where is order A1001?"
with support.run(question) as run: # one user message, one run
# run.system is your prompt, then the skill list.
messages = [{"role": "system", "content": run.system}, run.message]
while True:
reply = client.chat.completions.create(
model="gpt-5.4-mini",
messages=messages,
tools=[*TOOLS, *run.tools], # your tools, then load_skill
).choices[0].message
messages.append(reply)
if not reply.tool_calls:
break
for call in reply.tool_calls:
# load_skill is answered by the run; your own tools by FUNCTIONS.
function = FUNCTIONS.get(call.function.name)
messages.append(run.tool_result(call) or {
"role": "tool", "tool_call_id": call.id,
"content": function(**json.loads(call.function.arguments)) if function
else f"There is no tool named {call.function.name!r}.",
})
print(reply.content)
# The Responses API takes run.response_tools instead of run.tools.No DecimalAI package. The whole skill goes in every prompt, and nothing pins or updates it.
.agents/skills/tanstack-ai-core-adapter-configuration/SKILL.mdfrom pathlib import Path
from openai import OpenAI
client = OpenAI()
skill = Path(".agents/skills/tanstack-ai-core-adapter-configuration/SKILL.md").read_text()
reply = client.chat.completions.create(
model="gpt-5.4-mini",
messages=[
{"role": "system", "content": f"You are a helpful agent.\n\n{skill}"},
{"role": "user", "content": "Hello!"},
],
).choices[0].messageThis gets SKILL.md only. The skill's 8 other files are not included.
agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[anthropic]"
decimalai skills add tanstack-ai-core-adapter-configurationimport anthropic
from decimalai.anthropic import agent_config
client = anthropic.Anthropic()
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
question = "Hello!"
with support.run(question) as run: # one user message, one run
answer = client.beta.messages.tool_runner(
model="claude-haiku-5-5",
max_tokens=4096,
system=run.system, # your prompt, then the skill list
tools=[*run.tools], # your tools, then load_skill
messages=[{"role": "user", "content": question}],
).until_done()The same install from Python, for a notebook or a setup script. agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[anthropic]"from decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("tanstack-ai-core-adapter-configuration")
if result.exit_code:
raise SystemExit(result.problems)
import anthropic
from decimalai.anthropic import agent_config
client = anthropic.Anthropic()
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
question = "Hello!"
with support.run(question) as run: # one user message, one run
answer = client.beta.messages.tool_runner(
model="claude-haiku-5-5",
max_tokens=4096,
system=run.system, # your prompt, then the skill list
tools=[*run.tools], # your tools, then load_skill
messages=[{"role": "user", "content": question}],
).until_done()No DecimalAI package. The whole skill goes in every prompt, and nothing pins or updates it.
.agents/skills/tanstack-ai-core-adapter-configuration/SKILL.mdfrom pathlib import Path
import anthropic
client = anthropic.Anthropic()
skill = Path(".agents/skills/tanstack-ai-core-adapter-configuration/SKILL.md").read_text()
reply = client.messages.create(
model="claude-haiku-5-5",
max_tokens=4096,
system=f"You are a helpful agent.\n\n{skill}",
messages=[{"role": "user", "content": "Hello!"}],
)This gets SKILL.md only. The skill's 8 other files are not included.
agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[google-genai]"
decimalai skills add tanstack-ai-core-adapter-configurationfrom google import genai
from google.genai import types
from decimalai.google_genai import agent_config
client = genai.Client()
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
def lookup_order(order_id: str) -> str:
return f"Order {order_id} shipped yesterday."
# Your functions, as declarations for the model, and what answers each.
TOOLS = [{"function_declarations": [{
"name": "lookup_order",
"description": "Look up the status of an order.",
"parameters_json_schema": {"type": "object", "properties": {"order_id": {"type": "string"}},
"required": ["order_id"]},
}]}]
FUNCTIONS = {"lookup_order": lookup_order}
question = "Where is order A1001?"
with support.run(question) as run: # one user message, one run
config = types.GenerateContentConfig(
system_instruction=run.system, # your prompt, then the skill list
tools=[*TOOLS, *run.tool_params], # your functions, then load_skill
)
contents = [{"role": "user", "parts": [{"text": run.message}]}]
while True:
response = client.models.generate_content(
model="gemini-3.5-flash", contents=contents, config=config,
)
if not response.function_calls:
break
contents.append(response.candidates[0].content)
parts = []
for call in response.function_calls:
# load_skill is answered by the run; your own functions by FUNCTIONS.
# A loop of your own rather than automatic function calling, which
# raises KeyError when the model asks for a function you did not pass.
function = FUNCTIONS.get(call.name)
parts.append(run.tool_result(call) or {"function_response": {
"name": call.name, "id": call.id,
"response": {"result": function(**(call.args or {})) if function
else f"There is no function named {call.name!r}."},
}})
contents.append({"role": "user", "parts": parts})
print(response.text)The same install from Python, for a notebook or a setup script. agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[google-genai]"from decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("tanstack-ai-core-adapter-configuration")
if result.exit_code:
raise SystemExit(result.problems)
from google import genai
from google.genai import types
from decimalai.google_genai import agent_config
client = genai.Client()
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
def lookup_order(order_id: str) -> str:
return f"Order {order_id} shipped yesterday."
# Your functions, as declarations for the model, and what answers each.
TOOLS = [{"function_declarations": [{
"name": "lookup_order",
"description": "Look up the status of an order.",
"parameters_json_schema": {"type": "object", "properties": {"order_id": {"type": "string"}},
"required": ["order_id"]},
}]}]
FUNCTIONS = {"lookup_order": lookup_order}
question = "Where is order A1001?"
with support.run(question) as run: # one user message, one run
config = types.GenerateContentConfig(
system_instruction=run.system, # your prompt, then the skill list
tools=[*TOOLS, *run.tool_params], # your functions, then load_skill
)
contents = [{"role": "user", "parts": [{"text": run.message}]}]
while True:
response = client.models.generate_content(
model="gemini-3.5-flash", contents=contents, config=config,
)
if not response.function_calls:
break
contents.append(response.candidates[0].content)
parts = []
for call in response.function_calls:
# load_skill is answered by the run; your own functions by FUNCTIONS.
# A loop of your own rather than automatic function calling, which
# raises KeyError when the model asks for a function you did not pass.
function = FUNCTIONS.get(call.name)
parts.append(run.tool_result(call) or {"function_response": {
"name": call.name, "id": call.id,
"response": {"result": function(**(call.args or {})) if function
else f"There is no function named {call.name!r}."},
}})
contents.append({"role": "user", "parts": parts})
print(response.text)No DecimalAI package. The whole skill goes in every prompt, and nothing pins or updates it.
.agents/skills/tanstack-ai-core-adapter-configuration/SKILL.mdfrom pathlib import Path
from google import genai
from google.genai import types
client = genai.Client()
skill = Path(".agents/skills/tanstack-ai-core-adapter-configuration/SKILL.md").read_text()
response = client.models.generate_content(
model="gemini-3.5-flash",
contents="Hello!",
config=types.GenerateContentConfig(system_instruction=f"You are a helpful agent.\n\n{skill}"),
)This gets SKILL.md only. The skill's 8 other files are not included.
agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[langchain]" langchain-openai
decimalai skills add tanstack-ai-core-adapter-configurationfrom langchain.agents import create_agent
from decimalai.langchain import agent_config
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
agent = create_agent(
"openai:gpt-5.4-mini",
tools=[], # your tools; the middleware adds load_skill
middleware=[support.middleware], # your prompt, the skill list and load_skill
)
result = agent.invoke({"messages": [{"role": "user", "content": "Hello!"}]})The same install from Python, for a notebook or a setup script. agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[langchain]" langchain-openaifrom decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("tanstack-ai-core-adapter-configuration")
if result.exit_code:
raise SystemExit(result.problems)
from langchain.agents import create_agent
from decimalai.langchain import agent_config
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
agent = create_agent(
"openai:gpt-5.4-mini",
tools=[], # your tools; the middleware adds load_skill
middleware=[support.middleware], # your prompt, the skill list and load_skill
)
result = agent.invoke({"messages": [{"role": "user", "content": "Hello!"}]})No DecimalAI package. The whole skill goes in every prompt, and nothing pins or updates it.
.agents/skills/tanstack-ai-core-adapter-configuration/SKILL.mdfrom pathlib import Path
from langchain.agents import create_agent
skill = Path(".agents/skills/tanstack-ai-core-adapter-configuration/SKILL.md").read_text()
agent = create_agent("openai:gpt-5.4-mini", tools=[], system_prompt=f"You are a helpful agent.\n\n{skill}")
result = agent.invoke({"messages": [{"role": "user", "content": "Hello!"}]})This gets SKILL.md only. The skill's 8 other files are not included.
CrewAI's own skill loader reads the installed folder. No DecimalAI account needed.
pip install "decimalai>=0.16" "crewai>=1.15.27"
decimalai skills add tanstack-ai-core-adapter-configurationimport decimalai.crewai
from crewai import Agent, Crew, Task
agent = Agent(
role="Support", goal="Help the customer", backstory="A support agent.",
llm=llm, # your own CrewAI LLM
skills=decimalai.crewai.skills(), # the installed folder
tools=decimalai.crewai.skill_tools(), # reads a skill's files, runs its scripts
)
task = Task(description="Hello!", expected_output="an answer", agent=agent)
Crew(agents=[agent], tasks=[task]).kickoff()The same install from Python, for a notebook or a setup script. CrewAI's own skill loader reads the installed folder. No DecimalAI account needed.
pip install "decimalai>=0.16" "crewai>=1.15.27"from decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("tanstack-ai-core-adapter-configuration")
if result.exit_code:
raise SystemExit(result.problems)
import decimalai.crewai
from crewai import Agent, Crew, Task
agent = Agent(
role="Support", goal="Help the customer", backstory="A support agent.",
llm=llm, # your own CrewAI LLM
skills=decimalai.crewai.skills(), # the installed folder
tools=decimalai.crewai.skill_tools(), # reads a skill's files, runs its scripts
)
task = Task(description="Hello!", expected_output="an answer", agent=agent)
Crew(agents=[agent], tasks=[task]).kickoff()No DecimalAI package. CrewAI's own loader reads the folder, and nothing pins or updates it.
.agents/skills/tanstack-ai-core-adapter-configuration/SKILL.mdfrom pathlib import Path
from crewai import Agent, Crew, Task
agent = Agent(
role="Support", goal="Help the customer", backstory="A support agent.",
llm=llm, # your own CrewAI LLM
skills=[Path(".agents/skills")],
)
task = Task(description="Hello!", expected_output="an answer", agent=agent)
Crew(agents=[agent], tasks=[task]).kickoff()This gets SKILL.md only. The skill's 8 other files are not included.
agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[adk]"
decimalai skills add tanstack-ai-core-adapter-configurationfrom decimalai.adk import DecimalAIPlugin, agent_config
from google.adk.agents import LlmAgent
from google.adk.apps import App
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
agent = LlmAgent(
name="support", # ADK needs a Python identifier
model="gemini-3.5-flash",
instruction=support.instruction, # your prompt, then the skill list
tools=[*support.tools], # your tools, then load_skill
)
# DecimalAIPlugin makes each invocation of runners built from this App one trace.
app = App(name="support", root_agent=agent, plugins=[DecimalAIPlugin()])Prefer ADK's own loader? SkillToolset(skills=decimalai.adk.skills(), ...) reads the same folder. Use one or the other: a skill delivered both ways reaches the prompt twice.
The same install from Python, for a notebook or a setup script. agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[adk]"from decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("tanstack-ai-core-adapter-configuration")
if result.exit_code:
raise SystemExit(result.problems)
from decimalai.adk import DecimalAIPlugin, agent_config
from google.adk.agents import LlmAgent
from google.adk.apps import App
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
agent = LlmAgent(
name="support", # ADK needs a Python identifier
model="gemini-3.5-flash",
instruction=support.instruction, # your prompt, then the skill list
tools=[*support.tools], # your tools, then load_skill
)
# DecimalAIPlugin makes each invocation of runners built from this App one trace.
app = App(name="support", root_agent=agent, plugins=[DecimalAIPlugin()])Prefer ADK's own loader? SkillToolset(skills=decimalai.adk.skills(), ...) reads the same folder. Use one or the other: a skill delivered both ways reaches the prompt twice.
No DecimalAI package. The whole skill goes in every prompt, and nothing pins or updates it.
.agents/skills/tanstack-ai-core-adapter-configuration/SKILL.mdfrom pathlib import Path
from google.adk.agents import LlmAgent
skill = Path(".agents/skills/tanstack-ai-core-adapter-configuration/SKILL.md").read_text()
agent = LlmAgent(
name="support",
model="gemini-3.5-flash",
instruction=lambda context: f"You are a helpful agent.\n\n{skill}",
)This gets SKILL.md only. The skill's 8 other files are not included.
agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[pydantic-ai]"
decimalai skills add tanstack-ai-core-adapter-configurationfrom pydantic_ai import Agent
from decimalai.pydantic_ai import agent_config
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
agent = Agent(
"openai:gpt-4o-mini",
name=support.name,
tools=[], # your tools; the capability adds load_skill
capabilities=[support], # your prompt, the skill list and load_skill
)
# In a notebook or async code: result = await agent.run("Hello!")
result = agent.run_sync("Hello!")On the Pydantic AI harness, decimalai.pydantic_ai_harness.skills(project) uses its own loader instead.
The same install from Python, for a notebook or a setup script. agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[pydantic-ai]"from decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("tanstack-ai-core-adapter-configuration")
if result.exit_code:
raise SystemExit(result.problems)
from pydantic_ai import Agent
from decimalai.pydantic_ai import agent_config
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
agent = Agent(
"openai:gpt-4o-mini",
name=support.name,
tools=[], # your tools; the capability adds load_skill
capabilities=[support], # your prompt, the skill list and load_skill
)
# In a notebook or async code: result = await agent.run("Hello!")
result = agent.run_sync("Hello!")On the Pydantic AI harness, decimalai.pydantic_ai_harness.skills(project) uses its own loader instead.
No DecimalAI package. The whole skill goes in every prompt, and nothing pins or updates it.
.agents/skills/tanstack-ai-core-adapter-configuration/SKILL.mdfrom pathlib import Path
from pydantic_ai import Agent
skill = Path(".agents/skills/tanstack-ai-core-adapter-configuration/SKILL.md").read_text()
agent = Agent("openai:gpt-4o-mini", instructions=f"You are a helpful agent.\n\n{skill}")
# In a notebook or async code: result = await agent.run("Hello!")
result = agent.run_sync("Hello!")This gets SKILL.md only. The skill's 8 other files are not included.
Deep Agents' own skill loader reads the installed folder. No DecimalAI account needed.
pip install "decimalai>=0.16" deepagents
decimalai skills add tanstack-ai-core-adapter-configurationimport decimalai.deepagents
from deepagents import create_deep_agent
agent = create_deep_agent(model=model, **decimalai.deepagents.skills())
agent.invoke({"messages": [{"role": "user", "content": "Hello!"}]})The default backend reads files but has no shell. To run a skill's scripts, pass skills(backend=LocalShellBackend(...)).
The same install from Python, for a notebook or a setup script. Deep Agents' own skill loader reads the installed folder. No DecimalAI account needed.
pip install "decimalai>=0.16" deepagentsfrom decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("tanstack-ai-core-adapter-configuration")
if result.exit_code:
raise SystemExit(result.problems)
import decimalai.deepagents
from deepagents import create_deep_agent
agent = create_deep_agent(model=model, **decimalai.deepagents.skills())
agent.invoke({"messages": [{"role": "user", "content": "Hello!"}]})The default backend reads files but has no shell. To run a skill's scripts, pass skills(backend=LocalShellBackend(...)).
No DecimalAI package. The whole skill goes in every prompt, and nothing pins or updates it.
.agents/skills/tanstack-ai-core-adapter-configuration/SKILL.mdfrom pathlib import Path
from deepagents import create_deep_agent
skill = Path(".agents/skills/tanstack-ai-core-adapter-configuration/SKILL.md").read_text()
agent = create_deep_agent(model=model, system_prompt=f"You are a helpful agent.\n\n{skill}")
agent.invoke({"messages": [{"role": "user", "content": "Hello!"}]})This gets SKILL.md only. The skill's 8 other files are not included.
Its own skill loader reads the installed folder. No DecimalAI account needed.
pip install "decimalai>=0.16" agent-framework
decimalai skills add tanstack-ai-core-adapter-configurationimport asyncio
from agent_framework import Agent
from decimalai.agent_framework import skills
agent = Agent(client=client, instructions="You are a helpful agent.", context_providers=[skills()])
asyncio.run(agent.run("Hello!"))It caches skills by default, so restart the process after decimalai skills update.
The same install from Python, for a notebook or a setup script. Its own skill loader reads the installed folder. No DecimalAI account needed.
pip install "decimalai>=0.16" agent-frameworkfrom decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("tanstack-ai-core-adapter-configuration")
if result.exit_code:
raise SystemExit(result.problems)
import asyncio
from agent_framework import Agent
from decimalai.agent_framework import skills
agent = Agent(client=client, instructions="You are a helpful agent.", context_providers=[skills()])
asyncio.run(agent.run("Hello!"))It caches skills by default, so restart the process after decimalai skills update.
No DecimalAI package. The whole skill goes in every prompt, and nothing pins or updates it.
.agents/skills/tanstack-ai-core-adapter-configuration/SKILL.mdimport asyncio
from pathlib import Path
from agent_framework import Agent
skill = Path(".agents/skills/tanstack-ai-core-adapter-configuration/SKILL.md").read_text()
agent = Agent(client=client, instructions=f"You are a helpful agent.\n\n{skill}")
asyncio.run(agent.run("Hello!"))This gets SKILL.md only. The skill's 8 other files are not included.
Strands' own skill loader reads the installed folder. No DecimalAI account needed.
pip install "decimalai>=0.16" strands-agents
decimalai skills add tanstack-ai-core-adapter-configurationfrom decimalai.strands import skills
from strands import Agent
from strands.vended_tools import file_editor, shell
agent = Agent(
model=model, # always pass one: the default is Amazon Bedrock
system_prompt="You are a helpful agent.",
tools=[file_editor, shell],
plugins=[skills()],
)
agent("Hello!")Strands loads an agent's skills once, so build the agent again after decimalai skills update.
The same install from Python, for a notebook or a setup script. Strands' own skill loader reads the installed folder. No DecimalAI account needed.
pip install "decimalai>=0.16" strands-agentsfrom decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("tanstack-ai-core-adapter-configuration")
if result.exit_code:
raise SystemExit(result.problems)
from decimalai.strands import skills
from strands import Agent
from strands.vended_tools import file_editor, shell
agent = Agent(
model=model, # always pass one: the default is Amazon Bedrock
system_prompt="You are a helpful agent.",
tools=[file_editor, shell],
plugins=[skills()],
)
agent("Hello!")Strands loads an agent's skills once, so build the agent again after decimalai skills update.
No DecimalAI package. The whole skill goes in every prompt, and nothing pins or updates it.
.agents/skills/tanstack-ai-core-adapter-configuration/SKILL.mdfrom pathlib import Path
from strands import Agent
skill = Path(".agents/skills/tanstack-ai-core-adapter-configuration/SKILL.md").read_text()
agent = Agent(model=model, system_prompt=f"You are a helpful agent.\n\n{skill}")
agent("Hello!")This gets SKILL.md only. The skill's 8 other files are not included.
Adds every file the skill has, pinned so you can update it. Cursor, Copilot, Cline and Warp read this folder by themselves.
pip install "decimalai>=0.15"
decimalai skills add tanstack-ai-core-adapter-configurationNo Python or account. Cursor, Copilot, Cline and Warp read this folder by themselves.
curl -fsSL --create-dirs -o .agents/skills/tanstack-ai-core-adapter-configuration/SKILL.md https://app.decimal.ai/s/tanstack-ai-core-adapter-configuration/SKILL.md.agents/skills/tanstack-ai-core-adapter-configuration/SKILL.mdThis gets SKILL.md only. The skill's 8 other files are not included.
Give its text to your model as instructions. No account or package needed.
This gets SKILL.md only. The skill's 8 other files are not included.
Raw files: /s/tanstack-ai-core-adapter-configuration/SKILL.md · /s/tanstack-ai-core-adapter-configuration.json
Fetch the file and give its text to your model as instructions. No account or package needed.
curl -fsSL https://app.decimal.ai/s/tanstack-ai-core-adapter-configuration/SKILL.mdcurl -fsSL https://app.decimal.ai/s/tanstack-ai-core-adapter-configuration.jsonThis gets SKILL.md only. The skill's 8 other files are not included.
You don't need a DecimalAI account.
Saved prompts and skills, pinned versions, and traces of this skill in your runs. DecimalAI does not run your agent.
Claude Code reads files from your project, so DecimalAI config does not apply there.
On Claude Agent SDK: save the skill to an agent here, set DECIMAL_API_KEY, and the same code uses it with no command to run. Changes you save reach the next run with no redeploy.
On OpenAI Agents: save the skill to an agent here, set DECIMAL_API_KEY, and the same code uses it with no command to run. Changes you save reach the next run with no redeploy.
On OpenAI SDK: save the skill to an agent here, set DECIMAL_API_KEY, and the same code uses it with no command to run. Changes you save reach the next run with no redeploy.
On Anthropic: save the skill to an agent here, set DECIMAL_API_KEY, and the same code uses it with no command to run. Changes you save reach the next run with no redeploy.
On Gemini API: save the skill to an agent here, set DECIMAL_API_KEY, and the same code uses it with no command to run. Changes you save reach the next run with no redeploy.
On LangChain: save the skill to an agent here, set DECIMAL_API_KEY, and the same code uses it with no command to run. Changes you save reach the next run with no redeploy.
On CrewAI: set DECIMAL_API_KEY and add decimalai.init(crewai=True) to record runs and skill use. The skill version stays in your repo.
On Google ADK: save the skill to an agent here, set DECIMAL_API_KEY, and the same code uses it with no command to run. Changes you save reach the next run with no redeploy.
On Pydantic AI: save the skill to an agent here, set DECIMAL_API_KEY, and the same code uses it with no command to run. Changes you save reach the next run with no redeploy.
On Deep Agents: set DECIMAL_API_KEY and add decimalai.init(langchain=True) to record runs and skill use. The skill version stays in your repo.
On Microsoft Agent Framework: set DECIMAL_API_KEY and add decimalai.init(otel=True, agent_name="support") to record runs and skill use. The skill version stays in your repo.
On Strands: set DECIMAL_API_KEY and add decimalai.init(otel=True, agent_name="support") to record runs and skill use. The skill version stays in your repo.
Editors read files from your project, so DecimalAI config does not apply there.
Anywhere else: record a use with decimalai.log_skill_activation() inside a trace.
Dependency: This skill builds on ai-core. Read it first for critical rules.
Before implementing: Ask the user which provider and model they want. Then fetch the latest available models from the provider's source code (check the adapter's model metadata file, e.g.
packages/ai-openai/src/model-meta.ts) or from the provider's API/docs to recommend the most current model. The model lists in this skill and its reference files may be outdated. Always verify against the source before recommending a specific model.
Create an adapter and use it with chat():
import { chat, toServerSentEventsResponse } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
export async function POST(request: Request) {
const { messages, threadId } = await request.json()
const stream = chat({
adapter: openaiText('gpt-5.2'),
messages,
threadId,
modelOptions: {
temperature: 0.7,
max_output_tokens: 1000,
},
})
return toServerSentEventsResponse(stream)
}
The adapter factory function takes the model name as a string literal and an
optional config object (API key, base URL, etc.). The model name is passed
into the factory, not into chat().
Sampling options (temperature, token limits, top_p/topP, etc.) live
inside modelOptions using each provider's native key — they are not
top-level options on chat(). See the per-provider table in
Configuring Sampling below.
Each provider has a dedicated package with tree-shakeable adapter factories. The text adapter is the primary one for chat/completions:
| Provider | Package | Factory | Env Var |
|---|---|---|---|
| OpenAI | @tanstack/ai-openai | openaiText | OPENAI_API_KEY |
| Anthropic | @tanstack/ai-anthropic | anthropicText | ANTHROPIC_API_KEY |
| Gemini | @tanstack/ai-gemini | geminiText | GOOGLE_API_KEY or GEMINI_API_KEY |
| Grok (xAI) | @tanstack/ai-grok | grokText | XAI_API_KEY |
| Groq | @tanstack/ai-groq | groqText | GROQ_API_KEY |
| OpenRouter | @tanstack/ai-openrouter | openRouterText | OPENROUTER_API_KEY |
| Ollama | @tanstack/ai-ollama | ollamaText | OLLAMA_HOST (default: http://localhost:11434) |
| Bedrock | @tanstack/ai-bedrock | bedrockText | BEDROCK_API_KEY or AWS_BEARER_TOKEN_BEDROCK |
| BytePlus | @tanstack/ai-byteplus | byteplusText | ARK_API_KEY (falls back to BYTEPLUS_API_KEY) |
| OpenAI-compatible | @tanstack/ai-openai/compatible | openaiCompatible / openaiCompatibleText | provider-specific (passed via apiKey) |
| Cloudflare | @tanstack/ai-cloudflare | cloudflareText | CLOUDFLARE_ACCOUNT_ID + CLOUDFLARE_API_TOKEN, or { binding: env.AI } in a Worker |
| Neon AI Gateway | @tanstack/ai-neon | neonText | NEON_AI_GATEWAY_TOKEN + NEON_AI_GATEWAY_BASE_URL (branch host) |
BytePlus uses two keys.
byteplusText/byteplusVideo/byteplusImagereadARK_API_KEY(ModelArk,Authorization: Bearer), butbyteplusSpeech/byteplusTranscriptionare a separate product and readBYTEPLUS_VOICE_API_KEY(Seed Speech,X-Api-Key). Ark keys are also region-isolated — the default base URL is the ap-southeast endpoint.
// Each factory takes model as first arg, optional config as second
import { openaiText, createOpenaiChat } from '@tanstack/ai-openai'
import { anthropicText } from '@tanstack/ai-anthropic'
import { geminiText } from '@tanstack/ai-gemini'
import { grokText } from '@tanstack/ai-grok'
import { groqText } from '@tanstack/ai-groq'
import { openRouterText } from '@tanstack/ai-openrouter'
import { ollamaText } from '@tanstack/ai-ollama'
import { bedrockText } from '@tanstack/ai-bedrock'
import { byteplusText } from '@tanstack/ai-byteplus'
// Model string is passed to the factory, NOT to chat()
const adapter = openaiText('gpt-5.2')
const adapter2 = anthropicText('claude-sonnet-4-6')
const adapter3 = geminiText('gemini-2.5-pro')
const adapter4 = grokText('grok-4.6')
const adapter5 = groqText('openai/gpt-oss-120b')
const adapter6 = openRouterText('anthropic/claude-sonnet-5.5')
const adapter7 = ollamaText('llama3.3:latest')
const adapter8 = bedrockText('us.anthropic.claude-3-7-sonnet-20250219-v1:0')
const adapter9 = byteplusText('seed-2-0-lite-260428')
// Optional: pass an explicit API key via the create* sibling
// (the plain factory reads it from the environment)
const adapterWithKey = createOpenaiChat('gpt-5.2', 'sk-...')
@tanstack/ai-bedrock (Amazon Bedrock) branches on config.api:
bedrockText(model) or bedrockText(model, { api: 'converse' }) (the default) — Bedrock's native Converse API via @aws-sdk/client-bedrock-runtime (adapter name bedrock-converse). Reaches the broad catalog: Claude, Nova, Llama, Mistral, DeepSeek, and more.bedrockText(model, { api: 'chat' }) — OpenAI-compatible Chat Completions endpoint (adapter name bedrock). Open-weight models only (gpt-oss, DeepSeek V3.x, Gemma, Qwen, etc.). Does NOT reach Claude, Nova, or Llama.bedrockText(model, { api: 'responses' }) — OpenAI-compatible Responses API, mantle-only (adapter name bedrock-responses). Currently gpt-oss and Gemma 4.Use createBedrockText(model, apiKey, config?) to pass the key explicitly. Auth resolves from BEDROCK_API_KEY / AWS_BEARER_TOKEN_BEDROCK, or SigV4 via the standard AWS credential chain (no extra packages needed — handled by @aws-sdk/client-bedrock-runtime).
Use an adapter factory map to switch providers dynamically based on user input or configuration:
import { chat, toServerSentEventsResponse } from '@tanstack/ai'
import type { ModelMessage } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
import { anthropicText } from '@tanstack/ai-anthropic'
import { geminiText } from '@tanstack/ai-gemini'
// Define a map of provider+model to adapter factory calls
const adapters = {
'openai/gpt-5.2': () => openaiText('gpt-5.2'),
'anthropic/claude-sonnet-4-6': () => anthropicText('claude-sonnet-4-6'),
'gemini/gemini-2.5-pro': () => geminiText('gemini-2.5-pro'),
}
function isKnownProviderModel(key: string): key is keyof typeof adapters {
return key in adapters
}
export function handleChat(
providerModel: string,
messages: Array<ModelMessage>,
// The conversation id from the request. Never share one across users.
threadId: string,
) {
if (!isKnownProviderModel(providerModel)) {
throw new Error(`Unknown provider/model: ${providerModel}`)
}
const stream = chat({
adapter: adapters[providerModel](),
messages,
threadId,
})
return toServerSentEventsResponse(stream)
}
Set reasoning/thinking with the reasoning option on chat(). It is the same on every provider, and the types take only the levels of the selected model:
import { chat } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
import { anthropicText } from '@tanstack/ai-anthropic'
import { geminiText } from '@tanstack/ai-gemini'
const messages = [
{ role: 'user' as const, content: 'Plan a database migration.' },
]
// OpenAI: the level goes out as reasoning.effort, with a summary
const openaiStream = chat({
adapter: openaiText('gpt-5.2'),
messages,
threadId: 'thread-1',
reasoning: { level: 'high', summary: true },
})
// Anthropic: extended thinking with a token budget
const anthropicStream = chat({
adapter: anthropicText('claude-sonnet-4-6'),
messages,
threadId: 'thread-1',
modelOptions: { max_tokens: 16000 },
reasoning: { level: 'high', budgetTokens: 8000 },
})
// Anthropic: adaptive thinking (Sonnet 5, Fable 5, Opus 4.7+) — the level
// goes out as output_config.effort
const adaptiveStream = chat({
adapter: anthropicText('claude-sonnet-5'),
messages,
threadId: 'thread-1',
modelOptions: { max_tokens: 16000 },
reasoning: 'high', // 'low' | 'medium' | 'high' | 'xhigh' | 'max'
})
// Gemini: a thinking budget on Gemini 2.5, a thinking level on Gemini 3
const geminiStream = chat({
adapter: geminiText('gemini-2.5-pro'),
messages,
threadId: 'thread-1',
reasoning: { level: 'high', budgetTokens: 4096 },
})
Use extendAdapter() and createModel() to add custom or fine-tuned models
while preserving type safety for the original models:
import { extendAdapter, createModel } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
// Define custom models
const customModels = [
createModel('ft:gpt-5.2:my-org:custom-model:abc123', ['text', 'image']),
createModel('my-local-proxy-model', ['text']),
] as const
// Create extended factory - original models still fully typed
const myOpenai = extendAdapter(openaiText, customModels)
// Use original models - full type inference preserved
const gpt5 = myOpenai('gpt-5.2')
// Use custom models - accepted by the type system
const custom = myOpenai('ft:gpt-5.2:my-org:custom-model:abc123')
// Type error: 'nonexistent-model' is not a valid model
// myOpenai('nonexistent-model')
At runtime, extendAdapter simply passes through to the original factory.
The _customModels parameter is only used for type inference.
Sampling controls (temperature, token limits, nucleus sampling) are passed
inside modelOptions using each provider's native key. They are not
top-level fields on chat()/ai()/generate().
import { chat } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
import { anthropicText } from '@tanstack/ai-anthropic'
import { geminiText } from '@tanstack/ai-gemini'
import { ollamaText } from '@tanstack/ai-ollama'
const messages = [{ role: 'user' as const, content: 'Hello' }]
// OpenAI — native keys
chat({
adapter: openaiText('gpt-5.2'),
messages,
threadId: 'thread-1',
modelOptions: { temperature: 0.7, top_p: 0.9, max_output_tokens: 1000 },
})
// Anthropic
chat({
adapter: anthropicText('claude-sonnet-4-6'),
messages,
threadId: 'thread-1',
modelOptions: { temperature: 0.7, top_p: 0.9, max_tokens: 1000 },
})
// Gemini — camelCase
chat({
adapter: geminiText('gemini-2.5-pro'),
messages,
threadId: 'thread-1',
modelOptions: { temperature: 0.7, topP: 0.9, maxOutputTokens: 1000 },
})
// Ollama — NESTED under modelOptions.options
// (use the `family:tag` id — a bare `llama3.3` falls back to untyped options)
chat({
adapter: ollamaText('llama3.3:latest'),
messages,
threadId: 'thread-1',
modelOptions: {
options: { temperature: 0.7, top_p: 0.9, num_predict: 1000 },
},
})
Per-provider sampling keys (all live inside modelOptions):
| Provider | Temperature | Nucleus | Max output tokens |
|---|---|---|---|
| OpenAI | temperature | top_p | max_output_tokens |
| Anthropic | temperature | top_p | max_tokens |
| Gemini | temperature | topP | maxOutputTokens |
| Grok (xAI) | temperature | top_p | max_output_tokens |
| Groq | temperature | top_p | max_completion_tokens |
| OpenRouter (chat) | temperature | topP | maxCompletionTokens |
| Ollama | temperature | top_p | num_predict (nested in options) |
| BytePlus | temperature | top_p | max_tokens (or max_completion_tokens) |
temperature is the one key every provider names identically; token limits and
some sampling options use provider-native names. Ollama nests all sampling under
modelOptions.options.
Anthropic
max_tokensdefault: Anthropic's API requiresmax_tokens, so the adapter always sends one. When you omitmodelOptions.max_tokens, it defaults to the selected model's full output ceiling (itsmax_output_tokensfrom model metadata — e.g. 64K for Sonnet, 128K for Opus), not a low constant.max_tokensis a ceiling, not a reservation (billing is per token generated), so leaving it unset is the right default for codegen / agentic / long-form output and avoids silentstop_reason: "max_tokens"truncation. Set it only to cap output below the model ceiling. Other providers treat token limits as optional and don't apply this flooring.
supportsCombinedToolsAndSchemaAdapters can declare an optional capability method:
import { AnthropicTextAdapter } from '@tanstack/ai-anthropic'
// The TextAdapter contract:
// supportsCombinedToolsAndSchema?: (modelOptions?: TProviderOptions) => boolean
// Subclasses override it to narrow the capability:
class LegacyPathAnthropic extends AnthropicTextAdapter<'claude-sonnet-4-6'> {
override supportsCombinedToolsAndSchema(): boolean {
return false
}
}
When true, the engine wires outputSchema into the regular
chatStream call alongside tools and harvests the schema-constrained
JSON from the agent loop's final-turn text — skipping the separate
structuredOutput / structuredOutputStream finalization round-trip.
When false (or the method is omitted), the legacy finalization path
runs.
Current per-adapter status (#605):
| Adapter | Returns |
|---|---|
openaiText / openaiChatCompletions | true (all supported models) |
anthropicText | true for Claude 4.5+ (gated by ANTHROPIC_COMBINED_TOOLS_AND_SCHEMA_MODELS), false otherwise |
geminiText | true for Gemini 3.x (gated by GEMINI_COMBINED_TOOLS_AND_SCHEMA_MODELS), false otherwise |
grokText | true (all chat models — inherits the OpenAI Responses base; no per-model gate) |
groqText | false (Groq API rejects schema + tools + stream) |
openRouterText / openRouterResponsesText | Per model — true only when the model and every modelOptions.models fallback are in OPENROUTER_COMBINED_TOOLS_AND_SCHEMA_MODELS |
ollamaText | false (constrained-decoding vs tool-call grammar conflict) |
byteplusText | Per model — true only for the 10 ids in BYTEPLUS_STRUCTURED_OUTPUT_CHAT_MODELS, false otherwise |
Subclasses can override to narrow the capability. When extending an
adapter for a custom model that doesn't support the combination, return
false explicitly.
BytePlus has no JSON-mode fallback. On the 8 chat models outside
BYTEPLUS_STRUCTURED_OUTPUT_CHAT_MODELS, Ark rejectsjson_schemaandjson_object, sofalsehere does not buy a degraded path — it only keepsresponse_formatout of the streaming chat request.structuredOutput()throws andstructuredOutputStream()emitsRUN_ERRORon those models. Noteseed-2-0-lite-260428(the obvious default) is one of them; useseed-2-0-lite-260228ordola-seed-2-1-turbo-260628for typed output.
JSON mode (
json_object) is never combined. Some OpenRouter and Groq models reject strictjson_schemaand accept onlyjson_object(OPENROUTER_JSON_OBJECT_ONLY_MODELS, Groq models whosefeatureslistjson_objectwithoutjson_schema). For these, structured output sendsjson_objectand adds the schema as a system prompt. A caller can also set JSON mode inmodelOptions(responseFormat/text.formaton OpenRouter,response_format/text.formaton OpenAI-base adapters). In JSON modesupportsCombinedToolsAndSchema()returnsfalse, so the schema prompt goes only on the separate finalization request. A subclass of the OpenAI-base adapters marks its own JSON-only models by overridingprotected modelPrefersJsonObject(model: string): boolean. With a plain JSON SchemaoutputSchema, TanStack AI does not validate the reply; use a Standard Schema (Zod, Valibot, ArkType) to get it validated.
Any provider that implements the OpenAI Chat Completions API (DeepSeek,
Moonshot/Kimi, Together, Fireworks, Cerebras, Qwen/DashScope, Perplexity,
NVIDIA NIM, LM Studio, etc.) can be used through the generic
openaiCompatible factory from @tanstack/ai-openai/compatible — no
dedicated package required.
import { openaiCompatible } from '@tanstack/ai-openai/compatible'
import { chat, createModel } from '@tanstack/ai'
const messages = [{ role: 'user' as const, content: 'Hello' }]
// Provider-factory: configure baseURL + apiKey + models ONCE,
// then select a model per call (the model arg is a type-safe union).
const deepseek = openaiCompatible({
name: 'deepseek', // optional label for devtools/errors (default 'openai-compatible')
baseURL: 'https://api.deepseek.com/v1',
apiKey: process.env.DEEPSEEK_API_KEY!,
models: [
'deepseek-chat', // bare string → optimistic defaults: text/image in, streaming, tools, structured output
createModel('deepseek-reasoner', {
// rich def → precise per-model capabilities
input: ['text'],
features: ['reasoning', 'structured_outputs'],
}),
],
})
chat({ adapter: deepseek('deepseek-chat'), messages, threadId: 'thread-1' })
chat({ adapter: deepseek('deepseek-reasoner'), messages, threadId: 'thread-1' })
config also accepts any OpenAI SDK ClientOptions (notably defaultHeaders
and defaultQuery) for providers that need extra auth headers or query params.
For a single model, use the one-shot helper:
import { openaiCompatibleText } from '@tanstack/ai-openai/compatible'
import { chat } from '@tanstack/ai'
const messages = [{ role: 'user' as const, content: 'Hello' }]
chat({
adapter: openaiCompatibleText('deepseek-chat', {
baseURL: 'https://api.deepseek.com/v1',
apiKey: process.env.DEEPSEEK_API_KEY!,
}),
messages,
threadId: 'thread-1',
})
Pass api: 'responses' to target the OpenAI Responses API instead of Chat
Completions (only for the rare compatible provider that implements it, e.g.
Azure OpenAI); the default is 'chat-completions', which is what nearly all
compatible providers speak.
Verify the provider's current
baseURLand model ids against its live docs — they drift. Seedocs/adapters/openai-compatible.mdfor the full provider table.
Four providers expose a native Files/storage API as a tree-shakeable files
adapter: openaiFiles(), anthropicFiles(), geminiFiles() (each reads the
same env var as the provider's text adapter; create*Files(apiKey) variants
take an explicit key), and falFiles(config). Upload media once with
uploadFile(), then reference the returned FileHandle in messages via a
{ type: 'file' } content source instead of re-sending base64 each request:
import { chat, fileSourceFromHandle, uploadFile } from '@tanstack/ai'
import { openaiFiles, openaiText } from '@tanstack/ai-openai'
import { pdfBase64 } from './pdf-data'
const handle = await uploadFile({
adapter: openaiFiles(),
input: { data: pdfBase64, mimeType: 'application/pdf' },
})
chat({
adapter: openaiText('gpt-5.5'),
messages: [
{
role: 'user',
content: [
{ type: 'text', content: 'Summarize this document' },
{ type: 'document', source: fileSourceFromHandle(handle) },
],
},
],
threadId: 'thread-1',
})
Rules agents must respect:
fileSourceFromHandle
builds { type: 'file', value: 'file-…', provider: 'openai' }, matching the
AG-UI FileSource arm. A handle only resolves at the provider that issued
it, so an adapter throws when provider names a different adapter.
provider is optional, as on the AG-UI wire; a source without it is taken
as-is. To use the same bytes with two providers,
upload to each and send the matching handle.supportsFileSources. For adapters that don't (Groq, Bedrock, Mistral, OpenRouter, Ollama, BytePlus, Cohere, and anything
written before this feature), chat() / generateImage() /
generateVideo() / embed() reject file sources in preflight, before any
request is built — pass data/url sources there instead.getFile() / deleteFile() work for OpenAI, Anthropic,
Gemini, and Grok, and accept the handle itself (provider-literal typed, so a
foreign handle is a compile error). fal storage is upload-only, so those
calls throw for falFiles(). grokFiles().get() mints the public URL again,
so do not call it after revokePublicUrl().images/edits + Sora input_reference, Gemini Veo, and Chat Completions
image inputs throw endpoint-specific errors for file sources.fileSourceFromHandle(handle) straight into the sendMessage content, and
the server passes the messages to chat() as usual. fileSourceFromHandle
and the FileHandle type are exported from the browser-safe
@tanstack/ai/client entry.See docs/advanced/files-api.md for the full guide.
Every adapter's client config accepts baseURL and defaultHeaders. Use these
two names to route any adapter through Cloudflare AI Gateway, Vercel AI Gateway,
or a corporate proxy. The adapter maps them onto the vendor SDK's own option
names (Gemini httpOptions, Mistral serverURL, Ollama host, Cohere and
ElevenLabs baseUrl/headers). The vendor names still work; when both are
set, baseURL and defaultHeaders win.
import { createGeminiChat } from '@tanstack/ai-gemini'
const gateway = {
baseURL: 'https://gateway.example.com/google-ai-studio',
defaultHeaders: {
'cf-aig-authorization': `Bearer ${process.env.GATEWAY_TOKEN}`,
},
}
createGeminiChat('gemini-3.8-flash', process.env.GOOGLE_API_KEY!, {
...gateway,
})
The legacy openai() (and anthropic(), etc.) monolithic adapters were
removed in 1.0. They took the model in chat(), not in the factory.
// WRONG: Legacy monolithic adapter pattern (no longer exported)
import { openai } from '@tanstack/ai-openai'
chat({ adapter: openai(), model: 'gpt-5.2', messages, threadId: 'thread-1' })
// CORRECT: Tree-shakeable adapter, model in factory
import { chat } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
const messages = [{ role: 'user' as const, content: 'Hello' }]
chat({ adapter: openaiText('gpt-5.2'), messages, threadId: 'thread-1' })
Source: docs/migration/migration.md
Each provider uses a specific env var name. Using the wrong one causes a runtime error:
| Provider | Correct Env Var | Common Mistake |
|---|---|---|
| OpenAI | OPENAI_API_KEY | |
| Anthropic | ANTHROPIC_API_KEY | |
| Gemini | GOOGLE_API_KEY or GEMINI_API_KEY | GOOGLE_GENAI_API_KEY (does not work) |
| Grok (xAI) | XAI_API_KEY | GROK_API_KEY (does not work) |
| Groq | GROQ_API_KEY | |
| OpenRouter | OPENROUTER_API_KEY | |
| Ollama | OLLAMA_HOST | No API key needed, just the host URL (default: http://localhost:11434) |
| Bedrock | BEDROCK_API_KEY / AWS_BEARER_TOKEN_BEDROCK | Falls back to SigV4 credentials when no API key is set |
Source: adapter source code (utils/client.ts in each adapter package).
Detailed per-adapter reference files:
HIGH Tension: Type safety vs. quick prototyping -- Per-model type safety
requires specific model string literals. Quick prototyping wants dynamic
selection with string variables. Agents optimizing for quick setup silently
lose type safety. If model names come from user input or config files, use
extendAdapter() to add custom names.
ai-core/chat-experience/SKILL.md -- Adapter choice affects chat setupai-core/structured-outputs/SKILL.md -- outputSchema handles provider differences transparentlySkills that do similar work, with their headline benchmark lift.
From the same source and nearby in the catalogue.