Skip to content
204 changes: 102 additions & 102 deletions docs/api-reference/veryfront/chat.md

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion src/agent/react/use-chat/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,8 @@ export interface UseChatResult {
setInput: (input: string) => void;
/** Change the model for subsequent requests */
setModel: (model: string | undefined) => void;
sendMessage: (message: { text: string; files?: ChatFilePart[] }) => Promise<void>;
/** Send a message. A message model overrides the session model for this request only. */
sendMessage: (message: { text: string; files?: ChatFilePart[]; model?: string }) => Promise<void>;
/** Edit a user message and resubmit — truncates history to that point */
editMessage: (messageId: string, newText: string) => Promise<void>;
/** Get branch info for a message (returns { current, total }; total=1 if no branches) */
Expand Down
33 changes: 33 additions & 0 deletions src/agent/react/use-chat/use-chat.status.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,39 @@ describe("react/agent/useChat status lifecycle", () => {
}
});

it("uses a per-message model override for the request and response metadata", async () => {
const restoreDom = installDom();
const originalFetch = globalThis.fetch;
let requestBody: { model?: string } | undefined;
globalThis.fetch = (_input, init) => {
requestBody = JSON.parse(String((init as { body?: unknown } | undefined)?.body));
return Promise.resolve(sseResponse());
};
let latest: UseChatResult | null = null;

function Capture(): null {
latest = useChat({ api: "/api/ag-ui", model: "session-model" });
return null;
}

const root = createRoot(document.getElementById("root")!);
try {
flushSync(() => root.render(<Capture />));
await latest!.sendMessage({ text: "Hello", model: "flat-model" });
await settle();

assertEquals(requestBody?.model, "flat-model");
const assistant = latest!.messages.find((message) => message.role === "assistant");
assert(assistant, "the streamed assistant response should be retained");
assertEquals(assistant.metadata?.model, "flat-model");
} finally {
flushSync(() => root.unmount());
await settle();
globalThis.fetch = originalFetch;
restoreDom();
}
});

it("keeps a successful client tool output persistable", async () => {
const restoreDom = installDom();
let latest: UseChatResult | null = null;
Expand Down
6 changes: 4 additions & 2 deletions src/agent/react/use-chat/use-chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ function useChatState(options: UseChatOptions): ResettableUseChatResult {
message: {
text: string;
files?: ChatFilePart[];
model?: string;
baseMessages?: ChatMessage[];
userMessageId?: string;
},
Expand All @@ -241,6 +242,7 @@ function useChatState(options: UseChatOptions): ResettableUseChatResult {
let didError = false;
try {
const allMessages = [...base, userMessage];
const requestModel = message.model ?? model;

const abortController = new AbortController();
abortControllerRef.current = abortController;
Expand All @@ -257,7 +259,7 @@ function useChatState(options: UseChatOptions): ResettableUseChatResult {
credentials: options.credentials,
body: JSON.stringify({
messages: allMessages,
...(model ? { model } : {}),
...(requestModel ? { model: requestModel } : {}),
...options.body,
}),
signal: abortController.signal,
Expand Down Expand Up @@ -301,7 +303,7 @@ function useChatState(options: UseChatOptions): ResettableUseChatResult {
setStreamingMessageId(currentMessageIdRef.current);
};
// Mutable local — updated by onData before onMessage/onUpdate use it.
let serverModel: string | undefined = model;
let serverModel: string | undefined = requestModel;
setActiveModel((current) =>
isLatestRequest(requestIdRef.current, requestId) ? undefined : current
);
Expand Down
Loading