feat(llm): honor *_BASE_URL for kimi/gemini/deepseek backends (#1458)
The kimi, gemini, and deepseek backends hardcoded their base_url, so users behind an OpenAI-compatible proxy/gateway or running a self-hosted relay had no way to redirect them (unlike ollama/openai, which already read *_BASE_URL). Each backend now reads KIMI_BASE_URL / GEMINI_BASE_URL / DEEPSEEK_BASE_URL and falls back to its official default when unset, so behavior is unchanged for anyone who doesn't set the variable. Ported from PR #1458 by @jc2shile onto current v8. The PR branch carried 624 unrelated files from a stale base; this lands just the clean 16-line llm.py change. Added subprocess-based tests covering both the override and the default for all three backends (BACKENDS reads the env at import time, so each case runs in a fresh interpreter). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
committed by
safishamsi
co-authored by
Claude Opus 4.8
parent
7278e24d8a
commit
68dba89a99
@@ -6,6 +6,7 @@ Full release notes with details on each version: [GitHub Releases](https://githu
|
||||
|
||||
- Fix: `to_canvas` (Obsidian Canvas export) now lays out each community's node cards in the same `ceil(sqrt(n))`-column grid the group box is sized for. The box width assumed a roughly-square `sqrt(n)`-column layout, but the placement loop hardcoded 3 columns, so any community larger than ~9 members rendered as a cramped 3-wide strip in an over-wide, mostly-empty box. The column count is now computed once per community and reused for the box width, box height, and card placement, so the cards fill the box. Cosmetic, no data change (#1452, thanks @TPAteeq).
|
||||
- Fix: `to_obsidian` / `to_canvas` / `to_wiki` no longer silently overwrite notes whose labels differ only by case (e.g. a class `References` and a prose heading `references`). The filename dedup was keyed on the exact-case name, so two such labels counted as non-colliding and the second write clobbered the first on case-insensitive filesystems (macOS/APFS, Windows/NTFS) — no suffix, no warning. Dedup now folds case (keyed on the lowercased name) while still emitting the original-case filename, so any pair that would collide on disk gets a numeric suffix. The obsidian/canvas dedup is shared in one helper so they can't drift, `wiki`'s slug dedup gets the matching fix, the `_COMMUNITY_*.md` overview notes (which had no dedup) are covered, and a generated `base_1` is itself re-checked so it can't overwrite a node literally labelled `base_1` (#1453, thanks @TPAteeq).
|
||||
- Feat: the `kimi`, `gemini`, and `deepseek` semantic-extraction backends now honor `KIMI_BASE_URL`, `GEMINI_BASE_URL`, and `DEEPSEEK_BASE_URL` to point at any OpenAI-compatible endpoint (a proxy, gateway, or self-hosted relay), matching the existing `OLLAMA_BASE_URL` / `OPENAI_BASE_URL` overrides. Each falls back to its hardcoded official default when the variable is unset, so behavior is unchanged for everyone who doesn't set it (#1458, thanks @jc2shile).
|
||||
|
||||
## 0.8.49 (2026-06-24)
|
||||
|
||||
|
||||
+11
-3
@@ -70,7 +70,9 @@ BACKENDS: dict[str, dict] = {
|
||||
"vision": True,
|
||||
},
|
||||
"kimi": {
|
||||
"base_url": "https://api.moonshot.ai/v1",
|
||||
# KIMI_BASE_URL points the backend at any OpenAI-compatible server for
|
||||
# Moonshot's Kimi models (LiteLLM, self-hosted proxy, ...).
|
||||
"base_url": os.environ.get("KIMI_BASE_URL", "https://api.moonshot.ai/v1"),
|
||||
"default_model": "kimi-k2.6",
|
||||
"env_key": "MOONSHOT_API_KEY",
|
||||
# kimi-k2.6 is natively multimodal (MoonViT) and accepts the same
|
||||
@@ -89,7 +91,10 @@ BACKENDS: dict[str, dict] = {
|
||||
"max_tokens": 16384,
|
||||
},
|
||||
"gemini": {
|
||||
"base_url": "https://generativelanguage.googleapis.com/v1beta/openai/",
|
||||
# GEMINI_BASE_URL points the backend at any OpenAI-compatible server for
|
||||
# Gemini models (LiteLLM, self-hosted proxy, ...). Falls back to Google's
|
||||
# official OpenAI-compatible endpoint.
|
||||
"base_url": os.environ.get("GEMINI_BASE_URL", "https://generativelanguage.googleapis.com/v1beta/openai/"),
|
||||
"default_model": "gemini-3-flash-preview",
|
||||
"env_keys": ["GEMINI_API_KEY", "GOOGLE_API_KEY"],
|
||||
"model_env_key": "GRAPHIFY_GEMINI_MODEL",
|
||||
@@ -118,7 +123,10 @@ BACKENDS: dict[str, dict] = {
|
||||
"vision": True,
|
||||
},
|
||||
"deepseek": {
|
||||
"base_url": "https://api.deepseek.com",
|
||||
# DEEPSEEK_BASE_URL points the backend at any OpenAI-compatible server for
|
||||
# DeepSeek models (LiteLLM, self-hosted proxy, ...). Falls back to DeepSeek's
|
||||
# official API endpoint.
|
||||
"base_url": os.environ.get("DEEPSEEK_BASE_URL", "https://api.deepseek.com"),
|
||||
"default_model": "deepseek-v4-flash",
|
||||
"env_key": "DEEPSEEK_API_KEY",
|
||||
"model_env_key": "GRAPHIFY_DEEPSEEK_MODEL",
|
||||
|
||||
@@ -874,3 +874,49 @@ def test_native_extraction_prompt_matches_skill_spec_on_hyperedges():
|
||||
shared = "3 or more nodes clearly participate together"
|
||||
assert shared in spec, "skill extraction-spec changed its hyperedge wording"
|
||||
assert shared in llm._EXTRACTION_SYSTEM, "native prompt drifted from the skill hyperedge wording"
|
||||
|
||||
|
||||
# --- *_BASE_URL env overrides for kimi / gemini / deepseek (#1458) -------------
|
||||
# BACKENDS reads the env at import time, so each case runs in a fresh interpreter
|
||||
# (subprocess) to avoid reload contamination of the test session.
|
||||
import subprocess
|
||||
import sys as _sys
|
||||
|
||||
|
||||
def _backend_base_url(backend: str, env_extra: dict) -> str:
|
||||
out = subprocess.run(
|
||||
[_sys.executable, "-c",
|
||||
f"import graphify.llm as l; print(l.BACKENDS[{backend!r}]['base_url'])"],
|
||||
env={**os.environ, **env_extra}, capture_output=True, text=True, check=True,
|
||||
)
|
||||
return out.stdout.strip()
|
||||
|
||||
|
||||
import os # noqa: E402
|
||||
|
||||
|
||||
@pytest.mark.parametrize("backend,env_var,override", [
|
||||
("kimi", "KIMI_BASE_URL", "https://proxy.example/kimi/v1"),
|
||||
("gemini", "GEMINI_BASE_URL", "https://proxy.example/gemini"),
|
||||
("deepseek", "DEEPSEEK_BASE_URL", "https://proxy.example/deepseek"),
|
||||
])
|
||||
def test_base_url_env_overrides(backend, env_var, override):
|
||||
assert _backend_base_url(backend, {env_var: override}) == override
|
||||
|
||||
|
||||
@pytest.mark.parametrize("backend,default", [
|
||||
("kimi", "https://api.moonshot.ai/v1"),
|
||||
("gemini", "https://generativelanguage.googleapis.com/v1beta/openai/"),
|
||||
("deepseek", "https://api.deepseek.com"),
|
||||
])
|
||||
def test_base_url_defaults_without_env(backend, default):
|
||||
# Ensure the override env vars are unset so the hardcoded default is used.
|
||||
cleared = {k: "" for k in ("KIMI_BASE_URL", "GEMINI_BASE_URL", "DEEPSEEK_BASE_URL")}
|
||||
# empty string would be falsy-but-set; delete instead by reconstructing env without them
|
||||
env = {k: v for k, v in os.environ.items() if k not in cleared}
|
||||
out = subprocess.run(
|
||||
[_sys.executable, "-c",
|
||||
f"import graphify.llm as l; print(l.BACKENDS[{backend!r}]['base_url'])"],
|
||||
env=env, capture_output=True, text=True, check=True,
|
||||
)
|
||||
assert out.stdout.strip() == default
|
||||
|
||||
Reference in New Issue
Block a user