feat: switch from OpenAI API to native Ollama Python client
This commit refactors the LLM integration to use Ollama's native Python client instead of OpenAI-compatible API, while fixing critical template syntax errors and improving project structure. Key changes: - Replace openai package with ollama package in backend requirements - Rewrite llm.py to use ollama.AsyncClient for direct Ollama API calls - Update main.py to use non-streaming Ollama responses with thinking extraction - Fix template syntax error in MilkdownEditor.vue (GhostTextOverlay component tags) - Fix string截取错误 by using slice() instead of substring() - Add src/utils/api.js and src/utils/config.js for shared configuration - Add CORS middleware to FastAPI backend - Update prompt.py with clearer instructions for continuation generation - Add comprehensive README.md documentation BREAKING CHANGE: Environment variables OLLAMA_BASE_URL changed to OLLAMA_HOST (remove /v1/ suffix)
This commit is contained in:
+32
-49
@@ -1,69 +1,52 @@
|
||||
import os
|
||||
from typing import AsyncGenerator
|
||||
from openai import AsyncOpenAI
|
||||
import json
|
||||
import time
|
||||
import ollama
|
||||
from typing import AsyncGenerator
|
||||
|
||||
api_key = os.getenv('OPENAI_API_KEY', 'ollama')
|
||||
base_url = os.getenv('OLLAMA_BASE_URL', 'http://192.168.0.120:11434/v1/')
|
||||
model = os.getenv('OLLAMA_MODEL', 'gpt-oss:120b')
|
||||
OLLAMA_MODEL = os.getenv('OLLAMA_MODEL', 'gpt-oss:120b')
|
||||
OLLAMA_HOST = os.getenv('OLLAMA_BASE_URL', 'http://192.168.0.120:11434')
|
||||
|
||||
print(f"[LLM] API key configured: {'Yes' if api_key else 'No'}")
|
||||
print(f"[LLM] Base URL: {base_url}")
|
||||
print(f"[LLM] Model: {model}")
|
||||
# 移除 /v1/ 后缀(如果有的话),因为 Ollama Python 包使用原生 API
|
||||
if OLLAMA_HOST.endswith('/v1/'):
|
||||
OLLAMA_HOST = OLLAMA_HOST[:-4]
|
||||
elif OLLAMA_HOST.endswith('/v1'):
|
||||
OLLAMA_HOST = OLLAMA_HOST[:-3]
|
||||
|
||||
client = AsyncOpenAI(api_key=api_key, base_url=base_url)
|
||||
os.environ['OLLAMA_HOST'] = OLLAMA_HOST
|
||||
|
||||
print(f"[LLM] Ollama host: {OLLAMA_HOST}")
|
||||
print(f"[LLM] Model: {OLLAMA_MODEL}")
|
||||
|
||||
client = ollama.AsyncClient(host=OLLAMA_HOST)
|
||||
|
||||
async def stream_openai(prompt: str) -> AsyncGenerator[str, None]:
|
||||
"""
|
||||
调用 OpenAI/Ollama API 并流式返回补全内容。
|
||||
参考 completions-sample-code 的 streaming 逻辑。
|
||||
"""
|
||||
start_time = time.time()
|
||||
print(f"[LLM] ========== API Call Start ==========")
|
||||
print(f"[LLM] Prompt length: {len(prompt)}")
|
||||
print(f"[LLM] Model: {model}")
|
||||
print(f"[LLM] Calling Ollama API with prompt length: {len(prompt)}")
|
||||
|
||||
try:
|
||||
print(f"[LLM] Creating streaming chat completion...")
|
||||
stream = await client.chat.completions.create(
|
||||
model=model,
|
||||
messages=[{"role": "user", "content": prompt}],
|
||||
print(f"[LLM] Awaiting client.chat...")
|
||||
stream = await client.chat(
|
||||
model=OLLAMA_MODEL,
|
||||
messages=[{'role': 'user', 'content': prompt}],
|
||||
stream=True,
|
||||
max_tokens=128,
|
||||
temperature=0.2,
|
||||
options={
|
||||
'num_predict': 8192,
|
||||
'temperature': 0.2,
|
||||
}
|
||||
)
|
||||
|
||||
print(f"[LLM] Stream created successfully, iterating...")
|
||||
print(f"[LLM] Got stream object, starting iteration...")
|
||||
|
||||
chunk_count = 0
|
||||
first_chunk_time = None
|
||||
|
||||
async for chunk in stream:
|
||||
current_time = time.time()
|
||||
if first_chunk_time is None:
|
||||
first_chunk_time = current_time - start_time
|
||||
|
||||
chunk_count += 1
|
||||
choice = chunk.choices[0] if chunk.choices else None
|
||||
|
||||
if choice and choice.delta.content:
|
||||
content = choice.delta.content
|
||||
print(f"[LLM] Chunk {chunk_count}: '{content}' (latency: {current_time - start_time:.3f}s)")
|
||||
if chunk['message'] and chunk['message']['content']:
|
||||
content = chunk['message']['content']
|
||||
chunk_count += 1
|
||||
print(f"[LLM] Chunk {chunk_count}: {content}")
|
||||
yield json.dumps({"content": content})
|
||||
elif chunk.choices and hasattr(chunk.choices[0], 'finish_reason'):
|
||||
finish_reason = chunk.choices[0].finish_reason
|
||||
print(f"[LLM] Chunk {chunk_count}: finish_reason={finish_reason}")
|
||||
if finish_reason:
|
||||
break
|
||||
else:
|
||||
print(f"[LLM] Chunk {chunk_count}: empty or no content")
|
||||
|
||||
total_time = time.time() - start_time
|
||||
print(f"[LLM] Stream complete - chunks: {chunk_count}, first chunk latency: {first_chunk_time:.3f}s, total time: {total_time:.3f}s")
|
||||
print(f"[LLM] ========== API Call End ==========")
|
||||
print(f"[LLM] Stream complete, total chunks: {chunk_count}")
|
||||
except Exception as e:
|
||||
error_msg = f"Error: {str(e)}"
|
||||
print(f"[LLM] Error: {error_msg}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
yield json.dumps({"error": str(e), "type": type(e).__name__})
|
||||
yield json.dumps({"error": str(e)})
|
||||
|
||||
Reference in New Issue
Block a user