mirror of
https://github.com/sligter/LandPPT.git
synced 2026-08-28 23:31:06 +08:00
Add Anthropic reasoning controls and completion fixes
- Pass Anthropic reasoning effort through env, settings, tests, and UI - Allow max reasoning effort for OpenAI and Anthropic provider tests - Open generated scripts directly and download narration without popup tabs
This commit is contained in:
@@ -40,6 +40,8 @@ OPENAI_REASONING_EFFORT=medium
|
||||
# Get your API key from: https://console.anthropic.com/
|
||||
ANTHROPIC_API_KEY=your_anthropic_api_key_here
|
||||
ANTHROPIC_MODEL=claude-3-haiku-20240307
|
||||
ANTHROPIC_ENABLE_REASONING=false
|
||||
ANTHROPIC_REASONING_EFFORT=high
|
||||
|
||||
# Google Gemini Configuration
|
||||
# Get your API key from: https://aistudio.google.com/app/apikey
|
||||
|
||||
@@ -104,7 +104,7 @@ def strip_think_content(content: str) -> str:
|
||||
class OpenAIProvider(AIProvider):
|
||||
"""OpenAI API provider"""
|
||||
|
||||
SUPPORTED_REASONING_EFFORTS = {"none", "minimal", "low", "medium", "high", "xhigh"}
|
||||
SUPPORTED_REASONING_EFFORTS = {"none", "minimal", "low", "medium", "high", "xhigh", "max"}
|
||||
|
||||
def __init__(self, config: Dict[str, Any]):
|
||||
super().__init__(config)
|
||||
@@ -576,8 +576,12 @@ class AzureOpenAIProvider(OpenAIProvider):
|
||||
class AnthropicProvider(AIProvider):
|
||||
"""Anthropic Claude API provider"""
|
||||
|
||||
SUPPORTED_REASONING_EFFORTS = {"low", "medium", "high", "xhigh", "max"}
|
||||
|
||||
def __init__(self, config: Dict[str, Any]):
|
||||
super().__init__(config)
|
||||
self.enable_reasoning = OpenAIProvider._coerce_bool(config.get("enable_reasoning"))
|
||||
self.reasoning_effort = self._normalize_reasoning_effort(config.get("reasoning_effort")) or "high"
|
||||
try:
|
||||
import anthropic
|
||||
base_url = config.get("base_url")
|
||||
@@ -604,6 +608,24 @@ class AnthropicProvider(AIProvider):
|
||||
logger.warning("Anthropic library not installed. Install with: pip install anthropic")
|
||||
self.client = None
|
||||
|
||||
@classmethod
|
||||
def _normalize_reasoning_effort(cls, value: Any) -> Optional[str]:
|
||||
if value is None:
|
||||
return None
|
||||
normalized = str(value).strip().lower()
|
||||
return normalized if normalized in cls.SUPPORTED_REASONING_EFFORTS else None
|
||||
|
||||
def _apply_reasoning_config(self, request_kwargs: Dict[str, Any], config: Dict[str, Any]) -> None:
|
||||
if not OpenAIProvider._coerce_bool(config.get("enable_reasoning", self.enable_reasoning)):
|
||||
return
|
||||
|
||||
effort = (
|
||||
self._normalize_reasoning_effort(config.get("reasoning_effort"))
|
||||
or self.reasoning_effort
|
||||
or "high"
|
||||
)
|
||||
request_kwargs["output_config"] = {"effort": effort}
|
||||
|
||||
def _convert_message_to_anthropic(self, message: AIMessage) -> Dict[str, Any]:
|
||||
"""Convert AIMessage to Anthropic format, supporting multimodal content"""
|
||||
if message.role == MessageRole.TOOL:
|
||||
@@ -769,6 +791,8 @@ class AnthropicProvider(AIProvider):
|
||||
if name:
|
||||
request_kwargs["tool_choice"] = {"type": "tool", "name": name}
|
||||
|
||||
self._apply_reasoning_config(request_kwargs, config)
|
||||
|
||||
try:
|
||||
response = await self.client.messages.create(**request_kwargs)
|
||||
content, tool_calls, finish_reason, usage = self._extract_anthropic_response(response)
|
||||
@@ -866,6 +890,7 @@ class AnthropicProvider(AIProvider):
|
||||
}
|
||||
if system_message:
|
||||
body["system"] = system_message
|
||||
self._apply_reasoning_config(body, config)
|
||||
|
||||
# Try both authentication methods
|
||||
auth_methods = [
|
||||
|
||||
@@ -76,6 +76,8 @@ class AIConfig(BaseSettings):
|
||||
anthropic_api_key: Optional[str] = Field(default=None, env="ANTHROPIC_API_KEY")
|
||||
anthropic_base_url: str = Field(default="https://api.anthropic.com", env="ANTHROPIC_BASE_URL")
|
||||
anthropic_model: str = Field(default="claude-3-haiku-20240307", env="ANTHROPIC_MODEL")
|
||||
anthropic_enable_reasoning: bool = Field(default=False, env="ANTHROPIC_ENABLE_REASONING")
|
||||
anthropic_reasoning_effort: str = Field(default="high", env="ANTHROPIC_REASONING_EFFORT")
|
||||
|
||||
# Google Gemini Configuration
|
||||
google_api_key: Optional[str] = Field(default=None, env="GOOGLE_API_KEY")
|
||||
@@ -298,6 +300,8 @@ class AIConfig(BaseSettings):
|
||||
"api_key": self.anthropic_api_key,
|
||||
"base_url": self.anthropic_base_url,
|
||||
"model": self.anthropic_model,
|
||||
"enable_reasoning": self.anthropic_enable_reasoning,
|
||||
"reasoning_effort": self.anthropic_reasoning_effort,
|
||||
"max_tokens": self.max_tokens,
|
||||
"temperature": self.temperature,
|
||||
"top_p": self.top_p,
|
||||
@@ -424,6 +428,14 @@ def reload_ai_config():
|
||||
ai_config.anthropic_api_key = os.environ.get('ANTHROPIC_API_KEY', ai_config.anthropic_api_key)
|
||||
ai_config.anthropic_base_url = os.environ.get('ANTHROPIC_BASE_URL', ai_config.anthropic_base_url)
|
||||
ai_config.anthropic_model = os.environ.get('ANTHROPIC_MODEL', ai_config.anthropic_model)
|
||||
ai_config.anthropic_enable_reasoning = os.environ.get(
|
||||
'ANTHROPIC_ENABLE_REASONING',
|
||||
str(ai_config.anthropic_enable_reasoning),
|
||||
).lower() == 'true'
|
||||
ai_config.anthropic_reasoning_effort = os.environ.get(
|
||||
'ANTHROPIC_REASONING_EFFORT',
|
||||
ai_config.anthropic_reasoning_effort,
|
||||
)
|
||||
ai_config.google_api_key = os.environ.get('GOOGLE_API_KEY', ai_config.google_api_key)
|
||||
ai_config.google_base_url = os.environ.get('GOOGLE_BASE_URL', ai_config.google_base_url)
|
||||
ai_config.google_model = os.environ.get('GOOGLE_MODEL', ai_config.google_model)
|
||||
|
||||
@@ -45,6 +45,8 @@ class ConfigService:
|
||||
"anthropic_api_key": {"type": "password", "category": "ai_providers"},
|
||||
"anthropic_base_url": {"type": "url", "category": "ai_providers", "default": "https://api.anthropic.com"},
|
||||
"anthropic_model": {"type": "select", "category": "ai_providers", "default": "claude-3.5-haiku-20240307"},
|
||||
"anthropic_enable_reasoning": {"type": "boolean", "category": "ai_providers", "default": "false"},
|
||||
"anthropic_reasoning_effort": {"type": "select", "category": "ai_providers", "default": "high"},
|
||||
|
||||
"google_api_key": {"type": "password", "category": "ai_providers"},
|
||||
"google_base_url": {"type": "url", "category": "ai_providers", "default": "https://generativelanguage.googleapis.com"},
|
||||
|
||||
@@ -51,6 +51,8 @@ class DatabaseConfigService:
|
||||
"anthropic_api_key": {"type": "password", "category": "ai_providers"},
|
||||
"anthropic_base_url": {"type": "url", "category": "ai_providers", "default": "https://api.anthropic.com"},
|
||||
"anthropic_model": {"type": "select", "category": "ai_providers", "default": "claude-3.5-haiku-20240307"},
|
||||
"anthropic_enable_reasoning": {"type": "boolean", "category": "ai_providers", "default": "false"},
|
||||
"anthropic_reasoning_effort": {"type": "select", "category": "ai_providers", "default": "high"},
|
||||
|
||||
"google_api_key": {"type": "password", "category": "ai_providers"},
|
||||
"google_base_url": {"type": "url", "category": "ai_providers", "default": "https://generativelanguage.googleapis.com"},
|
||||
@@ -705,6 +707,8 @@ def _build_user_ai_provider_config(
|
||||
"api_key": user_config.get("anthropic_api_key"),
|
||||
"base_url": user_config.get("anthropic_base_url"),
|
||||
"model": user_config.get("anthropic_model"),
|
||||
"enable_reasoning": user_config.get("anthropic_enable_reasoning"),
|
||||
"reasoning_effort": user_config.get("anthropic_reasoning_effort"),
|
||||
"llm_timeout_seconds": user_config.get("llm_timeout_seconds"),
|
||||
},
|
||||
"google": {
|
||||
|
||||
@@ -94,12 +94,15 @@ def build_anthropic_messages_url(base_url: str) -> str:
|
||||
return f"{normalize_anthropic_test_base_url(base_url)}/messages"
|
||||
|
||||
|
||||
def build_anthropic_test_payload(prompt: str) -> Dict[str, Any]:
|
||||
def build_anthropic_test_payload(prompt: str, reasoning_effort: str = "") -> Dict[str, Any]:
|
||||
"""Anthropic 测试使用官方 messages 协议。"""
|
||||
return {
|
||||
payload = {
|
||||
"messages": [{"role": "user", "content": prompt}],
|
||||
"temperature": 0,
|
||||
}
|
||||
if reasoning_effort:
|
||||
payload["output_config"] = {"effort": reasoning_effort}
|
||||
return payload
|
||||
|
||||
|
||||
def extract_anthropic_test_result(data: Dict[str, Any]) -> Tuple[str, Dict[str, int]]:
|
||||
|
||||
@@ -122,6 +122,8 @@ class RuntimeConfigService:
|
||||
elif provider == 'anthropic':
|
||||
api_key = user_config.get('anthropic_api_key')
|
||||
base_url = user_config.get('anthropic_base_url')
|
||||
enable_reasoning = bool(user_config.get('anthropic_enable_reasoning'))
|
||||
reasoning_effort = str(user_config.get('anthropic_reasoning_effort') or 'high')
|
||||
elif provider == 'google' or provider == 'gemini':
|
||||
api_key = user_config.get('google_api_key')
|
||||
base_url = user_config.get('google_base_url')
|
||||
|
||||
@@ -78,6 +78,18 @@ def _coerce_bool(value: Any) -> bool:
|
||||
return str(value).strip().lower() in {"1", "true", "yes", "on"}
|
||||
|
||||
|
||||
_REASONING_EFFORTS = {
|
||||
"openai": {"none", "minimal", "low", "medium", "high", "xhigh", "max"},
|
||||
"anthropic": {"low", "medium", "high", "xhigh", "max"},
|
||||
}
|
||||
|
||||
|
||||
def _normalize_reasoning_effort(provider: str, value: Any) -> str:
|
||||
default = "high" if provider == "anthropic" else "medium"
|
||||
normalized = str(value or default).strip().lower()
|
||||
return normalized if normalized in _REASONING_EFFORTS.get(provider, set()) else default
|
||||
|
||||
|
||||
@router.get("/home", response_class=HTMLResponse)
|
||||
async def web_home(
|
||||
request: Request,
|
||||
@@ -218,6 +230,8 @@ async def get_ai_providers_config(
|
||||
"anthropic_api_key": current_config.get("anthropic_api_key", ""),
|
||||
"anthropic_base_url": current_config.get("anthropic_base_url", ""),
|
||||
"anthropic_model": current_config.get("anthropic_model", ""),
|
||||
"anthropic_enable_reasoning": current_config.get("anthropic_enable_reasoning", False),
|
||||
"anthropic_reasoning_effort": current_config.get("anthropic_reasoning_effort", "high"),
|
||||
"google_api_key": current_config.get("google_api_key", ""),
|
||||
"google_base_url": current_config.get("google_base_url", ""),
|
||||
"google_model": current_config.get("google_model", ""),
|
||||
@@ -611,10 +625,6 @@ async def test_provider_connection(
|
||||
else:
|
||||
config = await config_service.get_all_config(user_id=user.id)
|
||||
|
||||
def _normalize_reasoning_effort(value: Any) -> str:
|
||||
normalized = str(value or "medium").strip().lower()
|
||||
return normalized if normalized in {"none", "minimal", "low", "medium", "high", "xhigh"} else "medium"
|
||||
|
||||
api_key = data.get('api_key') or config.get(f"{provider}_api_key", "")
|
||||
base_url = data.get('base_url') or config.get(f"{provider}_base_url", "")
|
||||
model = data.get('model') or config.get(f"{provider}_model", "")
|
||||
@@ -626,12 +636,12 @@ async def test_provider_connection(
|
||||
enable_reasoning = (
|
||||
_coerce_bool(data.get("enable_reasoning"))
|
||||
if "enable_reasoning" in data
|
||||
else _coerce_bool(config.get("openai_enable_reasoning"))
|
||||
else _coerce_bool(config.get(f"{provider}_enable_reasoning"))
|
||||
)
|
||||
reasoning_effort = (
|
||||
_normalize_reasoning_effort(data.get("reasoning_effort"))
|
||||
_normalize_reasoning_effort(provider, data.get("reasoning_effort"))
|
||||
if "reasoning_effort" in data
|
||||
else _normalize_reasoning_effort(config.get("openai_reasoning_effort"))
|
||||
else _normalize_reasoning_effort(provider, config.get(f"{provider}_reasoning_effort"))
|
||||
)
|
||||
|
||||
if not api_key:
|
||||
@@ -731,7 +741,10 @@ async def test_provider_connection(
|
||||
|
||||
if provider == "anthropic":
|
||||
request_url = build_anthropic_messages_url(base_url)
|
||||
payload = build_anthropic_test_payload("Hi")
|
||||
payload = build_anthropic_test_payload(
|
||||
"Hi",
|
||||
reasoning_effort if enable_reasoning else "",
|
||||
)
|
||||
payload["model"] = model
|
||||
logger.info("Testing %s connection at: %s (transport=anthropic_messages)", provider, request_url)
|
||||
|
||||
@@ -934,9 +947,7 @@ async def test_openai_provider_proxy(
|
||||
model = data.get('model', 'gpt-4o')
|
||||
use_responses_api = str(data.get("use_responses_api", "")).strip().lower() in {"1", "true", "yes", "on"}
|
||||
enable_reasoning = str(data.get("enable_reasoning", "")).strip().lower() in {"1", "true", "yes", "on"}
|
||||
reasoning_effort = str(data.get("reasoning_effort") or "medium").strip().lower()
|
||||
if reasoning_effort not in {"none", "minimal", "low", "medium", "high", "xhigh"}:
|
||||
reasoning_effort = "medium"
|
||||
reasoning_effort = _normalize_reasoning_effort("openai", data.get("reasoning_effort"))
|
||||
|
||||
logger.info(f"Frontend requested test with: base_url={base_url}, model={model}")
|
||||
|
||||
@@ -1060,6 +1071,8 @@ async def test_anthropic_provider_proxy(
|
||||
base_url = data.get('base_url', 'https://api.anthropic.com')
|
||||
api_key = data.get('api_key', '')
|
||||
model = data.get('model', 'claude-3-5-sonnet-20241022')
|
||||
enable_reasoning = _coerce_bool(data.get("enable_reasoning"))
|
||||
reasoning_effort = _normalize_reasoning_effort("anthropic", data.get("reasoning_effort"))
|
||||
|
||||
logger.info(f"Frontend requested Anthropic test with: base_url={base_url}, model={model}")
|
||||
|
||||
@@ -1086,16 +1099,11 @@ async def test_anthropic_provider_proxy(
|
||||
'anthropic-version': '2023-06-01'
|
||||
}
|
||||
|
||||
payload = {
|
||||
"model": model,
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Say 'Hello, I am working!' in exactly 5 words."
|
||||
}
|
||||
],
|
||||
"temperature": 0
|
||||
}
|
||||
payload = build_anthropic_test_payload(
|
||||
"Say 'Hello, I am working!' in exactly 5 words.",
|
||||
reasoning_effort if enable_reasoning else "",
|
||||
)
|
||||
payload["model"] = model
|
||||
|
||||
async with session.post(
|
||||
messages_url,
|
||||
|
||||
@@ -280,10 +280,6 @@ function getNarrationLanguage() {
|
||||
|
||||
async function exportNarrationVideo() {
|
||||
try {
|
||||
// Open a blank tab synchronously (user gesture) to avoid popup blockers when we later
|
||||
// navigate to the download URL after async task completion.
|
||||
const downloadWindow = window.open('about:blank', '_blank');
|
||||
|
||||
const lang = getNarrationLanguage();
|
||||
const fps = getNarrationFps();
|
||||
const renderMode = getNarrationRenderMode();
|
||||
@@ -322,27 +318,10 @@ function getNarrationLanguage() {
|
||||
|
||||
closeProgressToast(progressToast);
|
||||
const downloadUrl = taskData.download_url || `/api/landppt/tasks/${taskId}/download`;
|
||||
if (downloadWindow && !downloadWindow.closed) {
|
||||
try {
|
||||
downloadWindow.location.href = downloadUrl;
|
||||
setTimeout(() => {
|
||||
try { downloadWindow.close(); } catch (_) { }
|
||||
}, 3000);
|
||||
} catch (_) {
|
||||
// Fall back to same-tab download trigger
|
||||
if (typeof triggerFileDownload === 'function') {
|
||||
triggerFileDownload(downloadUrl);
|
||||
} else {
|
||||
window.location.href = downloadUrl;
|
||||
}
|
||||
}
|
||||
if (typeof triggerFileDownload === 'function') {
|
||||
triggerFileDownload(downloadUrl);
|
||||
} else {
|
||||
// Pop-up blocked: use same-tab download trigger.
|
||||
if (typeof triggerFileDownload === 'function') {
|
||||
triggerFileDownload(downloadUrl);
|
||||
} else {
|
||||
window.location.href = downloadUrl;
|
||||
}
|
||||
window.location.href = downloadUrl;
|
||||
}
|
||||
showNotification('讲解视频导出完成,已开始下载', 'success');
|
||||
} catch (error) {
|
||||
|
||||
+4
-8
@@ -410,8 +410,7 @@
|
||||
setTimeout(() => {
|
||||
closeProgressToast(progressToast);
|
||||
showNotification('演讲稿生成完成!', 'success');
|
||||
console.log('Starting to poll for speech scripts...');
|
||||
pollForSpeechScripts();
|
||||
showCurrentSpeechScripts();
|
||||
}, 500);
|
||||
return true;
|
||||
}
|
||||
@@ -465,14 +464,11 @@
|
||||
clearInterval(interval);
|
||||
updateProgressToast(progressToast, progress.message, 100);
|
||||
|
||||
// Close progress toast and start polling for data
|
||||
// Close progress toast and display the committed scripts
|
||||
setTimeout(() => {
|
||||
closeProgressToast(progressToast);
|
||||
showNotification('演讲稿生成完成!', 'success');
|
||||
|
||||
// 使用轮询机制等待数据库数据可用
|
||||
console.log('Starting to poll for speech scripts...');
|
||||
pollForSpeechScripts();
|
||||
showCurrentSpeechScripts();
|
||||
}, 500);
|
||||
|
||||
} else if (progress.status === 'failed') {
|
||||
@@ -716,7 +712,7 @@
|
||||
generation_type: generationType,
|
||||
slide_indices: slideIndices,
|
||||
customization: customization,
|
||||
language: (document.getElementById('speechLanguage')?.value || 'zh')
|
||||
language: languageValue
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
-43
@@ -973,49 +973,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function pollForSpeechScripts() {
|
||||
const maxAttempts = 30; // 最多尝试30次
|
||||
const pollInterval = 1000; // 每1000ms查询一次
|
||||
let attempts = 0;
|
||||
|
||||
const poll = async () => {
|
||||
attempts++;
|
||||
console.log(`Polling attempt ${attempts}/${maxAttempts}...`);
|
||||
|
||||
try {
|
||||
const lang = getSpeechLanguage();
|
||||
const response = await fetch(`/api/projects/${window.landpptEditorConfig.projectId}/speech-scripts?language=${encodeURIComponent(lang)}`);
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success && result.scripts && result.scripts.length > 0) {
|
||||
console.log(`Found ${result.scripts.length} scripts, displaying...`);
|
||||
// 找到数据,显示演讲稿
|
||||
showCurrentSpeechScripts();
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果还没到最大尝试次数,继续轮询
|
||||
if (attempts < maxAttempts) {
|
||||
setTimeout(poll, pollInterval);
|
||||
} else {
|
||||
console.warn('Polling timeout: No scripts found after max attempts');
|
||||
showNotification('获取演讲稿超时,请手动刷新页面后查看', 'warning');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Polling error:', error);
|
||||
// 出错继续尝试
|
||||
if (attempts < maxAttempts) {
|
||||
setTimeout(poll, pollInterval);
|
||||
} else {
|
||||
showNotification('获取演讲稿失败,请刷新页面重试', 'error');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 开始轮询
|
||||
poll();
|
||||
}
|
||||
|
||||
async function showCurrentSpeechScripts() {
|
||||
try {
|
||||
// Close current dialog
|
||||
|
||||
@@ -178,6 +178,7 @@
|
||||
<option value="medium" {% if openai_reasoning_effort == 'medium' %}selected{% endif %}>medium</option>
|
||||
<option value="high" {% if openai_reasoning_effort == 'high' %}selected{% endif %}>high</option>
|
||||
<option value="xhigh" {% if openai_reasoning_effort == 'xhigh' %}selected{% endif %}>xhigh</option>
|
||||
<option value="max" {% if openai_reasoning_effort == 'max' %}selected{% endif %}>max</option>
|
||||
</select>
|
||||
<div style="margin-top: 6px; color: #777; font-size: 0.82em;">
|
||||
不同 OpenAI 模型支持的档位不同;若模型不支持,测试或调用会返回参数错误。
|
||||
@@ -215,6 +216,33 @@
|
||||
placeholder="https://api.anthropic.com"
|
||||
style="width: 100%; padding: 8px 10px; border: 1px solid #ddd; border-radius: 6px; font-size: 0.9em;">
|
||||
</div>
|
||||
<div class="form-group" style="margin: 0;">
|
||||
<label
|
||||
style="display: inline-flex; align-items: center; gap: 10px; cursor: pointer; font-size: 0.9em; color: #333;">
|
||||
<input type="checkbox" name="anthropic_enable_reasoning" {% if
|
||||
current_config.get('anthropic_enable_reasoning') %}checked{% endif %}>
|
||||
<span>启用推理程度参数</span>
|
||||
</label>
|
||||
<div style="margin-top: 6px; color: #777; font-size: 0.82em;">
|
||||
开启后会在 Anthropic 请求中附带 output_config.effort;关闭时保持模型默认行为。
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" style="margin: 0;">
|
||||
<label
|
||||
style="font-size: 0.85em; color: #555; margin-bottom: 4px; display: block;">推理程度</label>
|
||||
{% set anthropic_reasoning_effort = current_config.get('anthropic_reasoning_effort', 'high') %}
|
||||
<select name="anthropic_reasoning_effort" {% if not current_config.get('anthropic_enable_reasoning') %}disabled{% endif %}
|
||||
style="width: 100%; padding: 8px 10px; border: 1px solid #ddd; border-radius: 6px; font-size: 0.9em; background: #fff;">
|
||||
<option value="low" {% if anthropic_reasoning_effort == 'low' %}selected{% endif %}>low</option>
|
||||
<option value="medium" {% if anthropic_reasoning_effort == 'medium' %}selected{% endif %}>medium</option>
|
||||
<option value="high" {% if anthropic_reasoning_effort == 'high' %}selected{% endif %}>high</option>
|
||||
<option value="xhigh" {% if anthropic_reasoning_effort == 'xhigh' %}selected{% endif %}>xhigh</option>
|
||||
<option value="max" {% if anthropic_reasoning_effort == 'max' %}selected{% endif %}>max</option>
|
||||
</select>
|
||||
<div style="margin-top: 6px; color: #777; font-size: 0.82em;">
|
||||
不同 Anthropic 模型支持的档位不同;若模型不支持,测试或调用会返回参数错误。
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" style="margin: 0;">
|
||||
<label
|
||||
style="font-size: 0.85em; color: #555; margin-bottom: 4px; display: block;">默认模型</label>
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
// 检查图片服务状态
|
||||
checkImageServiceStatus();
|
||||
initOpenAIReasoningControls();
|
||||
initAnthropicReasoningControls();
|
||||
pruneEmptyProviderFormGroups();
|
||||
bindSearxngHostSync();
|
||||
});
|
||||
@@ -280,6 +281,24 @@
|
||||
syncOpenAIReasoningControls();
|
||||
}
|
||||
|
||||
function syncAnthropicReasoningControls() {
|
||||
const reasoningToggle = document.querySelector('input[name="anthropic_enable_reasoning"]');
|
||||
const reasoningEffortSelect = document.querySelector('select[name="anthropic_reasoning_effort"]');
|
||||
if (!reasoningToggle || !reasoningEffortSelect) return;
|
||||
|
||||
reasoningEffortSelect.disabled = !reasoningToggle.checked;
|
||||
reasoningEffortSelect.style.opacity = reasoningToggle.checked ? '1' : '0.65';
|
||||
}
|
||||
|
||||
function initAnthropicReasoningControls() {
|
||||
const reasoningToggle = document.querySelector('input[name="anthropic_enable_reasoning"]');
|
||||
if (reasoningToggle && reasoningToggle.dataset.bound !== 'true') {
|
||||
reasoningToggle.addEventListener('change', syncAnthropicReasoningControls);
|
||||
reasoningToggle.dataset.bound = 'true';
|
||||
}
|
||||
syncAnthropicReasoningControls();
|
||||
}
|
||||
|
||||
function pruneEmptyProviderFormGroups() {
|
||||
document.querySelectorAll('.provider-accordion-item .provider-content .form-group').forEach(group => {
|
||||
const hasControl = group.querySelector('input, select, textarea, .model-select-wrapper, .model-dropdown');
|
||||
@@ -361,6 +380,7 @@
|
||||
|
||||
enhanceOpenAIProviderUI();
|
||||
syncOpenAIReasoningControls();
|
||||
syncAnthropicReasoningControls();
|
||||
pruneEmptyProviderFormGroups();
|
||||
}
|
||||
|
||||
@@ -1622,6 +1642,15 @@
|
||||
if (reasoningEffortInput?.value) {
|
||||
payload.reasoning_effort = reasoningEffortInput.value;
|
||||
}
|
||||
} else if (provider === 'anthropic') {
|
||||
const reasoningToggleInput = document.querySelector('input[name="anthropic_enable_reasoning"]');
|
||||
if (reasoningToggleInput) {
|
||||
payload.enable_reasoning = reasoningToggleInput.checked;
|
||||
}
|
||||
const reasoningEffortInput = document.querySelector('select[name="anthropic_reasoning_effort"]');
|
||||
if (reasoningEffortInput?.value) {
|
||||
payload.reasoning_effort = reasoningEffortInput.value;
|
||||
}
|
||||
}
|
||||
|
||||
const response = await fetch('/api/ai/providers/test', {
|
||||
|
||||
@@ -763,14 +763,14 @@
|
||||
<script src="/static/js/pages/project/slides_editor/projectSlidesEditor.tools.js?v=20260402-split"></script>
|
||||
<script src="/static/js/pages/project/slides_editor/projectSlidesEditor.imageLibrary.js?v=20260402-split"></script>
|
||||
<script src="/static/js/pages/project/slides_editor/projectSlidesEditor.bulletEnhance.js?v=20260402-split"></script>
|
||||
<script src="/static/js/pages/project/slides_editor/projectSlidesEditor.speechScriptsDialog.js?v=20260402-split"></script>
|
||||
<script src="/static/js/pages/project/slides_editor/projectSlidesEditor.speechScriptsManage.js?v=20260509-custom-tts-api-v1"></script>
|
||||
<script src="/static/js/pages/project/slides_editor/projectSlidesEditor.speechScriptsDialog.js?v=20260802-auto-result-v1"></script>
|
||||
<script src="/static/js/pages/project/slides_editor/projectSlidesEditor.speechScriptsManage.js?v=20260802-auto-result-v1"></script>
|
||||
<script src="/static/js/pages/project/slides_editor/projectSlidesEditor.aiOptimize.js?v=20260614-modalui3"></script>
|
||||
<script src="/static/js/pages/project/slides_editor/projectSlidesEditor.exportBase.js?v=20260425-layer-clip-v21"></script>
|
||||
<script src="/static/js/pages/project/slides_editor/projectSlidesEditor.exportIcons.js?v=20260402-split"></script>
|
||||
<script src="/static/js/pages/project/slides_editor/projectSlidesEditor.exportRender.js?v=20260709-hybrid-raster-v2"></script>
|
||||
<script src="/static/js/pages/project/slides_editor/projectSlidesEditor.exportPptx.js?v=20260707-hybrid-raster-v1"></script>
|
||||
<script src="/static/js/pages/project/slides_editor/projectEditorNarration.js?v=20260509-custom-tts-api-v1"></script>
|
||||
<script src="/static/js/pages/project/slides_editor/projectEditorNarration.js?v=20260802-auto-result-v1"></script>
|
||||
<script src="/static/js/pages/project/slides_editor/projectSlidesEditor.init.js?v=20260402-split"></script>
|
||||
|
||||
<!-- Export PPTX Progress Overlay -->
|
||||
|
||||
@@ -38,6 +38,8 @@ class Settings:
|
||||
openai_enable_reasoning: bool = False
|
||||
openai_reasoning_effort: str = "medium"
|
||||
anthropic_api_key: Optional[str] = None
|
||||
anthropic_enable_reasoning: bool = False
|
||||
anthropic_reasoning_effort: str = "high"
|
||||
azure_openai_api_key: Optional[str] = None
|
||||
azure_openai_endpoint: Optional[str] = None
|
||||
azure_openai_api_version: str = "2024-02-15-preview"
|
||||
@@ -68,8 +70,16 @@ class Settings:
|
||||
max_tokens=self.max_tokens,
|
||||
target_language=target_language,
|
||||
use_responses_api=self.openai_use_responses_api if self.llm_provider == "openai" else False,
|
||||
enable_reasoning=self.openai_enable_reasoning if self.llm_provider == "openai" else False,
|
||||
reasoning_effort=self.openai_reasoning_effort if self.llm_provider == "openai" else "medium",
|
||||
enable_reasoning=(
|
||||
self.openai_enable_reasoning if self.llm_provider == "openai"
|
||||
else self.anthropic_enable_reasoning if self.llm_provider == "anthropic"
|
||||
else False
|
||||
),
|
||||
reasoning_effort=(
|
||||
self.openai_reasoning_effort if self.llm_provider == "openai"
|
||||
else self.anthropic_reasoning_effort if self.llm_provider == "anthropic"
|
||||
else "medium"
|
||||
),
|
||||
)
|
||||
|
||||
def get_llm_kwargs(self) -> Dict[str, Any]:
|
||||
@@ -89,6 +99,9 @@ class Settings:
|
||||
elif self.llm_provider == "anthropic":
|
||||
if self.anthropic_api_key:
|
||||
kwargs["api_key"] = self.anthropic_api_key
|
||||
if self.anthropic_enable_reasoning:
|
||||
kwargs["enable_reasoning"] = True
|
||||
kwargs["reasoning_effort"] = self.anthropic_reasoning_effort
|
||||
# 检查环境变量中的ANTHROPIC_BASE_URL
|
||||
anthropic_base_url = os.getenv("ANTHROPIC_BASE_URL")
|
||||
if anthropic_base_url:
|
||||
@@ -193,6 +206,8 @@ def load_settings(
|
||||
"OPENAI_REASONING_EFFORT": "openai_reasoning_effort",
|
||||
"OPENAI_MODEL": "llm_model", # 支持OPENAI_MODEL环境变量
|
||||
"ANTHROPIC_API_KEY": "anthropic_api_key",
|
||||
"ANTHROPIC_ENABLE_REASONING": "anthropic_enable_reasoning",
|
||||
"ANTHROPIC_REASONING_EFFORT": "anthropic_reasoning_effort",
|
||||
"AZURE_OPENAI_API_KEY": "azure_openai_api_key",
|
||||
"AZURE_OPENAI_ENDPOINT": "azure_openai_endpoint",
|
||||
"AZURE_OPENAI_API_VERSION": "azure_openai_api_version",
|
||||
@@ -225,7 +240,12 @@ def load_settings(
|
||||
except ValueError:
|
||||
logger.warning(f"无效的浮点值 {env_key}={env_value}")
|
||||
continue
|
||||
elif attr_name in {"debug_mode", "openai_use_responses_api", "openai_enable_reasoning"}:
|
||||
elif attr_name in {
|
||||
"debug_mode",
|
||||
"openai_use_responses_api",
|
||||
"openai_enable_reasoning",
|
||||
"anthropic_enable_reasoning",
|
||||
}:
|
||||
env_value = env_value.lower() in ("true", "1", "yes", "on")
|
||||
|
||||
setattr(settings, attr_name, env_value)
|
||||
@@ -266,6 +286,8 @@ def create_env_template():
|
||||
OPENAI_API_KEY=your_openai_api_key_here
|
||||
# OPENAI_BASE_URL=https://api.openai.com/v1 # 自定义OpenAI API端点(可选)
|
||||
ANTHROPIC_API_KEY=your_anthropic_api_key_here
|
||||
ANTHROPIC_ENABLE_REASONING=false
|
||||
ANTHROPIC_REASONING_EFFORT=high
|
||||
|
||||
# Azure OpenAI (if using Azure)
|
||||
AZURE_OPENAI_API_KEY=your_azure_openai_api_key_here
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import sys
|
||||
import types
|
||||
|
||||
import pytest
|
||||
|
||||
from landppt.ai.base import AIMessage, MessageRole
|
||||
from landppt.ai.providers import AnthropicProvider
|
||||
from landppt.services.db_config_service import _build_user_ai_provider_config
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_anthropic_reasoning_effort_reaches_messages_request(monkeypatch):
|
||||
calls = []
|
||||
|
||||
class FakeMessages:
|
||||
async def create(self, **kwargs):
|
||||
calls.append(kwargs)
|
||||
return types.SimpleNamespace(
|
||||
model=kwargs["model"],
|
||||
content=[types.SimpleNamespace(type="text", text="ok")],
|
||||
stop_reason="end_turn",
|
||||
usage=types.SimpleNamespace(input_tokens=1, output_tokens=1),
|
||||
)
|
||||
|
||||
class FakeAsyncAnthropic:
|
||||
def __init__(self, **kwargs):
|
||||
self.messages = FakeMessages()
|
||||
|
||||
monkeypatch.setitem(
|
||||
sys.modules,
|
||||
"anthropic",
|
||||
types.SimpleNamespace(AsyncAnthropic=FakeAsyncAnthropic),
|
||||
)
|
||||
|
||||
config = _build_user_ai_provider_config(
|
||||
{
|
||||
"default_ai_provider": "anthropic",
|
||||
"anthropic_api_key": "test-key",
|
||||
"anthropic_base_url": "https://api.anthropic.com",
|
||||
"anthropic_model": "claude-opus-4-6",
|
||||
"anthropic_enable_reasoning": True,
|
||||
"anthropic_reasoning_effort": "max",
|
||||
}
|
||||
)
|
||||
config.pop("provider_name")
|
||||
provider = AnthropicProvider(config)
|
||||
|
||||
await provider.chat_completion(
|
||||
[AIMessage(role=MessageRole.USER, content="hello")],
|
||||
tools=[{"name": "noop", "input_schema": {"type": "object"}}],
|
||||
)
|
||||
|
||||
assert AnthropicProvider.SUPPORTED_REASONING_EFFORTS == {
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"xhigh",
|
||||
"max",
|
||||
}
|
||||
assert calls[0]["output_config"] == {"effort": "max"}
|
||||
@@ -0,0 +1,32 @@
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def _read(relative_path: str) -> str:
|
||||
return (ROOT / relative_path).read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_generated_content_is_opened_or_downloaded_on_completion():
|
||||
speech = _read(
|
||||
"src/landppt/web/static/js/pages/project/slides_editor/"
|
||||
"projectSlidesEditor.speechScriptsDialog.js"
|
||||
)
|
||||
narration = _read(
|
||||
"src/landppt/web/static/js/pages/project/slides_editor/"
|
||||
"projectEditorNarration.js"
|
||||
)
|
||||
|
||||
speech_completion = speech.split("function startProgressTracking(", 1)[1].split(
|
||||
"function startSingleScriptProgressTracking(", 1
|
||||
)[0]
|
||||
video_export = narration.split("async function exportNarrationVideo()", 1)[1].split(
|
||||
"async function exportNarrationAudio()", 1
|
||||
)[0]
|
||||
|
||||
assert speech_completion.count("showCurrentSpeechScripts();") == 2
|
||||
assert "pollForSpeechScripts" not in speech_completion
|
||||
assert "language: languageValue" in speech
|
||||
assert "triggerFileDownload(downloadUrl);" in video_export
|
||||
assert "window.open('about:blank'" not in video_export
|
||||
@@ -56,8 +56,8 @@ def test_narration_audio_export_ui_is_wired():
|
||||
assert "triggerFileDownload(downloadUrl)" in narration_js_text
|
||||
assert "导出讲解音频" in speech_manage_js_text
|
||||
assert 'onclick="exportNarrationAudio()"' in speech_manage_js_text
|
||||
assert "projectEditorNarration.js?v=20260509-custom-tts-api-v1" in template_text
|
||||
assert "projectSlidesEditor.speechScriptsManage.js?v=20260509-custom-tts-api-v1" in template_text
|
||||
assert "projectEditorNarration.js?v=20260802-auto-result-v1" in template_text
|
||||
assert "projectSlidesEditor.speechScriptsManage.js?v=20260802-auto-result-v1" in template_text
|
||||
|
||||
|
||||
def test_xiaomimimo_tts_provider_is_wired():
|
||||
|
||||
@@ -106,10 +106,10 @@ async def test_openai_provider_chat_completion_uses_responses_api(monkeypatch):
|
||||
{
|
||||
"api_key": "test-key",
|
||||
"base_url": "https://api.openai.com/v1",
|
||||
"model": "gpt-4.1",
|
||||
"model": "gpt-5.6",
|
||||
"use_responses_api": True,
|
||||
"enable_reasoning": True,
|
||||
"reasoning_effort": "high",
|
||||
"reasoning_effort": "max",
|
||||
}
|
||||
)
|
||||
|
||||
@@ -123,7 +123,7 @@ async def test_openai_provider_chat_completion_uses_responses_api(monkeypatch):
|
||||
assert response.metadata["transport"] == "responses"
|
||||
assert len(instances) == 1
|
||||
assert instances[0].responses.create_calls[0]["input"][0]["content"] == "hello"
|
||||
assert instances[0].responses.create_calls[0]["reasoning"] == {"effort": "high"}
|
||||
assert instances[0].responses.create_calls[0]["reasoning"] == {"effort": "max"}
|
||||
assert "max_output_tokens" not in instances[0].responses.create_calls[0]
|
||||
|
||||
|
||||
|
||||
@@ -36,6 +36,12 @@ def test_anthropic_test_payload_does_not_set_output_token_limit():
|
||||
assert "max_tokens" not in payload
|
||||
|
||||
|
||||
def test_anthropic_test_payload_sets_reasoning_effort():
|
||||
payload = build_anthropic_test_payload("Hi", "xhigh")
|
||||
|
||||
assert payload["output_config"] == {"effort": "xhigh"}
|
||||
|
||||
|
||||
def test_extract_google_test_result_reads_text_and_usage():
|
||||
response_text, usage = extract_google_test_result(
|
||||
{
|
||||
|
||||
@@ -162,4 +162,4 @@ def test_speech_script_humanize_route_and_ui_are_wired():
|
||||
assert "showProgressToast(`正在处理第${slideIndex + 1}页演讲稿人话化...`, 0)" in js_text
|
||||
assert "speech-script-humanize-btn" in js_text
|
||||
assert "speechHumanizeAllBtn" in js_text
|
||||
assert "projectSlidesEditor.speechScriptsManage.js?v=20260509-custom-tts-api-v1" in template_text
|
||||
assert "projectSlidesEditor.speechScriptsManage.js?v=20260802-auto-result-v1" in template_text
|
||||
|
||||
Reference in New Issue
Block a user