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) ||