feat(tools): migrate to litellm for multi-provider support (OpenAI, Gemini, Claude)

This commit is contained in:
watsonk1998
2026-04-13 22:26:11 +08:00
parent d8ac6107bf
commit 818f8a2d15
5 changed files with 86 additions and 59 deletions

View File

@@ -1,2 +1,2 @@
anthropic>=0.40.0 litellm>=1.0.0
networkx>=3.2 networkx>=3.2

View File

@@ -25,7 +25,7 @@ import webbrowser
from pathlib import Path from pathlib import Path
from datetime import date from datetime import date
import anthropic import os
try: try:
import networkx as nx import networkx as nx
@@ -64,6 +64,23 @@ def read_file(path: Path) -> str:
return path.read_text(encoding="utf-8") if path.exists() else "" return path.read_text(encoding="utf-8") if path.exists() else ""
def call_llm(prompt: str, model_env: str, default_model: str, max_tokens: int = 4096) -> str:
try:
from litellm import completion
except ImportError:
print("Error: litellm not installed. Run: pip install litellm")
import sys
sys.exit(1)
model = os.getenv(model_env, default_model)
response = completion(
model=model,
messages=[{"role": "user", "content": prompt}],
max_tokens=max_tokens
)
return response.choices[0].message.content
def sha256(text: str) -> str: def sha256(text: str) -> str:
return hashlib.sha256(text.encode()).hexdigest() return hashlib.sha256(text.encode()).hexdigest()
@@ -143,8 +160,7 @@ def build_extracted_edges(pages: list[Path]) -> list[dict]:
def build_inferred_edges(pages: list[Path], existing_edges: list[dict], cache: dict) -> list[dict]: def build_inferred_edges(pages: list[Path], existing_edges: list[dict], cache: dict) -> list[dict]:
"""Pass 2: Claude-inferred semantic relationships.""" """Pass 2: API-inferred semantic relationships."""
client = anthropic.Anthropic()
new_edges = [] new_edges = []
# Only process pages that changed since last run # Only process pages that changed since last run
@@ -172,12 +188,7 @@ def build_inferred_edges(pages: list[Path], existing_edges: list[dict], cache: d
content = read_file(p)[:2000] # truncate for context efficiency content = read_file(p)[:2000] # truncate for context efficiency
src = page_id(p) src = page_id(p)
response = client.messages.create( prompt = f"""Analyze this wiki page and identify implicit semantic relationships to other pages in the wiki.
model="claude-haiku-4-5-20251001",
max_tokens=1024,
messages=[{
"role": "user",
"content": f"""Analyze this wiki page and identify implicit semantic relationships to other pages in the wiki.
Source page: {src} Source page: {src}
Content: Content:
@@ -200,10 +211,8 @@ Rules:
- Do not repeat edges already in the extracted list - Do not repeat edges already in the extracted list
- Return empty array [] if no new relationships found - Return empty array [] if no new relationships found
""" """
}] raw = call_llm(prompt, "LLM_MODEL_FAST", "claude-3-5-haiku-latest", max_tokens=1024)
) raw = raw.strip()
raw = response.content[0].text.strip()
raw = re.sub(r"^```(?:json)?\s*", "", raw) raw = re.sub(r"^```(?:json)?\s*", "", raw)
raw = re.sub(r"\s*```$", "", raw) raw = re.sub(r"\s*```$", "", raw)

View File

@@ -23,7 +23,7 @@ import re
from pathlib import Path from pathlib import Path
from datetime import date from datetime import date
import anthropic import os
REPO_ROOT = Path(__file__).parent.parent REPO_ROOT = Path(__file__).parent.parent
WIKI_DIR = REPO_ROOT / "wiki" WIKI_DIR = REPO_ROOT / "wiki"
@@ -41,6 +41,22 @@ def read_file(path: Path) -> str:
return path.read_text(encoding="utf-8") if path.exists() else "" return path.read_text(encoding="utf-8") if path.exists() else ""
def call_llm(prompt: str, max_tokens: int = 8192) -> str:
try:
from litellm import completion
except ImportError:
print("Error: litellm not installed. Run: pip install litellm")
sys.exit(1)
model = os.getenv("LLM_MODEL", "claude-3-5-sonnet-latest")
response = completion(
model=model,
messages=[{"role": "user", "content": prompt}],
max_tokens=max_tokens
)
return response.choices[0].message.content
def write_file(path: Path, content: str): def write_file(path: Path, content: str):
path.parent.mkdir(parents=True, exist_ok=True) path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(content, encoding="utf-8") path.write_text(content, encoding="utf-8")
@@ -105,7 +121,7 @@ def ingest(source_path: str):
wiki_context = build_wiki_context() wiki_context = build_wiki_context()
schema = read_file(SCHEMA_FILE) schema = read_file(SCHEMA_FILE)
client = anthropic.Anthropic() schema = read_file(SCHEMA_FILE)
prompt = f"""You are maintaining an LLM Wiki. Process this source document and integrate its knowledge into the wiki. prompt = f"""You are maintaining an LLM Wiki. Process this source document and integrate its knowledge into the wiki.
@@ -140,14 +156,8 @@ Return ONLY a valid JSON object with these fields (no markdown fences, no prose
}} }}
""" """
print(" calling Claude API...") print(f" calling API (model: ...)")
response = client.messages.create( raw = call_llm(prompt, max_tokens=8192)
model="claude-sonnet-4-6",
max_tokens=8192,
messages=[{"role": "user", "content": prompt}],
)
raw = response.content[0].text
try: try:
data = parse_json_from_response(raw) data = parse_json_from_response(raw)
except (ValueError, json.JSONDecodeError) as e: except (ValueError, json.JSONDecodeError) as e:

View File

@@ -21,7 +21,7 @@ from pathlib import Path
from collections import defaultdict from collections import defaultdict
from datetime import date from datetime import date
import anthropic import os
REPO_ROOT = Path(__file__).parent.parent REPO_ROOT = Path(__file__).parent.parent
WIKI_DIR = REPO_ROOT / "wiki" WIKI_DIR = REPO_ROOT / "wiki"
@@ -33,6 +33,22 @@ def read_file(path: Path) -> str:
return path.read_text(encoding="utf-8") if path.exists() else "" return path.read_text(encoding="utf-8") if path.exists() else ""
def call_llm(prompt: str, model_env: str, default_model: str, max_tokens: int = 4096) -> str:
try:
from litellm import completion
except ImportError:
print("Error: litellm not installed. Run: pip install litellm")
sys.exit(1)
model = os.getenv(model_env, default_model)
response = completion(
model=model,
messages=[{"role": "user", "content": prompt}],
max_tokens=max_tokens
)
return response.choices[0].message.content
def all_wiki_pages() -> list[Path]: def all_wiki_pages() -> list[Path]:
return [p for p in WIKI_DIR.rglob("*.md") return [p for p in WIKI_DIR.rglob("*.md")
if p.name not in ("index.md", "log.md", "lint-report.md")] if p.name not in ("index.md", "log.md", "lint-report.md")]
@@ -112,14 +128,8 @@ def run_lint():
rel = p.relative_to(REPO_ROOT) rel = p.relative_to(REPO_ROOT)
pages_context += f"\n\n### {rel}\n{read_file(p)[:1500]}" # truncate long pages pages_context += f"\n\n### {rel}\n{read_file(p)[:1500]}" # truncate long pages
client = anthropic.Anthropic() print(" running semantic lint via API...")
print(" running semantic lint via Claude API...") prompt = f"""You are linting an LLM Wiki. Review the pages below and identify:
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=3000,
messages=[{
"role": "user",
"content": f"""You are linting an LLM Wiki. Review the pages below and identify:
1. Contradictions between pages (claims that conflict) 1. Contradictions between pages (claims that conflict)
2. Stale content (summaries that newer sources have superseded) 2. Stale content (summaries that newer sources have superseded)
3. Data gaps (important questions the wiki can't answer — suggest specific sources to find) 3. Data gaps (important questions the wiki can't answer — suggest specific sources to find)
@@ -136,10 +146,7 @@ Return a markdown lint report with these sections:
Be specific — name the exact pages and claims involved. Be specific — name the exact pages and claims involved.
""" """
}] semantic_report = call_llm(prompt, "LLM_MODEL", "claude-3-5-sonnet-latest", max_tokens=3000)
)
semantic_report = response.content[0].text
# Compose full report # Compose full report
report_lines = [ report_lines = [

View File

@@ -19,7 +19,7 @@ import argparse
from pathlib import Path from pathlib import Path
from datetime import date from datetime import date
import anthropic import os
REPO_ROOT = Path(__file__).parent.parent REPO_ROOT = Path(__file__).parent.parent
WIKI_DIR = REPO_ROOT / "wiki" WIKI_DIR = REPO_ROOT / "wiki"
@@ -38,6 +38,22 @@ def write_file(path: Path, content: str):
print(f" saved: {path.relative_to(REPO_ROOT)}") print(f" saved: {path.relative_to(REPO_ROOT)}")
def call_llm(prompt: str, model_env: str, default_model: str, max_tokens: int = 4096) -> str:
try:
from litellm import completion
except ImportError:
print("Error: litellm not installed. Run: pip install litellm")
sys.exit(1)
model = os.getenv(model_env, default_model)
response = completion(
model=model,
messages=[{"role": "user", "content": prompt}],
max_tokens=max_tokens
)
return response.choices[0].message.content
def find_relevant_pages(question: str, index_content: str) -> list[Path]: def find_relevant_pages(question: str, index_content: str) -> list[Path]:
"""Extract linked pages from index that seem relevant to the question.""" """Extract linked pages from index that seem relevant to the question."""
# Pull all [[links]] and markdown links from index # Pull all [[links]] and markdown links from index
@@ -64,7 +80,6 @@ def append_log(entry: str):
def query(question: str, save_path: str | None = None): def query(question: str, save_path: str | None = None):
today = date.today().isoformat() today = date.today().isoformat()
client = anthropic.Anthropic()
# Step 1: Read index # Step 1: Read index
index_content = read_file(INDEX_FILE) index_content = read_file(INDEX_FILE)
@@ -77,16 +92,10 @@ def query(question: str, save_path: str | None = None):
# If no keyword match, ask Claude to identify relevant pages from the index # If no keyword match, ask Claude to identify relevant pages from the index
if not relevant_pages or len(relevant_pages) <= 1: if not relevant_pages or len(relevant_pages) <= 1:
print(" selecting relevant pages via Claude...") print(" selecting relevant pages via API...")
selection_response = client.messages.create( prompt = f"Given this wiki index:\n\n{index_content}\n\nWhich pages are most relevant to answering: \"{question}\"\n\nReturn ONLY a JSON array of relative file paths (as listed in the index), e.g. [\"sources/foo.md\", \"concepts/Bar.md\"]. Maximum 10 pages."
model="claude-haiku-4-5-20251001", raw = call_llm(prompt, "LLM_MODEL_FAST", "claude-3-5-haiku-latest", max_tokens=512)
max_tokens=512, raw = raw.strip()
messages=[{
"role": "user",
"content": f"Given this wiki index:\n\n{index_content}\n\nWhich pages are most relevant to answering: \"{question}\"\n\nReturn ONLY a JSON array of relative file paths (as listed in the index), e.g. [\"sources/foo.md\", \"concepts/Bar.md\"]. Maximum 10 pages."
}]
)
raw = selection_response.content[0].text.strip()
raw = re.sub(r"^```(?:json)?\s*", "", raw) raw = re.sub(r"^```(?:json)?\s*", "", raw)
raw = re.sub(r"\s*```$", "", raw) raw = re.sub(r"\s*```$", "", raw)
try: try:
@@ -108,12 +117,7 @@ def query(question: str, save_path: str | None = None):
# Step 4: Synthesize answer # Step 4: Synthesize answer
print(f" synthesizing answer from {len(relevant_pages)} pages...") print(f" synthesizing answer from {len(relevant_pages)} pages...")
response = client.messages.create( prompt = f"""You are querying an LLM Wiki to answer a question. Use the wiki pages below to synthesize a thorough answer. Cite sources using [[PageName]] wikilink syntax.
model="claude-sonnet-4-6",
max_tokens=4096,
messages=[{
"role": "user",
"content": f"""You are querying an LLM Wiki to answer a question. Use the wiki pages below to synthesize a thorough answer. Cite sources using [[PageName]] wikilink syntax.
Schema: Schema:
{schema} {schema}
@@ -125,10 +129,7 @@ Question: {question}
Write a well-structured markdown answer with headers, bullets, and [[wikilink]] citations. At the end, add a ## Sources section listing the pages you drew from. Write a well-structured markdown answer with headers, bullets, and [[wikilink]] citations. At the end, add a ## Sources section listing the pages you drew from.
""" """
}] answer = call_llm(prompt, "LLM_MODEL", "claude-3-5-sonnet-latest", max_tokens=4096)
)
answer = response.content[0].text
print("\n" + "=" * 60) print("\n" + "=" * 60)
print(answer) print(answer)
print("=" * 60) print("=" * 60)