diff --git a/mcpjam-inspector/client/src/hooks/__tests__/use-chat-session.fork.test.tsx b/mcpjam-inspector/client/src/hooks/__tests__/use-chat-session.fork.test.tsx index ff0768758..8465230f1 100644 --- a/mcpjam-inspector/client/src/hooks/__tests__/use-chat-session.fork.test.tsx +++ b/mcpjam-inspector/client/src/hooks/__tests__/use-chat-session.fork.test.tsx @@ -94,7 +94,7 @@ vi.mock("@/lib/apis/mcp-tokenizer-api", () => ({ })); vi.mock("@/lib/session-token", () => ({ - getAuthHeaders: vi.fn(() => ({})), + authFetch: vi.fn(), })); vi.mock("@workos-inc/authkit-react", () => ({ diff --git a/mcpjam-inspector/client/src/hooks/__tests__/use-chat-session.hosted.test.tsx b/mcpjam-inspector/client/src/hooks/__tests__/use-chat-session.hosted.test.tsx index e34993268..37afcf3e9 100644 --- a/mcpjam-inspector/client/src/hooks/__tests__/use-chat-session.hosted.test.tsx +++ b/mcpjam-inspector/client/src/hooks/__tests__/use-chat-session.hosted.test.tsx @@ -1,9 +1,18 @@ -import { describe, expect, it, vi } from "vitest"; -import { renderHook } from "@testing-library/react"; +import { act, renderHook, waitFor } from "@testing-library/react"; +import { describe, expect, it, beforeEach, vi } from "vitest"; import { useChatSession } from "../use-chat-session"; +function createDeferred() { + let resolve!: (value: T) => void; + let reject!: (reason?: unknown) => void; + const promise = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + return { promise, resolve, reject }; +} + const mockState = vi.hoisted(() => ({ - sendMessage: vi.fn(), stop: vi.fn(), setMessages: vi.fn(), addToolApprovalResponse: vi.fn(), @@ -27,15 +36,28 @@ const mockState = vi.hoisted(() => ({ tokenCounts: null, })), countTextTokens: vi.fn(async () => null), + authFetch: vi.fn(async () => new Response(null, { status: 200 })), + getHostedAuthorizationHeader: vi.fn(async () => "Bearer hosted-token"), + transportInstances: [] as Array<{ + options: any; + sendMessages: ReturnType; + }>, + renderTransports: [] as any[], + sendCalls: [] as Array<{ id: string; transport: any; message: any }>, })); -let lastTransportOptions: any; const baseModel = { - id: "gpt-4.1-mini", - name: "GPT-4.1 Mini", + id: "openai/gpt-5-mini", + name: "GPT-5 Mini", provider: "openai" as const, }; +async function resolveConfig(value: T | (() => T | Promise)) { + return typeof value === "function" + ? await (value as () => T | Promise)() + : value; +} + vi.mock("@/lib/config", () => ({ HOSTED_MODE: true, })); @@ -64,7 +86,7 @@ vi.mock("@/hooks/use-custom-providers", () => ({ vi.mock("@/hooks/use-persisted-model", () => ({ usePersistedModel: () => ({ - selectedModelId: "gpt-4.1-mini", + selectedModelId: "openai/gpt-5-mini", setSelectedModelId: mockState.setSelectedModelId, }), })); @@ -87,7 +109,11 @@ vi.mock("@/lib/apis/mcp-tokenizer-api", () => ({ })); vi.mock("@/lib/session-token", () => ({ - getAuthHeaders: vi.fn(() => ({})), + authFetch: mockState.authFetch, +})); + +vi.mock("@/lib/apis/web/context", () => ({ + getHostedAuthorizationHeader: mockState.getHostedAuthorizationHeader, })); vi.mock("@workos-inc/authkit-react", () => ({ @@ -103,22 +129,94 @@ vi.mock("convex/react", () => ({ }), })); -vi.mock("@ai-sdk/react", () => ({ - useChat: vi.fn(() => ({ - messages: [], - sendMessage: mockState.sendMessage, - stop: mockState.stop, - status: "ready", - error: undefined, - setMessages: mockState.setMessages, - addToolApprovalResponse: mockState.addToolApprovalResponse, - })), -})); +vi.mock("@ai-sdk/react", async () => { + const React = await import("react"); + + return { + useChat: vi.fn( + ({ + id, + transport, + }: { + id: string; + transport: { + sendMessages: (options: any) => Promise; + }; + }) => { + const latchedIdRef = React.useRef(id); + const latchedTransportRef = React.useRef(transport); + + if (latchedIdRef.current !== id) { + latchedIdRef.current = id; + latchedTransportRef.current = transport; + } + + mockState.renderTransports.push({ id, transport }); + + return { + messages: [], + sendMessage: async (message: any) => { + mockState.sendCalls.push({ + id: latchedIdRef.current, + transport: latchedTransportRef.current, + message, + }); + await latchedTransportRef.current.sendMessages({ + chatId: latchedIdRef.current, + messages: [ + { + id: "user-1", + role: "user", + parts: + "text" in message + ? [{ type: "text", text: message.text }] + : [], + }, + ], + abortSignal: new AbortController().signal, + metadata: undefined, + headers: undefined, + body: undefined, + trigger: "submit-message", + messageId: undefined, + }); + }, + stop: mockState.stop, + status: "ready", + error: undefined, + setMessages: mockState.setMessages, + addToolApprovalResponse: mockState.addToolApprovalResponse, + }; + }, + ), + }; +}); vi.mock("ai", () => ({ DefaultChatTransport: class MockTransport { - constructor(options: unknown) { - lastTransportOptions = options; + options: any; + sendMessages: ReturnType; + + constructor(options: any) { + this.options = options; + this.sendMessages = vi.fn(async (requestOptions: any) => { + const resolvedBody = await resolveConfig(this.options.body); + const resolvedHeaders = await resolveConfig(this.options.headers); + const requestBody = { + ...resolvedBody, + id: requestOptions.chatId, + messages: requestOptions.messages, + trigger: requestOptions.trigger, + messageId: requestOptions.messageId, + }; + await this.options.fetch?.(this.options.api, { + method: "POST", + headers: resolvedHeaders, + body: JSON.stringify(requestBody), + }); + return new ReadableStream(); + }); + mockState.transportInstances.push(this); } }, generateId: vi.fn(() => "chat-session-id"), @@ -126,39 +224,87 @@ vi.mock("ai", () => ({ })); describe("useChatSession hosted mode", () => { - it("includes chatSessionId in the hosted transport body", async () => { - const { result, unmount } = renderHook(() => + beforeEach(() => { + vi.clearAllMocks(); + mockState.authFetch.mockResolvedValue(new Response(null, { status: 200 })); + mockState.getHostedAuthorizationHeader.mockResolvedValue( + "Bearer hosted-token", + ); + mockState.transportInstances = []; + mockState.renderTransports = []; + mockState.sendCalls = []; + }); + + it("keeps the initial transport latched and resolves hosted auth at request time", async () => { + const authBootstrap = createDeferred(); + mockState.getHostedAuthorizationHeader.mockImplementationOnce( + () => authBootstrap.promise, + ); + const selectedServers = ["server-1"]; + const hostedSelectedServerIds = ["server-id-1"]; + + const { result } = renderHook(() => useChatSession({ - selectedServers: ["server-1"], + selectedServers, hostedWorkspaceId: "workspace-1", - hostedSelectedServerIds: ["server-id-1"], + hostedSelectedServerIds, hostedShareToken: "share-token", }), ); - const body = lastTransportOptions.body(); + expect(mockState.transportInstances).toHaveLength(1); + const initialTransport = mockState.transportInstances[0]; + + authBootstrap.resolve("Bearer hosted-token"); + + await waitFor(() => { + expect(result.current.isAuthReady).toBe(true); + }); + + expect(mockState.transportInstances).toHaveLength(1); + + act(() => { + result.current.sendMessage({ text: "hello" }); + }); + + await waitFor(() => { + expect(initialTransport.sendMessages).toHaveBeenCalledTimes(1); + }); + await waitFor(() => { + expect(mockState.authFetch).toHaveBeenCalledTimes(1); + }); + + const [api, init] = mockState.authFetch.mock.calls[0]; + expect(api).toBe("/api/web/chat-v2"); + expect(init.headers).toBeUndefined(); + + const requestBody = JSON.parse(String(init.body)); expect(result.current.chatSessionId).toBe("chat-session-id"); - expect(body).toMatchObject({ + expect(requestBody).toMatchObject({ workspaceId: "workspace-1", chatSessionId: "chat-session-id", selectedServerIds: ["server-id-1"], shareToken: "share-token", accessScope: "chat_v2", }); - unmount(); + expect(mockState.sendCalls[0]?.transport).toBe(initialTransport); }); - it("includes sandbox token in the hosted transport body", async () => { - const { unmount } = renderHook(() => + it("includes sandbox token in the hosted transport body", () => { + const selectedServers = ["server-1"]; + const hostedSelectedServerIds = ["server-id-2"]; + + renderHook(() => useChatSession({ - selectedServers: ["server-1"], + selectedServers, hostedWorkspaceId: "workspace-2", - hostedSelectedServerIds: ["server-id-2"], + hostedSelectedServerIds, hostedSandboxToken: "sandbox-token", }), ); - const body = lastTransportOptions.body(); + expect(mockState.transportInstances).toHaveLength(1); + const body = mockState.transportInstances[0].options.body(); expect(body).toMatchObject({ workspaceId: "workspace-2", chatSessionId: "chat-session-id", @@ -166,6 +312,5 @@ describe("useChatSession hosted mode", () => { sandboxToken: "sandbox-token", accessScope: "chat_v2", }); - unmount(); }); }); diff --git a/mcpjam-inspector/client/src/hooks/__tests__/use-chat-session.minimal-mode.test.tsx b/mcpjam-inspector/client/src/hooks/__tests__/use-chat-session.minimal-mode.test.tsx index 1fca8cfc1..a75e9fbc6 100644 --- a/mcpjam-inspector/client/src/hooks/__tests__/use-chat-session.minimal-mode.test.tsx +++ b/mcpjam-inspector/client/src/hooks/__tests__/use-chat-session.minimal-mode.test.tsx @@ -1,26 +1,46 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; -import { renderHook, waitFor } from "@testing-library/react"; +import { act, renderHook, waitFor } from "@testing-library/react"; import { useChatSession } from "../use-chat-session"; const mockGetToolsMetadata = vi.fn(); const mockCountTextTokens = vi.fn(); const mockSetMessages = vi.fn(); -const mockSendMessage = vi.fn(); const mockStop = vi.fn(); const mockAddToolApprovalResponse = vi.fn(); +const mockAuthFetch = vi.fn(); +const mockGetAccessToken = vi.fn(async () => null); +const mockTransportInstances: Array<{ + options: any; + sendMessages: ReturnType; +}> = []; const baseModel = { id: "gpt-4", name: "GPT-4", provider: "openai" as const, }; +const mcpJamModel = { + id: "openai/gpt-5-mini", + name: "GPT-5 Mini", + provider: "openai" as const, +}; +const mockModelState = { + availableModels: [baseModel], + selectedModelId: "gpt-4", +}; + +async function resolveConfig(value: T | (() => T | Promise)) { + return typeof value === "function" + ? await (value as () => T | Promise)() + : value; +} vi.mock("@/lib/config", () => ({ HOSTED_MODE: false, })); vi.mock("@/components/chat-v2/shared/model-helpers", () => ({ - buildAvailableModels: vi.fn(() => [baseModel]), + buildAvailableModels: vi.fn(() => mockModelState.availableModels), getDefaultModel: vi.fn(() => baseModel), })); @@ -43,7 +63,7 @@ vi.mock("@/hooks/use-custom-providers", () => ({ vi.mock("@/hooks/use-persisted-model", () => ({ usePersistedModel: () => ({ - selectedModelId: "gpt-4", + selectedModelId: mockModelState.selectedModelId, setSelectedModelId: vi.fn(), }), })); @@ -65,7 +85,7 @@ vi.mock("@/lib/apis/mcp-tokenizer-api", () => ({ })); vi.mock("@/lib/session-token", () => ({ - getAuthHeaders: vi.fn(() => ({})), + authFetch: (...args: unknown[]) => mockAuthFetch(...args), })); vi.mock("@/hooks/useSharedChatWidgetCapture", () => ({ @@ -74,7 +94,7 @@ vi.mock("@/hooks/useSharedChatWidgetCapture", () => ({ vi.mock("@workos-inc/authkit-react", () => ({ useAuth: () => ({ - getAccessToken: vi.fn(async () => null), + getAccessToken: mockGetAccessToken, }), })); @@ -85,21 +105,88 @@ vi.mock("convex/react", () => ({ }), })); -vi.mock("@ai-sdk/react", () => ({ - useChat: vi.fn(() => ({ - messages: [], - sendMessage: mockSendMessage, - stop: mockStop, - status: "ready", - error: undefined, - setMessages: mockSetMessages, - addToolApprovalResponse: mockAddToolApprovalResponse, - })), -})); +vi.mock("@ai-sdk/react", async () => { + const React = await import("react"); + + return { + useChat: vi.fn( + ({ + id, + transport, + }: { + id: string; + transport: { + sendMessages: (options: any) => Promise; + }; + }) => { + const latchedIdRef = React.useRef(id); + const latchedTransportRef = React.useRef(transport); + + if (latchedIdRef.current !== id) { + latchedIdRef.current = id; + latchedTransportRef.current = transport; + } + + return { + messages: [], + sendMessage: async (message: any) => { + await latchedTransportRef.current.sendMessages({ + chatId: latchedIdRef.current, + messages: [ + { + id: "user-1", + role: "user", + parts: + "text" in message + ? [{ type: "text", text: message.text }] + : [], + }, + ], + abortSignal: new AbortController().signal, + metadata: undefined, + headers: undefined, + body: undefined, + trigger: "submit-message", + messageId: undefined, + }); + }, + stop: mockStop, + status: "ready", + error: undefined, + setMessages: mockSetMessages, + addToolApprovalResponse: mockAddToolApprovalResponse, + }; + }, + ), + }; +}); vi.mock("ai", () => ({ DefaultChatTransport: class MockTransport { - constructor(_: unknown) {} + options: any; + sendMessages: ReturnType; + + constructor(options: any) { + this.options = options; + this.sendMessages = vi.fn(async (requestOptions: any) => { + const resolvedBody = await resolveConfig(this.options.body); + const resolvedHeaders = await resolveConfig(this.options.headers); + const requestBody = { + ...resolvedBody, + id: requestOptions.chatId, + messages: requestOptions.messages, + trigger: requestOptions.trigger, + messageId: requestOptions.messageId, + }; + await this.options.fetch?.(this.options.api, { + method: "POST", + headers: resolvedHeaders, + body: JSON.stringify(requestBody), + }); + return new ReadableStream(); + }); + mockTransportInstances.push(this); + } }, generateId: vi.fn(() => "chat-session-id"), lastAssistantMessageIsCompleteWithApprovalResponses: vi.fn(), @@ -108,6 +195,11 @@ vi.mock("ai", () => ({ describe("useChatSession minimal mode parity", () => { beforeEach(() => { vi.clearAllMocks(); + mockModelState.availableModels = [baseModel]; + mockModelState.selectedModelId = "gpt-4"; + mockGetAccessToken.mockResolvedValue(null); + mockAuthFetch.mockResolvedValue(new Response(null, { status: 200 })); + mockTransportInstances.length = 0; mockGetToolsMetadata.mockResolvedValue({ metadata: { create_view: { title: "Create view" } }, toolServerMap: { create_view: "server-1" }, @@ -117,9 +209,10 @@ describe("useChatSession minimal mode parity", () => { }); it("still prefetches tools metadata when minimalMode is true", async () => { + const selectedServers = ["server-1"]; renderHook(() => useChatSession({ - selectedServers: ["server-1"], + selectedServers, minimalMode: true, initialSystemPrompt: "You are a helpful assistant.", }), @@ -135,9 +228,10 @@ describe("useChatSession minimal mode parity", () => { }); it("still counts system prompt tokens when minimalMode is true", async () => { + const selectedServers = ["server-1"]; renderHook(() => useChatSession({ - selectedServers: ["server-1"], + selectedServers, minimalMode: true, initialSystemPrompt: "Custom prompt", }), @@ -157,10 +251,11 @@ describe("useChatSession minimal mode parity", () => { message: "Forbidden", }); const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + const selectedServers = ["server-1"]; const { result } = renderHook(() => useChatSession({ - selectedServers: ["server-1"], + selectedServers, minimalMode: true, hostedShareToken: "share-token", initialSystemPrompt: "Prompt", @@ -180,4 +275,63 @@ describe("useChatSession minimal mode parity", () => { expect(warnSpy).not.toHaveBeenCalled(); warnSpy.mockRestore(); }); + + it("routes non-hosted chat through authFetch without transport session headers", async () => { + const selectedServers = ["server-1"]; + const { result } = renderHook(() => + useChatSession({ + selectedServers, + minimalMode: true, + initialSystemPrompt: "Prompt", + }), + ); + + expect(mockTransportInstances).toHaveLength(1); + + act(() => { + result.current.sendMessage({ text: "hello" }); + }); + + await waitFor(() => { + expect(mockAuthFetch).toHaveBeenCalledTimes(1); + }); + + const [api, init] = mockAuthFetch.mock.calls[0]; + expect(api).toBe("/api/mcp/chat-v2"); + expect(init.headers).toBeUndefined(); + }); + + it("adds explicit Authorization only for the non-hosted MCPJam model path", async () => { + mockModelState.availableModels = [mcpJamModel]; + mockModelState.selectedModelId = mcpJamModel.id; + mockGetAccessToken.mockResolvedValue("convex-token"); + const selectedServers = ["server-1"]; + + const { result } = renderHook(() => + useChatSession({ + selectedServers, + minimalMode: true, + initialSystemPrompt: "Prompt", + }), + ); + + await waitFor(() => { + expect(result.current.isAuthReady).toBe(true); + }); + + act(() => { + result.current.sendMessage({ text: "hello" }); + }); + + await waitFor(() => { + expect(mockAuthFetch).toHaveBeenCalledTimes(1); + }); + + const [api, init] = mockAuthFetch.mock.calls[0]; + expect(api).toBe("/api/mcp/chat-v2"); + expect(init.headers).toEqual({ + Authorization: "Bearer convex-token", + }); + expect(init.headers).not.toHaveProperty("X-MCP-Session-Auth"); + }); }); diff --git a/mcpjam-inspector/client/src/hooks/use-chat-session.ts b/mcpjam-inspector/client/src/hooks/use-chat-session.ts index 4231d907d..830e123d7 100644 --- a/mcpjam-inspector/client/src/hooks/use-chat-session.ts +++ b/mcpjam-inspector/client/src/hooks/use-chat-session.ts @@ -47,7 +47,7 @@ import { import { DEFAULT_SYSTEM_PROMPT } from "@/components/chat-v2/shared/chat-helpers"; import { getToolsMetadata, ToolServerMap } from "@/lib/apis/mcp-tools-api"; import { countTextTokens } from "@/lib/apis/mcp-tokenizer-api"; -import { getAuthHeaders as getSessionAuthHeaders } from "@/lib/session-token"; +import { authFetch } from "@/lib/session-token"; import { HOSTED_MODE } from "@/lib/config"; import { useSharedChatWidgetCapture } from "@/hooks/useSharedChatWidgetCapture"; import { getHostedAuthorizationHeader } from "@/lib/apis/web/context"; @@ -202,6 +202,28 @@ function getAuthHeadersSignature( .join("|"); } +function resolveModelApiKey({ + selectedModel, + getCustomProviderByName, + getToken, +}: { + selectedModel: ModelDefinition; + getCustomProviderByName: ReturnType< + typeof useCustomProviders + >["getCustomProviderByName"]; + getToken: ReturnType["getToken"]; +}): string { + if (selectedModel.provider === "custom" && selectedModel.customProviderName) { + // For custom providers, the API key is embedded in the provider config. + const customProvider = getCustomProviderByName( + selectedModel.customProviderName, + ); + return customProvider?.apiKey || ""; + } + + return getToken(selectedModel.provider as keyof ProviderTokens); +} + export function useChatSession({ selectedServers, hostedWorkspaceId, @@ -321,77 +343,101 @@ export function useChatSession({ : false; }, [selectedModel]); - // Create transport - const transport = useMemo(() => { - let apiKey: string; - if ( - selectedModel.provider === "custom" && - selectedModel.customProviderName - ) { - // For custom providers, the API key is embedded in the provider config - const cp = getCustomProviderByName(selectedModel.customProviderName); - apiKey = cp?.apiKey || ""; - } else { - apiKey = getToken(selectedModel.provider as keyof ProviderTokens); - } - const isGpt5 = isGPT5Model(selectedModel.id); - - // Merge session auth headers with workos auth headers - const sessionHeaders = getSessionAuthHeaders(); - const mergedHeaders = { ...sessionHeaders, ...authHeaders } as Record< - string, - string - >; - - const chatApi = HOSTED_MODE ? "/api/web/chat-v2" : "/api/mcp/chat-v2"; - - return new DefaultChatTransport({ - api: chatApi, - body: () => ({ - model: selectedModel, - ...(HOSTED_MODE ? {} : { apiKey }), - ...(isGpt5 ? {} : { temperature }), - systemPrompt, - ...(HOSTED_MODE - ? { - workspaceId: hostedWorkspaceId, - chatSessionId, - selectedServerIds: hostedSelectedServerIds, - accessScope: "chat_v2" as const, - ...(hostedShareToken ? { shareToken: hostedShareToken } : {}), - ...(hostedSandboxToken - ? { sandboxToken: hostedSandboxToken } - : {}), - ...(hostedOAuthTokens && Object.keys(hostedOAuthTokens).length > 0 - ? { oauthTokens: hostedOAuthTokens } - : {}), - } - : { selectedServers }), - requireToolApproval: requireToolApprovalRef.current, - ...(!HOSTED_MODE && customProviders.length > 0 - ? { customProviders } - : {}), - }), - headers: - Object.keys(mergedHeaders).length > 0 ? mergedHeaders : undefined, - }); - }, [ + const transportConfigRef = useRef<{ + selectedModel: ModelDefinition; + apiKey: string; + temperature: number; + systemPrompt: string; + selectedServers: string[]; + hostedWorkspaceId?: string | null; + hostedSelectedServerIds: string[]; + hostedOAuthTokens?: Record; + hostedShareToken?: string; + hostedSandboxToken?: string; + chatSessionId: string; + requireToolApproval: boolean; + customProviders: typeof customProviders; + localAuthorizationHeader?: string; + } | null>(null); + const currentApiKey = resolveModelApiKey({ selectedModel, - getToken, getCustomProviderByName, - customProviders, - authHeaders, + getToken, + }); + transportConfigRef.current = { + selectedModel, + apiKey: currentApiKey, temperature, systemPrompt, selectedServers, hostedWorkspaceId, - chatSessionId, hostedSelectedServerIds, hostedOAuthTokens, hostedShareToken, hostedSandboxToken, - // requireToolApproval read from ref at request time - ]); + chatSessionId, + requireToolApproval: requireToolApprovalRef.current, + customProviders, + localAuthorizationHeader: + !HOSTED_MODE && isMcpJamModel ? authHeaders?.Authorization : undefined, + }; + + const transportRef = useRef | null>(null); + if (!transportRef.current) { + const chatApi = HOSTED_MODE ? "/api/web/chat-v2" : "/api/mcp/chat-v2"; + + // AI SDK useChat latches the transport instance until the chat id changes, + // so request-time config must be read from refs instead of render closures. + transportRef.current = new DefaultChatTransport({ + api: chatApi, + fetch: authFetch, + body: () => { + const config = transportConfigRef.current!; + const isGpt5 = isGPT5Model(config.selectedModel.id); + + return { + model: config.selectedModel, + ...(HOSTED_MODE ? {} : { apiKey: config.apiKey }), + ...(isGpt5 ? {} : { temperature: config.temperature }), + systemPrompt: config.systemPrompt, + ...(HOSTED_MODE + ? { + workspaceId: config.hostedWorkspaceId, + chatSessionId: config.chatSessionId, + selectedServerIds: config.hostedSelectedServerIds, + accessScope: "chat_v2" as const, + ...(config.hostedShareToken + ? { shareToken: config.hostedShareToken } + : {}), + ...(config.hostedSandboxToken + ? { sandboxToken: config.hostedSandboxToken } + : {}), + ...(config.hostedOAuthTokens && + Object.keys(config.hostedOAuthTokens).length > 0 + ? { oauthTokens: config.hostedOAuthTokens } + : {}), + } + : { selectedServers: config.selectedServers }), + requireToolApproval: config.requireToolApproval, + ...(!HOSTED_MODE && config.customProviders.length > 0 + ? { customProviders: config.customProviders } + : {}), + }; + }, + headers: () => { + if (HOSTED_MODE) { + return undefined; + } + + const localAuthorizationHeader = + transportConfigRef.current?.localAuthorizationHeader; + return localAuthorizationHeader + ? { Authorization: localAuthorizationHeader } + : undefined; + }, + }); + } + const transport = transportRef.current; // useChat hook const { @@ -404,7 +450,7 @@ export function useChatSession({ addToolApprovalResponse, } = useChat({ id: chatSessionId, - transport: transport!, + transport, sendAutomaticallyWhen: requireToolApproval ? lastAssistantMessageIsCompleteWithApprovalResponses : undefined, @@ -497,7 +543,7 @@ export function useChatSession({ onResetRef.current?.(); }, [setMessages]); - // Auth headers setup - reset chat after auth changes to ensure transport has correct headers + // Resolve auth state for submit gating and reset when the auth identity changes. useEffect(() => { let active = true; (async () => {