From a7fc47cf4e04cd3b973e142ecfa0624872afb2ea Mon Sep 17 00:00:00 2001 From: Prashant Vasudevan <71649489+vprashrex@users.noreply.github.com> Date: Wed, 1 Jul 2026 08:02:38 +0530 Subject: [PATCH 1/4] feat(config): make model provider/type config fully dynamic across all backend providers --- .../prompt-editor/ConfigEditorPane.tsx | 70 ++++++++++--------- app/hooks/useConfigPersistence.ts | 13 +++- app/lib/apiClient.ts | 29 +++++++- app/lib/chatClient.ts | 2 +- app/lib/modelSchema.ts | 46 ++++++------ app/lib/types/configs.ts | 2 +- app/lib/utils.ts | 2 - 7 files changed, 100 insertions(+), 64 deletions(-) diff --git a/app/components/prompt-editor/ConfigEditorPane.tsx b/app/components/prompt-editor/ConfigEditorPane.tsx index 73397c62..7d6852a0 100644 --- a/app/components/prompt-editor/ConfigEditorPane.tsx +++ b/app/components/prompt-editor/ConfigEditorPane.tsx @@ -2,10 +2,11 @@ import { useEffect, useRef, useState } from "react"; import { guardrailsFetch } from "@/app/lib/guardrailsClient"; import { ConfigEditorPaneProps, Tool } from "@/app/lib/types/promptEditor"; import { CompletionConfig, CompletionParams } from "@/app/lib/types/configs"; -import { ConfigType, MODEL_OPTIONS, getModelsForType } from "@/app/lib/models"; +import { ConfigType, getModelsForType } from "@/app/lib/models"; import { PROVIDER_TYPES } from "@/app/lib/constants"; import { getAllProviders, + getCompletionTypesForProvider, getModelSchema, getParamLabel, getProviderLabel, @@ -87,6 +88,10 @@ export default function ConfigEditorPane({ value: p, label: getProviderLabel(p), })); + const availableTypes = getCompletionTypesForProvider(provider); + const typeOptions = PROVIDER_TYPES.filter((t) => + availableTypes.includes(t.value as ConfigType), + ); const selectedConfig = savedConfigs.find((c) => c.id === selectedConfigId); const isBoundToSavedConfig = !!boundConfigId; @@ -110,35 +115,14 @@ export default function ConfigEditorPane({ }); }; - const handleProviderChange = (newProvider: string) => { - const candidates = getModelsForType(newProvider, currentType); - const fallback = MODEL_OPTIONS[newProvider]?.[0]?.value ?? ""; - const nextModel = candidates[0]?.value ?? fallback; - handleConfigChange({ - completion: { provider: newProvider as CompletionConfig["provider"] }, - params: { model: nextModel }, - }); - }; - - const handleTypeChange = (newType: ConfigType) => { - const provider = configBlob.completion.provider; - const candidates = getModelsForType(provider, newType); - const stillValid = candidates.some((m) => m.value === params.model); - const nextModel = stillValid - ? params.model - : (candidates[0]?.value ?? - MODEL_OPTIONS[provider]?.[0]?.value ?? - params.model); - handleConfigChange({ - completion: { type: newType }, - params: { model: nextModel }, - }); - }; - - const handleModelChange = (model: string) => { - const nextSchema = getModelSchema(provider, model); + const applyProviderTypeModel = ( + nextProvider: string, + nextType: ConfigType, + nextModel: string, + ) => { + const nextSchema = getModelSchema(nextProvider, nextModel); const nextSchemaParams = nextSchema - ? reconcileParamsForModel(provider, model, params) + ? reconcileParamsForModel(nextProvider, nextModel, params) : {}; const oldSchemaKeys = modelSchema ? Object.keys(modelSchema.config) : []; const carryover = { ...params }; @@ -148,11 +132,33 @@ export default function ConfigEditorPane({ ...configBlob, completion: { ...configBlob.completion, - params: { ...carryover, model, ...nextSchemaParams }, + provider: nextProvider, + type: nextType, + params: { ...carryover, model: nextModel, ...nextSchemaParams }, }, }); }; + const handleProviderChange = (newProvider: string) => { + const types = getCompletionTypesForProvider(newProvider); + const nextType = types.includes(currentType) + ? currentType + : (types[0] ?? currentType); + const nextModel = getModelsForType(newProvider, nextType)[0]?.value ?? ""; + applyProviderTypeModel(newProvider, nextType, nextModel); + }; + + const handleTypeChange = (newType: ConfigType) => { + const candidates = getModelsForType(provider, newType); + const stillValid = candidates.some((m) => m.value === params.model); + const nextModel = stillValid ? params.model : (candidates[0]?.value ?? ""); + applyProviderTypeModel(provider, newType, nextModel); + }; + + const handleModelChange = (model: string) => { + applyProviderTypeModel(provider, currentType, model); + }; + const saveDisabled = !configName.trim() || isSaving; return ( @@ -212,14 +218,14 @@ export default function ConfigEditorPane({ } className={inputClass} > - {PROVIDER_TYPES.map((option) => ( + {typeOptions.map((option) => ( ))}

- {PROVIDER_TYPES.find( + {typeOptions.find( (t) => t.value === (configBlob.completion.type || "text"), )?.description ?? ""}

diff --git a/app/hooks/useConfigPersistence.ts b/app/hooks/useConfigPersistence.ts index c2ab6516..24180869 100644 --- a/app/hooks/useConfigPersistence.ts +++ b/app/hooks/useConfigPersistence.ts @@ -94,14 +94,17 @@ export function useConfigPersistence({ : {}; const acceptsTools = !schema || "max_output_tokens" in schema.config; + const completionType = currentConfigBlob.completion.type || "text"; const configBlob: ConfigBlob = { completion: { provider: completionProvider, - type: currentConfigBlob.completion.type || "text", + type: completionType, params: { model, - instructions: currentContent, + // TTS params forbid `instructions` on the backend; only send it + // for types that accept it (text, stt). + ...(completionType !== "tts" && { instructions: currentContent }), ...modelParams, ...(allKnowledgeBaseIds.length > 0 && { knowledge_base_ids: allKnowledgeBaseIds, @@ -177,7 +180,11 @@ export function useConfigPersistence({ return true; } catch (e) { console.error("Failed to save config:", e); - toast.error("Failed to save configuration. Please try again."); + toast.error( + e instanceof Error && e.message + ? `Failed to save configuration: ${e.message}` + : "Failed to save configuration. Please try again.", + ); return false; } finally { setIsSaving(false); diff --git a/app/lib/apiClient.ts b/app/lib/apiClient.ts index a1952bfd..a737b7b5 100644 --- a/app/lib/apiClient.ts +++ b/app/lib/apiClient.ts @@ -37,16 +37,41 @@ export async function apiClient( } /** Parse an error body into a readable message string. */ +function stringifyValidationDetail(value: unknown): string { + if (!Array.isArray(value)) { + return typeof value === "string" ? value : ""; + } + return value + .map((item) => { + if (typeof item === "string") return item; + if (item && typeof item === "object") { + const rec = item as Record; + const field = Array.isArray(rec.loc) + ? rec.loc.filter((p) => p !== "body").join(".") + : (rec.field as string) || ""; + const msg = (rec.msg as string) || (rec.message as string) || ""; + return field ? `${field}: ${msg}` : msg; + } + return ""; + }) + .filter(Boolean) + .join("; "); +} + function extractErrorMessage( body: Record, fallback: string, ): string { + const detail = + stringifyValidationDetail(body.errors) || + stringifyValidationDetail(body.detail); const msg = (body.error as string) || (body.message as string) || - (body.detail as string) || + (typeof body.detail === "string" ? (body.detail as string) : "") || ""; - return msg || fallback; + const combined = [msg, detail].filter(Boolean).join(" — "); + return combined || fallback; } /** Dispatch the auth-expired event (client-side only). */ diff --git a/app/lib/chatClient.ts b/app/lib/chatClient.ts index 356f2d9c..15b7c410 100644 --- a/app/lib/chatClient.ts +++ b/app/lib/chatClient.ts @@ -243,7 +243,7 @@ export function configToBlob(config: SavedConfig): ConfigBlob { const blob: ConfigBlob = { completion: { - provider: config.provider as "openai", + provider: config.provider, type: config.type ?? "text", params, }, diff --git a/app/lib/modelSchema.ts b/app/lib/modelSchema.ts index 853a58a1..f7cca59b 100644 --- a/app/lib/modelSchema.ts +++ b/app/lib/modelSchema.ts @@ -7,7 +7,6 @@ import { getModelSchemaCache } from "@/app/lib/store/modelSchemaStore"; import type { ModelCompletionType, ModelSchema, - ParamSchema, RawModelEntry, } from "@/app/lib/types/models"; @@ -19,27 +18,18 @@ export type { RawModelEntry, } from "@/app/lib/types/models"; -export const SUPPORTED_PROVIDERS = ["openai", "google"] as const; - -export const SUPPORTED_PARAMS = new Set(["effort", "temperature"]); - export function flattenGroupedModels( grouped: Record, ): ModelSchema[] { const out: ModelSchema[] = []; - for (const [providerKey, entries] of Object.entries(grouped)) { - if (!SUPPORTED_PROVIDERS.includes(providerKey as never)) continue; + for (const entries of Object.values(grouped)) { for (const entry of entries) { if (entry.is_active === false) continue; - const config: Record = {}; - for (const [key, spec] of Object.entries(entry.config ?? {})) { - if (SUPPORTED_PARAMS.has(key)) config[key] = spec; - } out.push({ provider: entry.provider, model: entry.model_name, completion_type: entry.completion_type, - config, + config: entry.config ?? {}, is_active: entry.is_active, }); } @@ -47,20 +37,18 @@ export function flattenGroupedModels( return out; } -export const PROVIDER_LABELS: Record = { - openai: "OpenAI", - google: "Google", -}; +function toTitleCase(value: string): string { + return value + .split(/[-_\s]+/) + .filter(Boolean) + .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) + .join(" "); +} export function getProviderLabel(provider: string): string { - return PROVIDER_LABELS[provider] ?? provider; + return toTitleCase(provider); } -export const PARAM_LABELS: Record = { - effort: "Effort", - temperature: "Temperature", -}; - export const PARAM_VALUE_LABELS: Record> = { effort: { none: "None", @@ -106,8 +94,20 @@ export function getModelsForProviderAndType( ); } +export function getCompletionTypesForProvider( + provider: string, +): ModelCompletionType[] { + return Array.from( + new Set( + schemas() + .filter((m) => m.provider === provider) + .flatMap((m) => m.completion_type), + ), + ); +} + export function getParamLabel(key: string): string { - return PARAM_LABELS[key] ?? key; + return toTitleCase(key); } export function getParamValueLabel(key: string, value: unknown): string { diff --git a/app/lib/types/configs.ts b/app/lib/types/configs.ts index 5e6d47cc..327a2b92 100644 --- a/app/lib/types/configs.ts +++ b/app/lib/types/configs.ts @@ -64,7 +64,7 @@ export interface CompletionParams { } export interface CompletionConfig { - provider: "openai"; + provider: string; type?: "text" | "stt" | "tts"; params: CompletionParams; } diff --git a/app/lib/utils.ts b/app/lib/utils.ts index 144a1bbd..324483f8 100644 --- a/app/lib/utils.ts +++ b/app/lib/utils.ts @@ -9,7 +9,6 @@ import { } from "@/app/lib/types/configs"; import { SavedConfig, ConfigGroup } from "./types/configs"; import { - SUPPORTED_PARAMS, getModelSchema, getParamValueLabel, reconcileParamsForModel, @@ -277,7 +276,6 @@ export const pickModelParams = ( const out: Record = {}; for (const [key, value] of Object.entries(params)) { if (RESERVED_PARAM_KEYS.has(key)) continue; - if (!SUPPORTED_PARAMS.has(key)) continue; if (value === undefined || value === null) continue; out[key] = value; } From a93e3409b579cf22d918e57a20d10b3f02431c04 Mon Sep 17 00:00:00 2001 From: Prashant Vasudevan <71649489+vprashrex@users.noreply.github.com> Date: Wed, 1 Jul 2026 10:24:20 +0530 Subject: [PATCH 2/4] feat(config): enhance model provider handling and label mapping --- app/lib/modelSchema.ts | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/app/lib/modelSchema.ts b/app/lib/modelSchema.ts index f7cca59b..5a1df387 100644 --- a/app/lib/modelSchema.ts +++ b/app/lib/modelSchema.ts @@ -18,11 +18,18 @@ export type { RawModelEntry, } from "@/app/lib/types/models"; +export const SUPPORTED_PROVIDERS = [ + "openai", + "google", + "google-aistudio", +] as const; + export function flattenGroupedModels( grouped: Record, ): ModelSchema[] { const out: ModelSchema[] = []; - for (const entries of Object.values(grouped)) { + for (const [providerKey, entries] of Object.entries(grouped)) { + if (!SUPPORTED_PROVIDERS.includes(providerKey as never)) continue; for (const entry of entries) { if (entry.is_active === false) continue; out.push({ @@ -37,18 +44,24 @@ export function flattenGroupedModels( return out; } -function toTitleCase(value: string): string { - return value - .split(/[-_\s]+/) - .filter(Boolean) - .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) - .join(" "); -} +export const PROVIDER_LABELS: Record = { + openai: "OpenAI", + google: "Google", + "google-aistudio": "Google AI Studio", +}; export function getProviderLabel(provider: string): string { - return toTitleCase(provider); + return PROVIDER_LABELS[provider] ?? provider; } +export const PARAM_LABELS: Record = { + effort: "Effort", + temperature: "Temperature", + summary: "Summary", + voice: "Voice", + thinking_level: "Thinking Level", +}; + export const PARAM_VALUE_LABELS: Record> = { effort: { none: "None", @@ -107,7 +120,7 @@ export function getCompletionTypesForProvider( } export function getParamLabel(key: string): string { - return toTitleCase(key); + return PARAM_LABELS[key] ?? key; } export function getParamValueLabel(key: string, value: unknown): string { From 66a63e06885ec7bd76530632add9ec59be879a0f Mon Sep 17 00:00:00 2001 From: Prashant Vasudevan <71649489+vprashrex@users.noreply.github.com> Date: Wed, 1 Jul 2026 10:39:51 +0530 Subject: [PATCH 3/4] feat(config): refine model provider configuration handling and parameter support --- app/lib/modelSchema.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/app/lib/modelSchema.ts b/app/lib/modelSchema.ts index 5a1df387..0b195dd5 100644 --- a/app/lib/modelSchema.ts +++ b/app/lib/modelSchema.ts @@ -7,6 +7,7 @@ import { getModelSchemaCache } from "@/app/lib/store/modelSchemaStore"; import type { ModelCompletionType, ModelSchema, + ParamSchema, RawModelEntry, } from "@/app/lib/types/models"; @@ -24,6 +25,14 @@ export const SUPPORTED_PROVIDERS = [ "google-aistudio", ] as const; +export const SUPPORTED_PARAMS = new Set([ + "effort", + "temperature", + "summary", + "voice", + "thinking_level", +]); + export function flattenGroupedModels( grouped: Record, ): ModelSchema[] { @@ -32,11 +41,15 @@ export function flattenGroupedModels( if (!SUPPORTED_PROVIDERS.includes(providerKey as never)) continue; for (const entry of entries) { if (entry.is_active === false) continue; + const config: Record = {}; + for (const [key, spec] of Object.entries(entry.config ?? {})) { + if (SUPPORTED_PARAMS.has(key)) config[key] = spec; + } out.push({ provider: entry.provider, model: entry.model_name, completion_type: entry.completion_type, - config: entry.config ?? {}, + config, is_active: entry.is_active, }); } From 40da0c2f8ab041bddc8c4e893e62e71ddc2c6e56 Mon Sep 17 00:00:00 2001 From: Prashant Vasudevan <71649489+vprashrex@users.noreply.github.com> Date: Thu, 2 Jul 2026 10:13:54 +0530 Subject: [PATCH 4/4] refactor(apiClient): remove redundant error parsing comment --- app/lib/apiClient.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/lib/apiClient.ts b/app/lib/apiClient.ts index 19169d55..05aa7664 100644 --- a/app/lib/apiClient.ts +++ b/app/lib/apiClient.ts @@ -63,7 +63,6 @@ export async function apiClient< } as ApiClientResponse; } -/** Parse an error body into a readable message string. */ function stringifyValidationDetail(value: unknown): string { if (!Array.isArray(value)) { return typeof value === "string" ? value : ""; @@ -91,7 +90,9 @@ function extractErrorMessage( ): string { const detail = stringifyValidationDetail(body.errors) || - stringifyValidationDetail(body.detail); + (typeof body.detail === "string" + ? "" + : stringifyValidationDetail(body.detail)); const msg = (body.error as string) || (body.message as string) ||