Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 12 additions & 5 deletions packages/bruno-app/src/components/AiChatSidebar/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -555,7 +555,8 @@ const AiChatSidebar = ({ collection, variant = 'sidebar' }) => {
if (textareaRef.current) textareaRef.current.style.height = 'auto';

try {
await dispatch(sendAiMessage(activeTabUid, text, allContent, requestContext, selectedModel, contentType, aiVariables, appEnabled, aiRequests));
const modelFormat = selectedModelEntry?.apiFormat || null;
await dispatch(sendAiMessage(activeTabUid, text, allContent, requestContext, selectedModel, modelFormat, contentType, aiVariables, appEnabled, aiRequests));
setProcessingStage('applying');
setTimeout(() => setProcessingStage(null), 500);
} catch (err) {
Expand Down Expand Up @@ -667,11 +668,17 @@ const AiChatSidebar = ({ collection, variant = 'sidebar' }) => {
try { localStorage.setItem(SELECTED_MODEL_LS_KEY, modelId); } catch {}
};

const selectedModelLabel = useMemo(() => {
if (selectedModel === AUTO_MODEL_ID) return 'Auto';
return availableModels.find((m) => m.id === selectedModel)?.label || 'Auto';
const selectedModelEntry = useMemo(() => {
if (selectedModel === AUTO_MODEL_ID) return null;
return availableModels.find((m) => m.id === selectedModel) || null;
}, [availableModels, selectedModel]);

const selectedModelLabel = useMemo(() => {
if (!selectedModelEntry) return 'Auto';
const suffix = selectedModelEntry.apiFormat === 'responses' ? ' · Responses API' : '';
return `${selectedModelEntry.label}${suffix}`;
}, [selectedModelEntry]);

const ModelSelectorTrigger = forwardRef((props, ref) => (
<div ref={ref} className="model-btn" {...props}>
<IconSparkles size={14} strokeWidth={1.75} />
Expand All @@ -686,7 +693,7 @@ const AiChatSidebar = ({ collection, variant = 'sidebar' }) => {
{ id: AUTO_MODEL_ID, label: 'Auto', onClick: () => handleModelSelect(AUTO_MODEL_ID) },
...availableModels.map((model) => ({
id: model.id,
label: model.label,
label: `${model.label}${model.apiFormat === 'responses' ? ' · Responses API' : ''}`,
onClick: () => handleModelSelect(model.id)
}))
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,8 @@ const CompatEndpointCard = ({
onAddModel({
id: uuid(),
modelId: id,
label: newModelLabel.trim() || id
label: newModelLabel.trim() || id,
apiFormat: 'chat-completions'
});
setNewModelId('');
setNewModelLabel('');
Expand Down Expand Up @@ -375,6 +376,8 @@ const CompatEndpointCard = ({
{models.map((model) => {
const enabled = isModelEnabled(model.id);
const disabled = !provider.configured || !providerEnabled;
const apiFormat = model.apiFormat || 'chat-completions';
const modelName = model.label || model.modelId || model.id;
return (
<div
key={model.id}
Expand All @@ -401,9 +404,19 @@ const CompatEndpointCard = ({
placeholder="Model id"
onChange={(e) => onUpdateModel(model.id, { modelId: e.target.value })}
/>
<select
className="compat-inline-input flex-[0.9] text-xs"
value={apiFormat}
onChange={(e) => onUpdateModel(model.id, { apiFormat: e.target.value })}
disabled={disabled}
aria-label={`API format for ${modelName}`}
>
<option value="chat-completions">Chat Completions</option>
<option value="responses">Responses API</option>
</select>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
<button
type="button"
className="btn-icon danger w-6 h-6 box-border inline-flex items-center justify-center cursor-pointer"
className="btn-icon danger w-7 h-7 box-border inline-flex items-center justify-center cursor-pointer"
onClick={() => onRemoveModel(model.id)}
title="Remove model"
aria-label="Remove model"
Expand Down
3 changes: 2 additions & 1 deletion packages/bruno-app/src/components/Preferences/AI/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ const aiPreferencesSchema = Yup.object().shape({
Yup.object().shape({
id: Yup.string().required(),
label: Yup.string().max(120).nullable(),
modelId: Yup.string().max(200).nullable()
modelId: Yup.string().max(200).nullable(),
apiFormat: Yup.string().oneOf(['chat-completions', 'responses']).nullable()
})
)
})
Expand Down
2 changes: 2 additions & 0 deletions packages/bruno-app/src/providers/ReduxStore/slices/chat.js
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,7 @@ export const sendAiMessage = (
allContent,
requestContext,
model,
modelApiFormat,
contentType = 'app',
variables = [],
appEnabled = true,
Expand Down Expand Up @@ -394,6 +395,7 @@ export const sendAiMessage = (
requests,
requestId,
model,
modelApiFormat,
appEnabled
});
});
Expand Down
15 changes: 13 additions & 2 deletions packages/bruno-electron/src/ipc/ai/chat.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,18 @@ const registerChatIpc = ({ mainWindow, resolveModel, pickDefaultModelId, isAiEna
});

ipcMain.on('renderer:ai-chat-stream', async (_event, payload) => {
const { messages, allContent, contentType, requestContext, variables, requests, requestId, model: modelId, appEnabled } = payload || {};
const {
messages,
allContent,
contentType,
requestContext,
variables,
requests,
requestId,
model: modelId,
modelApiFormat,
appEnabled
} = payload || {};

const send = (channel, data) => {
if (mainWindow?.webContents && !mainWindow.webContents.isDestroyed()) {
Expand Down Expand Up @@ -149,7 +160,7 @@ const registerChatIpc = ({ mainWindow, resolveModel, pickDefaultModelId, isAiEna

let model;
try {
model = resolveModel(effectiveModelId);
model = resolveModel(effectiveModelId, { apiFormatOverride: modelApiFormat });
} catch (err) {
send('main:ai-chat-error', { requestId, error: err.message });
return;
Expand Down
5 changes: 3 additions & 2 deletions packages/bruno-electron/src/ipc/ai/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,13 +72,14 @@ const buildStatus = () => {
};
};

const resolveModel = (modelId) => {
const resolveModel = (modelId, { apiFormatOverride } = {}) => {
if (!isEnabled()) {
throw new Error('AI features are disabled. Enable them in Preferences > AI.');
}
return getModel(modelId, {
aiPreferences: getAiPrefs(),
getApiKey: (providerId) => aiKeyStore.getKey(providerId)
getApiKey: (providerId) => aiKeyStore.getKey(providerId),
apiFormatOverride
});
};

Expand Down
52 changes: 38 additions & 14 deletions packages/bruno-electron/src/ipc/ai/providers.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,14 @@ const PROVIDERS = {
// autocomplete drop those params for reasoning models to silence warnings.
const MODEL_DEFINITIONS = {
// OpenAI
'gpt-4o-mini': { provider: 'openai', modelId: 'gpt-4o-mini', label: 'GPT-4o Mini' },
'gpt-4o': { provider: 'openai', modelId: 'gpt-4o', label: 'GPT-4o' },
'gpt-5': { provider: 'openai', modelId: 'gpt-5', label: 'GPT-5', reasoning: true },
'gpt-5-mini': { provider: 'openai', modelId: 'gpt-5-mini', label: 'GPT-5 Mini', reasoning: true },
'gpt-4o-mini': { provider: 'openai', modelId: 'gpt-4o-mini', label: 'GPT-4o Mini', apiFormat: 'chat-completions' },
'gpt-4o': { provider: 'openai', modelId: 'gpt-4o', label: 'GPT-4o', apiFormat: 'chat-completions' },
'gpt-5': { provider: 'openai', modelId: 'gpt-5', label: 'GPT-5', reasoning: true, apiFormat: 'chat-completions' },
'gpt-5-mini': { provider: 'openai', modelId: 'gpt-5-mini', label: 'GPT-5 Mini', reasoning: true, apiFormat: 'chat-completions' },
// Anthropic
'claude-opus-4-7': { provider: 'anthropic', modelId: 'claude-opus-4-7', label: 'Claude Opus 4.7', reasoning: true },
'claude-sonnet-4-6': { provider: 'anthropic', modelId: 'claude-sonnet-4-6', label: 'Claude Sonnet 4.6', reasoning: true },
'claude-haiku-4-5': { provider: 'anthropic', modelId: 'claude-haiku-4-5-20251001', label: 'Claude Haiku 4.5', reasoning: true }
'claude-opus-4-7': { provider: 'anthropic', modelId: 'claude-opus-4-7', label: 'Claude Opus 4.7', reasoning: true, apiFormat: 'chat-completions' },
'claude-sonnet-4-6': { provider: 'anthropic', modelId: 'claude-sonnet-4-6', label: 'Claude Sonnet 4.6', reasoning: true, apiFormat: 'chat-completions' },
'claude-haiku-4-5': { provider: 'anthropic', modelId: 'claude-haiku-4-5-20251001', label: 'Claude Haiku 4.5', reasoning: true, apiFormat: 'chat-completions' }
};

const isReasoningModel = (modelId) => Boolean(MODEL_DEFINITIONS[modelId]?.reasoning);
Expand Down Expand Up @@ -133,7 +133,8 @@ const listModels = (aiPreferences) => {
id,
label: def.label,
provider: def.provider,
isCustom: false
isCustom: false,
apiFormat: def.apiFormat || 'chat-completions'
}));

const endpoints = Array.isArray(aiPreferences?.openaiCompatibleEndpoints)
Expand All @@ -149,7 +150,8 @@ const listModels = (aiPreferences) => {
id: model.id,
label: model.label || model.modelId,
provider: providerIdFromEndpointId(endpoint.id),
isCustom: true
isCustom: true,
apiFormat: model.apiFormat || 'chat-completions'
});
}
}
Expand All @@ -165,7 +167,8 @@ const resolveModelDefinition = (modelId, aiPreferences) => {
providerId: def.provider,
sdkModelId: def.modelId,
label: def.label,
baseURL: null
baseURL: null,
apiFormat: def.apiFormat || 'chat-completions'
};
}

Expand All @@ -180,7 +183,8 @@ const resolveModelDefinition = (modelId, aiPreferences) => {
providerId: providerIdFromEndpointId(endpoint.id),
sdkModelId: match.modelId,
label: match.label || match.modelId,
baseURL: endpoint.baseURL || ''
baseURL: endpoint.baseURL || '',
apiFormat: match.apiFormat || 'chat-completions'
};
}
}
Expand All @@ -203,7 +207,7 @@ const providerLabel = (providerId, aiPreferences) => {
* Resolve a Bruno model id to a vercel-ai SDK model instance.
* Throws if the provider isn't configured (no key) or the model is unknown.
*/
const getModel = (modelId, { aiPreferences, getApiKey }) => {
const getModel = (modelId, { aiPreferences, getApiKey, apiFormatOverride }) => {
const def = resolveModelDefinition(modelId, aiPreferences);
if (!def) throw new Error(`Unknown model: ${modelId}`);

Expand All @@ -223,7 +227,22 @@ const getModel = (modelId, { aiPreferences, getApiKey }) => {
}

const sdk = getSdk({ providerId: def.providerId, apiKey, baseURL: def.baseURL });
if (isOpenAiCompatibleProviderId(def.providerId)) return sdk.chat(def.sdkModelId);

const format = apiFormatOverride || def.apiFormat || 'chat-completions';
if (format && format !== 'chat-completions' && format !== 'responses') {
throw new Error(`Unsupported API format "${format}" for ${providerLabel(def.providerId, aiPreferences)}. Supported formats are Chat Completions and Responses.`);
}
if (format === 'responses') {
if (typeof sdk.responses === 'function') {
return sdk.responses(def.sdkModelId);
}
throw new Error(`${providerLabel(def.providerId, aiPreferences)} does not support the Responses API. Switch this model to Chat Completions in Preferences > AI.`);
}

if (typeof sdk.chat === 'function') {
return sdk.chat(def.sdkModelId);
}

return sdk(def.sdkModelId);
};

Expand All @@ -247,7 +266,12 @@ const getAvailableModels = ({ aiPreferences, hasApiKey }) => {
if (!endpoint?.baseURL) continue;
}

out.push({ id: model.id, label: model.label, provider: model.provider });
out.push({
id: model.id,
label: model.label,
provider: model.provider,
apiFormat: model.apiFormat || 'chat-completions'
});
}
return out;
};
Expand Down
20 changes: 19 additions & 1 deletion packages/bruno-electron/src/store/preferences.js
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,8 @@ const preferencesSchema = Yup.object().shape({
Yup.object({
id: Yup.string().required(),
label: Yup.string().max(120).nullable(),
modelId: Yup.string().max(200).nullable()
modelId: Yup.string().max(200).nullable(),
apiFormat: Yup.string().oneOf(['chat-completions', 'responses']).nullable()
})
)
})
Expand Down Expand Up @@ -375,6 +376,23 @@ class PreferencesStore {
this.store.set('preferences', preferences);
}

const endpoints = get(preferences, 'ai.openaiCompatibleEndpoints');
if (Array.isArray(endpoints)) {
let mutated = false;
for (const endpoint of endpoints) {
if (!endpoint || !Array.isArray(endpoint.models)) continue;
for (const model of endpoint.models) {
if (model && !model.apiFormat) {
model.apiFormat = 'chat-completions';
mutated = true;
}
}
}
if (mutated) {
this.store.set('preferences', preferences);
}
}

return merge({}, defaultPreferences, preferences);
}

Expand Down