diff --git a/packages/app/src/components/agent-form/agent-form-dropdowns.tsx b/packages/app/src/components/agent-form/agent-form-dropdowns.tsx index 6c41c530a..9b2146f0d 100644 --- a/packages/app/src/components/agent-form/agent-form-dropdowns.tsx +++ b/packages/app/src/components/agent-form/agent-form-dropdowns.tsx @@ -343,7 +343,8 @@ export function ComboSelect({ const anchorRef = useRef(null); const selectedOption = options.find((opt) => opt.id === value); - const displayValue = selectedOption?.label ?? (value || ""); + const displayValue = selectedOption?.label ?? ""; + const isEmpty = options.length === 0; const handleOpen = useCallback(() => setIsOpen(true), []); const handleOpenChange = useCallback((open: boolean) => setIsOpen(open), []); @@ -355,7 +356,7 @@ export function ComboSelect({ value={displayValue} placeholder={placeholder} onPress={handleOpen} - disabled={disabled} + disabled={disabled || isEmpty} isLoading={isLoading} controlRef={anchorRef} icon={icon} @@ -567,8 +568,8 @@ export function AgentConfigRow({ title="Select provider" value={selectedProvider} options={providerOptions} - placeholder="Select..." - disabled={disabled} + placeholder={providerOptions.length > 0 ? "Select..." : "No providers available"} + disabled={disabled || providerOptions.length === 0} onSelect={onSelectProvider} icon={} showLabel={false} diff --git a/packages/app/src/components/agent-status-bar.tsx b/packages/app/src/components/agent-status-bar.tsx index 2df180613..9b7ac1a2f 100644 --- a/packages/app/src/components/agent-status-bar.tsx +++ b/packages/app/src/components/agent-status-bar.tsx @@ -17,6 +17,14 @@ interface AgentStatusBarProps { serverId: string; } +function normalizeModelId(modelId: string | null | undefined): string | null { + const normalized = typeof modelId === "string" ? modelId.trim() : ""; + if (!normalized || normalized.toLowerCase() === "default") { + return null; + } + return normalized; +} + export function AgentStatusBar({ agentId, serverId }: AgentStatusBarProps) { const { theme } = useUnistyles(); const IS_WEB = Platform.OS === "web"; @@ -61,14 +69,16 @@ export function AgentStatusBar({ agentId, serverId }: AgentStatusBarProps) { }); } + const normalizedRuntimeModelId = normalizeModelId(agent.runtimeInfo?.model); + const normalizedConfiguredModelId = normalizeModelId(agent.model); + const preferredModelId = normalizedRuntimeModelId ?? normalizedConfiguredModelId; const selectedModel = useMemo(() => { - if (!models || !agent.model) return null; - return models.find((m) => m.id === agent.model) ?? null; - }, [models, agent.model]); + if (!models || !preferredModelId) return null; + return models.find((m) => m.id === preferredModelId) ?? null; + }, [models, preferredModelId]); - const displayModel = selectedModel - ? selectedModel.label - : agent.model ?? "default"; + const activeModelId = selectedModel?.id ?? preferredModelId ?? null; + const displayModel = selectedModel ? selectedModel.label : preferredModelId ?? "Auto"; const thinkingOptions = selectedModel?.thinkingOptions ?? null; const explicitThinkingId = @@ -156,7 +166,7 @@ export function AgentStatusBar({ agentId, serverId }: AgentStatusBarProps) { testID="agent-model-menu" > {models?.map((model) => { - const isActive = model.id === agent.model; + const isActive = model.id === activeModelId; return ( {models?.map((model) => { - const isActive = model.id === agent.model; + const isActive = model.id === activeModelId; return ( { describe("__private__.combineInitialValues", () => { @@ -146,5 +153,127 @@ describe("useAgentFormState", () => { expect(resolved.thinkingOptionId).toBe("xhigh"); }); + + it("normalizes legacy model id 'default' from initial values to auto", () => { + const resolved = __private__.resolveFormState( + { model: "default" }, + { provider: "codex" }, + codexModels, + { + serverId: false, + provider: false, + modeId: false, + model: false, + thinkingOptionId: false, + workingDir: false, + }, + { + serverId: null, + provider: "codex", + modeId: "", + model: "", + thinkingOptionId: "", + workingDir: "", + }, + new Set() + ); + + expect(resolved.model).toBe(""); + }); + + it("normalizes legacy model id 'default' from provider preferences to auto", () => { + const resolved = __private__.resolveFormState( + undefined, + { + provider: "codex", + providerPreferences: { + codex: { + model: "default", + }, + }, + }, + codexModels, + { + serverId: false, + provider: false, + modeId: false, + model: false, + thinkingOptionId: false, + workingDir: false, + }, + { + serverId: null, + provider: "codex", + modeId: "", + model: "", + thinkingOptionId: "", + workingDir: "", + }, + new Set() + ); + + expect(resolved.model).toBe(""); + }); + + it("resolves provider only from allowed provider map", () => { + const allowedProviderMap = new Map( + AGENT_PROVIDER_DEFINITIONS + .filter((definition) => definition.id === "claude") + .map((definition) => [definition.id as AgentProvider, definition]) + ); + const resolved = __private__.resolveFormState( + undefined, + { provider: "codex" }, + null, + { + serverId: false, + provider: false, + modeId: false, + model: false, + thinkingOptionId: false, + workingDir: false, + }, + { + serverId: null, + provider: "codex", + modeId: "", + model: "", + thinkingOptionId: "", + workingDir: "", + }, + new Set(), + allowedProviderMap + ); + + expect(resolved.provider).toBe("claude"); + }); + + it("does not force fallback provider when allowed provider map is empty", () => { + const resolved = __private__.resolveFormState( + undefined, + { provider: "codex" }, + null, + { + serverId: false, + provider: false, + modeId: false, + model: false, + thinkingOptionId: false, + workingDir: false, + }, + { + serverId: null, + provider: "codex", + modeId: "", + model: "", + thinkingOptionId: "", + workingDir: "", + }, + new Set(), + new Map() + ); + + expect(resolved.provider).toBe("codex"); + }); }); }); diff --git a/packages/app/src/hooks/use-agent-form-state.ts b/packages/app/src/hooks/use-agent-form-state.ts index c8dff74d6..4d19a5960 100644 --- a/packages/app/src/hooks/use-agent-form-state.ts +++ b/packages/app/src/hooks/use-agent-form-state.ts @@ -89,15 +89,25 @@ type UseAgentFormStateResult = { persistFormPreferences: () => Promise; }; -const providerDefinitions = AGENT_PROVIDER_DEFINITIONS; -const providerDefinitionMap = new Map( - providerDefinitions.map((definition) => [definition.id, definition]) +const allProviderDefinitions = AGENT_PROVIDER_DEFINITIONS; +const allProviderDefinitionMap = new Map( + allProviderDefinitions.map((definition) => [definition.id, definition]) ); -const fallbackDefinition = providerDefinitions[0]; +const fallbackDefinition = allProviderDefinitions[0]; const DEFAULT_PROVIDER: AgentProvider = fallbackDefinition?.id ?? "claude"; const DEFAULT_MODE_FOR_DEFAULT_PROVIDER = fallbackDefinition?.defaultModeId ?? ""; +function normalizeSelectedModelId( + modelId: string | null | undefined +): string { + const normalized = typeof modelId === "string" ? modelId.trim() : ""; + if (!normalized || normalized.toLowerCase() === "default") { + return ""; + } + return normalized; +} + function resolveDefaultModel( availableModels: AgentModelDefinition[] | null ): AgentModelDefinition | null { @@ -136,25 +146,33 @@ function resolveFormState( availableModels: AgentModelDefinition[] | null, userModified: UserModifiedFields, currentState: FormState, - validServerIds: Set + validServerIds: Set, + allowedProviderMap: Map = allProviderDefinitionMap ): FormState { // Start with current state - we only update non-user-modified fields const result = { ...currentState }; + const fallbackProvider = allowedProviderMap.keys().next().value as + | AgentProvider + | undefined; // 1. Resolve provider first (other fields depend on it) if (!userModified.provider) { - if (initialValues?.provider && providerDefinitionMap.has(initialValues.provider)) { + if (initialValues?.provider && allowedProviderMap.has(initialValues.provider)) { result.provider = initialValues.provider; } else if ( preferences?.provider && - providerDefinitionMap.has(preferences.provider as AgentProvider) + allowedProviderMap.has(preferences.provider as AgentProvider) ) { result.provider = preferences.provider as AgentProvider; + } else if (!allowedProviderMap.has(result.provider) && fallbackProvider) { + result.provider = fallbackProvider; } // else keep current (initialized to DEFAULT_PROVIDER) + } else if (!allowedProviderMap.has(result.provider) && fallbackProvider) { + result.provider = fallbackProvider; } - const providerDef = providerDefinitionMap.get(result.provider); + const providerDef = allowedProviderMap.get(result.provider); const providerPrefs = preferences?.providerPreferences?.[result.provider]; // 2. Resolve modeId (depends on provider) @@ -181,25 +199,24 @@ function resolveFormState( if (!userModified.model) { const isValidModel = (m: string) => availableModels?.some((am) => am.id === m) ?? false; + const initialModel = normalizeSelectedModelId(initialValues?.model); + const preferredModel = normalizeSelectedModelId(providerPrefs?.model); - if ( - typeof initialValues?.model === "string" && - initialValues.model.length > 0 - ) { + if (initialModel) { // If models aren't loaded yet, trust the initial value // It will be validated once models load - if (!availableModels || isValidModel(initialValues.model)) { - result.model = initialValues.model; - } else if (providerPrefs?.model && isValidModel(providerPrefs.model)) { - result.model = providerPrefs.model; + if (!availableModels || isValidModel(initialModel)) { + result.model = initialModel; + } else if (preferredModel && isValidModel(preferredModel)) { + result.model = preferredModel; } else { result.model = ""; } - } else if (typeof providerPrefs?.model === "string" && providerPrefs.model.length > 0) { + } else if (preferredModel) { // If models haven't loaded yet, optimistically apply the stored preference. // We'll validate once models load and clear it if it isn't available. - if (!availableModels || isValidModel(providerPrefs.model)) { - result.model = providerPrefs.model; + if (!availableModels || isValidModel(preferredModel)) { + result.model = preferredModel; } else { result.model = ""; } @@ -353,6 +370,45 @@ export function useAgentFormState( const client = sessionState?.client ?? null; const isConnected = sessionState?.connection?.isConnected ?? false; + const availableProvidersQuery = useQuery({ + queryKey: ["availableProviders", formState.serverId], + enabled: Boolean( + isVisible && isTargetDaemonReady && formState.serverId && client && isConnected + ), + staleTime: 60 * 1000, + queryFn: async () => { + if (!client) { + throw new Error("Host is not connected"); + } + const payload = await client.listAvailableProviders(); + if (payload.error) { + throw new Error(payload.error); + } + return payload.providers + .filter((entry) => entry.available) + .map((entry) => entry.provider); + }, + }); + + const providerDefinitions = useMemo(() => { + const availableProviders = availableProvidersQuery.data; + if (!availableProviders) { + return []; + } + const available = new Set(availableProviders); + return allProviderDefinitions.filter((definition) => + available.has(definition.id as AgentProvider) + ); + }, [availableProvidersQuery.data]); + + const providerDefinitionMap = useMemo( + () => + new Map( + providerDefinitions.map((definition) => [definition.id as AgentProvider, definition]) + ), + [providerDefinitions] + ); + const [debouncedCwd, setDebouncedCwd] = useState(undefined); useEffect(() => { const trimmed = formState.workingDir.trim(); @@ -363,7 +419,14 @@ export function useAgentFormState( const providerModelsQuery = useQuery({ queryKey: ["providerModels", formState.serverId, formState.provider, debouncedCwd], - enabled: Boolean(isVisible && isTargetDaemonReady && formState.serverId && client && isConnected), + enabled: Boolean( + isVisible && + isTargetDaemonReady && + formState.serverId && + client && + isConnected && + providerDefinitionMap.has(formState.provider) + ), staleTime: 5 * 60 * 1000, queryFn: async () => { if (!client) { @@ -403,7 +466,8 @@ export function useAgentFormState( availableModels, userModified, formStateRef.current, - validServerIds + validServerIds, + providerDefinitionMap ); // Only update if something changed @@ -428,6 +492,7 @@ export function useAgentFormState( availableModels, userModified, validServerIds, + providerDefinitionMap, ]); // Auto-select the first online host when: @@ -500,11 +565,11 @@ export function useAgentFormState( ...prev, provider, modeId: providerPrefs?.mode ?? providerDef?.defaultModeId ?? "", - model: providerPrefs?.model ?? "", + model: normalizeSelectedModelId(providerPrefs?.model), thinkingOptionId: providerPrefs?.thinkingOptionId ?? "", })); }, - [preferences?.providerPreferences, updatePreferences] + [preferences?.providerPreferences, providerDefinitionMap, updatePreferences] ); const setModeFromUser = useCallback( @@ -518,9 +583,10 @@ export function useAgentFormState( const setModelFromUser = useCallback( (modelId: string) => { - setFormState((prev) => ({ ...prev, model: modelId })); + const normalizedModelId = normalizeSelectedModelId(modelId); + setFormState((prev) => ({ ...prev, model: normalizedModelId })); setUserModified((prev) => ({ ...prev, model: true })); - void updateProviderPreferences(formState.provider, { model: modelId }); + void updateProviderPreferences(formState.provider, { model: normalizedModelId }); }, [formState.provider, updateProviderPreferences] ); @@ -639,6 +705,8 @@ export function useAgentFormState( setThinkingOptionFromUser, setWorkingDir, setWorkingDirFromUser, + providerDefinitions, + providerDefinitionMap, agentDefinition, modeOptions, availableModels, diff --git a/packages/app/src/screens/agent/draft-agent-screen.tsx b/packages/app/src/screens/agent/draft-agent-screen.tsx index 1d9738f5e..b69022f36 100644 --- a/packages/app/src/screens/agent/draft-agent-screen.tsx +++ b/packages/app/src/screens/agent/draft-agent-screen.tsx @@ -785,6 +785,13 @@ export function DraftAgentScreen({ dispatch({ type: "DRAFT_SET_ERROR", message: "No host selected" }); throw new Error("No host selected"); } + if (providerDefinitions.length === 0) { + dispatch({ + type: "DRAFT_SET_ERROR", + message: "No available providers on the selected host", + }); + throw new Error("No available providers on the selected host"); + } if (gitBlockingError) { dispatch({ type: "DRAFT_SET_ERROR", message: gitBlockingError }); throw new Error(gitBlockingError); @@ -894,6 +901,7 @@ export function DraftAgentScreen({ isDirectoryNotExists, isNonGitDirectory, modeOptions, + providerDefinitions, persistFormPreferences, router, selectedMode, diff --git a/packages/app/src/utils/extract-agent-model.test.ts b/packages/app/src/utils/extract-agent-model.test.ts index 00b1444a9..df52bd6fb 100644 --- a/packages/app/src/utils/extract-agent-model.test.ts +++ b/packages/app/src/utils/extract-agent-model.test.ts @@ -19,4 +19,13 @@ describe("extractAgentModel", () => { expect(extractAgentModel(agent)).toBe("gpt-5.1-codex"); }); + + it("treats legacy 'default' model ids as unset", () => { + const agent = { + model: "default", + runtimeInfo: { model: "default" }, + } as Partial as Agent; + + expect(extractAgentModel(agent)).toBeNull(); + }); }); diff --git a/packages/app/src/utils/extract-agent-model.ts b/packages/app/src/utils/extract-agent-model.ts index e96214881..a93e1f3a6 100644 --- a/packages/app/src/utils/extract-agent-model.ts +++ b/packages/app/src/utils/extract-agent-model.ts @@ -4,11 +4,17 @@ export function extractAgentModel(agent?: Agent | null): string | null { if (!agent) return null; const runtimeModel = agent.runtimeInfo?.model; const fallbackModel = agent.model; - if (typeof runtimeModel === "string" && runtimeModel.trim().length > 0) { - return runtimeModel.trim(); + if (typeof runtimeModel === "string") { + const normalized = runtimeModel.trim(); + if (normalized.length > 0 && normalized.toLowerCase() !== "default") { + return normalized; + } } - if (typeof fallbackModel === "string" && fallbackModel.trim().length > 0) { - return fallbackModel.trim(); + if (typeof fallbackModel === "string") { + const normalized = fallbackModel.trim(); + if (normalized.length > 0 && normalized.toLowerCase() !== "default") { + return normalized; + } } return null; } diff --git a/packages/server/src/client/daemon-client.test.ts b/packages/server/src/client/daemon-client.test.ts index b13ce5c21..ce1440c6d 100644 --- a/packages/server/src/client/daemon-client.test.ts +++ b/packages/server/src/client/daemon-client.test.ts @@ -418,6 +418,60 @@ describe("DaemonClient", () => { vi.useRealTimers(); }); + test("lists available providers via RPC", async () => { + const logger = createMockLogger(); + const mock = createMockTransport(); + + const client = new DaemonClient({ + url: "ws://test", + logger, + reconnect: { enabled: false }, + transportFactory: () => mock.transport, + }); + clients.push(client); + + const connectPromise = client.connect(); + mock.triggerOpen(); + await connectPromise; + + const promise = client.listAvailableProviders(); + expect(mock.sent).toHaveLength(1); + + const request = JSON.parse(mock.sent[0]) as { + type: "session"; + message: { type: "list_available_providers_request"; requestId: string }; + }; + expect(request.message.type).toBe("list_available_providers_request"); + + mock.triggerMessage( + JSON.stringify({ + type: "session", + message: { + type: "list_available_providers_response", + payload: { + providers: [ + { provider: "claude", available: true, error: null }, + { provider: "codex", available: false, error: "Missing binary" }, + ], + error: null, + fetchedAt: "2026-02-12T00:00:00.000Z", + requestId: request.message.requestId, + }, + }, + }) + ); + + await expect(promise).resolves.toEqual({ + providers: [ + { provider: "claude", available: true, error: null }, + { provider: "codex", available: false, error: "Missing binary" }, + ], + error: null, + fetchedAt: "2026-02-12T00:00:00.000Z", + requestId: request.message.requestId, + }); + }); + test("parses canonical agent_stream tool_call payloads without crashing", async () => { const logger = createMockLogger(); const mock = createMockTransport(); diff --git a/packages/server/src/client/daemon-client.ts b/packages/server/src/client/daemon-client.ts index 79efe75b7..b15558c04 100644 --- a/packages/server/src/client/daemon-client.ts +++ b/packages/server/src/client/daemon-client.ts @@ -30,6 +30,7 @@ import type { ListCommandsResponse, ExecuteCommandResponse, ListProviderModelsResponseMessage, + ListAvailableProvidersResponse, SpeechModelsListResponse, SpeechModelsDownloadResponse, ListTerminalsResponse, @@ -194,6 +195,7 @@ type PaseoWorktreeArchivePayload = PaseoWorktreeArchiveResponse["payload"]; type FileExplorerPayload = FileExplorerResponse["payload"]; type FileDownloadTokenPayload = FileDownloadTokenResponse["payload"]; type ListProviderModelsPayload = ListProviderModelsResponseMessage["payload"]; +type ListAvailableProvidersPayload = ListAvailableProvidersResponse["payload"]; type SpeechModelsListPayload = SpeechModelsListResponse["payload"]; type SpeechModelsDownloadPayload = SpeechModelsDownloadResponse["payload"]; type ListCommandsPayload = ListCommandsResponse["payload"]; @@ -2054,6 +2056,31 @@ export class DaemonClient { }); } + async listAvailableProviders(options?: { + requestId?: string; + }): Promise { + const resolvedRequestId = this.createRequestId(options?.requestId); + const message = SessionInboundMessageSchema.parse({ + type: "list_available_providers_request", + requestId: resolvedRequestId, + }); + return this.sendRequest({ + requestId: resolvedRequestId, + message, + timeout: 30000, + options: { skipQueue: true }, + select: (msg) => { + if (msg.type !== "list_available_providers_response") { + return null; + } + if (msg.payload.requestId !== resolvedRequestId) { + return null; + } + return msg.payload; + }, + }); + } + async listSpeechModels(requestId?: string): Promise { const resolvedRequestId = this.createRequestId(requestId); const message = SessionInboundMessageSchema.parse({ diff --git a/packages/server/src/server/agent/agent-manager.test.ts b/packages/server/src/server/agent/agent-manager.test.ts index 6a3b6ea9b..e724fb6a4 100644 --- a/packages/server/src/server/agent/agent-manager.test.ts +++ b/packages/server/src/server/agent/agent-manager.test.ts @@ -130,6 +130,28 @@ describe("AgentManager", () => { expect(snapshot.model).toBeUndefined(); }); + test("normalizeConfig strips legacy 'default' model id", async () => { + const workdir = mkdtempSync(join(tmpdir(), "agent-manager-test-")); + const storagePath = join(workdir, "agents"); + const storage = new AgentStorage(storagePath, logger); + const manager = new AgentManager({ + clients: { + codex: new TestAgentClient(), + }, + registry: storage, + logger, + idFactory: () => "00000000-0000-4000-8000-000000000102", + }); + + const snapshot = await manager.createAgent({ + provider: "codex", + cwd: workdir, + model: "default", + }); + + expect(snapshot.model).toBeUndefined(); + }); + test("createAgent fails when cwd does not exist", async () => { const workdir = mkdtempSync(join(tmpdir(), "agent-manager-test-")); const storagePath = join(workdir, "agents"); diff --git a/packages/server/src/server/agent/agent-manager.ts b/packages/server/src/server/agent/agent-manager.ts index 9235faec6..c37e71fc5 100644 --- a/packages/server/src/server/agent/agent-manager.ts +++ b/packages/server/src/server/agent/agent-manager.ts @@ -29,6 +29,7 @@ import type { PersistedAgentDescriptor, } from "./agent-sdk-types.js"; import type { AgentStorage } from "./agent-storage.js"; +import { AGENT_PROVIDER_IDS } from "./provider-manifest.js"; export { AGENT_LIFECYCLE_STATUSES, type AgentLifecycleStatus }; @@ -53,6 +54,12 @@ export type AgentAttentionCallback = (params: { reason: "finished" | "error" | "permission"; }) => void; +export type ProviderAvailability = { + provider: AgentProvider; + available: boolean; + error: string | null; +}; + export type AgentManagerOptions = { clients?: Partial>; maxTimelineItems?: number; @@ -333,6 +340,42 @@ export class AgentManager { .slice(0, limit); } + async listProviderAvailability(): Promise { + const checks = AGENT_PROVIDER_IDS.map(async (providerId) => { + const provider = providerId as AgentProvider; + const client = this.clients.get(provider); + if (!client) { + return { + provider, + available: false, + error: `No client registered for provider '${provider}'`, + } satisfies ProviderAvailability; + } + + try { + const available = await client.isAvailable(); + return { + provider, + available, + error: null, + } satisfies ProviderAvailability; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + this.logger.warn( + { err: error, provider }, + "Failed to check provider availability" + ); + return { + provider, + available: false, + error: message, + } satisfies ProviderAvailability; + } + }); + + return Promise.all(checks); + } + getAgent(id: string): ManagedAgent | null { const agent = this.agents.get(id); return agent ? { ...agent } : null; @@ -1460,7 +1503,9 @@ export class AgentManager { if (typeof normalized.model === "string") { const trimmed = normalized.model.trim(); - normalized.model = trimmed.length > 0 ? trimmed : undefined; + const normalizedId = trimmed.toLowerCase(); + normalized.model = + trimmed.length > 0 && normalizedId !== "default" ? trimmed : undefined; } return normalized; diff --git a/packages/server/src/server/session.ts b/packages/server/src/server/session.ts index d13b51398..7ec3f29d1 100644 --- a/packages/server/src/server/session.ts +++ b/packages/server/src/server/session.ts @@ -1286,6 +1286,10 @@ export class Session { await this.handleListProviderModelsRequest(msg); break; + case "list_available_providers_request": + await this.handleListAvailableProvidersRequest(msg); + break; + case "speech_models_list_request": await this.handleSpeechModelsListRequest(msg); break; @@ -2594,6 +2598,38 @@ export class Session { } } + private async handleListAvailableProvidersRequest( + msg: Extract + ): Promise { + const fetchedAt = new Date().toISOString(); + try { + const providers = await this.agentManager.listProviderAvailability(); + this.emit({ + type: "list_available_providers_response", + payload: { + providers, + error: null, + fetchedAt, + requestId: msg.requestId, + }, + }); + } catch (error) { + this.sessionLogger.error( + { err: error }, + "Failed to list provider availability" + ); + this.emit({ + type: "list_available_providers_response", + payload: { + providers: [], + error: (error as Error)?.message ?? String(error), + fetchedAt, + requestId: msg.requestId, + }, + }); + } + } + private async handleSpeechModelsListRequest( msg: Extract ): Promise { diff --git a/packages/server/src/shared/messages.ts b/packages/server/src/shared/messages.ts index 869ba67ca..e79ee25ac 100644 --- a/packages/server/src/shared/messages.ts +++ b/packages/server/src/shared/messages.ts @@ -564,6 +564,11 @@ export const ListProviderModelsRequestMessageSchema = z.object({ requestId: z.string(), }); +export const ListAvailableProvidersRequestMessageSchema = z.object({ + type: z.literal("list_available_providers_request"), + requestId: z.string(), +}); + export const SpeechModelsListRequestSchema = z.object({ type: z.literal("speech_models_list_request"), requestId: z.string(), @@ -987,6 +992,7 @@ export const SessionInboundMessageSchema = z.discriminatedUnion("type", [ DictationStreamCancelMessageSchema, CreateAgentRequestMessageSchema, ListProviderModelsRequestMessageSchema, + ListAvailableProvidersRequestMessageSchema, SpeechModelsListRequestSchema, SpeechModelsDownloadRequestSchema, ResumeAgentRequestMessageSchema, @@ -1635,6 +1641,22 @@ export const ListProviderModelsResponseMessageSchema = z.object({ }), }); +const ProviderAvailabilitySchema = z.object({ + provider: AgentProviderSchema, + available: z.boolean(), + error: z.string().nullable().optional(), +}); + +export const ListAvailableProvidersResponseSchema = z.object({ + type: z.literal("list_available_providers_response"), + payload: z.object({ + providers: z.array(ProviderAvailabilitySchema), + error: z.string().nullable().optional(), + fetchedAt: z.string(), + requestId: z.string(), + }), +}); + export const SpeechModelsListResponseSchema = z.object({ type: z.literal("speech_models_list_response"), payload: z.object({ @@ -1817,6 +1839,7 @@ export const SessionOutboundMessageSchema = z.discriminatedUnion("type", [ ProjectIconResponseSchema, FileDownloadTokenResponseSchema, ListProviderModelsResponseMessageSchema, + ListAvailableProvidersResponseSchema, SpeechModelsListResponseSchema, SpeechModelsDownloadResponseSchema, ListCommandsResponseSchema, @@ -1873,6 +1896,9 @@ export type AgentDeletedMessage = z.infer; export type ListProviderModelsResponseMessage = z.infer< typeof ListProviderModelsResponseMessageSchema >; +export type ListAvailableProvidersResponse = z.infer< + typeof ListAvailableProvidersResponseSchema +>; export type SpeechModelsListResponse = z.infer; export type SpeechModelsDownloadResponse = z.infer; export type InitializeAgentResponseMessage = z.infer; @@ -1897,6 +1923,9 @@ export type CreateAgentRequestMessage = z.infer; +export type ListAvailableProvidersRequestMessage = z.infer< + typeof ListAvailableProvidersRequestMessageSchema +>; export type SpeechModelsListRequestMessage = z.infer; export type SpeechModelsDownloadRequestMessage = z.infer< typeof SpeechModelsDownloadRequestSchema