From 818f8a2d15dad3ec97626bd8f9d985fd06d88f2b Mon Sep 17 00:00:00 2001 From: watsonk1998 <1515673657@qq.com> Date: Mon, 13 Apr 2026 22:26:11 +0800 Subject: [PATCH] feat(tools): migrate to litellm for multi-provider support (OpenAI, Gemini, Claude) --- requirements.txt | 2 +- tools/build_graph.py | 35 +++++++++++++++++++++------------- tools/ingest.py | 30 +++++++++++++++++++---------- tools/lint.py | 33 +++++++++++++++++++------------- tools/query.py | 45 ++++++++++++++++++++++---------------------- 5 files changed, 86 insertions(+), 59 deletions(-) diff --git a/requirements.txt b/requirements.txt index 300a854..a9c7b7b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ -anthropic>=0.40.0 +litellm>=1.0.0 networkx>=3.2 diff --git a/tools/build_graph.py b/tools/build_graph.py index aa9c55f..d4613eb 100644 --- a/tools/build_graph.py +++ b/tools/build_graph.py @@ -25,7 +25,7 @@ import webbrowser from pathlib import Path from datetime import date -import anthropic +import os try: 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 "" +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: 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]: - """Pass 2: Claude-inferred semantic relationships.""" - client = anthropic.Anthropic() + """Pass 2: API-inferred semantic relationships.""" new_edges = [] # 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 src = page_id(p) - response = client.messages.create( - 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. + prompt = f"""Analyze this wiki page and identify implicit semantic relationships to other pages in the wiki. Source page: {src} Content: @@ -200,10 +211,8 @@ Rules: - Do not repeat edges already in the extracted list - Return empty array [] if no new relationships found """ - }] - ) - - raw = response.content[0].text.strip() + raw = call_llm(prompt, "LLM_MODEL_FAST", "claude-3-5-haiku-latest", max_tokens=1024) + raw = raw.strip() raw = re.sub(r"^```(?:json)?\s*", "", raw) raw = re.sub(r"\s*```$", "", raw) diff --git a/tools/ingest.py b/tools/ingest.py index a0635b9..4ea20fc 100644 --- a/tools/ingest.py +++ b/tools/ingest.py @@ -23,7 +23,7 @@ import re from pathlib import Path from datetime import date -import anthropic +import os REPO_ROOT = Path(__file__).parent.parent 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 "" +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): path.parent.mkdir(parents=True, exist_ok=True) path.write_text(content, encoding="utf-8") @@ -105,7 +121,7 @@ def ingest(source_path: str): wiki_context = build_wiki_context() 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. @@ -140,14 +156,8 @@ Return ONLY a valid JSON object with these fields (no markdown fences, no prose }} """ - print(" calling Claude API...") - response = client.messages.create( - model="claude-sonnet-4-6", - max_tokens=8192, - messages=[{"role": "user", "content": prompt}], - ) - - raw = response.content[0].text + print(f" calling API (model: ...)") + raw = call_llm(prompt, max_tokens=8192) try: data = parse_json_from_response(raw) except (ValueError, json.JSONDecodeError) as e: diff --git a/tools/lint.py b/tools/lint.py index c0ec48e..c7997ee 100644 --- a/tools/lint.py +++ b/tools/lint.py @@ -21,7 +21,7 @@ from pathlib import Path from collections import defaultdict from datetime import date -import anthropic +import os REPO_ROOT = Path(__file__).parent.parent 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 "" +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]: return [p for p in WIKI_DIR.rglob("*.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) pages_context += f"\n\n### {rel}\n{read_file(p)[:1500]}" # truncate long pages - client = anthropic.Anthropic() - print(" running semantic lint via Claude API...") - 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: + print(" running semantic lint via API...") + prompt = f"""You are linting an LLM Wiki. Review the pages below and identify: 1. Contradictions between pages (claims that conflict) 2. Stale content (summaries that newer sources have superseded) 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. """ - }] - ) - - semantic_report = response.content[0].text + semantic_report = call_llm(prompt, "LLM_MODEL", "claude-3-5-sonnet-latest", max_tokens=3000) # Compose full report report_lines = [ diff --git a/tools/query.py b/tools/query.py index bc44419..0eaccbc 100644 --- a/tools/query.py +++ b/tools/query.py @@ -19,7 +19,7 @@ import argparse from pathlib import Path from datetime import date -import anthropic +import os REPO_ROOT = Path(__file__).parent.parent WIKI_DIR = REPO_ROOT / "wiki" @@ -38,6 +38,22 @@ def write_file(path: Path, content: str): 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]: """Extract linked pages from index that seem relevant to the question.""" # 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): today = date.today().isoformat() - client = anthropic.Anthropic() # Step 1: Read index 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 not relevant_pages or len(relevant_pages) <= 1: - print(" selecting relevant pages via Claude...") - selection_response = client.messages.create( - model="claude-haiku-4-5-20251001", - max_tokens=512, - 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() + print(" selecting relevant pages via API...") + 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." + raw = call_llm(prompt, "LLM_MODEL_FAST", "claude-3-5-haiku-latest", max_tokens=512) + raw = raw.strip() raw = re.sub(r"^```(?:json)?\s*", "", raw) raw = re.sub(r"\s*```$", "", raw) try: @@ -108,12 +117,7 @@ def query(question: str, save_path: str | None = None): # Step 4: Synthesize answer print(f" synthesizing answer from {len(relevant_pages)} pages...") - response = client.messages.create( - 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. + 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. 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. """ - }] - ) - - answer = response.content[0].text + answer = call_llm(prompt, "LLM_MODEL", "claude-3-5-sonnet-latest", max_tokens=4096) print("\n" + "=" * 60) print(answer) print("=" * 60)