Bug Fix·Pushed May 1, 2026·S
Structured output fixed for reasoning models
Models like DeepSeek-R1 and MiniMax prefix their output with reasoning traces, which broke gitpulse's structured story generation. The fix switches from LangChain's tool-calling to manual JSON extraction that strips those reasoning blocks.
Some AI models — particularly reasoning-focused ones like DeepSeek-R1 and MiniMax — prepend their responses with blocks before the actual content. gitpulse uses LangChain's withStructuredOutput to generate editorial summaries, but this relies on tool-calling, which chokes when the model outputs raw text with reasoning traces. The parser receives malformed content instead of a structured response.
The solution replaces tool-calling with plain chat completion. Instead of relying on LangChain's abstraction, the new approach manually extracts JSON from the raw response — stripping blocks, markdown code fences, and any surrounding prose, then validating against the same Zod schema.
This works across both reasoning and non-reasoning models, so gitpulse no longer has to choose between advanced reasoning capabilities and structured output.
In the [[code]]action/src/llm.ts[[/code]] summarizer.
Technical description
The summarizer in [[code ref=4]]summarize function[[/code]] was updated to work with models that emit reasoning traces. Previously, it used LangChain's [[code]]withStructuredOutput[[/code]] method with a tool call named [[code]]gitpulse_story[[/code]]. This broke on reasoning models because they prefix output with [[code]][[/code]] blocks — the tool-calling parser expects clean structured data, not raw text with reasoning metadata embedded.
The fix switches to plain [[code]]llm.invoke[[/code]] with a manually composed system prompt. The prompt now includes [[code ref=2]]SCHEMA_DESCRIPTION[[/code]] — an explicit JSON shape specification — alongside the existing [[code ref=1]]VOICE[[/code]] instructions that define editorial tone and style.
Two new helper functions handle the response parsing:
[[code ref=3]]extractJson[[/code]] performs multi-stage cleanup: it removes blocks via regex, extracts content from markdown code fences if present, then slices to the outermost {} to handle any remaining prose wrappers. This is more robust than relying on model-specific output formats.
[[code]]extractText[[/code]] handles multi-part content arrays, converting both plain strings and text-part objects into a single string before passing to [[code ref=3]]extractJson[[/code]].
The final [[code]]StorySchema.parse[[/code]] call validates the extracted JSON, ensuring field lengths and required structure match the expected format.
````mermaid
file=action/src/llm.ts
graph TD
A[llm.invoke] --> B[response.content]
B --> C[extractText]
C --> D[extractJson]
D --> E[Strip blocks]
E --> F[Extract from
```` fences]
F --> G[Slice to outermost {}]
G --> H[JSON.parse]
H --> I[StorySchema.parse]
I --> J[Validated Story]
```
The Zod schema itself was simplified — field descriptions were removed since they're now embedded in the system prompt rather than passed through LangChain's structured output API.
Files at a Glance:
- [[code]]action/src/llm.ts[[/code]] — Summarizer updated with manual JSON extraction, new helpers for handling reasoning model output
Categories
- Bug Fix (80%) — Fixes compatibility with reasoning models that emit blocks — these models broke structured tool-calling output
- Maintenance (20%) — Removes dependency on LangChain's withStructuredOutput abstraction, replacing with manual JSON extraction and validation