diff --git a/app/components/prompt-editor/ConfigEditorPane.tsx b/app/components/prompt-editor/ConfigEditorPane.tsx index 73397c6..721115b 100644 --- a/app/components/prompt-editor/ConfigEditorPane.tsx +++ b/app/components/prompt-editor/ConfigEditorPane.tsx @@ -1,11 +1,16 @@ 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 { + CompletionConfig, + CompletionParams, + ProviderType, +} from "@/app/lib/types/configs"; +import { ConfigType, getModelsForType } from "@/app/lib/models"; import { PROVIDER_TYPES } from "@/app/lib/constants"; import { getAllProviders, + getCompletionTypesForProvider, getModelSchema, getParamLabel, getProviderLabel, @@ -87,6 +92,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 +119,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 +136,33 @@ export default function ConfigEditorPane({ ...configBlob, completion: { ...configBlob.completion, - params: { ...carryover, model, ...nextSchemaParams }, + provider: nextProvider as ProviderType, + 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 +222,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 c2ab651..2418086 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 6c0f045..05aa766 100644 --- a/app/lib/apiClient.ts +++ b/app/lib/apiClient.ts @@ -63,17 +63,43 @@ 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 : ""; + } + 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) || + (typeof body.detail === "string" + ? "" + : 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 356f2d9..57e0731 100644 --- a/app/lib/chatClient.ts +++ b/app/lib/chatClient.ts @@ -10,6 +10,7 @@ import { apiFetch } from "@/app/lib/apiClient"; import { CompletionParams, ConfigBlob, + ProviderType, SavedConfig, Tool, } from "@/app/lib/types/configs"; @@ -243,7 +244,7 @@ export function configToBlob(config: SavedConfig): ConfigBlob { const blob: ConfigBlob = { completion: { - provider: config.provider as "openai", + provider: config.provider as ProviderType, type: config.type ?? "text", params, }, diff --git a/app/lib/modelSchema.ts b/app/lib/modelSchema.ts index 853a58a..0b195dd 100644 --- a/app/lib/modelSchema.ts +++ b/app/lib/modelSchema.ts @@ -19,9 +19,19 @@ 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 const SUPPORTED_PROVIDERS = [ + "openai", + "google", + "google-aistudio", +] as const; + +export const SUPPORTED_PARAMS = new Set([ + "effort", + "temperature", + "summary", + "voice", + "thinking_level", +]); export function flattenGroupedModels( grouped: Record, @@ -50,6 +60,7 @@ export function flattenGroupedModels( export const PROVIDER_LABELS: Record = { openai: "OpenAI", google: "Google", + "google-aistudio": "Google AI Studio", }; export function getProviderLabel(provider: string): string { @@ -59,6 +70,9 @@ export function getProviderLabel(provider: string): string { export const PARAM_LABELS: Record = { effort: "Effort", temperature: "Temperature", + summary: "Summary", + voice: "Voice", + thinking_level: "Thinking Level", }; export const PARAM_VALUE_LABELS: Record> = { @@ -106,6 +120,18 @@ 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; } diff --git a/app/lib/types/assessment/config.ts b/app/lib/types/assessment/config.ts index a1ea1e5..72ecd8b 100644 --- a/app/lib/types/assessment/config.ts +++ b/app/lib/types/assessment/config.ts @@ -3,6 +3,7 @@ import type { CompletionConfig, ConfigPublic, ConfigVersionItems, + ProviderType, } from "@/app/lib/types/configs"; import type { LabeledValue, ValueSetter } from "./core"; @@ -29,7 +30,7 @@ export interface ConfigParamDefinition { } export interface AssessmentModelConfig { - provider: "openai" | "google" | "google-aistudio" | "anthropic"; + provider: ProviderType; model_name: string; config: Record; } diff --git a/app/lib/types/configs.ts b/app/lib/types/configs.ts index 68b259d..1ab8a89 100644 --- a/app/lib/types/configs.ts +++ b/app/lib/types/configs.ts @@ -1,5 +1,11 @@ import type { AssessmentTag } from "@/app/lib/types/assessment"; +export type ProviderType = + | "openai" + | "google" + | "google-aistudio" + | "anthropic"; + export interface SavedConfig { id: string; config_id: string; @@ -66,7 +72,7 @@ export interface CompletionParams { } export interface CompletionConfig { - provider: "openai" | "google" | "google-aistudio" | "anthropic"; + provider: ProviderType; type?: "text" | "stt" | "tts"; params: CompletionParams; }