- {onInherit && (
-
-
- {safeLevels.includes('provider_default') && (
-
- )}
-
+ {safeLevels.includes('provider_default') && (
+
)}
{currentLabel}
diff --git a/web/src/app/home/components/reasoning/model-reasoning.ts b/web/src/app/home/components/reasoning/model-reasoning.ts
new file mode 100644
index 000000000..56715da00
--- /dev/null
+++ b/web/src/app/home/components/reasoning/model-reasoning.ts
@@ -0,0 +1,18 @@
+import { LANGBOT_MODELS_PROVIDER_REQUESTER } from '../models-dialog/types';
+
+interface ModelReasoningInfo {
+ abilities?: string[];
+ reasoning_capabilities?: { supported?: boolean };
+ provider?: { requester?: string };
+}
+
+export function hasModelReasoningAbility(
+ model: ModelReasoningInfo | undefined,
+ isLangBotModels = model?.provider?.requester ===
+ LANGBOT_MODELS_PROVIDER_REQUESTER,
+): boolean {
+ return Boolean(
+ model?.abilities?.includes('reasoning') ||
+ (isLangBotModels && model?.reasoning_capabilities?.supported === true),
+ );
+}
diff --git a/web/src/i18n/locales/en-US.ts b/web/src/i18n/locales/en-US.ts
index d7645a125..343a06209 100644
--- a/web/src/i18n/locales/en-US.ts
+++ b/web/src/i18n/locales/en-US.ts
@@ -264,8 +264,7 @@ const enUS = {
reasoningAbility: 'Reasoning',
reasoningLevel: 'Reasoning level',
reasoningLevels: {
- useModelSetting: 'Use model setting',
- providerDefault: 'Provider default',
+ providerDefault: 'Use provider default',
disabled: 'Off',
enabled: 'On',
minimal: 'Minimal',
diff --git a/web/src/i18n/locales/es-ES.ts b/web/src/i18n/locales/es-ES.ts
index fae6ed0a6..d393f25ab 100644
--- a/web/src/i18n/locales/es-ES.ts
+++ b/web/src/i18n/locales/es-ES.ts
@@ -265,8 +265,7 @@ const esES = {
reasoningAbility: 'Razonamiento',
reasoningLevel: 'Nivel de razonamiento',
reasoningLevels: {
- useModelSetting: 'Usar ajuste del modelo',
- providerDefault: 'Predeterminado del proveedor',
+ providerDefault: 'Usar valor predeterminado del proveedor',
disabled: 'Desactivado',
enabled: 'Activado',
minimal: 'Mínimo',
diff --git a/web/src/i18n/locales/ja-JP.ts b/web/src/i18n/locales/ja-JP.ts
index 02e729fc2..874996391 100644
--- a/web/src/i18n/locales/ja-JP.ts
+++ b/web/src/i18n/locales/ja-JP.ts
@@ -269,8 +269,7 @@ const jaJP = {
reasoningAbility: '推論',
reasoningLevel: '推論レベル',
reasoningLevels: {
- useModelSetting: 'モデル設定を使用',
- providerDefault: 'Provider デフォルト',
+ providerDefault: 'プロバイダーの既定値を使用',
disabled: 'オフ',
enabled: 'オン',
minimal: '最小',
diff --git a/web/src/i18n/locales/ru-RU.ts b/web/src/i18n/locales/ru-RU.ts
index 5c0ae0394..0329ba233 100644
--- a/web/src/i18n/locales/ru-RU.ts
+++ b/web/src/i18n/locales/ru-RU.ts
@@ -261,8 +261,7 @@ const ruRU = {
reasoningAbility: 'Рассуждение',
reasoningLevel: 'Уровень рассуждений',
reasoningLevels: {
- useModelSetting: 'Настройка модели',
- providerDefault: 'По умолчанию провайдера',
+ providerDefault: 'Использовать настройки поставщика',
disabled: 'Выключено',
enabled: 'Включено',
minimal: 'Минимальный',
diff --git a/web/src/i18n/locales/th-TH.ts b/web/src/i18n/locales/th-TH.ts
index 9d58e6053..fc5a55b8f 100644
--- a/web/src/i18n/locales/th-TH.ts
+++ b/web/src/i18n/locales/th-TH.ts
@@ -256,8 +256,7 @@ const thTH = {
reasoningAbility: 'ความสามารถในการให้เหตุผล',
reasoningLevel: 'ระดับการให้เหตุผล',
reasoningLevels: {
- useModelSetting: 'ใช้การตั้งค่าโมเดล',
- providerDefault: 'ค่าเริ่มต้นของผู้ให้บริการ',
+ providerDefault: 'ใช้ค่าเริ่มต้นของผู้ให้บริการ',
disabled: 'ปิด',
enabled: 'เปิด',
minimal: 'ต่ำสุด',
diff --git a/web/src/i18n/locales/vi-VN.ts b/web/src/i18n/locales/vi-VN.ts
index 21f277c9b..e922e7197 100644
--- a/web/src/i18n/locales/vi-VN.ts
+++ b/web/src/i18n/locales/vi-VN.ts
@@ -260,8 +260,7 @@ const viVN = {
reasoningAbility: 'Khả năng suy luận',
reasoningLevel: 'Mức độ suy luận',
reasoningLevels: {
- useModelSetting: 'Dùng cài đặt mô hình',
- providerDefault: 'Mặc định của nhà cung cấp',
+ providerDefault: 'Dùng giá trị mặc định của nhà cung cấp',
disabled: 'Tắt',
enabled: 'Bật',
minimal: 'Tối thiểu',
diff --git a/web/src/i18n/locales/zh-Hans.ts b/web/src/i18n/locales/zh-Hans.ts
index 1ca3b448e..eaa767481 100644
--- a/web/src/i18n/locales/zh-Hans.ts
+++ b/web/src/i18n/locales/zh-Hans.ts
@@ -254,8 +254,7 @@ const zhHans = {
reasoningAbility: '思考能力',
reasoningLevel: '思考档位',
reasoningLevels: {
- useModelSetting: '使用模型设置',
- providerDefault: 'Provider 默认',
+ providerDefault: '使用供应商默认值',
disabled: '关闭',
enabled: '开启',
minimal: '最低',
diff --git a/web/src/i18n/locales/zh-Hant.ts b/web/src/i18n/locales/zh-Hant.ts
index 4203387d6..246eab410 100644
--- a/web/src/i18n/locales/zh-Hant.ts
+++ b/web/src/i18n/locales/zh-Hant.ts
@@ -248,8 +248,7 @@ const zhHant = {
reasoningAbility: '思考能力',
reasoningLevel: '思考等級',
reasoningLevels: {
- useModelSetting: '使用模型設定',
- providerDefault: '供應商預設',
+ providerDefault: '使用供應商預設值',
disabled: '關閉',
enabled: '開啟',
minimal: '最低',
diff --git a/web/tests/e2e/reasoning-edit-semantics.spec.ts b/web/tests/e2e/reasoning-edit-semantics.spec.ts
index 98fe489f8..738137291 100644
--- a/web/tests/e2e/reasoning-edit-semantics.spec.ts
+++ b/web/tests/e2e/reasoning-edit-semantics.spec.ts
@@ -14,6 +14,7 @@ async function setup(
name = 'WeKnoraAgent',
agentId: null | 'absent' = null,
converted?: typeof canonical,
+ reasoningEnabled = true,
) {
const schema = contract.plugins[plugin].component;
const id = `plugin:langbot-team/${name}/default`;
@@ -127,8 +128,10 @@ async function setup(
name: uuid,
provider_uuid: 'provider-valid',
provider: { uuid: 'provider-valid', name: 'Mock Provider' },
+ abilities: reasoningEnabled ? ['reasoning'] : [],
reasoning: { level: 'high' },
reasoning_capabilities: {
+ supported: true,
levels: ['provider_default', 'low', 'high'],
},
})),
@@ -141,14 +144,19 @@ async function setup(
return { writes, raw, id };
}
-test('reasoning edit distinguishes provider default from model inheritance and preserves fallbacks', async ({
+test('explicit reasoning choices persist independently for primary and fallback models', async ({
page,
}) => {
const { writes, raw, id } = await setup(page, 'LocalAgent', 'LocalAgent');
await page
.getByRole('button', { name: 'Reasoning level: High', exact: true })
.click();
- await page.getByRole('slider').press('Home');
+ await expect(
+ page.getByRole('button', { name: 'Use model setting', exact: true }),
+ ).toHaveCount(0);
+ await page
+ .getByRole('button', { name: 'Use provider default', exact: true })
+ .click();
await page.keyboard.press('Escape');
await page.getByRole('button', { name: 'Save', exact: true }).click();
await expect.poll(() => writes.length).toBe(1);
@@ -161,67 +169,37 @@ test('reasoning edit distinguishes provider default from model inheritance and p
});
await page.reload();
await page.getByRole('tab', { name: 'AI', exact: true }).click();
- await page
- .getByRole('button', {
- name: 'Reasoning level: Provider default',
- exact: true,
- })
- .click();
- await page
- .getByRole('button', { name: 'Use model setting', exact: true })
- .click();
- await page.keyboard.press('Escape');
- await page.getByRole('button', { name: 'Save', exact: true }).click();
- await expect.poll(() => writes.length).toBe(2);
- expect(writes[1].config.ai.runner_config[id]).toEqual({
- ...raw,
- model: { ...(raw.model as object), reasoning: { 'llm-fallback': 'low' } },
- });
- expect(writes[1].config.output).toEqual(writes[0].config.output);
- await page.reload();
- await page.getByRole('tab', { name: 'AI', exact: true }).click();
await expect(
page.getByRole('button', {
- name: 'Reasoning level: Use model setting',
+ name: 'Reasoning level: Use provider default',
exact: true,
}),
).toBeVisible();
await expect(
page.getByRole('button', { name: 'Save', exact: true }),
).toBeDisabled();
- // The reverse transition must not require moving through another level.
- await page
- .getByRole('button', {
- name: 'Reasoning level: Use model setting',
- exact: true,
- })
- .click();
- await page
- .getByRole('button', { name: 'Provider default', exact: true })
- .click();
- await page.keyboard.press('Escape');
- await page.getByRole('button', { name: 'Save', exact: true }).click();
- await expect.poll(() => writes.length).toBe(3);
- expect(writes[2].config.ai.runner_config[id]).toEqual(
- writes[0].config.ai.runner_config[id],
- );
- // Clearing a fallback override must not clear the primary override.
await page
.getByRole('button', { name: 'Reasoning level: Low', exact: true })
.click();
- await page
- .getByRole('button', { name: 'Use model setting', exact: true })
- .click();
+ await page.getByRole('slider').press('End');
await page.keyboard.press('Escape');
await page.getByRole('button', { name: 'Save', exact: true }).click();
- await expect.poll(() => writes.length).toBe(4);
- expect(writes[3].config.ai.runner_config[id]).toEqual({
- ...raw,
- model: {
- ...(raw.model as object),
- reasoning: { 'llm-valid': 'provider_default' },
- },
+ await expect.poll(() => writes.length).toBe(2);
+ expect(writes[1].config.ai.runner_config[id].model.reasoning).toEqual({
+ 'llm-valid': 'provider_default',
+ 'llm-fallback': 'high',
});
+ expect(writes[1].config.output).toEqual(writes[0].config.output);
+});
+
+test('custom models without reasoning enabled hide budget controls despite inferred support', async ({
+ page,
+}) => {
+ await setup(page, 'LocalAgent', 'LocalAgent', null, undefined, false);
+ await expect(page.getByRole('combobox').first()).toBeVisible();
+ await expect(
+ page.getByRole('button', { name: /^Reasoning level:/ }),
+ ).toHaveCount(0);
});
test('standalone picker keeps its original levels and provider default API', async ({
@@ -267,7 +245,7 @@ test('standalone picker keeps its original levels and provider default API', asy
});
await page
.getByRole('button', {
- name: 'Reasoning level: Provider default',
+ name: 'Reasoning level: Use provider default',
exact: true,
})
.click();
@@ -285,3 +263,70 @@ test('standalone picker keeps its original levels and provider default API', asy
'provider_default',
);
});
+
+test('LangBot Models reasoning checkbox matches the capability icon', async ({
+ page,
+}) => {
+ await installLangBotApiMocks(page, { authenticated: true });
+ await page.route('**/api/v1/user/info', (route) =>
+ route.fulfill({
+ json: {
+ code: 0,
+ data: {
+ account_uuid: 'account-playwright',
+ user: 'admin@example.com',
+ account_type: 'space',
+ has_password: true,
+ },
+ },
+ }),
+ );
+ const provider = {
+ uuid: 'space-provider',
+ name: 'LangBot Models',
+ requester: 'space-chat-completions',
+ base_url: '',
+ api_keys: [],
+ llm_count: 1,
+ embedding_count: 0,
+ rerank_count: 0,
+ };
+ await page.route('**/api/v1/provider/**', (route) => {
+ const path = new URL(route.request().url()).pathname;
+ const ok = (data: unknown) => route.fulfill({ json: { code: 0, data } });
+ if (path.endsWith('/providers')) return ok({ providers: [provider] });
+ if (path.endsWith('/requesters')) return ok({ requesters: [] });
+ if (path.includes('/models/'))
+ return ok({
+ models: path.endsWith('/llm')
+ ? [
+ {
+ uuid: 'space-reasoning',
+ name: 'Space reasoning model',
+ provider_uuid: provider.uuid,
+ provider,
+ abilities: ['vision', 'func_call'],
+ extra_args: {},
+ reasoning_capabilities: {
+ supported: true,
+ levels: ['provider_default', 'low', 'high'],
+ },
+ },
+ ]
+ : [],
+ });
+ return ok({ provider });
+ });
+ await page.goto('/home/bots');
+ await page.getByRole('button', { name: 'Models', exact: true }).click();
+ const card = page
+ .locator('[data-slot="card"]')
+ .filter({ hasText: 'LangBot Models' });
+ await card.getByText('Space reasoning model', { exact: true }).click();
+ const checkbox = page.getByRole('checkbox', {
+ name: 'Reasoning',
+ exact: true,
+ });
+ await expect(checkbox).toBeChecked();
+ await expect(checkbox).toBeDisabled();
+});
diff --git a/web/tests/unit/reasoning-edit-semantics.test.mjs b/web/tests/unit/reasoning-edit-semantics.test.mjs
index 622e18e97..c44e93e22 100644
--- a/web/tests/unit/reasoning-edit-semantics.test.mjs
+++ b/web/tests/unit/reasoning-edit-semantics.test.mjs
@@ -33,10 +33,9 @@ test('provider default is an explicit override, not inheritance', () => {
b: 'low',
});
});
-test('inherit deletes only the selected override without mutating input', () => {
+test('changing one model preserves other overrides without mutating input', () => {
const input = Object.freeze({ a: 'provider_default', b: 'high' });
- const output = edit(input, 'a', undefined);
- assert.equal(Object.hasOwn(output, 'a'), false);
- assert.deepEqual(output, { b: 'high' });
+ const output = edit(input, 'a', 'low');
+ assert.deepEqual(output, { a: 'low', b: 'high' });
assert.deepEqual(input, { a: 'provider_default', b: 'high' });
});