- Node
- Python
The
@thesysai/genui-sdk package provides a makeC1Response function that can be used to add data related to thinking states to the response.Here’s a simple example of how to use thinking states:1
Create a c1Response object
Use the
makeC1Response function to create a c1Response object by importing it from the @thesysai/genui-sdk package, and start writing the LLM response
content to this object:app/api/chat/route.ts
import { NextRequest, NextResponse } from "next/server";
import OpenAI from "openai";
import type { ChatCompletionMessageParam } from "openai/resources.mjs";
import { transformStream } from "@crayonai/stream";
import { getMessageStore } from "./messageStore";
import { makeC1Response } from "@thesysai/genui-sdk/server";
export async function POST(req: NextRequest) {
const c1Response = makeC1Response();
const { prompt, threadId, responseId } = (await req.json()) as {
prompt: ChatCompletionMessageParam;
threadId: string;
responseId: string;
};
const client = new OpenAI({
baseURL: "https://api.thesys.dev/v1/embed",
apiKey: process.env.THESYS_API_KEY, // Use the API key you created in the previous step
});
const messageStore = getMessageStore(threadId);
messageStore.addMessage(prompt);
const llmStream = await client.chat.completions.create({
model: "c1/anthropic/claude-sonnet-4/v-20251230",
messages: messageStore.getOpenAICompatibleMessageList(),
stream: true,
});
// Unwrap the OpenAI stream to a C1 stream
transformStream(
llmStream,
(chunk) => {
const contentDelta = chunk.choices[0].delta.content;
if (contentDelta) {
c1Response.writeContent(contentDelta);
}
return contentDelta;
},
{
onEnd: ({ accumulated }) => {
c1Response.end(); // This is necessary to stop showing the "loading" state once the response is done streaming.
const message = accumulated.filter((chunk) => chunk).join("");
messageStore.addMessage({
id: responseId,
role: "assistant",
content: message,
});
},
}
) as ReadableStream<string>;
return new NextResponse(c1Response.responseStream, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache, no-transform",
Connection: "keep-alive",
},
});
}
2
Write a thinking state to the response object
To add a thinking state, use the
writeThinkItem method defined on the c1Response object:app/api/chat/route.ts
import { NextRequest, NextResponse } from "next/server";
import OpenAI from "openai";
import type { ChatCompletionMessageParam } from "openai/resources.mjs";
import { transformStream } from "@crayonai/stream";
import { getMessageStore } from "./messageStore";
import { makeC1Response } from "@thesysai/genui-sdk/server";
export async function POST(req: NextRequest) {
const c1Response = makeC1Response();
c1Response.writeThinkItem({
title: "Thinking...",
description: "Diving into the digital depths to craft you an answer.",
});
const { prompt, threadId, responseId } = (await req.json()) as {
prompt: ChatCompletionMessageParam;
threadId: string;
responseId: string;
};
const client = new OpenAI({
baseURL: "https://api.thesys.dev/v1/embed",
apiKey: process.env.THESYS_API_KEY, // Use the API key you created in the previous step
});
const messageStore = getMessageStore(threadId);
messageStore.addMessage(prompt);
const llmStream = await client.chat.completions.create({
model: "c1/anthropic/claude-sonnet-4/v-20251230",
messages: messageStore.getOpenAICompatibleMessageList(),
stream: true,
});
// Unwrap the OpenAI stream to a C1 stream
transformStream(
llmStream,
(chunk) => {
const contentDelta = chunk.choices[0].delta.content;
if (contentDelta) {
c1Response.writeContent(contentDelta);
}
return contentDelta;
},
{
onEnd: ({ accumulated }) => {
c1Response.end(); // This is necessary to stop showing the "loading" state once the response is done streaming.
const message = accumulated.filter((chunk) => chunk).join("");
messageStore.addMessage({
id: responseId,
role: "assistant",
content: message,
});
},
}
) as ReadableStream<string>;
return new NextResponse(c1Response.responseStream, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache, no-transform",
Connection: "keep-alive",
},
});
}
3
Use thinking states with long-running tool calls (optional)
If you’d like to use thinking states with long-running tool calls, simply call the aforementioned Next, pass the
writeThinkItem method inside the
tool call handler. For example, for the webSearch tool implemented in the Tool Calling guide, you can add a thinking state as follows:Next, modify the tool call handler to call a function that updates the thinking state:app/api/chat/tools.ts
import type { RunnableToolFunctionWithParse } from "openai/lib/RunnableFunction.mjs";
import { z } from "zod";
import { zodToJsonSchema } from "zod-to-json-schema";
import Exa from "exa-js";
import type { JSONSchema } from "openai/lib/jsonschema.mjs";
const exa = new Exa(process.env.EXA_API_KEY!);
export const getWebSearchTool = (
writeThinkingState: () => void
): RunnableToolFunctionWithParse<{ query: string }> => ({
type: "function",
function: {
name: "webSearch",
description: "Use this tool to perform a web search.",
parse: JSON.parse,
parameters: zodToJsonSchema(
z.object({
query: z.string().describe("The query to search for."),
})
) as JSONSchema,
function: async ({ query }: { query: string }) => {
writeThinkingState();
return await exa.search(query, { numResults: 5 });
},
strict: true,
},
});
writeThinkingState function to the tool call handler:app/api/chat/route.ts
const llmStream = await client.beta.chat.completions.runTools({
model: "c1/anthropic/claude-sonnet-4/v-20251230",
messages: [
{ role: "system", content: systemPrompt },
...messageStore.getOpenAICompatibleMessageList(),
],
stream: true,
tools: [
getWebSearchTool(() => {
c1Response.writeThinkItem({
title: "Searching the web...",
description:
"Scouring the digital universe for the most relevant and up-to-date insights.",
});
}),
],
toolChoice: "auto",
});
Full API route code with tool calling and thinking states
Full API route code with tool calling and thinking states
app/api/chat/route.ts
import { NextRequest, NextResponse } from "next/server";
import OpenAI from "openai";
import { transformStream } from "@crayonai/stream";
import { DBMessage, getMessageStore } from "./messageStore";
import { makeC1Response } from "@thesysai/genui-sdk/server";
import { getWebSearchTool } from "./tools";
import { systemPrompt } from "./systemPrompt";
export async function POST(req: NextRequest) {
const c1Response = makeC1Response();
c1Response.writeThinkItem({
title: "Thinking...",
description: "Diving into the digital depths to craft you an answer.",
});
const { prompt, threadId, responseId } = (await req.json()) as {
prompt: DBMessage;
threadId: string;
responseId: string;
};
const client = new OpenAI({
baseURL: "https://api.thesys.dev/v1/embed",
apiKey: process.env.THESYS_API_KEY,
});
const messageStore = getMessageStore(threadId);
messageStore.addMessage(prompt);
const llmStream = await client.beta.chat.completions.runTools({
model: "c1/anthropic/claude-sonnet-4/v-20251230",
messages: [
{ role: "system", content: systemPrompt },
...messageStore.getOpenAICompatibleMessageList(),
],
stream: true,
tools: [
getWebSearchTool(() => {
c1Response.writeThinkItem({
title: "Searching the web...",
description:
"Scouring the digital universe for the most relevant and up-to-date insights.",
});
}),
],
toolChoice: "auto",
});
transformStream(
llmStream,
(chunk) => {
const contentDelta = chunk.choices[0].delta.content;
if (contentDelta) {
c1Response.writeContent(contentDelta);
}
return contentDelta;
},
{
onEnd: ({ accumulated }) => {
c1Response.end();
const message = accumulated.filter((message) => message).join("");
messageStore.addMessage({
role: "assistant",
content: message,
id: responseId,
});
},
}
) as ReadableStream<string>;
return new NextResponse(c1Response.responseStream, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache, no-transform",
Connection: "keep-alive",
},
});
}
4
Add a custom think component (optional)
If you’d like to add a custom thinking state component, you can do so by passing a Example custom think component:You may pass your custom component to the
customizeC1 prop to the C1Chat component or the
useThreadManager hook.Your custom component should accept the following props:An array of thinking state items, where each item contains:
- title: The title of the thinking state
- content: The content/description of the thinking state
- ephemeral: Whether this thinking state should be temporary or persist after the response is done streaming
Indicates if a thinking state is active. Use this to display a loader or shimmer while processing.
import { ThinkComponent } from "@thesysai/genui-sdk";
import styles from "./styles.module.css";
const CustomThink: ThinkComponent = ({ thinkItems, thinkingInProgress }) => {
return (
<div className={styles.thinkContainer}>
<div className={styles.thinkTitle}>
{thinkingInProgress ? "Processing..." : "Processing complete!"}
</div>
<div className={styles.thinkItems}>
{thinkItems.map((item) => (
<div key={item.title} className={styles.thinkItem}>
{item.title}
</div>
))}
</div>
</div>
);
};
C1Chat component or the useThreadManager hook like this:<C1Chat
apiUrl="/api/chat"
customizeC1={{ thinkComponent: CustomThink }}
/>
const threadManager = useThreadManager({
// other threadManager parameters
customizeC1: { thinkComponent: CustomThink },
});
5
Test it out
You should now see the thinking state on the UI while the agent is processing the response:

The
thesys_genui_sdk package provides a C1Response class that can be used to add data related to thinking states to the response.
If you are using FastAPI, the package provides a handy decorator with_c1_response to make this even easier.1
Create a c1_response object
# main.py
from pydantic import BaseModel
from fastapi import FastAPI, Request
from thesys_genui_sdk.fast_api import with_c1_response
from thesys_genui_sdk.context import write_content, get_assistant_message
import openai
app = FastAPI()
openai_client = openai.OpenAI(
api_key=os.getenv("THESYS_API_KEY"),
base_url="https://api.thesys.dev/v1/embed",
)
@app.post("/chat")
# this decorator will add the c1_response in a context variable
# and internally return the stream from your endpoint.
@with_c1_response()
async def chat(request: ChatRequest):
await generate_llm_response(request)
class ChatRequest(BaseModel):
prompt: Prompt
threadId: str
responseId: str
async def generate_llm_response(request: ChatRequest):
stream = openai_client.chat.completions.create(
model="c1/anthropic/claude-sonnet-4/v-20251230",
messages=[{"role": "user", "content": request.prompt}],
stream=True,
)
async for chunk in stream:
content = chunk.choices[0].delta.content
if content:
await write_content(content)
# get_assistant_message() allows you to get the full response to store for message history
assistant_message_for_history = get_assistant_message()
# main.py
import asyncio
from thesys_genui_sdk import C1Response
import openai
openai_client = openai.OpenAI(
api_key=os.getenv("THESYS_API_KEY"),
base_url="https://api.thesys.dev/v1/embed",
)
async def generate_llm_response(c1_response: C1Response, prompt: str):
stream = openai_client.chat.completions.create(
model="c1/anthropic/claude-sonnet-4/v-20251230",
messages=[{"role": "user", "content": prompt}],
stream=True,
)
async for chunk in stream:
content = chunk.choices[0].delta.content
if content:
await c1_response.write_content(content)
# c1_response.get_assistant_message() allows you to
# get the full response to store for message history
assistant_message_for_history = c1_response.get_assistant_message()
await c1_response.end()
async def main():
c1_response = C1Response()
# In a web server, you would start an async task
# to generate the response
asyncio.create_task(generate_llm_response(c1_response, "Tell me about latest trends in AI."))
# This is the stream you'd return from your route
response_stream = c1_response.stream()
# Example of how to consume the stream
async for item in response_stream:
print(item, end="")
if __name__ == "__main__":
asyncio.run(main())
2
Write a thinking state to the response object
To add a thinking state, use the
write_think_item function. This is useful to let the user know that a long-running process is happening.# main.py
from fastapi import FastAPI, Request
from thesys_genui_sdk.fast_api import with_c1_response
from thesys_genui_sdk.context import write_content, get_assistant_message, write_think_item
import openai
app = FastAPI()
openai_client = openai.OpenAI(
api_key=os.getenv("THESYS_API_KEY"),
base_url="https://api.thesys.dev/v1/embed",
)
async def generate_llm_response(request: Request):
# ...
await write_think_item(
title="Thinking...",
description="Diving into the digital depths to craft you an answer."
)
stream = openai_client.chat.completions.create(
model="c1/anthropic/claude-sonnet-4/v-20251230",
messages=[{"role": "user", "content": request.prompt}],
stream=True,
)
# ...
# main.py
import asyncio
from thesys_genui_sdk.context import C1Response
import openai
openai_client = openai.OpenAI(
api_key=os.getenv("THESYS_API_KEY"),
base_url="https://api.thesys.dev/v1/embed",
)
async def generate_llm_response(c1_response: C1Response, prompt: str):
await c1_response.write_think_item(
title="Thinking...",
description="Diving into the digital depths to craft you an answer."
)
stream = openai_client.chat.completions.create(
model="c1/anthropic/claude-sonnet-4/v-20251230",
messages=[{"role": "user", "content": prompt}],
stream=True,
)
# ...
3
Use thinking states with long-running tool calls (optional)
If you are using tools, you can call
write_think_item before executing a long-running tool to provide feedback to the user.async def web_search(query: str):
await write_think_item(
title="Searching the web...",
description=f"Looking for information on '{query}'"
)
# ... perform web search
results = await some_search_api(query)
return results
async def web_search(query: str, c1_response: C1Response):
await c1_response.write_think_item(
title="Searching the web...",
description=f"Looking for information on '{query}'"
)
# ... perform web search
results = await some_search_api(query)
return results
4
Add a custom think component (optional)
This is a frontend customization and is independent of the backend implementation. You can follow the same guide as for Node.js by passing a Example custom think component:You may pass your custom component to the
customizeC1 prop to the C1Chat component or the useThreadManager hook.Your custom component should accept the following props:An array of thinking state items, where each item contains:
- title: The title of the thinking state
- content: The content/description of the thinking state
- ephemeral: Whether this thinking state should be temporary or persist after the response is done streaming
Indicates if a thinking state is active. Use this to display a loader or shimmer while processing.
import { ThinkComponent } from "@thesysai/genui-sdk";
import styles from "./styles.module.css";
const CustomThink: ThinkComponent = ({ thinkItems, thinkingInProgress }) => {
return (
<div className={styles.thinkContainer}>
<div className={styles.thinkTitle}>
{thinkingInProgress ? "Processing..." : "Processing complete!"}
</div>
<div className={styles.thinkItems}>
{thinkItems.map((item) => (
<div key={item.title} className={styles.thinkItem}>
{item.title}
</div>
))}
</div>
</div>
);
};
C1Chat component or the useThreadManager hook like this:<C1Chat
apiUrl="/api/chat"
customizeC1={{ thinkComponent: CustomThink }}
/>
const threadManager = useThreadManager({
// other threadManager parameters
customizeC1: { thinkComponent: CustomThink },
});
5
Test it out
You should now see the thinking state on the UI while the agent is processing the response:
