From 34eed2413f79549197877f00edcded19f22eca05 Mon Sep 17 00:00:00 2001 From: sudipnext Date: Mon, 2 Mar 2026 17:11:25 +0545 Subject: [PATCH 1/2] feat: update version to 0.6.1-beta and ensure array schemas have items in JSON responses --- electron/package-lock.json | 4 +- electron/package.json | 2 +- .../servers/fastapi/services/llm_client.py | 23 ++++------ .../servers/fastapi/utils/schema_utils.py | 42 +++++++++++++++++++ servers/fastapi/services/llm_client.py | 23 ++++------ servers/fastapi/utils/schema_utils.py | 42 +++++++++++++++++++ 6 files changed, 101 insertions(+), 35 deletions(-) diff --git a/electron/package-lock.json b/electron/package-lock.json index 59e05b9f..ce9ae9d8 100644 --- a/electron/package-lock.json +++ b/electron/package-lock.json @@ -1,12 +1,12 @@ { "name": "presenton", - "version": "0.6.0-beta", + "version": "0.6.1-beta", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "presenton", - "version": "0.6.0-beta", + "version": "0.6.1-beta", "hasInstallScript": true, "dependencies": { "@tailwindcss/cli": "^4.1.5", diff --git a/electron/package.json b/electron/package.json index d4740d54..d82264cf 100644 --- a/electron/package.json +++ b/electron/package.json @@ -1,7 +1,7 @@ { "name": "presenton", "productName": "Presenton Open Source", - "version": "0.6.0-beta", + "version": "0.6.1-beta", "main": "app_dist/main.js", "description": "Open-Source AI Presentation Generator", "homepage": "https://presenton.ai", diff --git a/electron/servers/fastapi/services/llm_client.py b/electron/servers/fastapi/services/llm_client.py index 9799cc6b..c7151f6f 100644 --- a/electron/servers/fastapi/services/llm_client.py +++ b/electron/servers/fastapi/services/llm_client.py @@ -66,6 +66,7 @@ from utils.set_env import ( from utils.llm_provider import get_llm_provider, get_model from utils.parsers import parse_bool_or_none from utils.schema_utils import ( + ensure_array_schemas_have_items, ensure_strict_json_schema, flatten_json_schema, remove_titles_from_schema, @@ -702,6 +703,7 @@ class LLMClient: path=(), root=response_schema, ) + response_schema = ensure_array_schemas_have_items(response_schema) if use_tool_calls_for_structured_output and depth == 0: if all_tools is None: all_tools = [] @@ -1599,6 +1601,7 @@ class LLMClient: path=(), root=response_schema, ) + response_schema = ensure_array_schemas_have_items(response_schema) if use_tool_calls_for_structured_output and depth == 0: if all_tools is None: @@ -1793,28 +1796,16 @@ class LLMClient: """ client: AsyncOpenAI = self._client response_schema = response_format - # Apply strict schema once at root + # Apply strict schema once at root (includes array "items" fix at lines 135–155). if strict and depth == 0: response_schema = ensure_strict_json_schema( response_schema, path=(), root=response_schema, ) - - # Codex Responses API requires all array schemas to specify `items`. - def _fix_arrays(node: Any) -> Any: - if isinstance(node, dict): - # Add default items for arrays missing them - if node.get("type") == "array" and "items" not in node: - node["items"] = {"type": "string"} - for key, value in list(node.items()): - node[key] = _fix_arrays(value) - elif isinstance(node, list): - for idx, value in enumerate(node): - node[idx] = _fix_arrays(value) - return node - - response_schema = _fix_arrays(response_schema) + # When we didn't run ensure_strict_json_schema, fix arrays for Codex API (strict=False or depth > 0). + else: + response_schema = ensure_array_schemas_have_items(response_schema) # Responses API tool format: flat {type, name, description, parameters} response_schema_tool = { diff --git a/electron/servers/fastapi/utils/schema_utils.py b/electron/servers/fastapi/utils/schema_utils.py index 92aafd97..1e0241aa 100644 --- a/electron/servers/fastapi/utils/schema_utils.py +++ b/electron/servers/fastapi/utils/schema_utils.py @@ -134,11 +134,25 @@ def ensure_strict_json_schema( # arrays # { 'type': 'array', 'items': {...} } + # OpenAI requires array schemas to have "items". Zod tuples may emit prefixItems only. items = json_schema.get("items") if isinstance(items, dict): json_schema["items"] = ensure_strict_json_schema( items, path=(*path, "items"), root=root ) + elif typ == "array": + prefix_items = json_schema.get("prefixItems") + if ( + isinstance(prefix_items, list) + and len(prefix_items) > 0 + and isinstance(prefix_items[0], dict) + ): + json_schema["items"] = ensure_strict_json_schema( + prefix_items[0], path=(*path, "items"), root=root + ) + json_schema.pop("prefixItems", None) + else: + json_schema["items"] = {"type": "string"} # unions any_of = json_schema.get("anyOf") @@ -281,6 +295,34 @@ def flatten_json_schema(schema: dict) -> dict: return result +def ensure_array_schemas_have_items(schema: dict) -> dict[str, Any]: + """ + Recursively ensure every JSON schema node with type="array" has an "items" key. + Codex Responses API requires array schemas to specify items. Mutates a deep copy. + """ + result = deepcopy(schema) + + def _is_array_schema_type(type_value: Any) -> bool: + if type_value == "array": + return True + if isinstance(type_value, list): + return "array" in type_value + return False + + def _ensure(node: Any) -> Any: + if isinstance(node, dict): + if _is_array_schema_type(node.get("type")) and "items" not in node: + node["items"] = {"type": "string"} + for key, value in list(node.items()): + node[key] = _ensure(value) + elif isinstance(node, list): + for idx, value in enumerate(node): + node[idx] = _ensure(value) + return node + + return _ensure(result) + + def remove_titles_from_schema(schema: dict) -> dict[str, Any]: def _strip_titles(node: Any) -> Any: diff --git a/servers/fastapi/services/llm_client.py b/servers/fastapi/services/llm_client.py index 9799cc6b..63a97de4 100644 --- a/servers/fastapi/services/llm_client.py +++ b/servers/fastapi/services/llm_client.py @@ -66,6 +66,7 @@ from utils.set_env import ( from utils.llm_provider import get_llm_provider, get_model from utils.parsers import parse_bool_or_none from utils.schema_utils import ( + ensure_array_schemas_have_items, ensure_strict_json_schema, flatten_json_schema, remove_titles_from_schema, @@ -702,6 +703,7 @@ class LLMClient: path=(), root=response_schema, ) + response_schema = ensure_array_schemas_have_items(response_schema) if use_tool_calls_for_structured_output and depth == 0: if all_tools is None: all_tools = [] @@ -1599,6 +1601,7 @@ class LLMClient: path=(), root=response_schema, ) + response_schema = ensure_array_schemas_have_items(response_schema) if use_tool_calls_for_structured_output and depth == 0: if all_tools is None: @@ -1793,28 +1796,16 @@ class LLMClient: """ client: AsyncOpenAI = self._client response_schema = response_format - # Apply strict schema once at root + # Apply strict schema once at root (includes array "items" fix in ensure_strict_json_schema). if strict and depth == 0: response_schema = ensure_strict_json_schema( response_schema, path=(), root=response_schema, ) - - # Codex Responses API requires all array schemas to specify `items`. - def _fix_arrays(node: Any) -> Any: - if isinstance(node, dict): - # Add default items for arrays missing them - if node.get("type") == "array" and "items" not in node: - node["items"] = {"type": "string"} - for key, value in list(node.items()): - node[key] = _fix_arrays(value) - elif isinstance(node, list): - for idx, value in enumerate(node): - node[idx] = _fix_arrays(value) - return node - - response_schema = _fix_arrays(response_schema) + # When we didn't run ensure_strict_json_schema, fix arrays for Codex API (strict=False or depth > 0). + else: + response_schema = ensure_array_schemas_have_items(response_schema) # Responses API tool format: flat {type, name, description, parameters} response_schema_tool = { diff --git a/servers/fastapi/utils/schema_utils.py b/servers/fastapi/utils/schema_utils.py index 9efed7c5..66e01746 100644 --- a/servers/fastapi/utils/schema_utils.py +++ b/servers/fastapi/utils/schema_utils.py @@ -185,6 +185,20 @@ def ensure_strict_json_schema( items, path=(*path, "items"), root=root ) + elif typ == "array": + prefix_items = json_schema.get("prefixItems") + if ( + isinstance(prefix_items, list) + and len(prefix_items) > 0 + and isinstance(prefix_items[0], dict) + ): + json_schema["items"] = ensure_strict_json_schema( + prefix_items[0], path=(*path, "items"), root=root + ) + json_schema.pop("prefixItems", None) + else: + json_schema["items"] = {"type": "string"} + # unions any_of = json_schema.get("anyOf") if isinstance(any_of, list): @@ -326,6 +340,34 @@ def flatten_json_schema(schema: dict) -> dict: return result +def ensure_array_schemas_have_items(schema: dict) -> dict[str, Any]: + """ + Recursively ensure every JSON schema node with type="array" has an "items" key. + Codex Responses API requires array schemas to specify items. Mutates a deep copy. + """ + result = deepcopy(schema) + + def _is_array_schema_type(type_value: Any) -> bool: + if type_value == "array": + return True + if isinstance(type_value, list): + return "array" in type_value + return False + + def _ensure(node: Any) -> Any: + if isinstance(node, dict): + if _is_array_schema_type(node.get("type")) and "items" not in node: + node["items"] = {"type": "string"} + for key, value in list(node.items()): + node[key] = _ensure(value) + elif isinstance(node, list): + for idx, value in enumerate(node): + node[idx] = _ensure(value) + return node + + return _ensure(result) + + def remove_titles_from_schema(schema: dict) -> dict[str, Any]: def _strip_titles(node: Any) -> Any: From 1523511e97558382316a80f4d3afe8dee6ec4290 Mon Sep 17 00:00:00 2001 From: sudipnext Date: Mon, 2 Mar 2026 17:17:46 +0545 Subject: [PATCH 2/2] feat: optimize asset fetching by starting tasks immediately for parallel processing --- .../servers/fastapi/api/v1/ppt/endpoints/presentation.py | 8 ++++---- servers/fastapi/api/v1/ppt/endpoints/presentation.py | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/electron/servers/fastapi/api/v1/ppt/endpoints/presentation.py b/electron/servers/fastapi/api/v1/ppt/endpoints/presentation.py index 3ad01ddd..27a01244 100644 --- a/electron/servers/fastapi/api/v1/ppt/endpoints/presentation.py +++ b/electron/servers/fastapi/api/v1/ppt/endpoints/presentation.py @@ -317,9 +317,9 @@ async def stream_presentation( # This will mutate slide and add placeholder assets process_slide_add_placeholder_assets(slide) - # This will mutate slide + # This will mutate slide - start task immediately so it runs in parallel with next slide LLM generation async_assets_generation_tasks.append( - process_slide_and_fetch_assets(image_generation_service, slide) + asyncio.create_task(process_slide_and_fetch_assets(image_generation_service, slide)) ) yield SSEResponse( @@ -721,9 +721,9 @@ async def generate_presentation_handler( slides.append(slide) batch_slides.append(slide) - # Start asset fetch tasks for just-generated slides so they run while next batch is processed + # Start asset fetch tasks immediately so they run in parallel with next batch's LLM calls asset_tasks = [ - process_slide_and_fetch_assets(image_generation_service, slide) + asyncio.create_task(process_slide_and_fetch_assets(image_generation_service, slide)) for slide in batch_slides ] async_assets_generation_tasks.extend(asset_tasks) diff --git a/servers/fastapi/api/v1/ppt/endpoints/presentation.py b/servers/fastapi/api/v1/ppt/endpoints/presentation.py index a2cbc4ed..6d377f2d 100644 --- a/servers/fastapi/api/v1/ppt/endpoints/presentation.py +++ b/servers/fastapi/api/v1/ppt/endpoints/presentation.py @@ -317,9 +317,9 @@ async def stream_presentation( # This will mutate slide and add placeholder assets process_slide_add_placeholder_assets(slide) - # This will mutate slide + # This will mutate slide - start task immediately so it runs in parallel with next slide LLM generation async_assets_generation_tasks.append( - process_slide_and_fetch_assets(image_generation_service, slide) + asyncio.create_task(process_slide_and_fetch_assets(image_generation_service, slide)) ) yield SSEResponse( @@ -716,9 +716,9 @@ async def generate_presentation_handler( slides.append(slide) batch_slides.append(slide) - # Start asset fetch tasks for just-generated slides so they run while next batch is processed + # Start asset fetch tasks immediately so they run in parallel with next batch's LLM calls asset_tasks = [ - process_slide_and_fetch_assets(image_generation_service, slide) + asyncio.create_task(process_slide_and_fetch_assets(image_generation_service, slide)) for slide in batch_slides ] async_assets_generation_tasks.extend(asset_tasks)