import json from openai import OpenAI class LLMClient: def __init__(self, base_url, api_key: str | None = None, model: str = "gpt-4o-mini"): self.model = model self.client = OpenAI(base_url=base_url, api_key=api_key or "ollama") def chat_with_schema(self, prompt: str, schema: dict, system: str | None = None) -> str | None: assert_strict_mode_clean(schema) kwargs = { "model": self.model, "messages": [ {"role": "system", "content": system}, {"role": "user", "content": prompt}, ] if system else [ {"role": "user", "content": prompt}, ], "response_format": schema, } resp = self.client.chat.completions.create(**kwargs) return resp.choices[0].message.content def chat(self, system: str, user: str, json_mode: bool = False, images: list[str] | None = None) -> str | None: images = images or [] if images: content = [{"type": "text", "text": user}] for b64 in images: content.append({ "type": "image_url", "image_url": {"url": f"data:image/png;base64,{b64}"}, }) else: content = user kwargs = { "model": self.model, "messages": [ {"role": "system", "content": system}, {"role": "user", "content": content}, ], } if json_mode: kwargs["response_format"] = {"type": "json_object"} resp = self.client.chat.completions.create(**kwargs) return resp.choices[0].message.content def assert_strict_mode_clean(schema: dict, path: str = "$") -> None: """Recursively verify a JSON Schema obeys OpenAI strict mode.""" if schema.get("type") == "object": props = schema.get("properties", {}) required = set(schema.get("required", [])) if schema.get("additionalProperties") is not False: raise ValueError(f"{path}: additionalProperties must be false") for name in props: if name not in required: raise ValueError(f"{path}.{name}: not in required") for name, sub in props.items(): assert_strict_mode_clean(sub, f"{path}.{name}") if schema.get("type") == "array": assert_strict_mode_clean(schema["items"], f"{path}[]") for name, sub in schema.get("$defs", {}).items(): assert_strict_mode_clean(sub, f"$defs.{name}") # OpenAI rejects these keywords in strict mode for banned in ("default", "oneOf"): if banned in schema: raise ValueError(f"{path}: '{banned}' not allowed in strict mode")