From 2c7a02f587b0ad60ecf111f74cda28316f3a175b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E2=80=9Cydy0615=E2=80=9D?= <“allenyuan410@gmail.com”>
Date: Tue, 2 Jun 2026 21:23:34 +0800
Subject: [PATCH] Enhance LLM functionality with PRO mode support and improved
prompt handling
- Added support for PRO mode in LLM with specific instruction handling and context awareness.
- Updated prompt building functions to include prefill options for better context management.
- Introduced new inline examples for PRO mode in JSON format.
- Enhanced system prompts to reflect PRO mode capabilities and rules.
- Modified API endpoints to accommodate new parameters and ensure backward compatibility.
- Improved test cases to validate new functionality and ensure comprehensive coverage.
---
backend/llm.py | 28 ++++--
backend/main.py | 6 +-
backend/pro_completions.py | 47 +++-------
backend/prompt.py | 107 ++++++++++++++++++++++-
backend/prompts/__init__.py | 8 ++
backend/prompts/inline_examples.json | 2 +-
backend/prompts/inline_examples_pro.json | 3 +
backend/prompts/system_prompt.json | 2 +-
backend/prompts/system_prompt_pro.json | 3 +
backend/tests/test_llm.py | 38 ++++----
backend/tests/test_main_endpoints.py | 2 +-
backend/tests/test_pro_completions.py | 15 ++--
12 files changed, 191 insertions(+), 70 deletions(-)
create mode 100644 backend/prompts/inline_examples_pro.json
create mode 100644 backend/prompts/system_prompt_pro.json
diff --git a/backend/llm.py b/backend/llm.py
index c69cf24..f2a6369 100644
--- a/backend/llm.py
+++ b/backend/llm.py
@@ -18,6 +18,9 @@ load_dotenv()
LLM_BASE_URL = os.getenv('LLM_BASE_URL', 'http://localhost:11434/v1/')
LLM_API_KEY = os.getenv('LLM_API_KEY', 'ollama')
+# Auth headers for upstream LLM service (OpenAI-compatible Bearer token)
+LLM_HEADERS = {'Authorization': f'Bearer {LLM_API_KEY}'}
+
# Model names (backward compat: fall back to OLLAMA_MODEL if LLM_MODEL not set)
_raw_model = os.getenv('LLM_MODEL') or os.getenv('OLLAMA_MODEL', 'gpt-oss:20b')
LLM_MODEL = _raw_model.strip() if _raw_model else 'gpt-oss:20b'
@@ -73,6 +76,7 @@ def _build_chat_payload(
thinking: str | None = None,
model: str | None = None,
use_pro_model: bool = False,
+ prefill: str | None = None,
) -> dict:
messages = []
sys_prompt = _resolve_system_prompt(system_prompt)
@@ -80,6 +84,9 @@ def _build_chat_payload(
messages.append({'role': 'system', 'content': sys_prompt})
messages.append({'role': 'user', 'content': prompt})
+ if prefill:
+ messages.append({'role': 'assistant', 'content': prefill})
+
payload = {
'model': _resolve_model_name(model, use_pro_model=use_pro_model),
'messages': messages,
@@ -101,6 +108,7 @@ def _build_chat_stream_payload(
thinking: str | None = None,
model: str | None = None,
use_pro_model: bool = False,
+ prefill: str | None = None,
) -> dict:
messages = []
sys_prompt = _resolve_system_prompt(system_prompt)
@@ -108,6 +116,9 @@ def _build_chat_stream_payload(
messages.append({'role': 'system', 'content': sys_prompt})
messages.append({'role': 'user', 'content': prompt})
+ if prefill:
+ messages.append({'role': 'assistant', 'content': prefill})
+
payload = {
'model': _resolve_model_name(model, use_pro_model=use_pro_model),
'messages': messages,
@@ -145,6 +156,7 @@ async def call_ollama(
thinking: str | None = None,
model: str | None = None,
use_pro_model: bool = False,
+ prefill: str | None = None,
) -> dict:
"""Call OpenAI-compatible chat completions (non-streaming) and return content/thinking."""
start = time.perf_counter()
@@ -161,13 +173,13 @@ async def call_ollama(
payload = _build_chat_payload(
prompt=prompt, system_prompt=system_prompt, temperature=temperature,
- thinking=thinking, model=model, use_pro_model=use_pro_model,
+ thinking=thinking, model=model, use_pro_model=use_pro_model, prefill=prefill,
)
http_timeout = httpx.Timeout(connect=10.0, read=None, write=30.0, pool=30.0)
try:
- async with httpx.AsyncClient(base_url=LLM_BASE_URL, timeout=http_timeout) as client:
+ async with httpx.AsyncClient(base_url=LLM_BASE_URL, headers=LLM_HEADERS, timeout=http_timeout) as client:
resp = await asyncio.wait_for(
client.post('/chat/completions', json=payload), timeout=COMPLETION_TIMEOUT,
)
@@ -229,6 +241,7 @@ async def stream_ollama(
thinking: str | None = None,
model: str | None = None,
use_pro_model: bool = False,
+ prefill: str | None = None,
) -> AsyncIterator[str]:
"""Stream text deltas from OpenAI-compatible chat completions."""
start = time.perf_counter()
@@ -246,13 +259,13 @@ async def stream_ollama(
payload = _build_chat_stream_payload(
prompt=prompt, system_prompt=system_prompt, temperature=temperature,
- thinking=thinking, model=model, use_pro_model=use_pro_model,
+ thinking=thinking, model=model, use_pro_model=use_pro_model, prefill=prefill,
)
http_timeout = httpx.Timeout(connect=10.0, read=None, write=30.0, pool=30.0)
try:
- async with httpx.AsyncClient(base_url=LLM_BASE_URL, timeout=http_timeout) as client:
+ async with httpx.AsyncClient(base_url=LLM_BASE_URL, headers=LLM_HEADERS, timeout=http_timeout) as client:
try:
async with client.stream('POST', '/chat/completions', json=payload) as response:
response.raise_for_status()
@@ -352,6 +365,7 @@ async def stream_ollama_events(
model: str | None = None,
use_pro_model: bool = False,
enable_thinking: bool = True,
+ prefill: str | None = None,
timeout: float | None = None,
) -> AsyncIterator[tuple[Literal['thinking', 'content'], str]]:
"""Stream (event_type, payload) tuples from OpenAI-compatible chat completions."""
@@ -370,7 +384,7 @@ async def stream_ollama_events(
payload = _build_chat_stream_payload(
prompt=prompt, system_prompt=system_prompt, temperature=temperature,
- thinking=thinking if enable_thinking else None, model=model, use_pro_model=use_pro_model,
+ thinking=thinking if enable_thinking else None, model=model, use_pro_model=use_pro_model, prefill=prefill,
)
effective_timeout = timeout if timeout is not None else COMPLETION_TIMEOUT
@@ -378,7 +392,7 @@ async def stream_ollama_events(
sent_thinking = False
try:
- async with httpx.AsyncClient(base_url=LLM_BASE_URL, timeout=http_timeout) as client:
+ async with httpx.AsyncClient(base_url=LLM_BASE_URL, headers=LLM_HEADERS, timeout=http_timeout) as client:
try:
async with client.stream('POST', '/chat/completions', json=payload) as response:
response.raise_for_status()
@@ -507,7 +521,7 @@ async def call_vlm_ocr(image_bytes: bytes, language: str = 'auto') -> str:
http_timeout = httpx.Timeout(connect=10.0, read=None, write=30.0, pool=30.0)
try:
- async with httpx.AsyncClient(base_url=LLM_BASE_URL, timeout=http_timeout) as client:
+ async with httpx.AsyncClient(base_url=LLM_BASE_URL, headers=LLM_HEADERS, timeout=http_timeout) as client:
resp = await asyncio.wait_for(
client.post('/chat/completions', json=payload), timeout=OCR_TIMEOUT,
)
diff --git a/backend/main.py b/backend/main.py
index ad2eb71..0d0480c 100644
--- a/backend/main.py
+++ b/backend/main.py
@@ -179,7 +179,7 @@ async def create_completion(request: Request, req: CompletionRequest, api_key: s
logger.info("[%s] llm_input_prefix=%r", request_tag, llm_prefix)
logger.info("[%s] llm_input_suffix=%r", request_tag, llm_suffix)
- system_prompt, user_prompt = build_completion_prompts(
+ system_prompt, user_prompt, prefill = build_completion_prompts(
req.prefix,
req.suffix,
req.languageId,
@@ -196,6 +196,7 @@ async def create_completion(request: Request, req: CompletionRequest, api_key: s
temperature=_clamp_temperature(req.temperature, 0.7),
thinking=req.model_thinking if req.model_thinking != "none" else None,
model=req.model,
+ prefill=prefill or None,
)
)
@@ -262,7 +263,7 @@ async def create_pro_completion_stream(request: Request, req: CompletionRequest,
logger.info("[%s] pro_llm_input_prefix=%r", request_tag, llm_prefix)
logger.info("[%s] pro_llm_input_suffix=%r", request_tag, llm_suffix)
- system_prompt, user_prompt = build_completion_prompts(
+ system_prompt, user_prompt, prefill = build_completion_prompts(
req.prefix,
req.suffix,
req.languageId,
@@ -282,6 +283,7 @@ async def create_pro_completion_stream(request: Request, req: CompletionRequest,
thinking=req.model_thinking if req.model_thinking != "none" else None,
model=req.model,
use_pro_model=True,
+ prefill=prefill or None,
):
chunks.append(delta)
await queue.put(("chunk", json.dumps({"delta": delta}, ensure_ascii=False)))
diff --git a/backend/pro_completions.py b/backend/pro_completions.py
index d43cd1a..5e72adc 100644
--- a/backend/pro_completions.py
+++ b/backend/pro_completions.py
@@ -16,6 +16,7 @@ from pydantic import BaseModel
from geoip import get_ip_location_text
from llm import stream_ollama_events
from models import UserPreferences
+from prompt import build_pro_completion_prompts
logger = logging.getLogger("api.pro")
@@ -125,44 +126,19 @@ def _build_pro_prompts(
suffix: str,
language_id: str,
instruction: str,
+ pro_thinking: str = "medium",
location: str = "",
preferences: UserPreferences | None = None,
) -> tuple[str, str]:
- safe_language = (language_id or "markdown").strip() or "markdown"
- safe_instruction = (instruction or "").strip()
- preference_lines: list[str] = []
- if preferences:
- if preferences.language and preferences.language != "auto":
- preference_lines.append(f"- Preferred language: {preferences.language}")
- if preferences.currency and preferences.currency != "auto":
- preference_lines.append(f"- Preferred currency: {preferences.currency}")
- if preferences.timezone and preferences.timezone != "auto":
- preference_lines.append(f"- Timezone: {preferences.timezone}")
- if location:
- preference_lines.append(f"- Location hint: {location}")
-
- system_prompt = f"""You edit Markdown documents.
-Return only the Markdown text to insert at the cursor.
-Do not explain, analyze, label the answer, or wrap the whole answer in a code fence.
-Match the document language, style, and Markdown structure.
-Language: {safe_language}."""
-
- preferences_text = "\n".join(preference_lines) if preference_lines else "- none"
- instruction_text = safe_instruction or "Continue the Markdown naturally."
- user_prompt = f"""Instruction:
-{instruction_text}
-
-User preferences:
-{preferences_text}
-
-Markdown before cursor:
-{prefix}
-
-Markdown after cursor:
-{suffix}
-
-Write only the Markdown that belongs at the cursor."""
- return system_prompt.strip(), user_prompt.strip()
+ return build_pro_completion_prompts(
+ prefix=prefix,
+ suffix=suffix,
+ instruction=instruction,
+ language_id=language_id,
+ location=location,
+ pro_thinking_level=pro_thinking,
+ preferences=preferences,
+ )
def _get_client_ip(request: Request) -> str:
@@ -237,6 +213,7 @@ def register_pro_completion_routes(app: FastAPI, get_api_key):
suffix=suffix,
language_id=req.languageId,
instruction=req.instruction,
+ pro_thinking=req.pro_thinking,
location=location,
preferences=req.user_preferences,
)
diff --git a/backend/prompt.py b/backend/prompt.py
index 9d9c2a7..a22c3ce 100644
--- a/backend/prompt.py
+++ b/backend/prompt.py
@@ -3,7 +3,13 @@ import re
from typing import Tuple
from models import UserPreferences
-from prompts import get_language_guidance_map, get_system_prompt_template, get_inline_examples
+from prompts import (
+ get_inline_examples,
+ get_inline_examples_pro,
+ get_language_guidance_map,
+ get_system_prompt_pro_template,
+ get_system_prompt_template,
+)
def _get_current_datetime(timezone_pref: str = "auto") -> str:
@@ -294,6 +300,17 @@ def build_inline_system_prompt(language_id: str = "markdown") -> str:
_INLINE_EXAMPLES = get_inline_examples()
+_PRO_INLINE_EXAMPLES = get_inline_examples_pro()
+
+
+def build_pro_system_prompt(language_id: str = "markdown") -> str:
+ safe_language_id = _canonical_language_id(language_id)
+ language_guidance = _language_guidance(safe_language_id)
+ template = get_system_prompt_pro_template()
+ system_prompt = template.replace("{language_id}", safe_language_id)
+ if language_guidance:
+ system_prompt = f"{system_prompt.rstrip()}\n{language_guidance.strip()}"
+ return system_prompt.strip()
def build_completion_prompts(
@@ -392,3 +409,91 @@ def build_prompt(
preferences=preferences,
)
return user_prompt
+
+
+def build_pro_completion_prompts(
+ prefix: str,
+ suffix: str,
+ instruction: str = "",
+ language_id: str = "markdown",
+ location: str = "",
+ pro_thinking_level: str = "medium",
+ preferences: UserPreferences | None = None,
+) -> Tuple[str, str]:
+ safe_language_id = _canonical_language_id(language_id)
+ recent_prefix, recent_suffix = _prepare_context(prefix, suffix)
+ recent_prefix = _normalize_newlines(recent_prefix)
+ recent_suffix = _normalize_newlines(recent_suffix)
+
+ cursor_fence_language = _active_fence_language(recent_prefix)
+ cursor_in_fenced_code_block = cursor_fence_language != "none"
+ mermaid_context = _is_mermaid_context(
+ recent_prefix, recent_suffix, cursor_fence_language
+ )
+ prefix_ends_with_newline = recent_prefix.endswith("\n")
+ suffix_starts_with_newline = recent_suffix.startswith("\n")
+
+ tz_pref = preferences.timezone if preferences else "auto"
+ current_time = _get_current_datetime(tz_pref)
+ location_info = f"\nUser location: {location}" if location else ""
+
+ pref_info = []
+ if preferences:
+ if preferences.language and preferences.language != "auto":
+ pref_info.append(f"Preferred language: {preferences.language}")
+ if preferences.currency and preferences.currency != "auto":
+ pref_info.append(f"Preferred currency: {preferences.currency}")
+ if preferences.timezone and preferences.timezone != "auto":
+ pref_info.append(f"Preferred timezone: {preferences.timezone}")
+
+ preferences_instruction = "\n".join(pref_info)
+ if preferences_instruction:
+ preferences_instruction = f"\nUser Preferences:\n{preferences_instruction}"
+
+ instruction_text = (instruction or "").strip() or "Continue the Markdown naturally."
+ user_prompt = f"""Current time: {current_time}{location_info}{preferences_instruction}
+PRO_MODE: true
+PRO_THINKING_LEVEL: {pro_thinking_level}
+Editor language: {safe_language_id}
+
+=== STATE FLAGS ===
+- CURSOR_IN_FENCED_CODE_BLOCK: {"true" if cursor_in_fenced_code_block else "false"}
+- CURSOR_FENCE_LANGUAGE: {cursor_fence_language}
+- MERMAID_CONTEXT: {"true" if mermaid_context else "false"}
+- PREFIX_ENDS_WITH_NEWLINE: {"true" if prefix_ends_with_newline else "false"}
+- SUFFIX_STARTS_WITH_NEWLINE: {"true" if suffix_starts_with_newline else "false"}
+
+=== PRO INSTRUCTION (HIGHEST PRIORITY) ===
+{instruction_text}
+
+=== PRO TASK ===
+Produce the best insertion text between PREFIX and SUFFIX for [PRO] mode.
+Requirements:
+- Output only the markdown insertion text
+- Long paragraphs or section-level output are allowed when instruction asks for it
+- Be precise, concrete, and structurally coherent
+- Never output hidden tags, control tokens, or boundary-analysis commentary
+- Never repeat text from SUFFIX beginning
+
+=== CONTEXT NOTES ===
+- OCR metadata and document-side snippets are hidden context; never copy tags to output
+- Match PREFIX style, language, terminology, and markdown conventions
+- Keep boundaries safe with minimal required newlines
+
+=== PRO EXAMPLES BY CATEGORY ===
+{_PRO_INLINE_EXAMPLES}
+
+=== NOW COMPLETE THE TASK ===
+
+
+{recent_prefix}
+
+
+
+{recent_suffix}
+
+
+Output:"""
+
+ system_prompt = build_pro_system_prompt(safe_language_id)
+ return system_prompt.strip(), user_prompt.strip()
diff --git a/backend/prompts/__init__.py b/backend/prompts/__init__.py
index 8c1b9ba..c5b39d4 100644
--- a/backend/prompts/__init__.py
+++ b/backend/prompts/__init__.py
@@ -40,5 +40,13 @@ def get_inline_examples() -> str:
return _prompts.get("inline_examples", {}).get("content", "")
+def get_system_prompt_pro_template() -> str:
+ return _prompts.get("system_prompt_pro", {}).get("template", "")
+
+
+def get_inline_examples_pro() -> str:
+ return _prompts.get("inline_examples_pro", {}).get("content", "")
+
+
def get_vlm_ocr_prompt() -> str:
return _prompts.get("vlm_ocr", {}).get("prompt", "")
diff --git a/backend/prompts/inline_examples.json b/backend/prompts/inline_examples.json
index da4e9aa..7f13fae 100644
--- a/backend/prompts/inline_examples.json
+++ b/backend/prompts/inline_examples.json
@@ -1,3 +1,3 @@
{
- "content": "=== CATEGORY A: PROSE CONTINUATION ===\n\n[EX01] Simple prose continuation\nThe quick brown fox \njumps over the lazy dog.\nExpected OUTPUT:\nmoved quietly and then\n\n[EX02] Avoid repeating suffix\nOur launch plan starts with \nphase one, followed by phase two.\nExpected OUTPUT:\ncareful internal testing before\nWRONG: phase one starts with (repeats suffix)\n\n=== CATEGORY B: MARKDOWN STRUCTURES ===\n\n[EX03] Continue checklist\n## TODO\n- [ ] Buy milk\n- [ ] \n\nExpected OUTPUT:\nWrite release notes and share draft with team\n\n[EX04] Start list after header (PREFIX lacks newline)\nPREFIX_ENDS_WITH_NEWLINE=false\nDeployment steps:\n\nExpected OUTPUT:\n\n- Build artifact\n- Deploy service\n\n[EX05] Continue table row\n| Name | Score |\n| --- | --- |\n| Alice | 92 |\n| Bob | \n\nExpected OUTPUT:\n88 |\n\n[EX06] Start new paragraph\nFirst paragraph ends.\n\nExpected OUTPUT:\n\nSecond paragraph starts.\nWRONG: Second paragraph starts. (missing leading \\n\\n)\n\n[EX07] Add newline before heading\nPREFIX_ENDS_WITH_NEWLINE=false\nEnd of previous section.\n## Next Heading\nExpected OUTPUT:\n\nWRONG: (would join with heading without separation)\n\n=== CATEGORY C: CODE BLOCKS ===\n\n[EX08] Outside fence: wrap code in fence\nCURSOR_IN_FENCED_CODE_BLOCK=false\nParse this JSON payload in Python:\n\nExpected OUTPUT:\n```python\nimport json\ndata = json.loads(payload)\n```\nWRONG: import json\\ndata = json.loads(payload) (no fence)\n\n[EX09] Inside fence: output code only\nCURSOR_IN_FENCED_CODE_BLOCK=true\n```python\ndef add(a, b):\nreturn \n\n```\nExpected OUTPUT:\na + b\nWRONG: ```python\\nreturn a + b\\n``` (duplicate fences)\n\n[EX10] Code inside fence uses single newline\nCURSOR_IN_FENCED_CODE_BLOCK=true\n```python\ndef hello():\n\n```\nExpected OUTPUT:\nprint(\"Hello\")\nreturn True\n(Note: single \\n between code lines, no markdown rules)\n\n=== CATEGORY D: MATH ===\n\n[EX11] Inline math\nThe derivative of x^2 is \n.\nExpected OUTPUT:\n$2x$\nWRONG: 2x (bare formula)\n\n[EX12] Block math\nWe can write the Gaussian integral as:\n\nExpected OUTPUT:\n$$\n\\int_{-\\infty}^{\\infty} e^{-x^2}\\,dx = \\sqrt{\\pi}\n$$\nWRONG: \\int... (bare formula without $$)\n\n=== CATEGORY E: MERMAID ===\n\n[EX13] Inside mermaid fence\nCURSOR_FENCE_LANGUAGE=mermaid\nCURSOR_IN_FENCED_CODE_BLOCK=true\n```mermaid\nflowchart TD\nA[Start] --> \n\n```\nExpected OUTPUT:\nB{Valid?}\nB -->|Yes| C[Done]\nWRONG: ```mermaid\\nB{Valid?}... (duplicate fence)\n\n[EX14] Outside fence with mermaid context\nCURSOR_IN_FENCED_CODE_BLOCK=false\nMERMAID_CONTEXT=true\nPlease provide a simple release pipeline diagram.\n\nExpected OUTPUT:\n```mermaid\nflowchart LR\nBuild --> Test --> Deploy\n```\n\n=== CATEGORY F: OCR METADATA ===\n\n[EX15] Use OCR as context, never output\n \nThe relationship is \n.\nExpected OUTPUT:\n$y = mx + b$\nWRONG: (OCR tag in output)"
+ "content": "=== CATEGORY A: PROSE CONTINUATION ===\n\n[EX01] Simple prose continuation\nThe quick brown fox \njumps over the lazy dog.\nExpected OUTPUT:\nmoved quietly and then\n\n[EX02] Avoid repeating suffix\nOur launch plan starts with \nphase one, followed by phase two.\nExpected OUTPUT:\ncareful internal testing before\nWRONG: phase one starts with (repeats suffix)\n\n=== CATEGORY B: MARKDOWN STRUCTURES ===\n\n[EX03] Continue checklist\n## TODO\n- [ ] Buy milk\n- [ ] \n\nExpected OUTPUT:\nWrite release notes and share draft with team\n\n[EX04] Start list after header (PREFIX lacks newline)\nPREFIX_ENDS_WITH_NEWLINE=false\nDeployment steps:\n\nExpected OUTPUT:\n\n- Build artifact\n- Deploy service\n\n[EX05] Continue table row\n| Name | Score |\n| --- | --- |\n| Alice | 92 |\n| Bob | \n\nExpected OUTPUT:\n88 |\n\n[EX06] Start new paragraph\nFirst paragraph ends.\n\nExpected OUTPUT:\n\nSecond paragraph starts.\nWRONG: Second paragraph starts. (missing leading \\n\\n)\n\n[EX07] Add newline before heading\nPREFIX_ENDS_WITH_NEWLINE=false\nEnd of previous section.\n## Next Heading\nExpected OUTPUT:\n\nWRONG: (would join with heading without separation)\n\n=== CATEGORY C: CODE BLOCKS ===\n\n[EX08] Outside fence: wrap code in fence\nCURSOR_IN_FENCED_CODE_BLOCK=false\nParse this JSON payload in Python:\n\nExpected OUTPUT:\n```python\nimport json\ndata = json.loads(payload)\n```\nWRONG: import json\\ndata = json.loads(payload) (no fence)\n\n[EX09] Inside fence: output code only\nCURSOR_IN_FENCED_CODE_BLOCK=true\n```python\ndef add(a, b):\n return \n\n```\nExpected OUTPUT:\na + b\nWRONG: ```python\\nreturn a + b\\n``` (duplicate fences)\n\n[EX10] Code inside fence uses single newline\nCURSOR_IN_FENCED_CODE_BLOCK=true\n```python\ndef hello():\n\n```\nExpected OUTPUT:\n print(\"Hello\")\n return True\n(Note: single \\n between code lines, no markdown rules)\n\n=== CATEGORY D: MATH ===\n\n[EX11] Inline math\nThe derivative of x^2 is \n.\nExpected OUTPUT:\n$2x$\nWRONG: 2x (bare formula)\n\n[EX12] Block math\nWe can write the Gaussian integral as:\n\nExpected OUTPUT:\n$$\n\\int_{-\\infty}^{\\infty} e^{-x^2}\\,dx = \\sqrt{\\pi}\n$$\nWRONG: \\int... (bare formula without $$)\n\n=== CATEGORY E: MERMAID ===\n\n[EX13] Inside mermaid fence\nCURSOR_FENCE_LANGUAGE=mermaid\nCURSOR_IN_FENCED_CODE_BLOCK=true\n```mermaid\nflowchart TD\n A[Start] --> \n\n```\nExpected OUTPUT:\nB{Valid?}\nB -->|Yes| C[Done]\nWRONG: ```mermaid\\nB{Valid?}... (duplicate fence)\n\n[EX14] Outside fence with mermaid context\nCURSOR_IN_FENCED_CODE_BLOCK=false\nMERMAID_CONTEXT=true\nPlease provide a simple release pipeline diagram.\n\nExpected OUTPUT:\n```mermaid\nflowchart LR\n Build --> Test --> Deploy\n```\n\n=== CATEGORY F: OCR METADATA ===\n\n[EX15] Use OCR as context, never output\n \nThe relationship is \n.\nExpected OUTPUT:\n$y = mx + b$\nWRONG: (OCR tag in output)"
}
diff --git a/backend/prompts/inline_examples_pro.json b/backend/prompts/inline_examples_pro.json
new file mode 100644
index 0000000..410b399
--- /dev/null
+++ b/backend/prompts/inline_examples_pro.json
@@ -0,0 +1,3 @@
+{
+ "content": "=== PRO CATEGORY A: CONTINUATION AND EXPANSION ===\n\n[PRO-EX01] Continue naturally without suffix repetition\nProject update: This week we completed \nand started preparing the release checklist.\nExpected OUTPUT:\nbackend integration tests\nWRONG: and started preparing (repeats suffix start)\n\n[PRO-EX02] Long paragraph expansion allowed\nINSTRUCTION: Expand into a fuller paragraph with concrete details.\nOur migration reduced incidents.\n\nExpected OUTPUT:\n\nIt also improved deployment confidence by cutting rollback frequency and clarifying ownership for each service boundary, which made post-release diagnosis significantly faster.\n\n=== PRO CATEGORY B: STRUCTURED MARKDOWN ===\n\n[PRO-EX03] Build a section with heading and bullets\nINSTRUCTION: Add a short risk section.\n## Launch Plan\nCurrent status is green.\n\nExpected OUTPUT:\n\n### Risks\n- Third-party API latency may delay webhook retries.\n- Data backfill window could overlap with peak traffic.\n\n[PRO-EX04] Preserve list numbering continuity\n1. Prepare schema\n2. Run dry-run\n3. \n\nExpected OUTPUT:\nValidate production metrics and sign off\n\n[PRO-EX05] Keep table shape valid\n| Metric | Before | After |\n| --- | --- | --- |\n| P95 latency | 420ms | \n\nExpected OUTPUT:\n260ms |\n\n=== PRO CATEGORY C: CODE AND TECHNICAL CONTEXT ===\n\n[PRO-EX06] Inside code fence: output code only\nCURSOR_IN_FENCED_CODE_BLOCK=true\n```python\ndef build_payload(user_id):\n return \n\n```\nExpected OUTPUT:\n{\"id\": user_id, \"active\": True}\n\n[PRO-EX07] Outside code fence: include fenced block when instruction asks code\nCURSOR_IN_FENCED_CODE_BLOCK=false\nINSTRUCTION: Show a minimal SQL query.\nFetch active users:\n\nExpected OUTPUT:\n\n```sql\nSELECT id, email\nFROM users\nWHERE active = TRUE;\n```\n\n=== PRO CATEGORY D: MATH AND MERMAID ===\n\n[PRO-EX08] Inline math remains inline\nThe expected value is \n under this distribution.\nExpected OUTPUT:\n$\\mu$\n\n[PRO-EX09] Mermaid inside mermaid fence\nCURSOR_FENCE_LANGUAGE=mermaid\nCURSOR_IN_FENCED_CODE_BLOCK=true\n```mermaid\nflowchart TD\nA[Input] --> \n\n```\nExpected OUTPUT:\nB{Validated?}\nB -->|Yes| C[Persist]\n\n[PRO-EX10] Mermaid outside fence with context\nMERMAID_CONTEXT=true\nCURSOR_IN_FENCED_CODE_BLOCK=false\nShow the pipeline as a diagram.\n\nExpected OUTPUT:\n\n```mermaid\nflowchart LR\nQueue --> Worker --> Storage\n```\n\n=== PRO CATEGORY E: INSTRUCTION-FIRST REWRITE ===\n\n[PRO-EX11] Rewrite style per instruction\nINSTRUCTION: Rewrite as concise executive tone in Chinese.\n这个方案看起来不错,但是细节很多,可能会拖慢推进。\n\nExpected OUTPUT:\n该方案方向正确,但需聚焦关键路径并压缩实现范围,以保障交付节奏。\n\n[PRO-EX12] Add constrained output length\nINSTRUCTION: Add one sentence under 25 words.\n结论:\n\nExpected OUTPUT:\n\n先完成最小可用版本,再按风险优先级迭代。\n\n=== PRO CATEGORY F: HIDDEN CONTEXT SAFETY ===\n\n[PRO-EX13] Never leak OCR tags\n \nTimeline:\n\nExpected OUTPUT:\n\nQ3 launch with weekly checkpoints and ownership tracking.\nWRONG: \n\n=== PRO CATEGORY G: BOUNDARY PRECISION ===\n\n[PRO-EX14] Add leading newline when needed\nPREFIX_ENDS_WITH_NEWLINE=false\nAction items:\n\nExpected OUTPUT:\n\n- Confirm rollout window\n- Notify on-call rotation\n\n[PRO-EX15] Do not break upcoming heading\nPREFIX_ENDS_WITH_NEWLINE=false\nSUFFIX_STARTS_WITH_NEWLINE=false\nSummary complete.\n## Next Steps\nExpected OUTPUT:\n\n\n=== PRO CATEGORY H: MIXED STRUCTURE ===\n\n[PRO-EX16] Combine short prose + list + code block\nINSTRUCTION: Add a short explanation, then checklist, then shell command.\nDeployment guide draft:\n\nExpected OUTPUT:\n\nUse the following sequence to reduce release risk:\n- Verify migrations on staging\n- Freeze non-critical merges\n- Capture rollback snapshot\n\n```bash\n./scripts/deploy.sh --env prod\n```"
+}
diff --git a/backend/prompts/system_prompt.json b/backend/prompts/system_prompt.json
index ecc7391..31abe5d 100644
--- a/backend/prompts/system_prompt.json
+++ b/backend/prompts/system_prompt.json
@@ -1,3 +1,3 @@
{
- "template": "You are an inline completion engine for a {language_id} editor with ghost-text suggestions.\n\nReturn only the insertion text that should be placed between PREFIX and SUFFIX.\n\nCORE PRINCIPLE: Output insertion text only. No explanations, no meta labels, no wrapper quotes, no analysis.\n\nNever output internal reasoning, chain-of-thought, boundary checks, or deliberation. Never output chat/template artifacts such as assistant, final, channel, <|start|>, <|end|>, <|fim_prefix|>, <|fim_suffix|>, or <|fim_middle|>.\n\nCONTEXT FLAGS:\n- CURSOR_IN_FENCED_CODE_BLOCK tells whether the cursor is inside a code fence.\n- CURSOR_FENCE_LANGUAGE gives the active fence language, or none.\n- PREFIX_ENDS_WITH_NEWLINE and SUFFIX_STARTS_WITH_NEWLINE describe the insertion boundary.\n- MERMAID_CONTEXT tells whether Mermaid syntax is likely expected.\n\nSPECIALIZED RULES:\n- If CURSOR_IN_FENCED_CODE_BLOCK=true: output only code lines, no triple backticks.\n- If CURSOR_IN_FENCED_CODE_BLOCK=false and a code block is needed: use a fenced block with a language tag, e.g. ```{language}.\n- Inline math must use $...$; block math must use $$...$$.\n- Inside latex/tex/katex fences, output raw LaTeX only.\n- If CURSOR_FENCE_LANGUAGE=mermaid: output Mermaid syntax only, no backticks or prose.\n- If MERMAID_CONTEXT=true outside a fence: output a complete ```mermaid fenced block only when the surrounding text asks for a diagram.\n\nMARKDOWN AND BOUNDARIES:\n- Use actual line breaks, never spelled-out escape sequences, unless the document text itself needs them.\n- Match PREFIX tone, style, indentation, list/table structure, and language.\n- Never repeat text from the beginning of SUFFIX.\n- If separation is needed, put the needed real newline directly in the insertion text without explaining it.\n\nPREFILL:\n- The prompt may place a short tail of PREFIX immediately after <|fim_middle|> to make continuation natural.\n- Continue from that PREFILL. Do not describe it or output control markers.\n\nHIDDEN CONTEXT:\n- OCR metadata like and document context are hidden context.\n- Use hidden context only as a semantic hint; never copy hidden tags to output."
+ "template": "You are an inline completion engine for a {language_id} editor with ghost-text suggestions.\n\nReturn only the insertion text that should be placed between PREFIX and SUFFIX.\n\nCORE PRINCIPLE: Output insertion text only. No explanations, no meta labels, no wrapper quotes.\n\n\nNever output chat/template artifacts such as assistant, final, channel, <|fim_prefix|>, <|fim_suffix|>, or <|fim_middle|>.\n\nPRIORITY 1: CONTEXT AWARENESS (Read these flags from user prompt)\n- CURSOR_IN_FENCED_CODE_BLOCK: Are you inside a code fence?\n- CURSOR_FENCE_LANGUAGE: What language is the current fence?\n- PREFIX_ENDS_WITH_NEWLINE: Does prefix end with newline?\n- SUFFIX_STARTS_WITH_NEWLINE: Does suffix start with newline?\n- MERMAID_CONTEXT: Is this a Mermaid diagram context?\n\nPRIORITY 2: SPECIALIZED CONTENT RULES\n\n2.1 Code Block Handling:\nIf CURSOR_IN_FENCED_CODE_BLOCK=true:\n- You are inside a code fence\n- Output code lines ONLY (no triple backticks)\n- Separate code lines with actual newline characters\n\nIf CURSOR_IN_FENCED_CODE_BLOCK=false and code needed:\n- Wrap code in fenced block with language tag:\n```{language}\ncode here\n```\n- Never use inline backticks for code snippets\n\n2.2 Math Formatting (KaTeX):\n- Inline math: wrap with $...$\n- Block math: wrap with $$...$$\n- Never output bare formulas\n- Exception: inside latex/tex/katex fence, output raw LaTeX\n\n2.3 Mermaid Diagrams:\nIf CURSOR_FENCE_LANGUAGE=mermaid:\n- Output Mermaid syntax ONLY\n- No backticks, no explanations\n\nIf MERMAID_CONTEXT=true and outside fence:\n- Output complete fenced block:\n```mermaid\ndiagram syntax\n```\n\nPRIORITY 3: MARKDOWN STRUCTURE\n\n3.1 Newline Semantics:\n- Use actual line breaks in output, not spelled-out escape sequences, unless the surrounding content explicitly needs that text\n- A single line break usually continues the current block\n- A blank line starts a new paragraph or block\n- Use blank lines for: new paragraphs, before headings, starting lists/tables\n- Use single line breaks for: continuation within blocks (list items, table cells)\n- Exception: inside code blocks, use actual newline characters freely for code lines\n\n3.2 Boundary Management:\nCheck PREFIX_ENDS_WITH_NEWLINE and SUFFIX_STARTS_WITH_NEWLINE:\n- If PREFIX lacks needed newline: start OUTPUT on a new line\n\n- If SUFFIX lacks needed newline: end OUTPUT with a trailing line break\n\n- Common cases requiring a leading line break:\n* Starting a list after \"Steps:\"\n* Creating new paragraph after text\n* Adding heading after paragraph\n- Common cases requiring a trailing line break:\n* Before new heading\n* End of section\n\n3.3 Context Stitching:\n- Never repeat text from SUFFIX beginning\n- Match PREFIX tone, style, indentation\n- Continue structures: lists, tables, quotes, headings\n\nPRIORITY 4: HIDDEN CONTEXT\n- OCR metadata like is hidden context\n- Never copy OCR tags to output\n- Use OCR content as semantic hint only"
}
diff --git a/backend/prompts/system_prompt_pro.json b/backend/prompts/system_prompt_pro.json
new file mode 100644
index 0000000..f66cbed
--- /dev/null
+++ b/backend/prompts/system_prompt_pro.json
@@ -0,0 +1,3 @@
+{
+ "template": "You are the [PRO] model for LLM-IN-TEXT, specializing in high-precision markdown insertion for a {language_id} editor.\n\nReturn only the insertion text that should be placed between PREFIX and SUFFIX.\n\nPRO CORE PRINCIPLE:\n- Output insertion text only. No explanations, no analysis, no labels, no wrapper quotes.\n- Never output chain-of-thought or internal reasoning.\n- Never output control markers like <|fim_prefix|>, <|fim_suffix|>, <|fim_middle|>, assistant, final, channel.\n\nPRO MODE INTENT:\n- This is PRO_MODE=true. You may produce longer, structured markdown when instruction requires it.\n- Prioritize instruction fidelity first, then boundary safety, then style continuity.\n- If instruction is vague, continue naturally with concrete and useful content.\n\nBOUNDARY AND CONTEXT RULES:\n- Respect CURSOR_IN_FENCED_CODE_BLOCK, CURSOR_FENCE_LANGUAGE, MERMAID_CONTEXT, PREFIX_ENDS_WITH_NEWLINE, and SUFFIX_STARTS_WITH_NEWLINE.\n- Never repeat text from the beginning of SUFFIX.\n- Use minimum necessary newlines to avoid boundary collision.\n- Match PREFIX tone, language, and formatting conventions.\n\nSYNTAX PRIORITY:\n- Code block contexts must keep valid syntax and indentation.\n- Math must use $...$ for inline and $$...$$ for blocks unless inside latex fences.\n- Mermaid contexts must output valid mermaid statements; do not duplicate fences when already inside one.\n\nHIDDEN CONTEXT SAFETY:\n- OCR metadata and document-side context are hidden hints only.\n- Never copy hidden tags (e.g., ) into output.\n\nQUALITY BAR FOR PRO:\n- Prefer specific, information-dense output over generic filler.\n- For structured requests, preserve headings/list hierarchy and produce coherent section flow.\n- Keep output directly insertable without post-edit cleanups."
+}
diff --git a/backend/tests/test_llm.py b/backend/tests/test_llm.py
index 5b35a60..bdc6cfa 100644
--- a/backend/tests/test_llm.py
+++ b/backend/tests/test_llm.py
@@ -128,9 +128,9 @@ def test_build_chat_stream_payload_with_thinking():
def test_call_ollama_non_streaming(monkeypatch):
captured = {}
- async def fake_post(url, json=None):
- captured["url"] = url
- captured["json"] = json
+ async def fake_post(*args, **kwargs):
+ captured["url"] = args[1] if len(args) > 1 else kwargs.get("url", "")
+ captured["json"] = kwargs.get("json")
class FakeResp:
def raise_for_status(self): pass
@@ -138,7 +138,7 @@ def test_call_ollama_non_streaming(monkeypatch):
return FakeResp()
- async def fake_client(*args, **kwargs):
+ def fake_client(*args, **kwargs):
class Ctx:
async def __aenter__(self2): return self2
async def __aexit__(*a): pass
@@ -168,7 +168,8 @@ def test_stream_ollama_text_deltas(monkeypatch):
])
class LineIterator:
- async def __anext__(self):
+ def __aiter__(self2): return self2
+ async def __anext__(self2):
try:
return next(lines_iter)
except StopIteration:
@@ -177,8 +178,8 @@ def test_stream_ollama_text_deltas(monkeypatch):
class Response:
def __init__(self2): self2._lines = LineIterator()
- async def raise_for_status(self2): pass
- async def aiter_lines(self2): return self2._lines
+ def raise_for_status(self2): pass
+ def aiter_lines(self2): return self2._lines
class StreamCtx:
async def __aenter__(self2): return Response()
@@ -186,10 +187,12 @@ def test_stream_ollama_text_deltas(monkeypatch):
class Client:
stream = lambda self2, *args, **kw: StreamCtx()
+ async def __aenter__(self2): return self2
+ async def __aexit__(*a): pass
return Client()
- async def fake_client(*args, **kwargs):
+ def fake_client(*args, **kwargs):
captured["called"] = True
return make_lines()
@@ -217,7 +220,8 @@ def test_stream_ollama_events_thinking_and_content(monkeypatch):
])
class LineIterator:
- async def __anext__(self):
+ def __aiter__(self2): return self2
+ async def __anext__(self2):
try:
return next(lines_iter)
except StopIteration:
@@ -226,8 +230,8 @@ def test_stream_ollama_events_thinking_and_content(monkeypatch):
class Response:
def __init__(self2): self2._lines = LineIterator()
- async def raise_for_status(self2): pass
- async def aiter_lines(self2): return self2._lines
+ def raise_for_status(self2): pass
+ def aiter_lines(self2): return self2._lines
class StreamCtx:
async def __aenter__(self2): return Response()
@@ -235,10 +239,12 @@ def test_stream_ollama_events_thinking_and_content(monkeypatch):
class Client:
stream = lambda self2, *args, **kw: StreamCtx()
+ async def __aenter__(self2): return self2
+ async def __aexit__(*a): pass
return Client()
- async def fake_client(*args, **kwargs):
+ def fake_client(*args, **kwargs):
captured["called"] = True
return make_lines()
@@ -260,9 +266,9 @@ def test_stream_ollama_events_thinking_and_content(monkeypatch):
def test_call_vlm_ocr(monkeypatch):
captured = {}
- async def fake_post(url, json=None):
- captured["url"] = url
- captured["json"] = json
+ async def fake_post(*args, **kwargs):
+ captured["url"] = args[1] if len(args) > 1 else kwargs.get("url", "")
+ captured["json"] = kwargs.get("json")
class FakeResp:
def raise_for_status(self): pass
@@ -270,7 +276,7 @@ def test_call_vlm_ocr(monkeypatch):
return FakeResp()
- async def fake_client(*args, **kwargs):
+ def fake_client(*args, **kwargs):
class Ctx:
async def __aenter__(self2): return self2
async def __aexit__(*a): pass
diff --git a/backend/tests/test_main_endpoints.py b/backend/tests/test_main_endpoints.py
index e925faf..fe63d75 100644
--- a/backend/tests/test_main_endpoints.py
+++ b/backend/tests/test_main_endpoints.py
@@ -138,7 +138,7 @@ def test_post_completions_privacy_mode(monkeypatch):
captured["kwargs"] = kwargs
return {"content": "done", "think": ""}
monkeypatch.setattr(main, "call_ollama", fake_call)
- monkeypatch.setattr(main, "build_completion_prompts", lambda *a, **k: ("sys", "user"))
+ monkeypatch.setattr(main, "build_completion_prompts", lambda *a, **k: ("sys", "user", ""))
monkeypatch.setattr(main, "prepare_prompt_context", lambda *a, **k: ("p", "s"))
client = TestClient(main.app)
diff --git a/backend/tests/test_pro_completions.py b/backend/tests/test_pro_completions.py
index 872feee..a5b8c85 100644
--- a/backend/tests/test_pro_completions.py
+++ b/backend/tests/test_pro_completions.py
@@ -56,19 +56,22 @@ def test_pro_status_missing_returns_404():
assert response.status_code == 404
-def test_pro_prompt_uses_simple_chat_instruction():
+def test_pro_prompt_uses_pro_specific_instruction():
system_prompt, user_prompt = pro_completions._build_pro_prompts(
prefix="欢迎使用 LLM-IN-TEXT\n\n即时可用的 LLM 系统",
suffix="",
language_id="markdown",
instruction="",
+ pro_thinking="high",
)
combined = f"{system_prompt}\n{user_prompt}".lower()
- assert "pro block" not in combined
- assert "replacement" not in combined
- assert "final answer" not in combined
- assert "markdown before cursor" in combined
- assert "markdown after cursor" in combined
+ assert "[pro] model for llm-in-text" in combined
+ assert "pro_mode: true" in combined
+ assert "pro_thinking_level: high" in combined
+ assert "long paragraphs or section-level output are allowed" in combined
+ assert "highest priority" in combined
+ assert "never copy tags to output" in combined
+ assert "write only the markdown that belongs at the cursor" not in combined
assert "continue the markdown naturally" in combined