Compare commits

..

4 Commits

Author SHA1 Message Date
fb78d813af Use free Telegram reactions for AI status (🤔/👍/👎)
Replace premium-only ⏳/✅/❌ with reactions available to all users:
🤔 while processing, 👍 on success, 👎 on error.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 16:02:13 +02:00
65088c2fdc Replace AI status message with emoji reactions
Instead of sending/editing a verbose status message, now reacts to the
command message: ⏳ while processing, ✅ on success, ❌ on error.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 15:19:14 +02:00
e4e47c4649 Fix UTF-8 decode error for providers returning invalid encoding (Ollama)
Some providers (e.g. Ollama) may return response bytes that aren't valid
UTF-8. json.loads() raises UnicodeDecodeError in that case. Add an httpx
response event hook that replaces invalid bytes before the SDK parses the
response body.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 15:14:27 +02:00
783c5c2b87 Fix Minimax chat completions base_url in .env.example
Chat completions API lives on api.minimax.io, not api.minimax.chat.
api.minimax.chat is only for the coding-plan MCP (image understanding).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 13:32:33 +02:00
3 changed files with 21 additions and 25 deletions

View File

@@ -2,4 +2,4 @@ TG_API_ID=123456
TG_API_HASH=your_api_hash_here
SESSION_DIR=sessions
ACCOUNTS_JSON=[{"name":"personal","session":"personal"},{"name":"work","session":"work"}]
# AI_CONFIG={"default_model":"mm","context_messages":12,"prompt":{"root":"prompts/ai","system_file":"system.md","parts_dir":"parts"},"image_understand":{"api_key":"your_minimax_token_plan_key_here","api_host":"https://api.minimax.chat","command":["uvx","minimax-coding-plan-mcp","-y"]},"providers":{"minimax":{"api_key":"your_minimax_api_key_here","base_url":"https://api.minimax.chat/v1"},"openrouter":{"api_key":"your_openrouter_api_key_here","base_url":"https://openrouter.ai/api/v1"}},"models":{"mm":{"provider":"minimax","model":"MiniMax-Text-01"},"or":{"provider":"openrouter","model":"openai/gpt-4o-mini"}}}
# AI_CONFIG={"default_model":"mm","context_messages":12,"prompt":{"root":"prompts/ai","system_file":"system.md","parts_dir":"parts"},"image_understand":{"api_key":"your_minimax_token_plan_key_here","api_host":"https://api.minimax.chat","command":["uvx","minimax-coding-plan-mcp","-y"]},"providers":{"minimax":{"api_key":"your_minimax_api_key_here","base_url":"https://api.minimax.io/v1"},"openrouter":{"api_key":"your_openrouter_api_key_here","base_url":"https://openrouter.ai/api/v1"}},"models":{"mm":{"provider":"minimax","model":"MiniMax-Text-01"},"or":{"provider":"openrouter","model":"openai/gpt-4o-mini"}}}

View File

@@ -7,6 +7,7 @@ from pathlib import Path
import tempfile
from typing import TYPE_CHECKING, Any, Awaitable, Callable
import httpx
from openai import AsyncOpenAI
from app.ai.image_understand import (
@@ -28,6 +29,15 @@ log = logging.getLogger(__name__)
ProgressCallback = Callable[[str], Awaitable[None] | None]
async def _sanitize_response_encoding(response: httpx.Response) -> None:
await response.aread()
raw = response.content
try:
raw.decode("utf-8")
except UnicodeDecodeError:
response._content = raw.decode("utf-8", errors="replace").encode("utf-8")
@dataclass(slots=True)
class AIResponse:
text: str
@@ -103,6 +113,9 @@ class AIService:
api_key=provider.api_key,
base_url=provider.base_url,
timeout=provider.timeout,
http_client=httpx.AsyncClient(
event_hooks={"response": [_sanitize_response_encoding]},
),
)
self._clients[provider.name] = client
return client

View File

@@ -309,26 +309,14 @@ async def ai(ctx: "CommandContext") -> None:
await ctx.reply(str(exc))
return
status = await ctx.reply(
"\n".join(
[
"⏳ AI request started",
f"model: {selected_alias}",
f"context: {invocation.context_messages} message(s)",
"tools: understand_image"
if invocation.use_image_understanding
else "tools: none",
f"prompt: {invocation.prompt[:200]}{'...' if len(invocation.prompt) > 200 else ''}",
]
)
)
async def _progress(text: str) -> None:
async def _react(emoji: str | None) -> None:
try:
await status.edit_text(text)
await ctx.client.send_reaction(ctx.message.chat.id, ctx.message.id, emoji=emoji)
except Exception:
pass
await _react("🤔")
try:
response = await ai_service.generate(
ctx,
@@ -338,16 +326,11 @@ async def ai(ctx: "CommandContext") -> None:
use_image_understanding=invocation.use_image_understanding,
prompt=invocation.prompt,
),
progress=_progress,
)
except (RuntimeError, ValueError) as exc:
await _progress(f"❌ {exc}")
await _react("👎")
await ctx.reply(str(exc))
return
final_text = f"✅\n{response.text}"
if len(final_text) <= 3900:
await _progress(final_text)
return
await _progress("✅ Response ready")
await _react("👍")
await _reply_long(ctx, response.text)