({
description: 'AIHubMix',
descriptionLocalize: ({ t }) => t('settings.pages.providers.provider.aihubmix.description'),
tasks: ['chat'],
+ capabilities: { chat: { reasoning: { modes: ['enabled', 'disabled'] } } },
icon: 'i-lobe-icons:aihubmix',
iconColor: 'i-lobe-icons:aihubmix-color',
@@ -41,11 +44,22 @@ export const providerAIHubMix = defineProvider
({
}),
}),
createProvider(config) {
- return merge(
+ const provider = merge(
createChatProvider({ apiKey: config.apiKey, baseURL: config.baseUrl! }),
createEmbedProvider({ apiKey: config.apiKey, baseURL: config.baseUrl! }),
createModelProvider({ apiKey: config.apiKey, baseURL: config.baseUrl! }),
)
+
+ return {
+ ...provider,
+ chat(model: string, options?: ChatRequestOptions) {
+ const request = provider.chat(model)
+ if (!options?.reasoning)
+ return request
+
+ return { ...request, reasoningEffort: options.reasoning === 'enabled' ? 'medium' : 'none' }
+ },
+ }
},
validationRequiredWhen(config) {
diff --git a/packages/stage-ui/src/libs/providers/providers/ark-shared.ts b/packages/stage-ui/src/libs/providers/providers/ark-shared.ts
index 3410a895b..79ab7373f 100644
--- a/packages/stage-ui/src/libs/providers/providers/ark-shared.ts
+++ b/packages/stage-ui/src/libs/providers/providers/ark-shared.ts
@@ -1,4 +1,4 @@
-import type { ModelInfo } from '../types'
+import type { ChatRequestOptions, ModelInfo } from '../types'
import { createOpenAI } from '@xsai-ext/providers/create'
import { z } from 'zod'
@@ -64,6 +64,7 @@ export function createArkChatProviderDefinition(options: ArkProviderDefinitionOp
description,
descriptionLocalize: ({ t }) => t(descriptionKey),
tasks: ['chat'],
+ capabilities: { chat: { reasoning: { modes: ['enabled', 'disabled'] } } },
icon,
iconColor,
@@ -86,8 +87,12 @@ export function createArkChatProviderDefinition(options: ArkProviderDefinitionOp
return {
...provider,
- chat(model: string) {
- return originalChat(stripModelPrefix(model, modelPrefix))
+ chat(model: string, requestOptions?: ChatRequestOptions) {
+ const request = originalChat(stripModelPrefix(model, modelPrefix))
+ if (!requestOptions?.reasoning)
+ return request
+
+ return { ...request, thinking: { type: requestOptions.reasoning } }
},
}
},
diff --git a/packages/stage-ui/src/libs/providers/providers/azure-ai-foundry/index.ts b/packages/stage-ui/src/libs/providers/providers/azure-ai-foundry/index.ts
index 2be1bfd18..e368dd043 100644
--- a/packages/stage-ui/src/libs/providers/providers/azure-ai-foundry/index.ts
+++ b/packages/stage-ui/src/libs/providers/providers/azure-ai-foundry/index.ts
@@ -1,4 +1,4 @@
-import type { ModelInfo } from '../../types'
+import type { ChatRequestOptions, ModelInfo, ProviderInstance } from '../../types'
import { createAzure } from '@xsai-ext/providers/special/create'
import { z } from 'zod'
@@ -22,6 +22,7 @@ export const providerAzureAIFoundry = defineProvider({
description: 'azure.com',
descriptionLocalize: ({ t }) => t('settings.pages.providers.provider.azure-ai-foundry.description'),
tasks: ['chat'],
+ capabilities: { chat: { reasoning: { modes: ['enabled', 'disabled'] } } },
icon: 'i-lobe-icons:microsoft',
createProviderConfig: ({ t }) => azureAIFoundryConfigSchema.extend({
@@ -49,11 +50,27 @@ export const providerAzureAIFoundry = defineProvider({
}),
}),
createProvider(config) {
- return createAzure({
+ const provider = createAzure({
apiKey: async () => config.apiKey.trim(),
resourceName: config.resourceName.trim(),
apiVersion: config.apiVersion?.trim(),
- }) as any
+ }).then(baseProvider => ({
+ ...baseProvider,
+ chat(model: string, options?: ChatRequestOptions) {
+ const request = baseProvider.chat(model)
+ if (!options?.reasoning)
+ return request
+
+ return { ...request, reasoningEffort: options.reasoning === 'enabled' ? 'medium' : 'none' }
+ },
+ }))
+
+ // NOTICE:
+ // Azure provider creation resolves its authentication-aware client asynchronously.
+ // ProviderDefinition still declares a synchronous result, while the runtime awaits it.
+ // Source: @xsai-ext/providers special/create and stores/providers/provider.ts.
+ // Remove this cast when ProviderDefinition accepts MaybePromise.
+ return provider as unknown as ProviderInstance
},
extraMethods: {
diff --git a/packages/stage-ui/src/libs/providers/providers/cerebras-ai/index.ts b/packages/stage-ui/src/libs/providers/providers/cerebras-ai/index.ts
index 675384a10..2c05b8d40 100644
--- a/packages/stage-ui/src/libs/providers/providers/cerebras-ai/index.ts
+++ b/packages/stage-ui/src/libs/providers/providers/cerebras-ai/index.ts
@@ -1,3 +1,5 @@
+import type { ChatRequestOptions } from '../../types'
+
import { createCerebras } from '@xsai-ext/providers/create'
import { z } from 'zod'
@@ -23,6 +25,7 @@ export const providerCerebrasAI = defineProvider({
description: 'cerebras.ai',
descriptionLocalize: ({ t }) => t('settings.pages.providers.provider.cerebras.description'),
tasks: ['chat'],
+ capabilities: { chat: { reasoning: { modes: ['enabled', 'disabled'] } } },
icon: 'i-lobe-icons:cerebras',
iconColor: 'i-lobe-icons:cerebras-color',
@@ -40,7 +43,17 @@ export const providerCerebrasAI = defineProvider({
}),
}),
createProvider(config) {
- return createCerebras(config.apiKey, config.baseUrl)
+ const provider = createCerebras(config.apiKey, config.baseUrl)
+ return {
+ ...provider,
+ chat(model: string, options?: ChatRequestOptions) {
+ const request = provider.chat(model)
+ if (!options?.reasoning)
+ return request
+
+ return { ...request, reasoningEffort: options.reasoning === 'enabled' ? 'medium' : 'none' }
+ },
+ }
},
validationRequiredWhen(config) {
diff --git a/packages/stage-ui/src/libs/providers/providers/deepseek/index.test.ts b/packages/stage-ui/src/libs/providers/providers/deepseek/index.test.ts
index 33d56494e..131a29bbe 100644
--- a/packages/stage-ui/src/libs/providers/providers/deepseek/index.test.ts
+++ b/packages/stage-ui/src/libs/providers/providers/deepseek/index.test.ts
@@ -1,15 +1,14 @@
import type {
- ChatProvider,
ChatProviderWithExtraOptions,
} from '@xsai-ext/providers/utils'
-import type { ProviderInstance } from '../../types'
+import type { ChatRequestOptions, ProviderInstance } from '../../types'
import { describe, expect, it } from 'vitest'
import { providerDeepSeek } from './index'
-type DeepSeekChatProvider = ChatProvider | ChatProviderWithExtraOptions
+type DeepSeekChatProvider = ChatProviderWithExtraOptions
function isDeepSeekChatProvider(provider: ProviderInstance): provider is DeepSeekChatProvider {
return 'chat' in provider && typeof provider.chat === 'function'
@@ -52,4 +51,12 @@ describe('providerDeepSeek.createProvider chat options', () => {
thinking: { type: 'enabled' },
})
})
+
+ it('should prioritize request reasoning over the provider setting', () => {
+ const provider = createDeepSeekChatProvider('enable')
+
+ expect(provider.chat('deepseek-chat', { reasoning: 'disabled' })).toMatchObject({
+ thinking: { type: 'disabled' },
+ })
+ })
})
diff --git a/packages/stage-ui/src/libs/providers/providers/deepseek/index.ts b/packages/stage-ui/src/libs/providers/providers/deepseek/index.ts
index b854d6cca..9b43b7c74 100644
--- a/packages/stage-ui/src/libs/providers/providers/deepseek/index.ts
+++ b/packages/stage-ui/src/libs/providers/providers/deepseek/index.ts
@@ -1,3 +1,5 @@
+import type { ChatRequestOptions } from '../../types'
+
import { createDeepSeek } from '@xsai-ext/providers/create'
import { z } from 'zod'
@@ -54,6 +56,7 @@ export const providerDeepSeek = defineProvider({
description: 'deepseek.com',
descriptionLocalize: ({ t }) => t('settings.pages.providers.provider.deepseek.description'),
tasks: ['chat'],
+ capabilities: { chat: { reasoning: { modes: ['enabled', 'disabled'] } } },
icon: 'i-lobe-icons:deepseek',
iconColor: 'i-lobe-icons:deepseek-color',
@@ -95,8 +98,11 @@ export const providerDeepSeek = defineProvider({
return {
...baseProvider,
- chat(model: string) {
+ chat(model: string, options?: ChatRequestOptions) {
const chatOptions = baseProvider.chat(model)
+ if (options?.reasoning)
+ return { ...chatOptions, thinking: { type: options.reasoning } }
+
const thinking = resolveDeepSeekThinking(config.thinkingMode)
if (thinking === undefined)
diff --git a/packages/stage-ui/src/libs/providers/providers/featherless-ai/index.ts b/packages/stage-ui/src/libs/providers/providers/featherless-ai/index.ts
index 5aa702a71..34c182cdc 100644
--- a/packages/stage-ui/src/libs/providers/providers/featherless-ai/index.ts
+++ b/packages/stage-ui/src/libs/providers/providers/featherless-ai/index.ts
@@ -1,3 +1,5 @@
+import type { ChatRequestOptions } from '../../types'
+
import { createOpenAI } from '@xsai-ext/providers/create'
import { z } from 'zod'
@@ -23,6 +25,7 @@ export const providerFeatherlessAI = defineProvider({
description: 'featherless.ai',
descriptionLocalize: ({ t }) => t('settings.pages.providers.provider.featherless.description'),
tasks: ['chat'],
+ capabilities: { chat: { reasoning: { modes: ['enabled', 'disabled'] } } },
icon: 'i-lobe-icons:featherless-color',
createProviderConfig: ({ t }) => featherlessConfigSchema.extend({
@@ -39,7 +42,17 @@ export const providerFeatherlessAI = defineProvider({
}),
}),
createProvider(config) {
- return createOpenAI(config.apiKey, config.baseUrl)
+ const provider = createOpenAI(config.apiKey, config.baseUrl)
+ return {
+ ...provider,
+ chat(model: string, options?: ChatRequestOptions) {
+ const request = provider.chat(model)
+ if (!options?.reasoning)
+ return request
+
+ return { ...request, chatTemplateKwargs: { enable_thinking: options.reasoning === 'enabled' } }
+ },
+ }
},
validationRequiredWhen(config) {
diff --git a/packages/stage-ui/src/libs/providers/providers/fireworks-ai/index.ts b/packages/stage-ui/src/libs/providers/providers/fireworks-ai/index.ts
index bc0c07fc7..9c6abbeec 100644
--- a/packages/stage-ui/src/libs/providers/providers/fireworks-ai/index.ts
+++ b/packages/stage-ui/src/libs/providers/providers/fireworks-ai/index.ts
@@ -1,3 +1,5 @@
+import type { ChatRequestOptions } from '../../types'
+
import { createFireworks } from '@xsai-ext/providers/create'
import { z } from 'zod'
@@ -23,6 +25,7 @@ export const providerFireworksAI = defineProvider({
description: 'fireworks.ai',
descriptionLocalize: ({ t }) => t('settings.pages.providers.provider.fireworks.description'),
tasks: ['chat'],
+ capabilities: { chat: { reasoning: { modes: ['enabled', 'disabled'] } } },
icon: 'i-lobe-icons:fireworks',
iconColor: 'i-lobe-icons:fireworks-color',
@@ -40,7 +43,17 @@ export const providerFireworksAI = defineProvider({
}),
}),
createProvider(config) {
- return createFireworks(config.apiKey, config.baseUrl)
+ const provider = createFireworks(config.apiKey, config.baseUrl)
+ return {
+ ...provider,
+ chat(model: string, options?: ChatRequestOptions) {
+ const request = provider.chat(model)
+ if (!options?.reasoning)
+ return request
+
+ return { ...request, reasoningEffort: options.reasoning === 'enabled' ? 'medium' : 'none' }
+ },
+ }
},
validationRequiredWhen(config) {
diff --git a/packages/stage-ui/src/libs/providers/providers/google-generative-ai/index.ts b/packages/stage-ui/src/libs/providers/providers/google-generative-ai/index.ts
index 791fcf936..6f5ca046d 100644
--- a/packages/stage-ui/src/libs/providers/providers/google-generative-ai/index.ts
+++ b/packages/stage-ui/src/libs/providers/providers/google-generative-ai/index.ts
@@ -1,3 +1,5 @@
+import type { ChatRequestOptions } from '../../types'
+
import { createGoogleGenerativeAI } from '@xsai-ext/providers/create'
import { z } from 'zod'
@@ -24,6 +26,7 @@ export const providerGoogleGenerativeAI = defineProvider
description: 'ai.google.dev',
descriptionLocalize: ({ t }) => t('settings.pages.providers.provider.google-generative-ai.description'),
tasks: ['chat'],
+ capabilities: { chat: { reasoning: { modes: ['enabled', 'disabled'] } } },
icon: 'i-lobe-icons:gemini',
iconColor: 'i-lobe-icons:gemini-color',
@@ -41,7 +44,17 @@ export const providerGoogleGenerativeAI = defineProvider
}),
}),
createProvider(config) {
- return createGoogleGenerativeAI(config.apiKey, config.baseUrl)
+ const provider = createGoogleGenerativeAI(config.apiKey, config.baseUrl)
+ return {
+ ...provider,
+ chat(model: string, options?: ChatRequestOptions) {
+ const request = provider.chat(model)
+ if (!options?.reasoning)
+ return request
+
+ return { ...request, reasoningEffort: options.reasoning === 'enabled' ? 'medium' : 'none' }
+ },
+ }
},
validationRequiredWhen(config) {
diff --git a/packages/stage-ui/src/libs/providers/providers/groq/index.ts b/packages/stage-ui/src/libs/providers/providers/groq/index.ts
index d567379e0..221542ecb 100644
--- a/packages/stage-ui/src/libs/providers/providers/groq/index.ts
+++ b/packages/stage-ui/src/libs/providers/providers/groq/index.ts
@@ -1,3 +1,5 @@
+import type { ChatRequestOptions } from '../../types'
+
import { createOpenAI } from '@xsai-ext/providers/create'
import { z } from 'zod'
@@ -23,6 +25,7 @@ export const providerGroq = defineProvider({
description: 'groq.com',
descriptionLocalize: ({ t }) => t('settings.pages.providers.provider.groq.description'),
tasks: ['chat'],
+ capabilities: { chat: { reasoning: { modes: ['enabled', 'disabled'] } } },
icon: 'i-lobe-icons:groq',
createProviderConfig: ({ t }) => groqConfigSchema.extend({
@@ -39,7 +42,17 @@ export const providerGroq = defineProvider({
}),
}),
createProvider(config) {
- return createOpenAI(config.apiKey, config.baseUrl)
+ const provider = createOpenAI(config.apiKey, config.baseUrl)
+ return {
+ ...provider,
+ chat(model: string, options?: ChatRequestOptions) {
+ const request = provider.chat(model)
+ if (!options?.reasoning)
+ return request
+
+ return { ...request, reasoningEffort: options.reasoning === 'enabled' ? 'medium' : 'none' }
+ },
+ }
},
validationRequiredWhen(config) {
diff --git a/packages/stage-ui/src/libs/providers/providers/mimo/index.ts b/packages/stage-ui/src/libs/providers/providers/mimo/index.ts
index 6d40c5807..a0baa1dd5 100644
--- a/packages/stage-ui/src/libs/providers/providers/mimo/index.ts
+++ b/packages/stage-ui/src/libs/providers/providers/mimo/index.ts
@@ -1,3 +1,5 @@
+import type { ChatRequestOptions } from '../../types'
+
import { createXiaomi } from '@xsai-ext/providers/create'
import { z } from 'zod'
@@ -24,6 +26,7 @@ export const providerMimo = defineProvider({
description: 'api.xiaomimimo.com',
descriptionLocalize: ({ t }) => t('settings.pages.providers.provider.mimo.description'),
tasks: ['chat'],
+ capabilities: { chat: { reasoning: { modes: ['enabled', 'disabled'] } } },
icon: 'i-simple-icons:xiaomi',
createProviderConfig: ({ t }) => mimoConfigSchema.extend({
@@ -40,7 +43,17 @@ export const providerMimo = defineProvider({
}),
}),
createProvider(config) {
- return createXiaomi(config.apiKey, config.baseUrl)
+ const provider = createXiaomi(config.apiKey, config.baseUrl)
+ return {
+ ...provider,
+ chat(model: string, options?: ChatRequestOptions) {
+ const request = provider.chat(model)
+ if (!options?.reasoning)
+ return request
+
+ return { ...request, thinking: { type: options.reasoning } }
+ },
+ }
},
validationRequiredWhen(config) {
diff --git a/packages/stage-ui/src/libs/providers/providers/moonshot-ai/index.ts b/packages/stage-ui/src/libs/providers/providers/moonshot-ai/index.ts
index b5c5dfd5c..344ce3641 100644
--- a/packages/stage-ui/src/libs/providers/providers/moonshot-ai/index.ts
+++ b/packages/stage-ui/src/libs/providers/providers/moonshot-ai/index.ts
@@ -1,3 +1,5 @@
+import type { ChatRequestOptions } from '../../types'
+
import { createMoonshotai } from '@xsai-ext/providers/create'
import { z } from 'zod'
@@ -23,6 +25,7 @@ export const providerMoonshotAI = defineProvider({
description: 'moonshot.ai',
descriptionLocalize: ({ t }) => t('settings.pages.providers.provider.moonshot.description'),
tasks: ['chat'],
+ capabilities: { chat: { reasoning: { modes: ['enabled', 'disabled'] } } },
icon: 'i-lobe-icons:moonshot',
createProviderConfig: ({ t }) => moonshotConfigSchema.extend({
@@ -39,7 +42,17 @@ export const providerMoonshotAI = defineProvider({
}),
}),
createProvider(config) {
- return createMoonshotai(config.apiKey, config.baseUrl)
+ const provider = createMoonshotai(config.apiKey, config.baseUrl)
+ return {
+ ...provider,
+ chat(model: string, options?: ChatRequestOptions) {
+ const request = provider.chat(model)
+ if (!options?.reasoning)
+ return request
+
+ return { ...request, thinking: { type: options.reasoning } }
+ },
+ }
},
validationRequiredWhen(config) {
diff --git a/packages/stage-ui/src/libs/providers/providers/novita-ai/index.ts b/packages/stage-ui/src/libs/providers/providers/novita-ai/index.ts
index 51e16ae6e..e186ac46e 100644
--- a/packages/stage-ui/src/libs/providers/providers/novita-ai/index.ts
+++ b/packages/stage-ui/src/libs/providers/providers/novita-ai/index.ts
@@ -1,3 +1,5 @@
+import type { ChatRequestOptions } from '../../types'
+
import { createNovita } from '@xsai-ext/providers/create'
import { z } from 'zod'
@@ -23,6 +25,7 @@ export const providerNovitaAI = defineProvider({
description: 'novita.ai',
descriptionLocalize: ({ t }) => t('settings.pages.providers.provider.novita.description'),
tasks: ['chat'],
+ capabilities: { chat: { reasoning: { modes: ['enabled', 'disabled'] } } },
icon: 'i-lobe-icons:novita',
iconColor: 'i-lobe-icons:novita-color',
@@ -40,7 +43,17 @@ export const providerNovitaAI = defineProvider({
}),
}),
createProvider(config) {
- return createNovita(config.apiKey, config.baseUrl)
+ const provider = createNovita(config.apiKey, config.baseUrl)
+ return {
+ ...provider,
+ chat(model: string, options?: ChatRequestOptions) {
+ const request = provider.chat(model)
+ if (!options?.reasoning)
+ return request
+
+ return { ...request, enableThinking: options.reasoning === 'enabled' }
+ },
+ }
},
validationRequiredWhen(config) {
diff --git a/packages/stage-ui/src/libs/providers/providers/nvidia/index.ts b/packages/stage-ui/src/libs/providers/providers/nvidia/index.ts
index b3a0995a1..131294df3 100644
--- a/packages/stage-ui/src/libs/providers/providers/nvidia/index.ts
+++ b/packages/stage-ui/src/libs/providers/providers/nvidia/index.ts
@@ -1,3 +1,5 @@
+import type { ChatRequestOptions } from '../../types'
+
import { isStageTamagotchi } from '@proj-airi/stage-shared'
import { createOpenAI } from '@xsai-ext/providers/create'
import { z } from 'zod'
@@ -24,6 +26,7 @@ export const providerNvidia = defineProvider({
description: 'build.nvidia.com',
descriptionLocalize: ({ t }) => t('settings.pages.providers.provider.nvidia.description'),
tasks: ['chat'],
+ capabilities: { chat: { reasoning: { modes: ['enabled', 'disabled'] } } },
icon: 'i-simple-icons:nvidia',
isAvailableBy: isStageTamagotchi,
@@ -41,7 +44,17 @@ export const providerNvidia = defineProvider({
}),
}),
createProvider(config) {
- return createOpenAI(config.apiKey, config.baseUrl)
+ const provider = createOpenAI(config.apiKey, config.baseUrl)
+ return {
+ ...provider,
+ chat(model: string, options?: ChatRequestOptions) {
+ const request = provider.chat(model)
+ if (!options?.reasoning)
+ return request
+
+ return { ...request, chatTemplateKwargs: { enable_thinking: options.reasoning === 'enabled' } }
+ },
+ }
},
validationRequiredWhen(config) {
diff --git a/packages/stage-ui/src/libs/providers/providers/ollama/index.test.ts b/packages/stage-ui/src/libs/providers/providers/ollama/index.test.ts
index d724bfe86..4eae28bf2 100644
--- a/packages/stage-ui/src/libs/providers/providers/ollama/index.test.ts
+++ b/packages/stage-ui/src/libs/providers/providers/ollama/index.test.ts
@@ -1,71 +1,78 @@
+import type { ChatProviderWithExtraOptions } from '@xsai-ext/providers/utils'
+
+import type { ChatRequestOptions, ProviderInstance } from '../../types'
+
import { describe, expect, it } from 'vitest'
-import { providerOllama, resolveOllamaThink } from './index'
+import { providerOllama, resolveOllamaReasoningEffort } from './index'
-describe('providerOllama.resolveOllamaThink', () => {
+type OllamaChatProvider = ChatProviderWithExtraOptions
+
+function isOllamaChatProvider(provider: ProviderInstance): provider is OllamaChatProvider {
+ return 'chat' in provider && typeof provider.chat === 'function'
+}
+
+function createOllamaChatProvider(thinkingMode: 'auto' | 'disable' | 'enable'): OllamaChatProvider {
+ const provider = providerOllama.createProvider({
+ baseUrl: 'http://localhost:11434/v1/',
+ thinkingMode,
+ })
+ if (!isOllamaChatProvider(provider))
+ throw new Error('Ollama provider must support chat')
+
+ return provider
+}
+
+describe('providerOllama.resolveOllamaReasoningEffort', () => {
it('should return undefined for auto mode', () => {
- expect(resolveOllamaThink('qwen3:8b', 'auto')).toBeUndefined()
+ expect(resolveOllamaReasoningEffort('auto')).toBeUndefined()
})
- it('should map disable/enable to booleans for non gpt-oss models', () => {
- expect(resolveOllamaThink('qwen3:8b', 'disable')).toBe(false)
- expect(resolveOllamaThink('qwen3:8b', 'enable')).toBe(true)
- })
-
- it('should map disable/enable to levels for gpt-oss models', () => {
- expect(resolveOllamaThink('gpt-oss:20b', 'disable')).toBe('low')
- expect(resolveOllamaThink('gpt-oss:20b', 'enable')).toBe('medium')
+ it('should map disable/enable to OpenAI-compatible effort values', () => {
+ expect(resolveOllamaReasoningEffort('disable')).toBe('none')
+ expect(resolveOllamaReasoningEffort('enable')).toBe('medium')
})
it('should pass level modes through unchanged', () => {
- expect(resolveOllamaThink('qwen3:8b', 'low')).toBe('low')
- expect(resolveOllamaThink('qwen3:8b', 'medium')).toBe('medium')
- expect(resolveOllamaThink('qwen3:8b', 'high')).toBe('high')
+ expect(resolveOllamaReasoningEffort('low')).toBe('low')
+ expect(resolveOllamaReasoningEffort('medium')).toBe('medium')
+ expect(resolveOllamaReasoningEffort('high')).toBe('high')
})
it('should fallback invalid values to auto mode', () => {
- expect(resolveOllamaThink('qwen3:8b', 'invalid')).toBeUndefined()
+ expect(resolveOllamaReasoningEffort('invalid')).toBeUndefined()
})
})
describe('providerOllama.createProvider chat options', () => {
- it('should not set think when thinkingMode is auto', () => {
- const provider = providerOllama.createProvider({
- baseUrl: 'http://localhost:11434/v1/',
- thinkingMode: 'auto',
- }) as any
+ it('should not set reasoning effort when thinkingMode is auto', () => {
+ const provider = createOllamaChatProvider('auto')
- const chatOptions = provider.chat('qwen3:8b') as Record
- expect('think' in chatOptions).toBe(false)
+ expect(provider.chat('qwen3:8b')).not.toHaveProperty('reasoningEffort')
})
- it('should set think=false for non gpt-oss when thinkingMode is disable', () => {
- const provider = providerOllama.createProvider({
- baseUrl: 'http://localhost:11434/v1/',
- thinkingMode: 'disable',
- }) as any
+ it('should set reasoning effort to none for non gpt-oss when thinkingMode is disable', () => {
+ const provider = createOllamaChatProvider('disable')
- const chatOptions = provider.chat('qwen3:8b') as Record
- expect(chatOptions.think).toBe(false)
+ expect(provider.chat('qwen3:8b')).toMatchObject({ reasoningEffort: 'none' })
})
- it('should set think=medium for gpt-oss when thinkingMode is enable', () => {
- const provider = providerOllama.createProvider({
- baseUrl: 'http://localhost:11434/v1/',
- thinkingMode: 'enable',
- }) as any
+ it('should set reasoning effort to medium when thinkingMode is enable', () => {
+ const provider = createOllamaChatProvider('enable')
- const chatOptions = provider.chat('gpt-oss:20b') as Record
- expect(chatOptions.think).toBe('medium')
+ expect(provider.chat('gpt-oss:20b')).toMatchObject({ reasoningEffort: 'medium' })
})
- it('should set think=low for gpt-oss when thinkingMode is disable', () => {
- const provider = providerOllama.createProvider({
- baseUrl: 'http://localhost:11434/v1/',
- thinkingMode: 'disable',
- }) as any
+ it('should set reasoning effort to none when thinkingMode is disable', () => {
+ const provider = createOllamaChatProvider('disable')
- const chatOptions = provider.chat('gpt-oss:20b') as Record
- expect(chatOptions.think).toBe('low')
+ expect(provider.chat('gpt-oss:20b')).toMatchObject({ reasoningEffort: 'none' })
+ })
+
+ it('should apply request reasoning without checking the model name', () => {
+ const provider = createOllamaChatProvider('auto')
+
+ expect(provider.chat('llama3.2', { reasoning: 'disabled' })).toMatchObject({ reasoningEffort: 'none' })
+ expect(provider.chat('gpt-oss:20b', { reasoning: 'enabled' })).toMatchObject({ reasoningEffort: 'medium' })
})
})
diff --git a/packages/stage-ui/src/libs/providers/providers/ollama/index.ts b/packages/stage-ui/src/libs/providers/providers/ollama/index.ts
index badc72dec..6cfac97c1 100644
--- a/packages/stage-ui/src/libs/providers/providers/ollama/index.ts
+++ b/packages/stage-ui/src/libs/providers/providers/ollama/index.ts
@@ -1,3 +1,5 @@
+import type { ChatRequestOptions } from '../../types'
+
import { createOllama } from '@xsai-ext/providers/create'
import { z } from 'zod'
@@ -5,7 +7,7 @@ import { ProviderValidationCheck } from '../../types'
import { createOpenAICompatibleValidators } from '../../validators'
import { defineProvider } from '../registry'
-type OllamaThinkValue = boolean | 'high' | 'low' | 'medium'
+type OllamaReasoningEffort = 'high' | 'low' | 'medium' | 'none'
type OllamaThinkingMode = 'auto' | 'disable' | 'enable' | 'high' | 'low' | 'medium'
const ollamaConfigSchema = z.object({
@@ -19,10 +21,6 @@ const ollamaConfigSchema = z.object({
type OllamaConfig = z.input
-function isGptOssModel(model: string): boolean {
- return model.toLowerCase().includes('gpt-oss')
-}
-
function normalizeOllamaThinkingMode(value: unknown): OllamaThinkingMode {
switch (value) {
case 'auto':
@@ -37,19 +35,23 @@ function normalizeOllamaThinkingMode(value: unknown): OllamaThinkingMode {
}
}
-export function resolveOllamaThink(model: string, modeRaw: unknown): OllamaThinkValue | undefined {
+/**
+ * Maps the persisted Ollama setting to its OpenAI-compatible effort value.
+ *
+ * @example
+ * resolveOllamaReasoningEffort('disable')
+ * // => 'none'
+ */
+export function resolveOllamaReasoningEffort(modeRaw: unknown): OllamaReasoningEffort | undefined {
const mode = normalizeOllamaThinkingMode(modeRaw)
- const isGptOss = isGptOssModel(model)
switch (mode) {
case 'auto':
return undefined
case 'disable':
- // NOTICE: GPT-OSS ignores boolean `think`, so "disable" degrades to `low`.
- return isGptOss ? 'low' : false
+ return 'none'
case 'enable':
- // NOTICE: GPT-OSS requires levels; map generic "enable" to medium effort.
- return isGptOss ? 'medium' : true
+ return 'medium'
case 'low':
case 'medium':
case 'high':
@@ -67,6 +69,7 @@ export const providerOllama = defineProvider({
description: 'Local Ollama server for fast model iteration.',
descriptionLocalize: ({ t }) => t('settings.pages.providers.provider.ollama.description'),
tasks: ['chat'],
+ capabilities: { chat: { reasoning: { modes: ['enabled', 'disabled'] } } },
icon: 'i-lobe-icons:ollama',
createProviderConfig: ({ t }) => ollamaConfigSchema.extend({
@@ -122,14 +125,21 @@ export const providerOllama = defineProvider({
return {
...baseProvider,
- chat(model: string) {
+ chat(model: string, options?: ChatRequestOptions) {
const chatOptions = baseProvider.chat(model)
- const think = resolveOllamaThink(model, config.thinkingMode)
+ if (options?.reasoning) {
+ return {
+ ...chatOptions,
+ reasoningEffort: options.reasoning === 'enabled' ? 'medium' : 'none',
+ }
+ }
- if (think === undefined)
+ const reasoningEffort = resolveOllamaReasoningEffort(config.thinkingMode)
+
+ if (reasoningEffort === undefined)
return chatOptions
- return { ...chatOptions, think }
+ return { ...chatOptions, reasoningEffort }
},
}
},
diff --git a/packages/stage-ui/src/libs/providers/providers/openai/index.ts b/packages/stage-ui/src/libs/providers/providers/openai/index.ts
index 2f073530d..6dc4c08dc 100644
--- a/packages/stage-ui/src/libs/providers/providers/openai/index.ts
+++ b/packages/stage-ui/src/libs/providers/providers/openai/index.ts
@@ -1,3 +1,5 @@
+import type { ChatRequestOptions } from '../../types'
+
import { createOpenAI } from '@xsai-ext/providers/create'
import { z } from 'zod'
@@ -24,6 +26,7 @@ export const providerOpenAI = defineProvider({
description: 'OpenAI',
descriptionLocalize: ({ t }) => t('settings.pages.providers.provider.openai.description'),
tasks: ['chat'],
+ capabilities: { chat: { reasoning: { modes: ['enabled', 'disabled'] } } },
icon: 'i-lobe-icons:openai',
createProviderConfig: ({ t }) => openAICompatibleConfigSchema.extend({
@@ -40,7 +43,17 @@ export const providerOpenAI = defineProvider({
}),
}),
createProvider(config) {
- return createOpenAI(config.apiKey, config.baseUrl)
+ const provider = createOpenAI(config.apiKey, config.baseUrl)
+ return {
+ ...provider,
+ chat(model: string, options?: ChatRequestOptions) {
+ const request = provider.chat(model)
+ if (!options?.reasoning)
+ return request
+
+ return { ...request, reasoningEffort: options.reasoning === 'enabled' ? 'medium' : 'none' }
+ },
+ }
},
validationRequiredWhen(config) {
diff --git a/packages/stage-ui/src/libs/providers/providers/openrouter-ai/index.test.ts b/packages/stage-ui/src/libs/providers/providers/openrouter-ai/index.test.ts
index a587d0f6d..991b8a86e 100644
--- a/packages/stage-ui/src/libs/providers/providers/openrouter-ai/index.test.ts
+++ b/packages/stage-ui/src/libs/providers/providers/openrouter-ai/index.test.ts
@@ -1,5 +1,8 @@
+import type { ChatProviderWithExtraOptions } from '@xsai-ext/providers/utils'
import type { JsonSchema } from 'xsschema'
+import type { ChatRequestOptions } from '../../types'
+
import { afterEach, describe, expect, it, vi } from 'vitest'
import { createSparkCommandTool } from '../../../../tools/character/orchestrator/spark-command'
@@ -33,6 +36,19 @@ describe('providerOpenRouterAI tool schemas', () => {
vi.unstubAllGlobals()
})
+ it('maps AIRI reasoning modes to OpenRouter request fields', () => {
+ const provider = providerOpenRouterAI.createProvider({
+ apiKey: 'test-key',
+ }) as ChatProviderWithExtraOptions
+
+ expect(provider.chat('openai/gpt-test', { reasoning: 'disabled' })).toMatchObject({
+ reasoning: { effort: 'none' },
+ })
+ expect(provider.chat('openai/gpt-test', { reasoning: 'enabled' })).toMatchObject({
+ reasoning: { effort: 'medium' },
+ })
+ })
+
it('keeps the canonical nullable anyOf when it sends a chat request', async () => {
const tools = await createSparkCommandTool({
sendSparkCommand: () => undefined,
diff --git a/packages/stage-ui/src/libs/providers/providers/openrouter-ai/index.ts b/packages/stage-ui/src/libs/providers/providers/openrouter-ai/index.ts
index c92eba518..39aa1095d 100644
--- a/packages/stage-ui/src/libs/providers/providers/openrouter-ai/index.ts
+++ b/packages/stage-ui/src/libs/providers/providers/openrouter-ai/index.ts
@@ -1,3 +1,5 @@
+import type { ChatRequestOptions } from '../../types'
+
import { createOpenRouter } from '@xsai-ext/providers/create'
import { z } from 'zod'
@@ -29,6 +31,7 @@ export const providerOpenRouterAI = defineProvider({
description: 'openrouter.ai',
descriptionLocalize: ({ t }) => t('settings.pages.providers.provider.openrouter.description'),
tasks: ['chat'],
+ capabilities: { chat: { reasoning: { modes: ['enabled', 'disabled'] } } },
icon: 'i-lobe-icons:openrouter',
createProviderConfig: ({ t }) => openRouterConfigSchema.extend({
@@ -48,15 +51,22 @@ export const providerOpenRouterAI = defineProvider({
const base = createOpenRouter(config.apiKey, config.baseUrl)
return {
...base,
- chat: (model: string) => ({
- ...base.chat(model),
- fetch: (input: RequestInfo | URL, init?: RequestInit) => {
- const headers = new Headers(init?.headers)
- for (const [k, v] of Object.entries(OPENROUTER_ATTRIBUTION_HEADERS))
- headers.set(k, v)
- return globalThis.fetch(input, { ...init, headers })
- },
- }),
+ chat(model: string, options?: ChatRequestOptions) {
+ const request = {
+ ...base.chat(model),
+ fetch: (input: RequestInfo | URL, init?: RequestInit) => {
+ const headers = new Headers(init?.headers)
+ for (const [k, v] of Object.entries(OPENROUTER_ATTRIBUTION_HEADERS))
+ headers.set(k, v)
+ return globalThis.fetch(input, { ...init, headers })
+ },
+ }
+
+ if (!options?.reasoning)
+ return request
+
+ return { ...request, reasoning: { effort: options.reasoning === 'enabled' ? 'medium' : 'none' } }
+ },
}
},
diff --git a/packages/stage-ui/src/libs/providers/providers/together-ai/index.ts b/packages/stage-ui/src/libs/providers/providers/together-ai/index.ts
index 09a7fd462..f03f9a22f 100644
--- a/packages/stage-ui/src/libs/providers/providers/together-ai/index.ts
+++ b/packages/stage-ui/src/libs/providers/providers/together-ai/index.ts
@@ -1,3 +1,5 @@
+import type { ChatRequestOptions } from '../../types'
+
import { createTogetherAI } from '@xsai-ext/providers/create'
import { z } from 'zod'
@@ -23,6 +25,7 @@ export const providerTogetherAI = defineProvider({
description: 'together.ai',
descriptionLocalize: ({ t }) => t('settings.pages.providers.provider.together.description'),
tasks: ['chat'],
+ capabilities: { chat: { reasoning: { modes: ['enabled', 'disabled'] } } },
icon: 'i-lobe-icons:together',
iconColor: 'i-lobe-icons:together-color',
@@ -40,7 +43,17 @@ export const providerTogetherAI = defineProvider({
}),
}),
createProvider(config) {
- return createTogetherAI(config.apiKey, config.baseUrl)
+ const provider = createTogetherAI(config.apiKey, config.baseUrl)
+ return {
+ ...provider,
+ chat(model: string, options?: ChatRequestOptions) {
+ const request = provider.chat(model)
+ if (!options?.reasoning)
+ return request
+
+ return { ...request, reasoning: { enabled: options.reasoning === 'enabled' } }
+ },
+ }
},
validationRequiredWhen(config) {
diff --git a/packages/stage-ui/src/libs/providers/providers/zai/index.ts b/packages/stage-ui/src/libs/providers/providers/zai/index.ts
index ce1a3e2c2..80b548b51 100644
--- a/packages/stage-ui/src/libs/providers/providers/zai/index.ts
+++ b/packages/stage-ui/src/libs/providers/providers/zai/index.ts
@@ -1,3 +1,5 @@
+import type { ChatRequestOptions } from '../../types'
+
import { createZai } from '@xsai-ext/providers/create'
import { z } from 'zod'
@@ -23,6 +25,7 @@ export const providerZai = defineProvider({
description: 'z.ai',
descriptionLocalize: ({ t }) => t('settings.pages.providers.provider.zai.description'),
tasks: ['chat'],
+ capabilities: { chat: { reasoning: { modes: ['enabled', 'disabled'] } } },
icon: 'i-lobe-icons:zai',
createProviderConfig: ({ t }) => zaiConfigSchema.extend({
@@ -39,7 +42,17 @@ export const providerZai = defineProvider({
}),
}),
createProvider(config) {
- return createZai(config.apiKey, config.baseUrl)
+ const provider = createZai(config.apiKey, config.baseUrl)
+ return {
+ ...provider,
+ chat(model: string, options?: ChatRequestOptions) {
+ const request = provider.chat(model)
+ if (!options?.reasoning)
+ return request
+
+ return { ...request, thinking: { type: options.reasoning } }
+ },
+ }
},
validationRequiredWhen(config) {
diff --git a/packages/stage-ui/src/libs/providers/types.ts b/packages/stage-ui/src/libs/providers/types.ts
index 3bab60adf..91d4f49b2 100644
--- a/packages/stage-ui/src/libs/providers/types.ts
+++ b/packages/stage-ui/src/libs/providers/types.ts
@@ -204,6 +204,9 @@ export interface ProviderDefinition {
validateProvider?: Array<(contextOptions: { t: ComposerTranslation }) => ProviderRuntimeValidator>
}
capabilities?: {
+ chat?: {
+ reasoning?: ChatReasoningCapability
+ }
transcription?: {
protocol: 'websocket' | 'http'
generateOutput: boolean
@@ -252,3 +255,18 @@ export interface ProviderDefinition {
}
}
}
+
+/** Reasoning modes that AIRI can request from a chat provider. */
+export type ChatReasoningMode = 'disabled' | 'enabled'
+
+/** User-selected options that a provider applies to one chat request. */
+export interface ChatRequestOptions {
+ /** Requested reasoning mode. */
+ reasoning: ChatReasoningMode
+}
+
+/** Describes the reasoning controls that AIRI implements for a provider. */
+export interface ChatReasoningCapability {
+ /** Modes that AIRI can pass to the provider. */
+ modes: readonly ChatReasoningMode[]
+}
diff --git a/packages/stage-ui/src/stores/character/orchestrator/index.test.ts b/packages/stage-ui/src/stores/character/orchestrator/index.test.ts
index 97b69a1a6..7eea9538f 100644
--- a/packages/stage-ui/src/stores/character/orchestrator/index.test.ts
+++ b/packages/stage-ui/src/stores/character/orchestrator/index.test.ts
@@ -113,9 +113,9 @@ describe('store character-orchestrator', () => {
sendSparkCommandMock.mockReset()
mockedStore(useModsServerChannelStore, pinia).send = sendSparkCommandMock
- const mockGetProviderInstance = vi.fn()
- mockedStore(useProviderStore, pinia).getProviderInstance = mockGetProviderInstance
- mockedStore(useProviderStore, pinia).getProviderInstance.mockResolvedValue({ chat: (_model: string) => ({} as any) })
+ const mockGetChatProviderInstance = vi.fn()
+ mockedStore(useProviderStore, pinia).getChatProviderInstance = mockGetChatProviderInstance
+ mockedStore(useProviderStore, pinia).getChatProviderInstance.mockResolvedValue({ chat: (_model: string) => ({} as any) })
const consciousnessStore = useConsciousnessStore(pinia)
consciousnessStore.activeProvider = 'mock-provider'
diff --git a/packages/stage-ui/src/stores/character/orchestrator/store.ts b/packages/stage-ui/src/stores/character/orchestrator/store.ts
index b66ec380c..a735793cc 100644
--- a/packages/stage-ui/src/stores/character/orchestrator/store.ts
+++ b/packages/stage-ui/src/stores/character/orchestrator/store.ts
@@ -1,6 +1,5 @@
import type { SparkNotifyResponseControl } from '@proj-airi/core-agent/agents/spark-notify'
import type { WebSocketBaseEvent, WebSocketEventOf, WebSocketEvents } from '@proj-airi/server-sdk'
-import type { ChatProvider } from '@xsai-ext/providers/utils'
import { createSparkNotifyAgent, createSparkNotifyReactionPlugin } from '@proj-airi/core-agent/agents/spark-notify'
import { defineStore, storeToRefs } from 'pinia'
@@ -10,14 +9,13 @@ import { useCharacterNotebookStore, useCharacterStore } from '../'
import { useLLM } from '../../ai/chat-llm/llm'
import { useModsServerChannelStore } from '../../mods/api/channel-server'
import { useConsciousnessStore } from '../../modules/consciousness'
-import { useProviderStore } from '../../providers/provider'
export { sparkNotifyCommandSchema } from '@proj-airi/core-agent/agents/spark-notify'
export const useCharacterOrchestratorStore = defineStore('character-orchestrator', () => {
const { stream } = useLLM()
- const { activeProvider, activeModel } = storeToRefs(useConsciousnessStore())
- const providersStore = useProviderStore()
+ const consciousnessStore = useConsciousnessStore()
+ const { activeProvider, activeModel } = storeToRefs(consciousnessStore)
const characterStore = useCharacterStore()
const notebookStore = useCharacterNotebookStore()
const { systemPrompt } = storeToRefs(characterStore)
@@ -123,7 +121,7 @@ export const useCharacterOrchestratorStore = defineStore('character-orchestrator
return undefined
}
- const provider = await providersStore.getProviderInstance(providerId)
+ const provider = await consciousnessStore.getChatProviderInstance(providerId)
processing.value = true
try {
diff --git a/packages/stage-ui/src/stores/chat.contract.test.ts b/packages/stage-ui/src/stores/chat.contract.test.ts
index 20e15de59..7e2e801a7 100644
--- a/packages/stage-ui/src/stores/chat.contract.test.ts
+++ b/packages/stage-ui/src/stores/chat.contract.test.ts
@@ -1,3 +1,4 @@
+import type { StreamOptions } from '@proj-airi/core-agent'
import type { ChatProvider } from '@xsai-ext/providers/utils'
import type { Message, Tool } from '@xsai/shared-chat'
@@ -13,6 +14,7 @@ import {
AIRI_CHAT_SESSION_ID_HEADER,
} from '../libs/analytics-headers'
import { useChatStore } from './chat'
+import { useConsciousnessSettingsStore } from './modules/consciousness-settings'
vi.hoisted(() => {
;(globalThis as any).window = {
@@ -65,7 +67,7 @@ const forkSessionMock = vi.fn()
const ensureSessionMock = vi.fn()
const loadSessionMock = vi.fn()
const deleteSessionMock = vi.fn()
-const getProviderInstanceMock = vi.fn()
+const getChatProviderInstanceMock = vi.fn()
const getToolsByNamesMock = vi.fn<(names: string[]) => Tool[]>()
const activeSessionIdRef = ref('session-1')
@@ -178,12 +180,6 @@ vi.mock('./ai/chat-llm/tools', () => ({
}),
}))
-vi.mock('./providers/provider', () => ({
- useProviderStore: () => ({
- getProviderInstance: getProviderInstanceMock,
- }),
-}))
-
vi.mock('./ai/chat-llm/toolset-prompts', () => ({
useLlmToolsetPromptsStore: () => ({
activeToolsetPrompt: 'Plugin toolset guidance.',
@@ -194,6 +190,9 @@ vi.mock('./modules/consciousness', () => ({
useConsciousnessStore: () => ({
activeModel: activeModelRef,
activeProvider: activeProviderRef,
+ getChatProviderInstance: (providerId: string) => getChatProviderInstanceMock(providerId, {
+ reasoning: useConsciousnessSettingsStore().reasoning ? 'enabled' : 'disabled',
+ }),
}),
}))
@@ -241,7 +240,7 @@ describe('chat store contract', () => {
ensureSessionMock.mockReset()
loadSessionMock.mockReset().mockResolvedValue(true)
deleteSessionMock.mockReset().mockResolvedValue(undefined)
- getProviderInstanceMock.mockReset().mockResolvedValue(provider)
+ getChatProviderInstanceMock.mockReset().mockResolvedValue(provider)
getToolsByNamesMock.mockReset().mockImplementation(names => names.map(name => ({
type: 'function',
function: {
@@ -285,8 +284,8 @@ describe('chat store contract', () => {
text: 'continue',
})
- expect(getProviderInstanceMock).toHaveBeenCalledTimes(2)
- expect(getProviderInstanceMock).toHaveBeenCalledWith('mock-provider')
+ expect(getChatProviderInstanceMock).toHaveBeenCalledTimes(2)
+ expect(getChatProviderInstanceMock).toHaveBeenCalledWith('mock-provider', { reasoning: 'disabled' })
expect(() => structuredClone(result)).not.toThrow()
expect(resolvedToolNames).toEqual([
['stage_widgets'],
@@ -294,6 +293,20 @@ describe('chat store contract', () => {
])
})
+ it('passes the current consciousness reasoning option to the chat provider', async () => {
+ const settings = useConsciousnessSettingsStore()
+ await settings.setReasoning(true)
+ llmStreamMock.mockImplementationOnce(async (_model: string, _chatProvider: ChatProvider, _messages: Message[], options: StreamOptions) => {
+ await options.onStreamEvent?.({ type: 'finish', finishReason: 'stop' })
+ })
+
+ const store = useChatStore()
+ await store.send({ sessionId: 'session-1', text: 'reply without changing provider defaults' })
+
+ expect(getChatProviderInstanceMock).toHaveBeenCalledWith('mock-provider', { reasoning: 'enabled' })
+ await settings.setReasoning(false)
+ })
+
// https://github.com/moeru-ai/airi/issues/2085
it('hydrates the target session before sending for Issue #2085', async () => {
// ROOT CAUSE:
diff --git a/packages/stage-ui/src/stores/chat.ts b/packages/stage-ui/src/stores/chat.ts
index be592540b..58004fddc 100644
--- a/packages/stage-ui/src/stores/chat.ts
+++ b/packages/stage-ui/src/stores/chat.ts
@@ -36,7 +36,6 @@ import { useAiriCardStore } from './modules/airi-card'
import { useAutonomousArtistryStore } from './modules/artistry-autonomous'
import { useConsciousnessStore } from './modules/consciousness'
import { useWebSearchStore } from './modules/web-search'
-import { useProviderStore } from './providers/provider'
import { executeToolCallRerun } from './tool-call-rerun'
interface ForkOptions {
@@ -146,7 +145,6 @@ export const useChatStore = defineStore('chat', () => {
// without its paired prompt-injection defense.
useWebSearchStore()
const consciousnessStore = useConsciousnessStore()
- const providerStore = useProviderStore()
const artistryAutonomousStore = useAutonomousArtistryStore()
const { activeModel, activeProvider } = storeToRefs(consciousnessStore)
const chatSession = useChatSessionStore()
@@ -363,7 +361,7 @@ export const useChatStore = defineStore('chat', () => {
throw new Error('Failed to load the target chat session')
const messageCount = chatSession.getSessionMessages(payload.sessionId).length
- const chatProvider = await providerStore.getProviderInstance(providerId)
+ const chatProvider = await consciousnessStore.getChatProviderInstance(providerId)
if (!chatProvider)
throw new Error(`Failed to resolve chat provider "${providerId}"`)
diff --git a/packages/stage-ui/src/stores/mods/api/context-bridge-performance-call.test.ts b/packages/stage-ui/src/stores/mods/api/context-bridge-performance-call.test.ts
index d19cb71d5..102488d03 100644
--- a/packages/stage-ui/src/stores/mods/api/context-bridge-performance-call.test.ts
+++ b/packages/stage-ui/src/stores/mods/api/context-bridge-performance-call.test.ts
@@ -51,6 +51,7 @@ vi.mock('../../modules/consciousness', () => ({
useConsciousnessStore: () => ({
activeProvider: ref(undefined),
activeModel: ref(undefined),
+ getChatProviderInstance: vi.fn(async () => ({})),
}),
}))
diff --git a/packages/stage-ui/src/stores/mods/api/context-bridge.contract.browser.test.ts b/packages/stage-ui/src/stores/mods/api/context-bridge.contract.browser.test.ts
index 510b2967f..06acb1e19 100644
--- a/packages/stage-ui/src/stores/mods/api/context-bridge.contract.browser.test.ts
+++ b/packages/stage-ui/src/stores/mods/api/context-bridge.contract.browser.test.ts
@@ -262,6 +262,7 @@ vi.mock('../../modules/consciousness', () => ({
useConsciousnessStore: () => ({
activeProvider: activeProviderRef,
activeModel: activeModelRef,
+ getChatProviderInstance: getProviderInstanceMock,
}),
}))
diff --git a/packages/stage-ui/src/stores/mods/api/context-bridge.ts b/packages/stage-ui/src/stores/mods/api/context-bridge.ts
index 61b3546b2..68d071bfd 100644
--- a/packages/stage-ui/src/stores/mods/api/context-bridge.ts
+++ b/packages/stage-ui/src/stores/mods/api/context-bridge.ts
@@ -23,7 +23,6 @@ import { useChatSessionStore } from '../../chat/session-store'
import { useChatStreamStore } from '../../chat/stream-store'
import { useContextObservabilityStore } from '../../devtools/context-observability'
import { useConsciousnessStore } from '../../modules/consciousness'
-import { useProviderStore } from '../../providers/provider'
import { useModsServerChannelStore } from './channel-server'
import { createContextChannel } from './context-channel'
@@ -57,7 +56,6 @@ export const useContextBridgeStore = defineStore('mods:api:context-bridge', () =
const contextObservability = useContextObservabilityStore()
const characterOrchestratorStore = useCharacterOrchestratorStore()
const consciousnessStore = useConsciousnessStore()
- const providersStore = useProviderStore()
const { activeProvider, activeModel } = storeToRefs(consciousnessStore)
const streamingControl = useLlmStreamingControlStore()
@@ -685,10 +683,10 @@ export const useContextBridgeStore = defineStore('mods:api:context-bridge', () =
if (activeProvider.value && activeModel.value) {
let chatProvider: ChatProvider
try {
- chatProvider = await providersStore.getProviderInstance(activeProvider.value)
+ chatProvider = await consciousnessStore.getChatProviderInstance(activeProvider.value)
}
catch (err) {
- console.error('[context-bridge] getProviderInstance failed for provider:', activeProvider.value, err)
+ console.error('[context-bridge] getChatProviderInstance failed for provider:', activeProvider.value, err)
return
}
diff --git a/packages/stage-ui/src/stores/modules/consciousness-settings.browser.test.ts b/packages/stage-ui/src/stores/modules/consciousness-settings.browser.test.ts
new file mode 100644
index 000000000..0a6a7b409
--- /dev/null
+++ b/packages/stage-ui/src/stores/modules/consciousness-settings.browser.test.ts
@@ -0,0 +1,97 @@
+import type { LeadershipMode, SyncedPiniaRuntime } from 'pinia-plugin-synced'
+
+import { createPinia, disposePinia, setActivePinia } from 'pinia'
+import { createSyncedPiniaPlugin } from 'pinia-plugin-synced'
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
+import { createApp } from 'vue'
+
+import { useConsciousnessSettingsStore } from './consciousness-settings'
+
+const syncedContexts: Array<{
+ pinia: ReturnType
+ runtime: SyncedPiniaRuntime
+}> = []
+
+function createSyncedContext(namespace: string, leadership: LeadershipMode) {
+ const pinia = createPinia()
+ const runtime = createSyncedPiniaPlugin({
+ callTimeout: 1000,
+ leadership,
+ namespace,
+ })
+ pinia.use(runtime.plugin)
+ createApp({}).use(pinia)
+ syncedContexts.push({ pinia, runtime })
+ return { pinia, runtime }
+}
+
+describe('consciousness settings synchronization', () => {
+ beforeEach(() => {
+ localStorage.clear()
+ })
+
+ afterEach(() => {
+ for (const context of syncedContexts.splice(0)) {
+ context.runtime.dispose()
+ disposePinia(context.pinia)
+ }
+ localStorage.clear()
+ })
+
+ it('applies one remote snapshot without publishing it again', async () => {
+ const namespace = `consciousness-settings:${crypto.randomUUID()}`
+ const leaderContext = createSyncedContext(namespace, 'leader-only')
+ await vi.waitFor(() => expect(leaderContext.runtime.isLeader()).toBe(true))
+
+ setActivePinia(leaderContext.pinia)
+ const leaderStore = useConsciousnessSettingsStore()
+
+ const followerContext = createSyncedContext(namespace, 'follower-only')
+ setActivePinia(followerContext.pinia)
+ const followerStore = useConsciousnessSettingsStore()
+ await vi.waitFor(() => expect(followerContext.runtime.getLeaderId()).toBe(leaderContext.runtime.participantId))
+
+ let leaderMutations = 0
+ let followerMutations = 0
+ let followerActions = 0
+ leaderStore.$subscribe(() => leaderMutations++, { flush: 'sync' })
+ followerStore.$subscribe(() => followerMutations++, { flush: 'sync' })
+ followerStore.$onAction(() => followerActions++)
+
+ leaderStore.reasoning = true
+ await vi.waitFor(() => expect(followerStore.reasoning).toBe(true))
+ await new Promise(resolve => setTimeout(resolve, 50))
+
+ expect(leaderMutations).toBe(1)
+ expect(followerMutations).toBe(1)
+ expect(followerActions).toBe(0)
+ expect(localStorage.getItem('settings/consciousness/reasoning')).toBeNull()
+ })
+
+ it('persists a follower update through one leader-owned action', async () => {
+ const namespace = `consciousness-settings:${crypto.randomUUID()}`
+ const leaderContext = createSyncedContext(namespace, 'leader-only')
+ await vi.waitFor(() => expect(leaderContext.runtime.isLeader()).toBe(true))
+
+ setActivePinia(leaderContext.pinia)
+ const leaderStore = useConsciousnessSettingsStore()
+
+ const followerContext = createSyncedContext(namespace, 'follower-only')
+ setActivePinia(followerContext.pinia)
+ const followerStore = useConsciousnessSettingsStore()
+ await vi.waitFor(() => expect(followerContext.runtime.getLeaderId()).toBe(leaderContext.runtime.participantId))
+
+ let leaderActions = 0
+ leaderStore.$onAction(({ name }) => {
+ if (name === 'setReasoning')
+ leaderActions++
+ })
+
+ await followerStore.setReasoning(true)
+ await vi.waitFor(() => expect(followerStore.reasoning).toBe(true))
+
+ expect(leaderStore.reasoning).toBe(true)
+ expect(leaderActions).toBe(1)
+ expect(localStorage.getItem('settings/consciousness/reasoning')).toBe('true')
+ })
+})
diff --git a/packages/stage-ui/src/stores/modules/consciousness-settings.test.ts b/packages/stage-ui/src/stores/modules/consciousness-settings.test.ts
new file mode 100644
index 000000000..08a4613d3
--- /dev/null
+++ b/packages/stage-ui/src/stores/modules/consciousness-settings.test.ts
@@ -0,0 +1,50 @@
+// @vitest-environment jsdom
+
+import { createPinia, setActivePinia } from 'pinia'
+import { beforeEach, describe, expect, it } from 'vitest'
+
+import { useConsciousnessSettingsStore } from './consciousness-settings'
+
+describe('consciousness settings store', () => {
+ beforeEach(() => {
+ localStorage.clear()
+ setActivePinia(createPinia())
+ })
+
+ it('turns model reasoning off by default', () => {
+ const store = useConsciousnessSettingsStore()
+
+ expect(store.reasoning).toBe(false)
+ })
+
+ it('loads the persisted value', () => {
+ localStorage.setItem('settings/consciousness/reasoning', 'true')
+ const store = useConsciousnessSettingsStore()
+
+ expect(store.reasoning).toBe(true)
+ })
+
+ it('persists changes through store actions', async () => {
+ const store = useConsciousnessSettingsStore()
+ await store.setReasoning(true)
+
+ expect(store.reasoning).toBe(true)
+ expect(localStorage.getItem('settings/consciousness/reasoning')).toBe('true')
+
+ await store.resetState()
+
+ expect(store.reasoning).toBe(false)
+ expect(localStorage.getItem('settings/consciousness/reasoning')).toBe('false')
+ })
+
+ it('ignores storage events because Pinia owns cross-window synchronization', () => {
+ const store = useConsciousnessSettingsStore()
+
+ window.dispatchEvent(new StorageEvent('storage', {
+ key: 'settings/consciousness/reasoning',
+ newValue: 'true',
+ }))
+
+ expect(store.reasoning).toBe(false)
+ })
+})
diff --git a/packages/stage-ui/src/stores/modules/consciousness-settings.ts b/packages/stage-ui/src/stores/modules/consciousness-settings.ts
new file mode 100644
index 000000000..b83d098bb
--- /dev/null
+++ b/packages/stage-ui/src/stores/modules/consciousness-settings.ts
@@ -0,0 +1,53 @@
+import type {} from 'pinia-plugin-synced'
+
+import { defineStore } from 'pinia'
+import { shallowRef } from 'vue'
+
+function loadReasoning() {
+ // Non-renderer runtimes have no durable settings owner. They use the product
+ // default until a synchronized renderer snapshot arrives.
+ if (typeof localStorage === 'undefined')
+ return false
+
+ return localStorage.getItem('settings/consciousness/reasoning') === 'true'
+}
+
+function persistReasoning(value: boolean) {
+ if (typeof localStorage === 'undefined')
+ return
+
+ localStorage.setItem('settings/consciousness/reasoning', String(value))
+}
+
+/**
+ * Stores request policies for the consciousness module.
+ *
+ * Consciousness chat request preparation reads this state before inference.
+ * Each provider maps the reasoning value to its own request fields.
+ */
+export const useConsciousnessSettingsStore = defineStore('consciousness-settings', () => {
+ // Pinia owns live cross-window state. Only synchronized actions write the
+ // durable value, so a follower cannot persist an uncommitted proposal.
+ const reasoning = shallowRef(loadReasoning())
+
+ async function setReasoning(value: boolean) {
+ reasoning.value = value
+ persistReasoning(value)
+ }
+
+ async function resetState() {
+ reasoning.value = false
+ persistReasoning(false)
+ }
+
+ return {
+ reasoning,
+ setReasoning,
+ resetState,
+ }
+}, {
+ synced: {
+ actions: ['resetState', 'setReasoning'],
+ state: true,
+ },
+})
diff --git a/packages/stage-ui/src/stores/modules/consciousness.test.ts b/packages/stage-ui/src/stores/modules/consciousness.test.ts
index b8bf62e2c..da1488adc 100644
--- a/packages/stage-ui/src/stores/modules/consciousness.test.ts
+++ b/packages/stage-ui/src/stores/modules/consciousness.test.ts
@@ -7,6 +7,7 @@ import { nextTick } from 'vue'
import { useProviderConfigStore } from '../providers/config'
import { useProviderStore } from '../providers/provider'
import { useConsciousnessStore } from './consciousness'
+import { useConsciousnessSettingsStore } from './consciousness-settings'
vi.mock('vue-i18n', () => ({
useI18n: () => ({
@@ -76,6 +77,21 @@ describe('consciousness store provider selection', () => {
expect(definition?.name).toBe('OpenAI')
})
+ it('resolves chat providers with the current reasoning mode', async () => {
+ const providerConfigStore = useProviderConfigStore()
+ providerConfigStore.ensureProvider('openai', 'openai', { apiKey: 'sk-test' })
+ const consciousnessStore = useConsciousnessStore()
+ const settingsStore = useConsciousnessSettingsStore()
+
+ const disabledProvider = await consciousnessStore.getChatProviderInstance('openai')
+ expect(disabledProvider.chat('test-model')).toMatchObject({ reasoningEffort: 'none' })
+
+ await settingsStore.setReasoning(true)
+
+ const enabledProvider = await consciousnessStore.getChatProviderInstance('openai')
+ expect(enabledProvider.chat('test-model')).toMatchObject({ reasoningEffort: 'medium' })
+ })
+
// ROOT CAUSE:
//
// The model selection was only cleared on provider switches by a watcher in
diff --git a/packages/stage-ui/src/stores/modules/consciousness.ts b/packages/stage-ui/src/stores/modules/consciousness.ts
index f3b63d387..4aa02b275 100644
--- a/packages/stage-ui/src/stores/modules/consciousness.ts
+++ b/packages/stage-ui/src/stores/modules/consciousness.ts
@@ -6,9 +6,11 @@ import { defineStore } from 'pinia'
import { computed, watch } from 'vue'
import { useProviderStore } from '../providers/provider'
+import { useConsciousnessSettingsStore } from './consciousness-settings'
export const useConsciousnessStore = defineStore('consciousness', () => {
const providersStore = useProviderStore()
+ const settingsStore = useConsciousnessSettingsStore()
// Pinia synchronization owns live cross-window state. localStorage remains
// durable persistence, but storage events must not reflect state back into
@@ -93,6 +95,13 @@ export const useConsciousnessStore = defineStore('consciousness', () => {
return []
}
+ /** Resolves a provider with the reasoning mode shared by every Consciousness input path. */
+ async function getChatProviderInstance(provider: string) {
+ return providersStore.getChatProviderInstance(provider, {
+ reasoning: settingsStore.reasoning ? 'enabled' : 'disabled',
+ })
+ }
+
const configured = computed(() => {
return !!activeProvider.value && !!activeModel.value
})
@@ -122,6 +131,7 @@ export const useConsciousnessStore = defineStore('consciousness', () => {
resetModelSelection,
loadModelsForProvider,
getModelsForProvider,
+ getChatProviderInstance,
resetState,
}
}, {
diff --git a/packages/stage-ui/src/stores/modules/index.ts b/packages/stage-ui/src/stores/modules/index.ts
index c5693824e..077fc6cc9 100644
--- a/packages/stage-ui/src/stores/modules/index.ts
+++ b/packages/stage-ui/src/stores/modules/index.ts
@@ -1,5 +1,6 @@
export * from './airi-card'
export * from './consciousness'
+export * from './consciousness-settings'
export * from './discord'
export * from './gaming-factorio'
export * from './gaming-minecraft'
diff --git a/packages/stage-ui/src/stores/providers/provider.test.ts b/packages/stage-ui/src/stores/providers/provider.test.ts
index e9cc6df1f..4f7aa86fd 100644
--- a/packages/stage-ui/src/stores/providers/provider.test.ts
+++ b/packages/stage-ui/src/stores/providers/provider.test.ts
@@ -1,3 +1,5 @@
+import type { ChatProvider } from '@xsai-ext/providers/utils'
+
import { createPinia, setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { nextTick } from 'vue'
@@ -103,6 +105,25 @@ describe('provider store synchronization boundary', () => {
expect(second).toEqual([])
})
+ it('applies provider-owned reasoning options without changing the cached provider', async () => {
+ const store = useProviderStore()
+ const configStore = useProviderConfigStore()
+ configStore.ensureProvider('openai', 'openai', {
+ apiKey: 'test-key',
+ baseUrl: 'https://api.openai.com/v1/',
+ })
+
+ const baseProvider = await store.getProviderInstance('openai')
+ const reasoningDisabledProvider = await store.getChatProviderInstance('openai', { reasoning: 'disabled' })
+ const reasoningEnabledProvider = await store.getChatProviderInstance('openai', { reasoning: 'enabled' })
+
+ expect(reasoningDisabledProvider).not.toBe(baseProvider)
+ expect(reasoningEnabledProvider).not.toBe(baseProvider)
+ expect(reasoningDisabledProvider.chat('any-model')).toMatchObject({ reasoningEffort: 'none' })
+ expect(reasoningEnabledProvider.chat('any-model')).toMatchObject({ reasoningEffort: 'medium' })
+ expect(baseProvider.chat('any-model')).not.toHaveProperty('reasoningEffort')
+ })
+
// ROOT CAUSE:
//
// A model request kept a reference to its runtime entry across an await.
diff --git a/packages/stage-ui/src/stores/providers/provider.ts b/packages/stage-ui/src/stores/providers/provider.ts
index 778b7bc71..127d127bd 100644
--- a/packages/stage-ui/src/stores/providers/provider.ts
+++ b/packages/stage-ui/src/stores/providers/provider.ts
@@ -11,7 +11,7 @@ import type {
import type {} from 'pinia-plugin-synced'
import type { ProviderMetadata, ProviderValidationPlan } from '../../libs/providers'
-import type { ModelInfo, ProviderDefinition, ProviderInstance, VoiceInfo } from '../../libs/providers/types'
+import type { ChatRequestOptions, ModelInfo, ProviderDefinition, ProviderInstance, VoiceInfo } from '../../libs/providers/types'
import { errorMessageFrom } from '@moeru/std'
import { isCustomProvidersDisabled } from '@proj-airi/stage-shared'
@@ -48,6 +48,20 @@ export interface ProviderRuntimeState {
const emptyProviderModels: ModelInfo[] = []
Object.freeze(emptyProviderModels)
+function withChatRequestOptions(
+ provider: ChatProviderWithExtraOptions,
+ options: ChatRequestOptions,
+): ChatProvider {
+ const decorated = {
+ ...provider,
+ chat(model: string) {
+ return provider.chat(model, options)
+ },
+ }
+
+ return decorated
+}
+
// Only the provider data plane crosses renderer boundaries. Async derived refs
// stay in useProviderStore and recompute locally instead of being patched as
// authoritative state by pinia-plugin-synced.
@@ -754,9 +768,9 @@ export const useProviderStore = defineStore('provider', () => {
throw new Error(`Provider credentials for ${providerId} not found`)
try {
- const instance = await definition.createProvider(config || {}) as R
+ const instance = await definition.createProvider(config || {})
providerInstanceCache.set(providerId, instance)
- return instance
+ return instance as R
}
catch (error) {
console.error(`Error creating provider instance for ${providerId}:`, error)
@@ -764,6 +778,23 @@ export const useProviderStore = defineStore('provider', () => {
}
}
+ /**
+ * Passes AIRI chat options to the provider that owns their wire representation.
+ * The cached base instance remains unchanged for consumers that do not opt in.
+ */
+ async function getChatProviderInstance(
+ providerId: string,
+ options: ChatRequestOptions,
+ ): Promise {
+ const provider = await getProviderInstance>(providerId)
+ const definition = findProviderDefinition(providerId)
+ const reasoning = definition?.capabilities?.chat?.reasoning
+ if (!reasoning?.modes.includes(options.reasoning))
+ return provider
+
+ return withChatRequestOptions(provider, options)
+ }
+
async function disposeProviderInstance(providerId: string) {
const instance = providerInstanceCache.get(providerId) as { dispose?: () => Promise | void } | undefined
if (instance?.dispose)
@@ -879,6 +910,7 @@ export const useProviderStore = defineStore('provider', () => {
loadProviderModel,
loadModelsForConfiguredProviders,
getProviderInstance,
+ getChatProviderInstance,
disposeProviderInstance,
resetProviderSettings,
forceProviderConfigured,