Skip to content
Merged
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
80 changes: 80 additions & 0 deletions ui/desktop/src/acp/__tests__/providers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { getAcpClient } from '../acpConnection';
import { acpSetSessionProviderModel } from '../providers';

vi.mock('../acpConnection', () => ({
getAcpClient: vi.fn(),
}));

function selectConfigOption(id: string, currentValue: string) {
return {
id,
name: id,
type: 'select',
currentValue,
options: [],
};
}

describe('ACP providers', () => {
beforeEach(() => {
vi.clearAllMocks();
});

it('sets thinking effort after provider and model, then returns the final config response', async () => {
const client = {
setSessionConfigOption: vi
.fn()
.mockResolvedValueOnce({
configOptions: [
selectConfigOption('provider', 'anthropic'),
selectConfigOption('model', 'provider-default-model'),
],
})
.mockResolvedValueOnce({
configOptions: [
selectConfigOption('provider', 'anthropic'),
selectConfigOption('model', 'claude-sonnet-4-5'),
],
})
.mockResolvedValueOnce({
configOptions: [
selectConfigOption('provider', 'anthropic'),
selectConfigOption('model', 'claude-sonnet-4-5'),
selectConfigOption('thinking_effort', 'high'),
],
}),
};
vi.mocked(getAcpClient).mockResolvedValue(
client as unknown as Awaited<ReturnType<typeof getAcpClient>>
);

const applied = await acpSetSessionProviderModel(
'session-1',
'anthropic',
'claude-sonnet-4-5',
'high'
);

expect(client.setSessionConfigOption).toHaveBeenCalledTimes(3);
expect(client.setSessionConfigOption).toHaveBeenNthCalledWith(1, {
sessionId: 'session-1',
configId: 'provider',
value: 'anthropic',
});
expect(client.setSessionConfigOption).toHaveBeenNthCalledWith(2, {
sessionId: 'session-1',
configId: 'model',
value: 'claude-sonnet-4-5',
});
expect(client.setSessionConfigOption).toHaveBeenNthCalledWith(3, {
sessionId: 'session-1',
configId: 'thinking_effort',
value: 'high',
});
expect(applied).toEqual({
providerId: 'anthropic',
modelId: 'claude-sonnet-4-5',
});
});
});
75 changes: 71 additions & 4 deletions ui/desktop/src/acp/providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,55 @@ export async function acpSaveThinkingEffort(effort: ThinkingEffort): Promise<voi
});
}

export type AppliedSessionProviderModel = {
providerId?: string;
modelId?: string;
};

function extractAppliedSessionProviderModel(configOptions: unknown): AppliedSessionProviderModel {
if (!Array.isArray(configOptions)) {
return {};
}

const applied: AppliedSessionProviderModel = {};

for (const option of configOptions) {
if (!option || typeof option !== 'object') {
continue;
}

const id = 'id' in option ? option.id : undefined;
if (id !== 'provider' && id !== 'model') {
continue;
}

const currentValue = selectCurrentValue(option);
if (typeof currentValue !== 'string') {
continue;
}

if (id === 'provider') {
applied.providerId = currentValue;
} else {
applied.modelId = currentValue;
}
}

return applied;
}

function selectCurrentValue(kind: unknown): unknown {
if (!kind || typeof kind !== 'object') {
return undefined;
}

if ('type' in kind && kind.type === 'select' && 'currentValue' in kind) {
return kind.currentValue;
}

return undefined;
}

/**
* Switch the provider (and model) for an active session via ACP config options.
*
Expand All @@ -207,11 +256,29 @@ export async function acpSaveThinkingEffort(effort: ThinkingEffort): Promise<voi
export async function acpSetSessionProviderModel(
sessionId: string,
providerId: string,
modelId?: string | null
): Promise<void> {
modelId?: string | null,
thinkingEffort?: ThinkingEffort | null
): Promise<AppliedSessionProviderModel> {
const client = await getAcpClient();
await client.setSessionConfigOption({ sessionId, configId: 'provider', value: providerId });
let response = await client.setSessionConfigOption({
sessionId,
configId: 'provider',
value: providerId,
});
if (modelId) {
await client.setSessionConfigOption({ sessionId, configId: 'model', value: modelId });
response = await client.setSessionConfigOption({
sessionId,
configId: 'model',
value: modelId,
});
}
if (thinkingEffort != null) {
response = await client.setSessionConfigOption({
sessionId,
configId: 'thinking_effort',
value: thinkingEffort,
});
}

return extractAppliedSessionProviderModel(response.configOptions);
}
37 changes: 35 additions & 2 deletions ui/desktop/src/components/ModelAndProviderContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@ import React, { createContext, useContext, useState, useEffect, useMemo, useCall
import { toastError, toastSuccess } from '../toasts';
import Model, { getProviderMetadata } from './settings/models/modelInterface';
import { ProviderMetadata } from '../api';
import { acpReadDefaults, acpSaveDefaults, acpSetSessionProviderModel } from '../acp/providers';
import { acpChatSessionActions, acpChatSessionStore } from '../acp/chatSessionStore';
import {
acpReadDefaults,
acpSaveDefaults,
acpSetSessionProviderModel,
type AppliedSessionProviderModel,
} from '../acp/providers';
import { errorMessage } from '../utils/conversionUtils';
import {
getModelDisplayName,
Expand Down Expand Up @@ -57,6 +63,27 @@ const ModelAndProviderContext = createContext<ModelAndProviderContextType | unde

export { i18n as modelAndProviderMessages };

function patchAcpSessionProviderModel(
sessionId: string,
{ providerId, modelId }: AppliedSessionProviderModel
) {
if (!providerId && !modelId) return;

const currentSession = acpChatSessionStore.getSnapshot(sessionId)?.session;
if (!currentSession) return;

acpChatSessionActions.setSessionMetadata(sessionId, {
...currentSession,
provider_name: providerId ?? currentSession.provider_name,
model_config: modelId
? {
...(currentSession.model_config ?? { toolshim: false }),
model_name: modelId,
}
: currentSession.model_config,
});
}

export const ModelAndProviderProvider: React.FC<ModelAndProviderProviderProps> = ({ children }) => {
const [currentModel, setCurrentModel] = useState<string | null>(null);
const [currentProvider, setCurrentProvider] = useState<string | null>(null);
Expand All @@ -70,7 +97,13 @@ export const ModelAndProviderProvider: React.FC<ModelAndProviderProviderProps> =

try {
if (sessionId) {
await acpSetSessionProviderModel(sessionId, providerName, modelName);
const applied = await acpSetSessionProviderModel(
sessionId,
providerName,
modelName,
model.request_params?.thinking_effort ?? null
);
patchAcpSessionProviderModel(sessionId, applied);
}

// Only update the global config default when there's no session
Expand Down
Loading