Skip to content

Commit 28c9aa6

Browse files
committed
Revert "fix(llm): simplify JSON schemas for better Ollama and LLM compliance (#1292)"
This reverts commit 5b1c348.
1 parent 98593f9 commit 28c9aa6

3 files changed

Lines changed: 1 addition & 92 deletions

File tree

hindsight-api-slim/hindsight_api/config.py

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -132,13 +132,11 @@ def normalize_config_dict(config: dict[str, Any]) -> dict[str, Any]:
132132
ENV_LLM_GROQ_SERVICE_TIER = "HINDSIGHT_API_LLM_GROQ_SERVICE_TIER"
133133
ENV_LLM_OPENAI_SERVICE_TIER = "HINDSIGHT_API_LLM_OPENAI_SERVICE_TIER"
134134
ENV_LLM_EXTRA_BODY = "HINDSIGHT_API_LLM_EXTRA_BODY"
135-
ENV_LLM_SIMPLIFY_JSON_SCHEMA = "HINDSIGHT_API_LLM_SIMPLIFY_JSON_SCHEMA"
136135

137136
# Defaults for service tiers
138137
DEFAULT_LLM_GROQ_SERVICE_TIER = "auto" # "on_demand", "flex", or "auto"
139138
DEFAULT_LLM_OPENAI_SERVICE_TIER = None # None (default) or "flex" (50% cheaper)
140139
DEFAULT_LLM_EXTRA_BODY = None # None = no extra body params; JSON dict merged into OpenAI extra_body
141-
DEFAULT_LLM_SIMPLIFY_JSON_SCHEMA = True # Flatten $ref/$defs/anyOf in JSON schemas for better LLM compliance
142140

143141
# Per-operation LLM configuration (optional, falls back to global LLM config)
144142
ENV_RETAIN_LLM_PROVIDER = "HINDSIGHT_API_RETAIN_LLM_PROVIDER"
@@ -844,7 +842,6 @@ class HindsightConfig:
844842
llm_extra_body: (
845843
dict | None
846844
) # Extra body params merged into OpenAI-compatible API calls (e.g. {"chat_template_kwargs": {"enable_thinking": true}})
847-
llm_simplify_json_schema: bool # Flatten $ref/$defs/anyOf in JSON schemas for better LLM compliance (default: true)
848845

849846
# Vertex AI configuration
850847
llm_vertexai_project_id: str | None
@@ -1342,10 +1339,6 @@ def from_env(cls) -> "HindsightConfig":
13421339
llm_groq_service_tier=os.getenv(ENV_LLM_GROQ_SERVICE_TIER, DEFAULT_LLM_GROQ_SERVICE_TIER),
13431340
llm_openai_service_tier=os.getenv(ENV_LLM_OPENAI_SERVICE_TIER, DEFAULT_LLM_OPENAI_SERVICE_TIER),
13441341
llm_extra_body=json.loads(os.getenv(ENV_LLM_EXTRA_BODY, "null")),
1345-
llm_simplify_json_schema=os.getenv(
1346-
ENV_LLM_SIMPLIFY_JSON_SCHEMA, str(DEFAULT_LLM_SIMPLIFY_JSON_SCHEMA)
1347-
).lower()
1348-
in ("true", "1"),
13491342
# Vertex AI
13501343
llm_vertexai_project_id=os.getenv(ENV_LLM_VERTEXAI_PROJECT_ID) or DEFAULT_LLM_VERTEXAI_PROJECT_ID,
13511344
llm_vertexai_region=os.getenv(ENV_LLM_VERTEXAI_REGION, DEFAULT_LLM_VERTEXAI_REGION),

hindsight-api-slim/hindsight_api/engine/providers/openai_compatible_llm.py

Lines changed: 1 addition & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -61,77 +61,6 @@ def _strip_code_fences(content: str) -> str:
6161
return content
6262

6363

64-
def _simplify_json_schema(schema: dict[str, Any]) -> dict[str, Any]:
65-
"""Simplify a Pydantic JSON schema for maximum LLM compatibility.
66-
67-
Pydantic v2's model_json_schema() produces schemas with $ref/$defs, anyOf
68-
(for Optional fields), and const — features that Ollama's grammar-based
69-
constrained decoding silently fails on, and that confuse weaker models when
70-
the schema appears as a text hint in the prompt.
71-
72-
This function:
73-
1. Resolves all $ref/$defs by inlining referenced definitions
74-
2. Simplifies anyOf nullable unions (e.g. anyOf: [{type: "string"}, {type: "null"}])
75-
to just the non-null type, keeping default/description
76-
3. Replaces const with single-element enum
77-
"""
78-
defs = schema.get("$defs", {})
79-
80-
def _resolve(node: Any) -> Any:
81-
if not isinstance(node, dict):
82-
if isinstance(node, list):
83-
return [_resolve(item) for item in node]
84-
return node
85-
86-
# Resolve $ref first
87-
if "$ref" in node:
88-
ref_path = node["$ref"] # e.g. "#/$defs/Entity"
89-
ref_name = ref_path.rsplit("/", 1)[-1]
90-
if ref_name in defs:
91-
# Inline the definition, merging any sibling keys (e.g. description)
92-
resolved = _resolve(dict(defs[ref_name]))
93-
# Preserve sibling keys from the referencing node
94-
for k, v in node.items():
95-
if k != "$ref":
96-
resolved[k] = _resolve(v)
97-
return resolved
98-
return node # unresolvable ref, leave as-is
99-
100-
result: dict[str, Any] = {}
101-
for key, value in node.items():
102-
if key == "$defs":
103-
continue # drop $defs — everything is inlined now
104-
105-
if key == "anyOf" and isinstance(value, list):
106-
# Simplify nullable anyOf: [{type: "string"}, {type: "null"}] → {type: "string"}
107-
non_null = [_resolve(v) for v in value if not (isinstance(v, dict) and v.get("type") == "null")]
108-
if len(non_null) == 1:
109-
# Single non-null type — inline it, preserving sibling keys
110-
simplified = dict(non_null[0])
111-
for k, v in node.items():
112-
if k not in ("anyOf",) and k not in simplified:
113-
simplified[k] = _resolve(v)
114-
return simplified
115-
elif len(non_null) > 1:
116-
# Multiple non-null types — keep anyOf but resolved
117-
result["anyOf"] = non_null
118-
else:
119-
# All null — just use null
120-
result["type"] = "null"
121-
continue
122-
123-
if key == "const":
124-
# Replace const with single-element enum for broader compatibility
125-
result["enum"] = [value]
126-
continue
127-
128-
result[key] = _resolve(value)
129-
130-
return result
131-
132-
return _resolve(schema)
133-
134-
13564
def _summarize_status_error(e: APIStatusError, body_max: int = 400) -> str:
13665
"""Render an APIStatusError with status code + truncated response body.
13766
@@ -441,12 +370,6 @@ async def call(
441370
schema = None
442371
if hasattr(response_format, "model_json_schema"):
443372
schema = response_format.model_json_schema()
444-
# Simplify schema for better LLM compliance — resolves $ref/$defs,
445-
# simplifies anyOf nullables, replaces const with enum.
446-
from hindsight_api.config import get_config
447-
448-
if get_config().llm_simplify_json_schema:
449-
schema = _simplify_json_schema(schema)
450373

451374
if strict_schema and schema is not None:
452375
# Use OpenAI's strict JSON schema enforcement
@@ -931,14 +854,8 @@ async def _call_ollama_native(
931854
"""
932855
start_time = time.time()
933856

934-
# Get the JSON schema from the Pydantic model and simplify it for Ollama's
935-
# grammar engine, which doesn't support $ref, anyOf, or const.
857+
# Get the JSON schema from the Pydantic model
936858
schema = response_format.model_json_schema() if hasattr(response_format, "model_json_schema") else None
937-
if schema:
938-
from hindsight_api.config import get_config
939-
940-
if get_config().llm_simplify_json_schema:
941-
schema = _simplify_json_schema(schema)
942859

943860
# Build the base URL for Ollama's native API
944861
# Default OpenAI-compatible URL is http://localhost:11434/v1

hindsight-docs/docs/developer/configuration.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,6 @@ To switch between backends:
174174
| `HINDSIGHT_API_LLM_GROQ_SERVICE_TIER` | Groq service tier: `on_demand`, `flex`, `auto` | `auto` |
175175
| `HINDSIGHT_API_LLM_OPENAI_SERVICE_TIER` | OpenAI service tier: `flex` for 50% cost savings (OpenAI Flex Processing) | None (default) |
176176
| `HINDSIGHT_API_LLM_EXTRA_BODY` | JSON dict merged into `extra_body` for all OpenAI-compatible API calls. Useful for custom model servers (e.g., vLLM `chat_template_kwargs`). | `null` |
177-
| `HINDSIGHT_API_LLM_SIMPLIFY_JSON_SCHEMA` | Flatten `$ref`/`$defs`/`anyOf` in JSON schemas before sending to LLMs. Required for Ollama structured output; improves compliance for all providers. Set to `false` to send raw Pydantic schemas. | `true` |
178177
| `HINDSIGHT_API_LLM_GEMINI_SAFETY_SETTINGS` | JSON-encoded list of `{category, threshold}` dicts for Gemini/VertexAI content safety filtering | `null` |
179178

180179
**Provider Examples**

0 commit comments

Comments
 (0)