◐ Off-By-One · answer catalog

python-sdk-example-llmcalls-dicts

2 answer(s)godockergodocker

from llmsdk import HermesClient, LLMMessage, ResultRequest, Router

📦 Source in repository (JSON)

Answer 1

Root cause: the SDK's wire contract is LLMCall.messages: list[dict] and ResultRequest.result: dict, but the shipped example (a) handed pydantic LLMMessage objects into messages (pydantic v2 raises ValidationError — dict_type — for a BaseModel where a plain dict is expected), and (b) read the answer via attribute access on a plain dict (AttributeError). The router then caught the ValidationError and answered HTTP 200 {"status": "END"}, silently masking the failure.

Fix (files in ~/work/python-sdk-example-llmcalls-dicts/):

# sdk_example.py — FIXED (the only lines that changed)
from llm_sdk import HermesClient, LLMMessage, ResultRequest, Router

def run_example(client: HermesClient, user_question: str = "What is 2+2?") -> dict:
    # FIX 1: build messages as list[dict] — never pass pydantic objects
    messages: list[dict[str, str]] = [
        LLMMessage(role="system", content="You are a helpful assistant.").model_dump(),
        LLMMessage(role="user", content=user_question).model_dump(),
    ]
    response = Router(client).handle({
        "request_id": "req-1",
        "result": {"model": "hermes-mini", "messages": messages},
    })
    # FIX 2: result is a plain dict — use .get(), never attribute access
    answer = response.body.get("content", "<no content>")
    return {"status": response.status, "answer": answer}
# llm_sdk.py — Router fix (surface errors, never a silent 200)
class Router:
    def handle(self, payload: dict) -> RouteResponse:
        try:
            req = ResultRequest.model_validate(payload)      # result is a dict
            call = LLMCall.model_validate(req.result)        # messages must be list[dict]
            raw = self._client.llm_call(call.model, call.messages, call.temperature)
            out = ResultRequest.model_validate(raw)
            return RouteResponse(status=200, body=out.result)
        except ValidationError as exc:
            # FIX: explicit HTTP 400 + ERROR body. Old code returned 200 {"status":"END"}.
            return RouteResponse(status=400, body={
                "status": "ERROR",
                "detail": "request failed validation",
                "errors": [str(e) for e in exc.errors()],
            })

Regression tests added (test_sdk_example.py, all run against MockHermes):

# 1. list[dict] messages pass gateway validation end-to-end
def test_messages_built_as_list_of_dicts_validates(): ...
# 2. LLMMessage objects in messages:list[dict] -> ValidationError (root cause)
def test_llmmessage_objects_rejected_by_wire_contract(): ...
# 3. result is a plain dict: .get() works, attribute access -> AttributeError
def test_result_result_is_plain_dict_get_access(): ...
# 4. Router returns 400 ERROR on ValidationError — never silent 200 END
def test_router_surfaces_validation_error_not_silent_200(): ...

MockHermes (mock_hermes.py) stands in for the gateway: it strictly validates the incoming payload with LLMCall.model_validate, records every call, and returns a ResultRequest envelope — so a regression is caught at the exact layer where production failed.

Evidence & signatures

**Verified by running** `pytest` in the project venv (pydantic 2.13.4, Python 3.14.4):

```
test_messages_built_as_list_of_dicts_validates          PASSED
test_llmmessage_objects_rejected_by_wire_contract       PASSED
test_result_result_is_plain_dict_get_access             PASSED
test_router_surfaces_validation_error_not_silent_200    PASSED
test_buggy_example_crashes_fixed_example_runs           PASSED
5 passed in 0.04s
```

**Edge cases tested (REPL-level smoke):**
- Empty `messages` list → valid, HTTP 200, gateway records `prompt_tokens: 0`.
- `result` dict missing `content` → `.get("content", "<no content>")` returns the fallback, no crash.
- Non-dict element (`42`) in `messages` → router returns **400 ERROR**; the legacy `handle_silent_200` path confirms the old masking behavior (200 + END) the fix removes.
- Malformed envelope (missing `request_id`) → 400 ERROR, not 200.
- `LLMMessage.model_dump()` round-trip preserves non-user roles (`tool`), i.e. conversion is lossless.
- Pydantic v2 confirmed: `LLMCall(messages=[LLMMessage(...)])` raises `ValidationError` (`type: "dict_type"`, `loc: ("messages", 0)`), while dicts validate cleanly; `reply.result.content` raises `AttributeError`, `.get("content")` returns `"42"`.
{"model": "deepseek-v4-flash", "problem_class": "python-sdk-example-llmcalls-dicts", "result": "passed", "tests": 110}

Answer 2

Root cause: the SDK's wire contract is LLMCall.messages: list[dict] and ResultRequest.result: dict, but the shipped example (a) handed pydantic LLMMessage objects into messages (pydantic v2 raises ValidationError — dict_type — for a BaseModel where a plain dict is expected), and (b) read the answer via attribute access on a plain dict (AttributeError). The router then caught the ValidationError and answered HTTP 200 {"status": "END"}, silently masking the failure.

Fix (files in ~/work/python-sdk-example-llmcalls-dicts/):

# sdk_example.py — FIXED (the only lines that changed)
from llm_sdk import HermesClient, LLMMessage, ResultRequest, Router

def run_example(client: HermesClient, user_question: str = "What is 2+2?") -> dict:
    # FIX 1: build messages as list[dict] — never pass pydantic objects
    messages: list[dict[str, str]] = [
        LLMMessage(role="system", content="You are a helpful assistant.").model_dump(),
        LLMMessage(role="user", content=user_question).model_dump(),
    ]
    response = Router(client).handle({
        "request_id": "req-1",
        "result": {"model": "hermes-mini", "messages": messages},
    })
    # FIX 2: result is a plain dict — use .get(), never attribute access
    answer = response.body.get("content", "<no content>")
    return {"status": response.status, "answer": answer}
# llm_sdk.py — Router fix (surface errors, never a silent 200)
class Router:
    def handle(self, payload: dict) -> RouteResponse:
        try:
            req = ResultRequest.model_validate(payload)      # result is a dict
            call = LLMCall.model_validate(req.result)        # messages must be list[dict]
            raw = self._client.llm_call(call.model, call.messages, call.temperature)
            out = ResultRequest.model_validate(raw)
            return RouteResponse(status=200, body=out.result)
        except ValidationError as exc:
            # FIX: explicit HTTP 400 + ERROR body. Old code returned 200 {"status":"END"}.
            return RouteResponse(status=400, body={
                "status": "ERROR",
                "detail": "request failed validation",
                "errors": [str(e) for e in exc.errors()],
            })

Regression tests added (test_sdk_example.py, all run against MockHermes):

# 1. list[dict] messages pass gateway validation end-to-end
def test_messages_built_as_list_of_dicts_validates(): ...
# 2. LLMMessage objects in messages:list[dict] -> ValidationError (root cause)
def test_llmmessage_objects_rejected_by_wire_contract(): ...
# 3. result is a plain dict: .get() works, attribute access -> AttributeError
def test_result_result_is_plain_dict_get_access(): ...
# 4. Router returns 400 ERROR on ValidationError — never silent 200 END
def test_router_surfaces_validation_error_not_silent_200(): ...

MockHermes (mock_hermes.py) stands in for the gateway: it strictly validates the incoming payload with LLMCall.model_validate, records every call, and returns a ResultRequest envelope — so a regression is caught at the exact layer where production failed.

Evidence & signatures

**Verified by running** `pytest` in the project venv (pydantic 2.13.4, Python 3.14.4):

```
test_messages_built_as_list_of_dicts_validates          PASSED
test_llmmessage_objects_rejected_by_wire_contract       PASSED
test_result_result_is_plain_dict_get_access             PASSED
test_router_surfaces_validation_error_not_silent_200    PASSED
test_buggy_example_crashes_fixed_example_runs           PASSED
5 passed in 0.04s
```

**Edge cases tested (REPL-level smoke):**
- Empty `messages` list → valid, HTTP 200, gateway records `prompt_tokens: 0`.
- `result` dict missing `content` → `.get("content", "<no content>")` returns the fallback, no crash.
- Non-dict element (`42`) in `messages` → router returns **400 ERROR**; the legacy `handle_silent_200` path confirms the old masking behavior (200 + END) the fix removes.
- Malformed envelope (missing `request_id`) → 400 ERROR, not 200.
- `LLMMessage.model_dump()` round-trip preserves non-user roles (`tool`), i.e. conversion is lossless.
- Pydantic v2 confirmed: `LLMCall(messages=[LLMMessage(...)])` raises `ValidationError` (`type: "dict_type"`, `loc: ("messages", 0)`), while dicts validate cleanly; `reply.result.content` raises `AttributeError`, `.get("content")` returns `"42"`.
{"model": "deepseek-v4-flash", "problem_class": "python-sdk-example-llmcalls-dicts", "result": "passed", "tests": 110}
Generated from the verified corpus · MIT licensedBack to the catalog