diff --git a/studio/frontend/src/components/assistant-ui/thread.tsx b/studio/frontend/src/components/assistant-ui/thread.tsx index c0d577b12d..431e568205 100644 --- a/studio/frontend/src/components/assistant-ui/thread.tsx +++ b/studio/frontend/src/components/assistant-ui/thread.tsx @@ -18,6 +18,7 @@ import { import { ToolFallback } from "@/components/assistant-ui/tool-fallback"; import { ToolGroup } from "@/components/assistant-ui/tool-group"; import { CodeExecutionToolUI } from "@/components/assistant-ui/tool-ui-code-execution"; +import { ImageGenerationToolUI } from "@/components/assistant-ui/tool-ui-image-generation"; import { PythonToolUI } from "@/components/assistant-ui/tool-ui-python"; import { TerminalToolUI } from "@/components/assistant-ui/tool-ui-terminal"; import { WebSearchToolUI } from "@/components/assistant-ui/tool-ui-web-search"; @@ -68,6 +69,7 @@ import { DownloadIcon, GlobeIcon, HeadphonesIcon, + ImageIcon, LightbulbIcon, LightbulbOffIcon, MicIcon, @@ -870,6 +872,42 @@ const CodeToolsToggle: FC = () => { ); }; +const ImagesToggle: FC = () => { + const modelLoaded = useChatRuntimeStore( + (s) => !!s.params.checkpoint && !s.modelLoading, + ); + // OpenAI cloud Responses-API models advertise image_generation as a + // server-side tool; no local runtime fallback exists. Mirror of + // shared-composer's imageDisabled / showImagePill so the in-thread + // composer surfaces the same control as the empty-state composer. + const supportsBuiltinImageGeneration = useChatRuntimeStore( + (s) => s.supportsBuiltinImageGeneration, + ); + const imageToolsEnabled = useChatRuntimeStore((s) => s.imageToolsEnabled); + const setImageToolsEnabled = useChatRuntimeStore( + (s) => s.setImageToolsEnabled, + ); + if (!supportsBuiltinImageGeneration) { + return null; + } + const disabled = !modelLoaded; + return ( + + ); +}; + const ToolStatusDisplay: FC = () => { const toolStatus = useChatRuntimeStore((s) => s.toolStatus); const isThreadRunning = useAuiState(({ thread }) => thread.isRunning); @@ -941,6 +979,7 @@ const ComposerAction: FC<{ disabled?: boolean; blockSend?: () => boolean }> = ({ +
@@ -1067,6 +1106,7 @@ const AssistantMessage: FC = () => { python: PythonToolUI, terminal: TerminalToolUI, code_execution: CodeExecutionToolUI, + image_generation: ImageGenerationToolUI, }, Fallback: ToolFallback, }, diff --git a/studio/frontend/src/components/assistant-ui/tool-ui-image-generation.tsx b/studio/frontend/src/components/assistant-ui/tool-ui-image-generation.tsx new file mode 100644 index 0000000000..246d8b6978 --- /dev/null +++ b/studio/frontend/src/components/assistant-ui/tool-ui-image-generation.tsx @@ -0,0 +1,132 @@ +// SPDX-License-Identifier: AGPL-3.0-only +// Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 + +"use client"; + +import { type ToolCallMessagePartComponent, useAuiState } from "@assistant-ui/react"; +import { ImageIcon, LoaderIcon } from "lucide-react"; +import { memo, useEffect, useState } from "react"; +import { + ToolFallbackContent, + ToolFallbackRoot, + ToolFallbackTrigger, +} from "./tool-fallback"; + +/** + * Renders the synthetic `_toolEvent` chunks emitted by + * `_stream_openai_responses` when OpenAI's Responses-API + * `image_generation` tool fires. The backend stashes the base64 + * PNG/WebP/JPEG (the gpt-image backbone output) on an `image_b64` + * field of the tool_end event so the JSON result stays small, and the + * adapter repackages it into a structured `result` shape: + * + * { + * image_b64: string, + * image_mime: string, // e.g. "image/png" + * size?: string, // "1024x1024" etc + * quality?: string, + * background?: string, + * } + * + * The corresponding `tool_start` carries the prompt as + * `args.prompt` (after gpt-image's revision pass) plus `args.kind: + * "image"`. Without this component the generic ToolFallback would + * print the prompt as JSON args text with an empty Result block -- + * which is exactly the "no image" symptom users hit before this UI + * landed. + */ +interface ImageGenerationArgs { + prompt?: string; + kind?: string; +} + +interface ImageGenerationResult { + image_b64?: string; + image_mime?: string; + size?: string; + quality?: string; + background?: string; +} + +const ImageGenerationToolUIImpl: ToolCallMessagePartComponent = ({ + args, + result, + status, +}) => { + const parsedArgs = (args as ImageGenerationArgs) ?? {}; + const prompt = parsedArgs.prompt ?? ""; + const isRunning = status?.type === "running"; + + const isImageResult = + !!result && + typeof result === "object" && + typeof (result as ImageGenerationResult).image_b64 === "string"; + const imageResult = isImageResult ? (result as ImageGenerationResult) : null; + const mime = imageResult?.image_mime || "image/png"; + const imageSrc = imageResult?.image_b64 + ? `data:${mime};base64,${imageResult.image_b64}` + : null; + + // Collapse the card once the model has resumed streaming prose + // after the image. Mirrors CodeExecutionToolUI so the inline image + // doesn't collapse mid-stream and the user can click to re-expand. + const hasText = useAuiState(({ message }) => + message.content.some( + (p) => + p.type === "text" && + "text" in p && + (p as { text: string }).text.length > 0, + ), + ); + const [open, setOpen] = useState(true); + useEffect(() => { + if (isRunning) { + setOpen(true); + } else if (hasText && !imageSrc) { + setOpen(false); + } + }, [isRunning, hasText, imageSrc]); + + const runningLabel = "Generating image…"; + const completedLabel = prompt + ? prompt.length > 80 + ? `Generated image: ${prompt.slice(0, 80)}…` + : `Generated image: ${prompt}` + : "Generated image"; + + return ( + + + + {isRunning && !imageSrc ? ( +
+ + {runningLabel} +
+ ) : imageSrc ? ( +
+ {prompt + {prompt ? ( +
+ {prompt} +
+ ) : null} +
+ ) : null} +
+
+ ); +}; + +export const ImageGenerationToolUI = memo( + ImageGenerationToolUIImpl, +) as unknown as ToolCallMessagePartComponent; +ImageGenerationToolUI.displayName = "ImageGenerationToolUI"; diff --git a/studio/frontend/src/features/chat/api/chat-adapter.ts b/studio/frontend/src/features/chat/api/chat-adapter.ts index c214c17c0a..0521ca2e10 100644 --- a/studio/frontend/src/features/chat/api/chat-adapter.ts +++ b/studio/frontend/src/features/chat/api/chat-adapter.ts @@ -23,6 +23,7 @@ import { getExternalReasoningCapabilities, getProviderCapabilities, providerSupportsBuiltinCodeExecution, + providerSupportsBuiltinImageGeneration, providerSupportsBuiltinWebFetch, providerSupportsBuiltinWebSearch, } from "../provider-capabilities"; @@ -830,7 +831,7 @@ export function createOpenAIStreamAdapter(): ChatModelAdapter { // Re-read store after potential auto-load / model ready wait runtime = useChatRuntimeStore.getState(); const { params } = runtime; - const { supportsTools, toolsEnabled, codeToolsEnabled } = runtime; + const { supportsTools, toolsEnabled, codeToolsEnabled, imageToolsEnabled } = runtime; const externalSelection = parseExternalModelId(params.checkpoint); const isExternalRequest = externalSelection !== null; if ( @@ -900,6 +901,20 @@ export function createOpenAIStreamAdapter(): ChatModelAdapter { externalProvider && providerSupportsBuiltinWebFetch(externalProvider.providerType), ); + // OpenAI Responses-API image_generation server tool. Pill is + // gated on OpenAI cloud + a Responses-API model id; the backend + // additionally re-checks is_openai_cloud before appending + // {type:"image_generation"} to the request tools array. + const imageGenerationEnabledForThisTurn = Boolean( + externalProvider && + externalSelection && + imageToolsEnabled && + providerSupportsBuiltinImageGeneration( + externalProvider.providerType, + externalSelection.modelId, + externalProvider.baseUrl, + ), + ); const outboundMessages = messages .map(toOpenAIMessage) @@ -1396,7 +1411,8 @@ export function createOpenAIStreamAdapter(): ChatModelAdapter { // body["tools"] inside _stream_anthropic. ...(webSearchEnabledForThisTurn || webFetchEnabledForThisTurn || - codeExecEnabledForThisTurn + codeExecEnabledForThisTurn || + imageGenerationEnabledForThisTurn ? { enable_tools: true, enabled_tools: [ @@ -1410,6 +1426,13 @@ export function createOpenAIStreamAdapter(): ChatModelAdapter { // tool. There is no separate UI toggle yet. ...(webFetchEnabledForThisTurn ? ["web_fetch"] : []), ...(codeExecEnabledForThisTurn ? ["code_execution"] : []), + // OpenAI Responses-API only: `image_generation` + // returns inline image_generation_call output + // items; the backend's _stream_openai_responses + // path translates them to assistant tool events. + ...(imageGenerationEnabledForThisTurn + ? ["image_generation"] + : []), ], } : {}), @@ -1587,8 +1610,36 @@ export function createOpenAIStreamAdapter(): ChatModelAdapter { const imgIdx = rawResult.lastIndexOf(imgMarker); let parsedResult: | string - | { text: string; images: string[]; sessionId: string }; - if (imgIdx !== -1) { + | { text: string; images: string[]; sessionId: string } + | { + image_b64: string; + image_mime: string; + size?: string; + quality?: string; + background?: string; + }; + const imageB64 = toolEvent.image_b64 as string | undefined; + if ( + toolCallParts[idx].toolName === "image_generation" && + typeof imageB64 === "string" && + imageB64 + ) { + // OpenAI Responses image_generation_call: the + // backend stashes the base64 PNG/WebP/JPEG on + // separate `image_b64` / `image_mime` fields on + // the synthetic _toolEvent so the JSON result + // string stays small enough to log. Repackage as + // a structured result for the dedicated tool UI. + parsedResult = { + image_b64: imageB64, + image_mime: + (toolEvent.image_mime as string | undefined) ?? + "image/png", + size: toolEvent.size as string | undefined, + quality: toolEvent.quality as string | undefined, + background: toolEvent.background as string | undefined, + }; + } else if (imgIdx !== -1) { const text = rawResult.slice(0, imgIdx); // Fall back to "_default" to match the backend sandbox directory // used when no session_id is provided (see tools.py _get_workdir). diff --git a/studio/frontend/src/features/chat/chat-page.tsx b/studio/frontend/src/features/chat/chat-page.tsx index 2f062568dd..0943e26313 100644 --- a/studio/frontend/src/features/chat/chat-page.tsx +++ b/studio/frontend/src/features/chat/chat-page.tsx @@ -56,6 +56,7 @@ import { getExternalReasoningCapabilities, getProviderCapabilities, providerSupportsBuiltinCodeExecution, + providerSupportsBuiltinImageGeneration, providerSupportsBuiltinWebSearch, } from "./provider-capabilities"; import { ChatRuntimeProvider } from "./runtime-provider"; @@ -68,6 +69,7 @@ import { } from "./shared-composer"; import { CHAT_CODE_TOOLS_ENABLED_KEY, + CHAT_IMAGE_TOOLS_ENABLED_KEY, CHAT_TOOLS_ENABLED_KEY, loadOptionalBool, useChatRuntimeStore, @@ -771,6 +773,12 @@ export function ChatPage(): ReactElement { selection.modelId, provider?.baseUrl, ); + const supportsBuiltinImageGeneration = + providerSupportsBuiltinImageGeneration( + provider?.providerType, + selection.modelId, + provider?.baseUrl, + ); // Kimi's k2.6/k2.5 default to thinking enabled on the server side // (per https://platform.kimi.ai/docs/models). Mirror that default // in the UI so the Think pill comes up clicked when the user picks @@ -790,6 +798,9 @@ export function ChatPage(): ReactElement { provider?.providerType === "openai"); const storedToolsEnabled = loadOptionalBool(CHAT_TOOLS_ENABLED_KEY); const storedCodeToolsEnabled = loadOptionalBool(CHAT_CODE_TOOLS_ENABLED_KEY); + const storedImageToolsEnabled = loadOptionalBool( + CHAT_IMAGE_TOOLS_ENABLED_KEY, + ); const nextToolsEnabled = supportsBuiltinWebSearch ? isKimi ? false @@ -811,19 +822,25 @@ export function ChatPage(): ReactElement { : state.reasoningEnabled, supportsPreserveThinking: false, // External models never give us a local tool runtime (no - // python sandbox), so `supportsTools` must be false. The two + // python sandbox), so `supportsTools` must be false. The three // `supportsBuiltin*` flags pick up the slack for providers that // run the tool server-side: `supportsBuiltinWebSearch` lights // up the Search pill (OpenAI / Anthropic / OpenRouter / Kimi), // `supportsBuiltinCodeExecution` lights up the Code pill - // (Anthropic Claude 4.x only, today). + // (Anthropic Claude 4.x and OpenAI gpt-5.5), and + // `supportsBuiltinImageGeneration` lights up the Images pill + // (OpenAI cloud Responses-API models only). supportsTools: false, supportsBuiltinWebSearch, supportsBuiltinCodeExecution, + supportsBuiltinImageGeneration, toolsEnabled: nextToolsEnabled, codeToolsEnabled: supportsBuiltinCodeExecution ? (storedCodeToolsEnabled ?? false) : false, + imageToolsEnabled: supportsBuiltinImageGeneration + ? (storedImageToolsEnabled ?? false) + : false, }); }, [externalProvidersForChat, inferenceParams.checkpoint]); const canCompare = useMemo(() => { @@ -988,6 +1005,12 @@ export function ChatPage(): ReactElement { selectedExternal?.modelId, selectedProvider?.baseUrl, ); + const supportsBuiltinImageGeneration = + providerSupportsBuiltinImageGeneration( + selectedProvider?.providerType, + selectedExternal?.modelId, + selectedProvider?.baseUrl, + ); // See sibling useEffect above: Kimi's k2.x default to thinking // enabled, so the Think pill comes up clicked. Search pill stays // off by default; mutual exclusion flips them via the composer. @@ -1003,6 +1026,9 @@ export function ChatPage(): ReactElement { const storedCodeToolsEnabled = loadOptionalBool( CHAT_CODE_TOOLS_ENABLED_KEY, ); + const storedImageToolsEnabled = loadOptionalBool( + CHAT_IMAGE_TOOLS_ENABLED_KEY, + ); const nextToolsEnabled = supportsBuiltinWebSearch ? isKimi ? false @@ -1029,18 +1055,24 @@ export function ChatPage(): ReactElement { : store.reasoningEnabled, supportsPreserveThinking: false, // External models have no local tool runtime → supportsTools - // stays false. The two supportsBuiltin* flags carry the + // stays false. The three supportsBuiltin* flags carry the // server-side capability info for each pill: // - Search → providerSupportsBuiltinWebSearch // - Code → providerSupportsBuiltinCodeExecution - // (Anthropic Claude 4.x only, today) + // (Anthropic Claude 4.x + OpenAI gpt-5.5) + // - Images → providerSupportsBuiltinImageGeneration + // (OpenAI cloud Responses-API models) supportsTools: false, supportsBuiltinWebSearch, supportsBuiltinCodeExecution, + supportsBuiltinImageGeneration, toolsEnabled: nextToolsEnabled, codeToolsEnabled: supportsBuiltinCodeExecution ? (storedCodeToolsEnabled ?? false) : false, + imageToolsEnabled: supportsBuiltinImageGeneration + ? (storedImageToolsEnabled ?? false) + : false, ...(stillOnOpenRouterFree ? {} : { lastOpenRouterChosenModel: null }), }); return; diff --git a/studio/frontend/src/features/chat/provider-capabilities.ts b/studio/frontend/src/features/chat/provider-capabilities.ts index 8b16c2c85f..da1d6e3431 100644 --- a/studio/frontend/src/features/chat/provider-capabilities.ts +++ b/studio/frontend/src/features/chat/provider-capabilities.ts @@ -217,6 +217,44 @@ export function providerSupportsBuiltinCodeExecution( return false; } +/** + * Whether the selected external provider/model exposes OpenAI's + * Responses-API server-side image_generation tool. Lit on for OpenAI + * cloud (`api.openai.com`) when the picked model is a Responses-API + * family id (gpt-5.x today). The backend additionally gates on + * `is_openai_cloud`; mirror that here so the pill is hidden on custom + * OpenAI-compat backends (ollama / llama.cpp / vLLM) that report + * `provider_type="openai"` but would 400 on a `{type:"image_generation"}` + * tool. See backend/core/inference/external_provider.py near line 2770 + * for the dispatch and backend/tests/test_openai_image_generation.py + * for the round-trip coverage. + */ +const OPENAI_IMAGE_GENERATION_MODEL_PREFIXES = [ + "gpt-5.5-pro", + "gpt-5.5", + "gpt-5.4-pro", + "gpt-5.4", + "gpt-5.3", + "gpt-5.2", + "gpt-5.1", + "gpt-5", + "o3", +] as const; + +export function providerSupportsBuiltinImageGeneration( + providerType: string | null | undefined, + modelId: string | null | undefined, + baseUrl?: string | null, +): boolean { + if (providerType !== "openai") return false; + if (!isOpenAICloudBaseUrl(baseUrl)) return false; + const normalized = modelId?.trim().toLowerCase() ?? ""; + if (!normalized) return false; + return OPENAI_IMAGE_GENERATION_MODEL_PREFIXES.some((prefix) => + normalized.startsWith(prefix), + ); +} + /** * Per-provider minimum on the outbound max_tokens. Kimi's docs require * `max_tokens >= 16000` whenever a thinking model is in use so the diff --git a/studio/frontend/src/features/chat/shared-composer.tsx b/studio/frontend/src/features/chat/shared-composer.tsx index 57a8a132d7..246fd81510 100644 --- a/studio/frontend/src/features/chat/shared-composer.tsx +++ b/studio/frontend/src/features/chat/shared-composer.tsx @@ -21,7 +21,7 @@ import { isTauri } from "@/lib/api-base"; import { isMultimodalResponse } from "./types/api"; import { getImageInputUnavailableReason } from "./utils/image-input-support"; import { useAui } from "@assistant-ui/react"; -import { ArrowUpIcon, GlobeIcon, HeadphonesIcon, LightbulbIcon, LightbulbOffIcon, MicIcon, PlusIcon, SquareIcon, XIcon } from "lucide-react"; +import { ArrowUpIcon, GlobeIcon, HeadphonesIcon, ImageIcon, LightbulbIcon, LightbulbOffIcon, MicIcon, PlusIcon, SquareIcon, XIcon } from "lucide-react"; import { toast } from "@/lib/toast"; import { loadModel, validateModel } from "./api/chat-api"; import { parseExternalModelId, providerTypeSupportsVision } from "./external-providers"; @@ -33,6 +33,7 @@ import { import { getExternalReasoningCapabilities, providerSupportsBuiltinCodeExecution, + providerSupportsBuiltinImageGeneration, } from "./provider-capabilities"; import { type CompositionEvent, @@ -331,6 +332,10 @@ export function SharedComposer({ const setToolsEnabled = useChatRuntimeStore((s) => s.setToolsEnabled); const codeToolsEnabled = useChatRuntimeStore((s) => s.codeToolsEnabled); const setCodeToolsEnabled = useChatRuntimeStore((s) => s.setCodeToolsEnabled); + const imageToolsEnabled = useChatRuntimeStore((s) => s.imageToolsEnabled); + const setImageToolsEnabled = useChatRuntimeStore( + (s) => s.setImageToolsEnabled, + ); const lastOpenRouterChosenModel = useChatRuntimeStore( (s) => s.lastOpenRouterChosenModel, ); @@ -416,10 +421,22 @@ export function SharedComposer({ effectiveExternalModelId, selectedExternalProvider?.baseUrl, ); + const supportsBuiltinImageGeneration = providerSupportsBuiltinImageGeneration( + selectedExternalProvider?.providerType, + effectiveExternalModelId, + selectedExternalProvider?.baseUrl, + ); const searchDisabled = !modelLoaded || !(supportsTools || supportsBuiltinWebSearch); const codeDisabled = !modelLoaded || !(supportsTools || supportsBuiltinCodeExecution); + // Images pill is only ever lit on OpenAI cloud's Responses-API models. + // No local tool runtime fallback because the only image-generation + // server tool we wire today is OpenAI's; local models cannot dispatch + // it. Hidden entirely when the active model does not advertise it so + // the pill row stays compact for providers without the capability. + const imageDisabled = !modelLoaded || !supportsBuiltinImageGeneration; + const showImagePill = supportsBuiltinImageGeneration; // Backwards-compatible alias for any other call site that may still // reference `toolsDisabled` (rare; both pills used it before). const toolsDisabled = codeDisabled; @@ -1074,6 +1091,21 @@ export function SharedComposer({ Code + {showImagePill && ( + + )}
{dictationSupported && ( diff --git a/studio/frontend/src/features/chat/stores/chat-runtime-store.ts b/studio/frontend/src/features/chat/stores/chat-runtime-store.ts index 287dd1fa8b..a00b53a44c 100644 --- a/studio/frontend/src/features/chat/stores/chat-runtime-store.ts +++ b/studio/frontend/src/features/chat/stores/chat-runtime-store.ts @@ -24,6 +24,8 @@ const HF_TOKEN_KEY = "unsloth_hf_token"; export const CHAT_REASONING_ENABLED_KEY = "unsloth_chat_reasoning_enabled"; export const CHAT_TOOLS_ENABLED_KEY = "unsloth_chat_tools_enabled"; export const CHAT_CODE_TOOLS_ENABLED_KEY = "unsloth_chat_code_tools_enabled"; +export const CHAT_IMAGE_TOOLS_ENABLED_KEY = "unsloth_chat_image_tools_enabled"; + // External provider selection is encoded into `params.checkpoint` as // `external::::`. PersistedChatSettings deliberately // Omits `checkpoint` because the local-model side is mirrored by the @@ -253,8 +255,16 @@ type ChatRuntimeStore = { * execution server-side. Read by both composers' Code pill gate. */ supportsBuiltinCodeExecution: boolean; + /** + * Whether the active external provider exposes a server-side + * image-generation tool (OpenAI's Responses-API `image_generation` + * today). Gates the chat composer's Images pill. Local models never + * receive the tool because their runtime cannot dispatch it. + */ + supportsBuiltinImageGeneration: boolean; toolsEnabled: boolean; codeToolsEnabled: boolean; + imageToolsEnabled: boolean; toolStatus: string | null; generatingStatus: string | null; autoHealToolCalls: boolean; @@ -313,6 +323,7 @@ type ChatRuntimeStore = { setPreserveThinking: (value: boolean) => void; setToolsEnabled: (enabled: boolean, options?: { persist?: boolean }) => void; setCodeToolsEnabled: (enabled: boolean) => void; + setImageToolsEnabled: (enabled: boolean) => void; setToolStatus: (status: string | null) => void; setGeneratingStatus: (status: string | null) => void; setAutoHealToolCalls: (enabled: boolean) => void; @@ -552,8 +563,10 @@ export const useChatRuntimeStore = create((set, get) => ({ supportsTools: false, supportsBuiltinWebSearch: false, supportsBuiltinCodeExecution: false, + supportsBuiltinImageGeneration: false, toolsEnabled: loadBool(CHAT_TOOLS_ENABLED_KEY, false), codeToolsEnabled: loadBool(CHAT_CODE_TOOLS_ENABLED_KEY, false), + imageToolsEnabled: loadBool(CHAT_IMAGE_TOOLS_ENABLED_KEY, false), toolStatus: null, generatingStatus: null, autoHealToolCalls: true, @@ -730,8 +743,10 @@ export const useChatRuntimeStore = create((set, get) => ({ supportsTools: false, supportsBuiltinWebSearch: false, supportsBuiltinCodeExecution: false, + supportsBuiltinImageGeneration: false, toolsEnabled: false, codeToolsEnabled: false, + imageToolsEnabled: false, toolStatus: null, kvCacheDtype: null, loadedKvCacheDtype: null, @@ -786,6 +801,11 @@ export const useChatRuntimeStore = create((set, get) => ({ saveBool(CHAT_CODE_TOOLS_ENABLED_KEY, codeToolsEnabled); return { codeToolsEnabled }; }), + setImageToolsEnabled: (imageToolsEnabled) => + set(() => { + saveBool(CHAT_IMAGE_TOOLS_ENABLED_KEY, imageToolsEnabled); + return { imageToolsEnabled }; + }), setToolStatus: (toolStatus) => set({ toolStatus }), setGeneratingStatus: (generatingStatus) => set({ generatingStatus }), setAutoHealToolCalls: (autoHealToolCalls) =>