Skip to content

Commit 341e5f4

Browse files
authored
fix(Config): fix Google provider models via dynamic multi-provider config (#239)
1 parent e646b0d commit 341e5f4

7 files changed

Lines changed: 122 additions & 45 deletions

File tree

‎app/components/prompt-editor/ConfigEditorPane.tsx‎

Lines changed: 43 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
import { useEffect, useRef, useState } from "react";
22
import { guardrailsFetch } from "@/app/lib/guardrailsClient";
33
import { ConfigEditorPaneProps, Tool } from "@/app/lib/types/promptEditor";
4-
import { CompletionConfig, CompletionParams } from "@/app/lib/types/configs";
5-
import { ConfigType, MODEL_OPTIONS, getModelsForType } from "@/app/lib/models";
4+
import {
5+
CompletionConfig,
6+
CompletionParams,
7+
ProviderType,
8+
} from "@/app/lib/types/configs";
9+
import { ConfigType, getModelsForType } from "@/app/lib/models";
610
import { PROVIDER_TYPES } from "@/app/lib/constants";
711
import {
812
getAllProviders,
13+
getCompletionTypesForProvider,
914
getModelSchema,
1015
getParamLabel,
1116
getProviderLabel,
@@ -87,6 +92,10 @@ export default function ConfigEditorPane({
8792
value: p,
8893
label: getProviderLabel(p),
8994
}));
95+
const availableTypes = getCompletionTypesForProvider(provider);
96+
const typeOptions = PROVIDER_TYPES.filter((t) =>
97+
availableTypes.includes(t.value as ConfigType),
98+
);
9099

91100
const selectedConfig = savedConfigs.find((c) => c.id === selectedConfigId);
92101
const isBoundToSavedConfig = !!boundConfigId;
@@ -110,35 +119,14 @@ export default function ConfigEditorPane({
110119
});
111120
};
112121

113-
const handleProviderChange = (newProvider: string) => {
114-
const candidates = getModelsForType(newProvider, currentType);
115-
const fallback = MODEL_OPTIONS[newProvider]?.[0]?.value ?? "";
116-
const nextModel = candidates[0]?.value ?? fallback;
117-
handleConfigChange({
118-
completion: { provider: newProvider as CompletionConfig["provider"] },
119-
params: { model: nextModel },
120-
});
121-
};
122-
123-
const handleTypeChange = (newType: ConfigType) => {
124-
const provider = configBlob.completion.provider;
125-
const candidates = getModelsForType(provider, newType);
126-
const stillValid = candidates.some((m) => m.value === params.model);
127-
const nextModel = stillValid
128-
? params.model
129-
: (candidates[0]?.value ??
130-
MODEL_OPTIONS[provider]?.[0]?.value ??
131-
params.model);
132-
handleConfigChange({
133-
completion: { type: newType },
134-
params: { model: nextModel },
135-
});
136-
};
137-
138-
const handleModelChange = (model: string) => {
139-
const nextSchema = getModelSchema(provider, model);
122+
const applyProviderTypeModel = (
123+
nextProvider: string,
124+
nextType: ConfigType,
125+
nextModel: string,
126+
) => {
127+
const nextSchema = getModelSchema(nextProvider, nextModel);
140128
const nextSchemaParams = nextSchema
141-
? reconcileParamsForModel(provider, model, params)
129+
? reconcileParamsForModel(nextProvider, nextModel, params)
142130
: {};
143131
const oldSchemaKeys = modelSchema ? Object.keys(modelSchema.config) : [];
144132
const carryover = { ...params };
@@ -148,11 +136,33 @@ export default function ConfigEditorPane({
148136
...configBlob,
149137
completion: {
150138
...configBlob.completion,
151-
params: { ...carryover, model, ...nextSchemaParams },
139+
provider: nextProvider as ProviderType,
140+
type: nextType,
141+
params: { ...carryover, model: nextModel, ...nextSchemaParams },
152142
},
153143
});
154144
};
155145

146+
const handleProviderChange = (newProvider: string) => {
147+
const types = getCompletionTypesForProvider(newProvider);
148+
const nextType = types.includes(currentType)
149+
? currentType
150+
: (types[0] ?? currentType);
151+
const nextModel = getModelsForType(newProvider, nextType)[0]?.value ?? "";
152+
applyProviderTypeModel(newProvider, nextType, nextModel);
153+
};
154+
155+
const handleTypeChange = (newType: ConfigType) => {
156+
const candidates = getModelsForType(provider, newType);
157+
const stillValid = candidates.some((m) => m.value === params.model);
158+
const nextModel = stillValid ? params.model : (candidates[0]?.value ?? "");
159+
applyProviderTypeModel(provider, newType, nextModel);
160+
};
161+
162+
const handleModelChange = (model: string) => {
163+
applyProviderTypeModel(provider, currentType, model);
164+
};
165+
156166
const saveDisabled = !configName.trim() || isSaving;
157167

158168
return (
@@ -212,14 +222,14 @@ export default function ConfigEditorPane({
212222
}
213223
className={inputClass}
214224
>
215-
{PROVIDER_TYPES.map((option) => (
225+
{typeOptions.map((option) => (
216226
<option key={option.value} value={option.value}>
217227
{option.label}
218228
</option>
219229
))}
220230
</select>
221231
<p className="text-xs mt-1.5 text-text-secondary">
222-
{PROVIDER_TYPES.find(
232+
{typeOptions.find(
223233
(t) => t.value === (configBlob.completion.type || "text"),
224234
)?.description ?? ""}
225235
</p>

‎app/hooks/useConfigPersistence.ts‎

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -94,14 +94,17 @@ export function useConfigPersistence({
9494
: {};
9595

9696
const acceptsTools = !schema || "max_output_tokens" in schema.config;
97+
const completionType = currentConfigBlob.completion.type || "text";
9798

9899
const configBlob: ConfigBlob = {
99100
completion: {
100101
provider: completionProvider,
101-
type: currentConfigBlob.completion.type || "text",
102+
type: completionType,
102103
params: {
103104
model,
104-
instructions: currentContent,
105+
// TTS params forbid `instructions` on the backend; only send it
106+
// for types that accept it (text, stt).
107+
...(completionType !== "tts" && { instructions: currentContent }),
105108
...modelParams,
106109
...(allKnowledgeBaseIds.length > 0 && {
107110
knowledge_base_ids: allKnowledgeBaseIds,
@@ -177,7 +180,11 @@ export function useConfigPersistence({
177180
return true;
178181
} catch (e) {
179182
console.error("Failed to save config:", e);
180-
toast.error("Failed to save configuration. Please try again.");
183+
toast.error(
184+
e instanceof Error && e.message
185+
? `Failed to save configuration: ${e.message}`
186+
: "Failed to save configuration. Please try again.",
187+
);
181188
return false;
182189
} finally {
183190
setIsSaving(false);

‎app/lib/apiClient.ts‎

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,17 +63,43 @@ export async function apiClient<
6363
} as ApiClientResponse<TData, TResponseType>;
6464
}
6565

66-
/** Parse an error body into a readable message string. */
66+
function stringifyValidationDetail(value: unknown): string {
67+
if (!Array.isArray(value)) {
68+
return typeof value === "string" ? value : "";
69+
}
70+
return value
71+
.map((item) => {
72+
if (typeof item === "string") return item;
73+
if (item && typeof item === "object") {
74+
const rec = item as Record<string, unknown>;
75+
const field = Array.isArray(rec.loc)
76+
? rec.loc.filter((p) => p !== "body").join(".")
77+
: (rec.field as string) || "";
78+
const msg = (rec.msg as string) || (rec.message as string) || "";
79+
return field ? `${field}: ${msg}` : msg;
80+
}
81+
return "";
82+
})
83+
.filter(Boolean)
84+
.join("; ");
85+
}
86+
6787
function extractErrorMessage(
6888
body: Record<string, unknown>,
6989
fallback: string,
7090
): string {
91+
const detail =
92+
stringifyValidationDetail(body.errors) ||
93+
(typeof body.detail === "string"
94+
? ""
95+
: stringifyValidationDetail(body.detail));
7196
const msg =
7297
(body.error as string) ||
7398
(body.message as string) ||
74-
(body.detail as string) ||
99+
(typeof body.detail === "string" ? (body.detail as string) : "") ||
75100
"";
76-
return msg || fallback;
101+
const combined = [msg, detail].filter(Boolean).join(" — ");
102+
return combined || fallback;
77103
}
78104

79105
/** Dispatch the auth-expired event (client-side only). */

‎app/lib/chatClient.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { apiFetch } from "@/app/lib/apiClient";
1010
import {
1111
CompletionParams,
1212
ConfigBlob,
13+
ProviderType,
1314
SavedConfig,
1415
Tool,
1516
} from "@/app/lib/types/configs";
@@ -243,7 +244,7 @@ export function configToBlob(config: SavedConfig): ConfigBlob {
243244

244245
const blob: ConfigBlob = {
245246
completion: {
246-
provider: config.provider as "openai",
247+
provider: config.provider as ProviderType,
247248
type: config.type ?? "text",
248249
params,
249250
},

‎app/lib/modelSchema.ts‎

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,19 @@ export type {
1919
RawModelEntry,
2020
} from "@/app/lib/types/models";
2121

22-
export const SUPPORTED_PROVIDERS = ["openai", "google"] as const;
23-
24-
export const SUPPORTED_PARAMS = new Set(["effort", "temperature"]);
22+
export const SUPPORTED_PROVIDERS = [
23+
"openai",
24+
"google",
25+
"google-aistudio",
26+
] as const;
27+
28+
export const SUPPORTED_PARAMS = new Set([
29+
"effort",
30+
"temperature",
31+
"summary",
32+
"voice",
33+
"thinking_level",
34+
]);
2535

2636
export function flattenGroupedModels(
2737
grouped: Record<string, RawModelEntry[]>,
@@ -50,6 +60,7 @@ export function flattenGroupedModels(
5060
export const PROVIDER_LABELS: Record<string, string> = {
5161
openai: "OpenAI",
5262
google: "Google",
63+
"google-aistudio": "Google AI Studio",
5364
};
5465

5566
export function getProviderLabel(provider: string): string {
@@ -59,6 +70,9 @@ export function getProviderLabel(provider: string): string {
5970
export const PARAM_LABELS: Record<string, string> = {
6071
effort: "Effort",
6172
temperature: "Temperature",
73+
summary: "Summary",
74+
voice: "Voice",
75+
thinking_level: "Thinking Level",
6276
};
6377

6478
export const PARAM_VALUE_LABELS: Record<string, Record<string, string>> = {
@@ -106,6 +120,18 @@ export function getModelsForProviderAndType(
106120
);
107121
}
108122

123+
export function getCompletionTypesForProvider(
124+
provider: string,
125+
): ModelCompletionType[] {
126+
return Array.from(
127+
new Set(
128+
schemas()
129+
.filter((m) => m.provider === provider)
130+
.flatMap((m) => m.completion_type),
131+
),
132+
);
133+
}
134+
109135
export function getParamLabel(key: string): string {
110136
return PARAM_LABELS[key] ?? key;
111137
}

‎app/lib/types/assessment/config.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type {
33
CompletionConfig,
44
ConfigPublic,
55
ConfigVersionItems,
6+
ProviderType,
67
} from "@/app/lib/types/configs";
78
import type { LabeledValue, ValueSetter } from "./core";
89

@@ -29,7 +30,7 @@ export interface ConfigParamDefinition {
2930
}
3031

3132
export interface AssessmentModelConfig {
32-
provider: "openai" | "google" | "google-aistudio" | "anthropic";
33+
provider: ProviderType;
3334
model_name: string;
3435
config: Record<string, ConfigParamDefinition>;
3536
}

‎app/lib/types/configs.ts‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
import type { AssessmentTag } from "@/app/lib/types/assessment";
22

3+
export type ProviderType =
4+
| "openai"
5+
| "google"
6+
| "google-aistudio"
7+
| "anthropic";
8+
39
export interface SavedConfig {
410
id: string;
511
config_id: string;
@@ -66,7 +72,7 @@ export interface CompletionParams {
6672
}
6773

6874
export interface CompletionConfig {
69-
provider: "openai" | "google" | "google-aistudio" | "anthropic";
75+
provider: ProviderType;
7076
type?: "text" | "stt" | "tts";
7177
params: CompletionParams;
7278
}

0 commit comments

Comments
 (0)