Skip to content

response_format json_object is a no-op on the default OpenRouter model #468

Description

@sjgold

response_format: json_object is a no-op on the default OpenRouter model

Summary

extractEntities() sends response_format: { type: "json_object" } on the OpenRouter call, with the comment that it's there because proxied models otherwise wrap JSON in prose. The default model on that path is anthropic/claude-haiku-4-5 (CLASSIFIER_MODEL_OPENROUTER in _shared/config.ts), and Anthropic models don't implement that OpenAI-shaped parameter. It passes through and is ignored.

So the guard the comment describes isn't active in the default configuration. Nothing forces JSON, and parseExtractionResult throws whenever the model returns anything else.

Reproducing

The failure isn't random, which is what makes it worth reporting. It reproduces on thought content that reads as an instruction.

I ran the worker over ~800 thoughts. Exactly one failed, and it failed all 5 attempts identically. Its content was a directive written to an LLM about how a downstream system should behave, phrased with "do not refuse" and "this supersedes" language. Roughly 750 characters, nothing exotic.

The model appears to respond to the directive rather than extracting entities from it, returns prose, and the parse fails. wrapThoughtContent() does its job here, the content isn't escaping its tags, but wrapping doesn't stop a model from treating wrapped text as something to answer.

Every other thought in the corpus extracted fine, including ones with far more unusual content. The common factor in the failure is that the text is instruction-shaped.

This class of content isn't unusual in a personal knowledge base. Notes about how you want your tooling to behave are a normal thing to write down, and every one of them is a candidate for this failure.

Possible directions

Not proposing a specific fix, since the tradeoffs are yours to weigh:

  1. Retry once on parse failure, appending something like "Return only the JSON object, no other text" to the prompt. Cheapest option. Costs an extra call only on the failure path.
  2. Use Anthropic's native tool-use / structured output on the Anthropic path rather than the OpenAI-shaped parameter. Correct fix, larger change, and it forks the request-building code per provider.
  3. Leave the behavior, fix the comment, so the next person doesn't assume the guard is active. Fine on its own if neither of the above is worth the complexity.

Happy to open a PR for any of these if one is preferred.

Environment

  • Supabase Edge Functions
  • OpenRouter as the only configured provider
  • anthropic/claude-haiku-4-5 (default)
  • ~800 thoughts, 1 deterministic failure

Related

Separate PR fixes the error reporting that made this hard to diagnose: the failure surfaced as "No LLM API key configured" rather than the real OpenRouter error.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions