You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@hugegraph.apache.org by ji...@apache.org on 2026/05/31 03:55:51 UTC
(hugegraph-ai) 06/06: fix(llm): validate request-scoped config
This is an automated email from the ASF dual-hosted git repository.
imbajin pushed a commit to branch goal-scan
in repository https://gitbox.apache.org/repos/asf/hugegraph-ai.git
commit 11e5b407b428e86b63edc57c44564771cd33ddff
Author: imbajin <ji...@apache.org>
AuthorDate: Sun May 31 11:54:47 2026 +0800
fix(llm): validate request-scoped config
- update graph settings only for explicit client_config fields
- reject unsupported provider types before global mutation
- add API contract tests for CS-011 and CS-012
- update code-scan ledgers
---
.workflow/code-scan/code-scan-state.json | 4 +-
.../code-scan/reports/final-code-scan-report.md | 4 +-
.workflow/code-scan/reports/issues.md | 12 ++--
.workflow/code-scan/reports/test-quality-ledger.md | 2 +
.../src/hugegraph_llm/api/models/rag_requests.py | 7 ++-
hugegraph-llm/src/hugegraph_llm/api/rag_api.py | 22 ++++---
hugegraph-llm/src/tests/api/test_rag_api.py | 68 +++++++++++++++++++++-
7 files changed, 100 insertions(+), 19 deletions(-)
diff --git a/.workflow/code-scan/code-scan-state.json b/.workflow/code-scan/code-scan-state.json
index 16845f25..704cd470 100644
--- a/.workflow/code-scan/code-scan-state.json
+++ b/.workflow/code-scan/code-scan-state.json
@@ -59,6 +59,8 @@
"CS-002",
"CS-004",
"CS-008",
+ "CS-011",
+ "CS-012",
"CS-013"
],
"files_touched": [
@@ -125,5 +127,5 @@
"uv run ruff check <edited-python-files>",
"git status --short"
],
- "next_recommended_action": "Fix global config mutation and provider validation: CS-011 and CS-012."
+ "next_recommended_action": "Replace fake integration tests with production-flow smoke coverage."
}
diff --git a/.workflow/code-scan/reports/final-code-scan-report.md b/.workflow/code-scan/reports/final-code-scan-report.md
index d6dc2eed..2ca80e8e 100644
--- a/.workflow/code-scan/reports/final-code-scan-report.md
+++ b/.workflow/code-scan/reports/final-code-scan-report.md
@@ -79,7 +79,7 @@ None.
1. Fix `CS-001` immediately: lock down `/logs` path handling and default admin credentials. Status: fixed after the scan.
2. Fix the client error-contract cluster: `CS-002`, `CS-004`, `CS-008`. Status: fixed after the scan.
3. Fix graph import correctness: `CS-013`, then add the missing round-trip tests. Status: fixed after the scan.
-4. Fix global config mutation and provider validation: `CS-011`, `CS-012`.
+4. Fix global config mutation and provider validation: `CS-011`, `CS-012`. Status: fixed after the scan.
5. Replace fake integration tests with production-flow smoke coverage before broader refactors.
## Fix Progress After Scan
@@ -89,3 +89,5 @@ None.
- `CS-004`: fixed by raising `ResponseParseError` for malformed successful responses.
- `CS-008`: fixed by preserving lower-level exceptions from Gremlin execution instead of wrapping every failure as `NotFoundError`.
- `CS-013`: fixed by preserving extracted property value types through the full extraction-to-commit path.
+- `CS-011`: fixed by updating global graph settings only for explicitly supplied `client_config` fields.
+- `CS-012`: fixed by rejecting unsupported provider types at request validation before global settings are mutated.
diff --git a/.workflow/code-scan/reports/issues.md b/.workflow/code-scan/reports/issues.md
index 67e56604..63dbc69f 100644
--- a/.workflow/code-scan/reports/issues.md
+++ b/.workflow/code-scan/reports/issues.md
@@ -81,7 +81,7 @@ Issue IDs use `CS-NNN`. Priorities are P0 highest through P5 lowest.
- Module: `hugegraph-python-client`
- Layer: client-api
- Paths: `hugegraph-python-client/src/pyhugegraph/api/graph.py:68`, `hugegraph-python-client/src/pyhugegraph/api/graph.py:73`, `hugegraph-python-client/src/pyhugegraph/api/graph.py:173`, `hugegraph-python-client/src/pyhugegraph/utils/huge_requests.py:144`
-- Status: open
+- Status: fixed
- Evidence: `getVertexByPage()`, `getVertexByCondition()`, and `getEdgeByPage()` concatenate labels and JSON properties directly into the URL.
- Impact: Property values containing `&`, `?`, `#`, `=`, or spaces can corrupt filters and query different data.
- Recommendation: Use `params=` or `urllib.parse.urlencode()` for all query parameters.
@@ -144,8 +144,8 @@ Issue IDs use `CS-NNN`. Priorities are P0 highest through P5 lowest.
- Status: open
- Evidence: `GraphConfigRequest` has concrete defaults and `set_graph_config()` writes every field to global `huge_settings` whenever `client_config` exists.
- Impact: A request intended to override one field can clear credentials, graph name, or graphspace for the whole process.
-- Recommendation: Make nested fields optional and update only explicitly provided values, or keep request config out of global settings.
-- Test note: Added a `FIXME:` in `test_rag_api.py`.
+- Fix: request-scoped graph config updates now use `model_fields_set`, so only explicitly provided nested fields mutate `huge_settings`.
+- Test note: Added `/rag` contract coverage proving partial `client_config` updates only the supplied graph fields.
### CS-012: Unsupported provider types are written into global settings before validation
@@ -153,11 +153,11 @@ Issue IDs use `CS-NNN`. Priorities are P0 highest through P5 lowest.
- Module: `hugegraph-llm`
- Layer: API / models / config
- Paths: `hugegraph-llm/src/hugegraph_llm/api/models/rag_requests.py:106`, `hugegraph-llm/src/hugegraph_llm/api/rag_api.py:163`, `hugegraph-llm/src/hugegraph_llm/api/rag_api.py:188`, `hugegraph-llm/src/hugegraph_llm/config/llm_config.py:29`
-- Status: open
+- Status: fixed
- Evidence: Request models use bare `str` for `llm_type` and `reranker_type`; handlers write values to global settings before branch validation.
- Impact: One bad request can poison process-global provider state for subsequent requests.
-- Recommendation: Validate provider types with `Literal`/Enum before mutating global config.
-- Test note: Added a `FIXME:` in `test_rag_api.py`.
+- Fix: LLM and reranker request models now validate provider types with `Literal`, causing unsupported providers to fail with `422` before handler-side global mutation.
+- Test note: Added config API coverage proving unsupported provider requests do not call apply callbacks or mutate global provider settings.
### CS-013: Property graph extraction stringifies values that importer rejects
diff --git a/.workflow/code-scan/reports/test-quality-ledger.md b/.workflow/code-scan/reports/test-quality-ledger.md
index a909f039..4f41704a 100644
--- a/.workflow/code-scan/reports/test-quality-ledger.md
+++ b/.workflow/code-scan/reports/test-quality-ledger.md
@@ -58,3 +58,5 @@ Added specific `FIXME:` comments in these files:
- `CS-004`: response-validation tests now prove malformed successful JSON raises `ResponseParseError`.
- `CS-008`: Gremlin tests now prove lower-level auth exceptions keep their original type through `GremlinManager.exec()`.
- `CS-013`: extraction-to-commit round-trip tests now prove schema-typed numeric values survive `PropertyGraphExtract.run()` and are accepted by `Commit2Graph`.
+- `CS-011`: `/rag` contract tests now prove partial `client_config` only updates explicitly supplied graph settings.
+- `CS-012`: config API tests now prove unsupported LLM/reranker providers fail validation before mutating global settings.
diff --git a/hugegraph-llm/src/hugegraph_llm/api/models/rag_requests.py b/hugegraph-llm/src/hugegraph_llm/api/models/rag_requests.py
index 433438ee..77155978 100644
--- a/hugegraph-llm/src/hugegraph_llm/api/models/rag_requests.py
+++ b/hugegraph-llm/src/hugegraph_llm/api/models/rag_requests.py
@@ -23,6 +23,9 @@ from pydantic import BaseModel, field_validator
from hugegraph_llm.config import prompt
+LLMProviderType = Literal["openai", "ollama/local", "litellm"]
+RerankerProviderType = Literal["cohere", "siliconflow"]
+
class GraphConfigRequest(BaseModel):
url: str = Query("127.0.0.1:8080", description="hugegraph client url.")
@@ -104,7 +107,7 @@ class GraphRAGRequest(BaseModel):
class LLMConfigRequest(BaseModel):
- llm_type: str
+ llm_type: LLMProviderType
# The common parameters shared by OpenAI, LiteLLM,
# and OLLAMA platforms.
api_key: str
@@ -120,7 +123,7 @@ class LLMConfigRequest(BaseModel):
class RerankerConfigRequest(BaseModel):
reranker_model: str
- reranker_type: str
+ reranker_type: RerankerProviderType
api_key: str
cohere_base_url: Optional[str] = None
diff --git a/hugegraph-llm/src/hugegraph_llm/api/rag_api.py b/hugegraph-llm/src/hugegraph_llm/api/rag_api.py
index bce7de0d..9b9c7d35 100644
--- a/hugegraph-llm/src/hugegraph_llm/api/rag_api.py
+++ b/hugegraph-llm/src/hugegraph_llm/api/rag_api.py
@@ -90,12 +90,18 @@ def rag_http_api(
}
def set_graph_config(req):
- if req.client_config:
- huge_settings.graph_url = req.client_config.url
- huge_settings.graph_name = req.client_config.graph
- huge_settings.graph_user = req.client_config.user
- huge_settings.graph_pwd = req.client_config.pwd
- huge_settings.graph_space = req.client_config.gs
+ if req.client_config is None:
+ return
+ field_map = {
+ "url": "graph_url",
+ "graph": "graph_name",
+ "user": "graph_user",
+ "pwd": "graph_pwd",
+ "gs": "graph_space",
+ }
+ for request_field, settings_field in field_map.items():
+ if request_field in req.client_config.model_fields_set:
+ setattr(huge_settings, settings_field, getattr(req.client_config, request_field))
@router.post("/rag/graph", status_code=status.HTTP_200_OK)
def graph_rag_recall_api(req: GraphRAGRequest):
@@ -163,7 +169,7 @@ def rag_http_api(
def llm_config_api(req: LLMConfigRequest):
llm_settings.chat_llm_type = req.llm_type
- if req.llm_type == "openai":
+ if req.llm_type in ("openai", "litellm"):
res = apply_llm_conf(
req.api_key,
req.api_base,
@@ -179,7 +185,7 @@ def rag_http_api(
def embedding_config_api(req: LLMConfigRequest):
llm_settings.embedding_type = req.llm_type
- if req.llm_type == "openai":
+ if req.llm_type in ("openai", "litellm"):
res = apply_embedding_conf(req.api_key, req.api_base, req.language_model, origin_call="http")
else:
res = apply_embedding_conf(req.host, req.port, req.language_model, origin_call="http")
diff --git a/hugegraph-llm/src/tests/api/test_rag_api.py b/hugegraph-llm/src/tests/api/test_rag_api.py
index c06a52ff..24e1a219 100644
--- a/hugegraph-llm/src/tests/api/test_rag_api.py
+++ b/hugegraph-llm/src/tests/api/test_rag_api.py
@@ -22,13 +22,14 @@ from fastapi import APIRouter, FastAPI, status
from fastapi.testclient import TestClient
from hugegraph_llm.api.rag_api import rag_http_api
+from hugegraph_llm.config import huge_settings, llm_settings
pytestmark = pytest.mark.contract
# FIXME: add negative-path tests for each /config/* endpoint that assert
# provider/auth/connection failures surface distinct error details.
# FIXME: add happy-path HTTP contract tests for /rag and /rag/graph response
-# shaping, output-flag filtering, and request-scoped graph config behavior.
+# shaping and output-flag filtering.
# FIXME: distinguish graph/provider dependency failures from programmer errors
# instead of asserting one generic unexpected-error response for all failures.
@@ -168,6 +169,71 @@ def test_rag_api_invalid_request_body_returns_validation_shape():
assert response.json()["detail"][0]["loc"][-1] == "query"
+def test_rag_client_config_updates_only_explicit_graph_fields(monkeypatch):
+ monkeypatch.setattr(huge_settings, "graph_url", "http://original:8080")
+ monkeypatch.setattr(huge_settings, "graph_name", "original_graph")
+ monkeypatch.setattr(huge_settings, "graph_user", "original_user")
+ monkeypatch.setattr(huge_settings, "graph_pwd", "original_pwd")
+ monkeypatch.setattr(huge_settings, "graph_space", "original_space")
+ client, callbacks = _make_test_client()
+
+ response = client.post(
+ "/rag",
+ json={
+ "query": "find vertices",
+ "client_config": {
+ "url": "http://override:8080",
+ },
+ },
+ )
+
+ assert response.status_code == status.HTTP_200_OK
+ callbacks["rag_answer_func"].assert_called_once()
+ assert huge_settings.graph_url == "http://override:8080"
+ assert huge_settings.graph_name == "original_graph"
+ assert huge_settings.graph_user == "original_user"
+ assert huge_settings.graph_pwd == "original_pwd"
+ assert huge_settings.graph_space == "original_space"
+
+
+def test_llm_config_rejects_unsupported_provider_without_mutating(monkeypatch):
+ monkeypatch.setattr(llm_settings, "chat_llm_type", "openai")
+ client, callbacks = _make_test_client()
+
+ response = client.post(
+ "/config/llm",
+ json={
+ "llm_type": "unsupported",
+ "api_key": "sk-test",
+ "api_base": "https://api.example.test",
+ "language_model": "gpt-test",
+ "max_tokens": "1024",
+ },
+ )
+
+ assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
+ callbacks["apply_llm_conf"].assert_not_called()
+ assert llm_settings.chat_llm_type == "openai"
+
+
+def test_rerank_config_rejects_unsupported_provider_without_mutating(monkeypatch):
+ monkeypatch.setattr(llm_settings, "reranker_type", "cohere")
+ client, callbacks = _make_test_client()
+
+ response = client.post(
+ "/config/rerank",
+ json={
+ "reranker_type": "unsupported",
+ "api_key": "rerank-key",
+ "reranker_model": "rerank-test",
+ },
+ )
+
+ assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
+ callbacks["apply_reranker_conf"].assert_not_called()
+ assert llm_settings.reranker_type == "cohere"
+
+
def test_text2gremlin_callback_exception_returns_stable_response():
client, _ = _make_test_client(gremlin_generate_selective_func=Mock(side_effect=RuntimeError("callback failed")))