From 5d28d108b4258457cbd8cdc58c713c051bf4865e Mon Sep 17 00:00:00 2001 From: Deodate-Lawson Date: Sun, 23 Aug 2026 19:00:38 -0400 Subject: [PATCH 1/2] fix(converter): send to_formats as a repeated field, not a JSON array MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit docling-serve validates each to_formats entry against its OutputFormat enum. Sending the JSON string '["md"]' makes the whole string arrive as a single, bogus enum member, so every conversion failed: {"detail":[{"type":"enum","loc":["body","to_formats",0], "msg":"Input should be 'md', 'json', 'yaml', 'html', ..."}]} The converter surfaced that as a typed 502 docling-failed, the worker retried three times and parked the outbox event, and the document produced zero chunks. Retrieval then found nothing and the QA route answered "No relevant content found for the given question" — identical to the message for an empty workspace, which made a broken ingestion pipeline look like an empty one. Verified against the running service: '["md"]' returns 422 and "md" returns 200 with the expected md_content. After the fix the parked event processed on first retry and produced 8 embedded retrieval chunks. The unit test asserted the JSON-array form, so it locked in the broken shape and would have blocked this fix in CI. Updated to match. Nothing in this repo changed to cause the break: docker-compose.yml pins docling-serve to an implicit `latest`, so the image moved under us. Pinning it is left as a separate decision. Co-Authored-By: Claude Opus 5 --- services/document-converter/src/docling.ts | 7 +++++-- services/document-converter/test/docling.test.ts | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/services/document-converter/src/docling.ts b/services/document-converter/src/docling.ts index 684cca60b..cda96abf3 100644 --- a/services/document-converter/src/docling.ts +++ b/services/document-converter/src/docling.ts @@ -1,6 +1,6 @@ /** * docling-serve client — the consolidation of the old ocr-worker's - * docling_runner.py. Same conversion parameters (to_formats=["md"], do_ocr, + * docling_runner.py. Same conversion parameters (to_formats=md, do_ocr, * do_table_structure, image_export_mode=placeholder); failures surface as * typed ServiceErrors instead of opaque 500s. */ @@ -25,7 +25,10 @@ export const doclingConvertFile: DoclingConvert = async (config, file, filename) const form = new FormData(); form.append("files", new Blob([new Uint8Array(file)]), filename); - form.append("to_formats", '["md"]'); + // One repeated form field per format. docling-serve validates each entry + // against its OutputFormat enum, so a JSON-encoded array ("[\"md\"]") + // arrives as a single bogus member and the request fails 422. + form.append("to_formats", "md"); form.append("do_ocr", "true"); form.append("do_table_structure", "true"); form.append("image_export_mode", "placeholder"); diff --git a/services/document-converter/test/docling.test.ts b/services/document-converter/test/docling.test.ts index 79b4b9dcc..3f2a89c9d 100644 --- a/services/document-converter/test/docling.test.ts +++ b/services/document-converter/test/docling.test.ts @@ -39,7 +39,7 @@ describe("doclingConvertFile outgoing request", () => { expect(form).toBeInstanceOf(FormData); expect(form!.get("md_page_break_placeholder")).toBe(PAGE_BREAK_PLACEHOLDER); // Pre-existing conversion parameters stay intact. - expect(form!.get("to_formats")).toBe('["md"]'); + expect(form!.get("to_formats")).toBe("md"); expect(form!.get("do_ocr")).toBe("true"); expect(form!.get("do_table_structure")).toBe("true"); expect(form!.get("image_export_mode")).toBe("placeholder"); From b42225d0fb7cf7542165c303f8377bdd2cad7df9 Mon Sep 17 00:00:00 2001 From: Deodate-Lawson Date: Sun, 23 Aug 2026 19:00:38 -0400 Subject: [PATCH 2/2] fix(chat): use OpenRouter-prefixed model ids to match CHAT_BASE_URL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit chat-models.yaml declared bare Gemini ids (gemini-2.5-flash) while CHAT_BASE_URL points at https://openrouter.ai/api/v1. Ids are sent to the endpoint verbatim, and OpenRouter does not serve the bare names — it serves google/gemini-2.5-flash — so every chat request 404'd. Only the `id:` fields change. The `preset:` keys already carried the google/ prefix because they name a bundled behavior entry rather than a model on the endpoint, so they were correct as written. Also refreshes the comment above the block, which described the ids as "the BARE ids Google's endpoint expects" and no longer matched. It now explains both directions: keep the prefix for OpenRouter, drop it when talking to Google's own compatibility endpoint. Co-Authored-By: Claude Opus 5 --- apps/web/config/chat-models.yaml | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/apps/web/config/chat-models.yaml b/apps/web/config/chat-models.yaml index e85903206..a59c68636 100644 --- a/apps/web/config/chat-models.yaml +++ b/apps/web/config/chat-models.yaml @@ -17,27 +17,29 @@ version: 1 models: - # Default: Google Gemini. Reached at - # CHAT_BASE_URL=https://generativelanguage.googleapis.com/v1beta/openai, - # which is also where chat falls back when CHAT_BASE_URL is unset. + # Default: Google Gemini, reached through OpenRouter + # (CHAT_BASE_URL=https://openrouter.ai/api/v1). # - # Model ids here are the BARE ids Google's endpoint expects. The `google/` - # prefix belongs only to the `preset:` field, which names a bundled behavior - # entry — see packages/core/src/llm/presets.ts. + # Ids are sent to the endpoint verbatim, so they carry OpenRouter's `google/` + # vendor prefix. Talking to Google's own compatibility endpoint + # (https://generativelanguage.googleapis.com/v1beta/openai, also the fallback + # when CHAT_BASE_URL is unset) instead? Drop the prefix from `id:` — it + # expects the bare ids. The `preset:` field is unaffected either way: it names + # a bundled behavior entry, not a model on the endpoint. primary: - id: gemini-2.5-flash + id: google/gemini-2.5-flash preset: google/gemini-2.5-flash # The cheap tier. Its preset sets reasoning `default: none`, so routine work # does not pay for thinking tokens. fast: - id: gemini-2.5-flash-lite + id: google/gemini-2.5-flash-lite preset: google/gemini-2.5-flash-lite # The reasoning tier. 2.5 Pro always reasons, so its preset omits the `none` # level rather than claiming a capability the endpoint would reject. deep: - id: gemini-2.5-pro + id: google/gemini-2.5-pro preset: google/gemini-2.5-pro # Talking to something else instead? Any OpenAI-compatible endpoint works —