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
7 changes: 7 additions & 0 deletions astrbot/core/config/default.py
Original file line number Diff line number Diff line change
Expand Up @@ -1225,6 +1225,7 @@ class ChatProviderTemplate(TypedDict):
"api_base": "http://127.0.0.1:11434/v1",
"proxy": "",
"custom_headers": {},
"ollama_disable_thinking": False,
},
"LM Studio": {
"id": "lm_studio",
Expand Down Expand Up @@ -1754,6 +1755,12 @@ class ChatProviderTemplate(TypedDict):
"items": {},
"hint": "此处添加的键值对将被合并到 OpenAI SDK 的 default_headers 中,用于自定义 HTTP 请求头。值必须为字符串。",
},
"ollama_disable_thinking": {
"description": "关闭思考模式",
"type": "bool",
"hint": "仅对 Ollama 提供商生效。启用后会通过 OpenAI 兼容接口注入 reasoning_effort=none,以稳定关闭 thinking;比 think:false 更可靠。",
"condition": {"provider": "ollama"},
},
"custom_extra_body": {
"description": "自定义请求体参数",
"type": "dict",
Expand Down
22 changes: 22 additions & 0 deletions astrbot/core/provider/sources/openai_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,26 @@ def __init__(self, provider_config, provider_settings) -> None:

self.reasoning_key = "reasoning_content"

def _ollama_disable_thinking_enabled(self) -> bool:
value = self.provider_config.get("ollama_disable_thinking", False)
if isinstance(value, str):
return value.strip().lower() in {"1", "true", "yes", "on"}
return bool(value)

def _apply_provider_specific_extra_body_overrides(
self, extra_body: dict[str, Any]
) -> None:
if self.provider_config.get("provider") != "ollama":
return
if not self._ollama_disable_thinking_enabled():
return

# Ollama's OpenAI-compatible endpoint reliably maps reasoning_effort=none
# to think=false, while direct think=false passthrough is not stable.
extra_body.pop("reasoning", None)
extra_body.pop("think", None)
extra_body["reasoning_effort"] = "none"

async def get_models(self):
try:
models_str = []
Expand Down Expand Up @@ -245,6 +265,7 @@ async def _query(self, payloads: dict, tools: ToolSet | None) -> LLMResponse:
custom_extra_body = self.provider_config.get("custom_extra_body", {})
if isinstance(custom_extra_body, dict):
extra_body.update(custom_extra_body)
self._apply_provider_specific_extra_body_overrides(extra_body)

model = payloads.get("model", "").lower()

Expand Down Expand Up @@ -295,6 +316,7 @@ async def _query_stream(
to_del.append(key)
for key in to_del:
del payloads[key]
self._apply_provider_specific_extra_body_overrides(extra_body)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

您好,此处的改动是正确的,但在审查代码时,我注意到 _query_stream 方法中构造 extra_body 的逻辑与 _query 方法中不一致。

_query_stream 中(当前方法):

  1. custom_extra_body 更新 extra_body
  2. payloads 中的参数再次更新 extra_body
    这意味着 payloads 中的参数会覆盖 custom_extra_body 中的同名参数。

_query 中:

  1. payloads 中的参数构造 extra_body
  2. custom_extra_body 更新 extra_body
    这意味着 custom_extra_body 会覆盖 payloads 中的同名参数。

通常 custom_extra_body(来自静态配置)的优先级应该更高。为了保持行为一致性并避免潜在的 bug,建议将 _query_stream 中的逻辑调整为与 _query 一致。

虽然这超出了本次变更的核心范围,但由于您接触了这部分代码,这是一个很好的改进机会。


stream = await self.client.chat.completions.create(
**payloads,
Expand Down
25 changes: 19 additions & 6 deletions dashboard/src/composables/useProviderSources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,19 +203,18 @@ export function useProviderSources(options: UseProviderSourcesOptions) {
const advancedSourceConfig = computed(() => {
if (!editableProviderSource.value) return null

const excluded = ['id', 'key', 'api_base', 'enable', 'type', 'provider_type', 'provider']
const excluded = new Set(['id', 'key', 'api_base', 'enable', 'type', 'provider_type', 'provider'])
const advanced: Record<string, any> = {}

for (const key of Object.keys(editableProviderSource.value)) {
if (excluded.includes(key)) continue
Object.defineProperty(advanced, key, {
get() {
return editableProviderSource.value![key]
},
set(val) {
editableProviderSource.value![key] = val
},
enumerable: true
enumerable: !excluded.has(key)
})
}

Expand Down Expand Up @@ -344,7 +343,9 @@ export function useProviderSources(options: UseProviderSourcesOptions) {
selectedProviderSource.value = source
selectedProviderSourceOriginalId.value = source?.id || null
suppressSourceWatch = true
editableProviderSource.value = source ? JSON.parse(JSON.stringify(source)) : null
editableProviderSource.value = source
? ensureProviderSourceDefaults(JSON.parse(JSON.stringify(source)))
: null
nextTick(() => {
suppressSourceWatch = false
})
Expand All @@ -353,6 +354,18 @@ export function useProviderSources(options: UseProviderSourcesOptions) {
isSourceModified.value = false
}

function ensureProviderSourceDefaults(source: any) {
if (!source || typeof source !== 'object') {
return source
}

if (source.provider === 'ollama' && source.ollama_disable_thinking === undefined) {
source.ollama_disable_thinking = false
}

return source
}

function extractSourceFieldsFromTemplate(template: Record<string, any>) {
const sourceFields: Record<string, any> = {}
const excludeKeys = ['id', 'enable', 'model', 'provider_source_id', 'modalities', 'custom_extra_body']
Expand Down Expand Up @@ -388,14 +401,14 @@ export function useProviderSources(options: UseProviderSourcesOptions) {
}

const newId = generateUniqueSourceId(template.id)
const newSource = {
const newSource = ensureProviderSourceDefaults({
...extractSourceFieldsFromTemplate(template),
id: newId,
type: template.type,
provider_type: template.provider_type,
provider: template.provider,
enable: true
}
})

providerSources.value.push(newSource)
selectedProviderSource.value = newSource
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1068,6 +1068,10 @@
"description": "Custom request headers",
"hint": "Key/value pairs added here are merged into the OpenAI SDK default_headers for custom HTTP headers. Values must be strings."
},
"ollama_disable_thinking": {
"description": "Disable thinking mode",
"hint": "Only applies to the Ollama provider. When enabled, AstrBot injects reasoning_effort=none on the OpenAI-compatible endpoint, which is more reliable than think:false."
},
"custom_extra_body": {
"description": "Custom request body parameters",
"hint": "Add extra parameters to requests, such as temperature, top_p, max_tokens, etc.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1071,6 +1071,10 @@
"description": "自定义添加请求头",
"hint": "此处添加的键值对将被合并到 OpenAI SDK 的 default_headers 中,用于自定义 HTTP 请求头。值必须为字符串。"
},
"ollama_disable_thinking": {
"description": "关闭思考模式",
"hint": "仅对 Ollama 提供商生效。启用后会通过 OpenAI 兼容接口注入 reasoning_effort=none,以稳定关闭 thinking;比 think:false 更可靠。"
},
"custom_extra_body": {
"description": "自定义请求体参数",
"hint": "用于在请求时添加额外的参数,如 temperature、top_p、max_tokens 等。",
Expand Down
86 changes: 86 additions & 0 deletions tests/test_openai_source.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from types import SimpleNamespace

import pytest
from openai.types.chat.chat_completion import ChatCompletion

from astrbot.core.provider.sources.openai_source import ProviderOpenAIOfficial

Expand Down Expand Up @@ -380,3 +381,88 @@ async def test_handle_api_error_unknown_image_error_raises():
)
finally:
await provider.terminate()


@pytest.mark.asyncio
async def test_apply_provider_specific_extra_body_overrides_disables_ollama_thinking():
provider = _make_provider(
{
"provider": "ollama",
"ollama_disable_thinking": True,
}
)
try:
extra_body = {
"reasoning": {"effort": "high"},
"reasoning_effort": "low",
"think": True,
"temperature": 0.2,
}

provider._apply_provider_specific_extra_body_overrides(extra_body)

assert extra_body["reasoning_effort"] == "none"
assert "reasoning" not in extra_body
assert "think" not in extra_body
assert extra_body["temperature"] == 0.2
finally:
await provider.terminate()


@pytest.mark.asyncio
async def test_query_injects_reasoning_effort_none_for_ollama(monkeypatch):
provider = _make_provider(
{
"provider": "ollama",
"ollama_disable_thinking": True,
"custom_extra_body": {
"reasoning": {"effort": "high"},
"temperature": 0.1,
},
}
)
try:
captured_kwargs = {}

async def fake_create(**kwargs):
captured_kwargs.update(kwargs)
return ChatCompletion.model_validate(
{
"id": "chatcmpl-test",
"object": "chat.completion",
"created": 0,
"model": "qwen3.5:4b",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "ok",
},
"finish_reason": "stop",
}
],
"usage": {
"prompt_tokens": 1,
"completion_tokens": 1,
"total_tokens": 2,
},
}
)

monkeypatch.setattr(provider.client.chat.completions, "create", fake_create)

await provider._query(
payloads={
"model": "qwen3.5:4b",
"messages": [{"role": "user", "content": "hello"}],
},
tools=None,
)

extra_body = captured_kwargs["extra_body"]
assert extra_body["reasoning_effort"] == "none"
assert "reasoning" not in extra_body
assert extra_body["temperature"] == 0.1
finally:
await provider.terminate()