diff --git a/services/mana-llm/src/models/requests.py b/services/mana-llm/src/models/requests.py index 38b06c3dc..c470696cd 100644 --- a/services/mana-llm/src/models/requests.py +++ b/services/mana-llm/src/models/requests.py @@ -1,6 +1,6 @@ """Request models for OpenAI-compatible API.""" -from typing import Literal +from typing import Any, Literal from pydantic import BaseModel, Field @@ -35,6 +35,21 @@ class Message(BaseModel): content: MessageContent +class ResponseFormat(BaseModel): + """OpenAI structured-output response_format hint. + + Two shapes are accepted: + - {"type": "json_object"} — free-form JSON + - {"type": "json_schema", + "json_schema": {"name": "...", "schema": {...}, "strict": bool}} + — schema-constrained JSON; passed through to providers that + support it (e.g. Ollama 0.5+ via its native `format` field). + """ + + type: Literal["json_object", "json_schema"] + json_schema: dict[str, Any] | None = None + + class ChatCompletionRequest(BaseModel): """Request body for chat completions endpoint.""" @@ -47,6 +62,7 @@ class ChatCompletionRequest(BaseModel): frequency_penalty: float | None = Field(default=None, ge=-2.0, le=2.0) presence_penalty: float | None = Field(default=None, ge=-2.0, le=2.0) stop: str | list[str] | None = None + response_format: ResponseFormat | None = None class EmbeddingRequest(BaseModel): diff --git a/services/mana-llm/src/providers/ollama.py b/services/mana-llm/src/providers/ollama.py index 5e8c5262f..5990d1237 100644 --- a/services/mana-llm/src/providers/ollama.py +++ b/services/mana-llm/src/providers/ollama.py @@ -127,24 +127,16 @@ class OllamaProvider(LLMProvider): # generateObject() helper. if request.response_format is not None: rf = request.response_format - rf_type = getattr(rf, "type", None) or ( - rf.get("type") if isinstance(rf, dict) else None - ) - if rf_type == "json_object": + if rf.type == "json_object": + payload["format"] = "json" + elif rf.type == "json_schema" and rf.json_schema is not None: + # rf.json_schema is the OpenAI envelope: + # {"name": "...", "schema": {...}, "strict": true} + # Ollama wants just the inner schema dict. + inner = rf.json_schema.get("schema") + payload["format"] = inner if inner is not None else "json" + else: payload["format"] = "json" - elif rf_type == "json_schema": - rf_schema = ( - getattr(rf, "json_schema", None) - or (rf.get("json_schema") if isinstance(rf, dict) else None) - ) - if rf_schema is not None: - inner = ( - getattr(rf_schema, "schema", None) - or (rf_schema.get("schema") if isinstance(rf_schema, dict) else None) - ) - payload["format"] = inner if inner is not None else "json" - else: - payload["format"] = "json" # Add optional parameters options: dict[str, Any] = {}