Skip to content

feat: respect Accept header for response format negotiation - #290

Open
rothnic wants to merge 6 commits into
decolua:masterfrom
rothnic:feat/accept-header-support
Open

rothnic wants to merge 6 commits into
decolua:masterfrom
rothnic:feat/accept-header-support

Conversation

@rothnic

@rothnic rothnic commented Mar 12, 2026

Copy link
Copy Markdown
Contributor

Problem

9router ignores the client's Accept header when determining response format. This causes AI SDK compatibility issues where clients send Accept: application/json but receive SSE streaming responses.

Closes #289

Changes

  • Check clientRawRequest.headers.accept in handleChatCore()
  • Return non-streaming JSON when Accept: application/json
  • Return SSE when Accept: text/event-stream or header not specified
  • Only applies when body.stream is not explicitly set to true

Testing

Tested with:

  • AI SDK generateObject() - now works without workarounds
  • Direct curl requests with various Accept headers
  • Backward compatible with existing clients

Related

…ders

Providers like Antigravity maintain separate quota buckets per model family
(e.g. Claude vs Gemini). A 429 on claude-opus previously locked the entire
account, preventing gemini-pro requests even though its quota was full.

This adds in-memory per-model locking so that only the specific model is
skipped during account selection while other models remain accessible.

Changes:
- Add model-aware lock tracking in auth.js (Map<connectionId:model, expiry>)
- Pass model context from chat handler to auth service
- Multi-bucket behavior gated to known providers (MULTI_BUCKET_PROVIDERS set)
- No database schema changes — locks are in-memory and clear on restart

Closes decolua#110
When models (like kimi) return JSON wrapped in markdown code blocks
(\`\`\`json...\`\`\`), strip the markers before sending to client.

Changes:
- Add markdown stripping to streaming text_delta responses
- Add markdown stripping to non-streaming Claude format responses
- Translate response_format to system prompts for JSON schema/object modes
- Add jsonExtractor utility for consistent markdown removal

This fixes AI_APICallError: Invalid JSON response when using
AI SDK generateObject with kimi models through 9router.
Makes 9router check the client's Accept header when determining whether
to return streaming (SSE) or non-streaming (JSON) responses.

Changes:
- Check clientRawRequest.headers.accept in handleChatCore()
- Return non-streaming JSON when Accept: application/json
- Return SSE when Accept: text/event-stream or not specified
- Only applies when body.stream is not explicitly set to true

This fixes AI_APICallError: Invalid JSON response when using AI SDK
with 9router, as AI SDK sends Accept: application/json but previously
received SSE format responses.

Fixes decolua#289
Documents why markdown stripping is needed even when using
response_format with kimi models.

Key finding: kimi/kimi-k2.5-thinking adds markdown code blocks
despite response_format settings, but kimi/kimi-k2.5 works
without markdown.

Markdown stripping remains as defensive backstop.
- Move KIMI_JSON_MODE_INVESTIGATION.md to docs/investigations/
- Update .gitignore to allow docs/investigations/ tracking
- Better project organization for documentation
@decolua

decolua commented Mar 13, 2026

Copy link
Copy Markdown
Owner

Hi @rothnic, thanks for this contribution! 🙏

We've cherry-picked the core changes from this PR into our fork:

What we merged:

  • feat: Respect Accept: application/json header in handleChatCore() to return non-streaming JSON instead of SSE — this fixes AI SDK generateObject/generateText compatibility.
  • fix: Strip markdown code block markers (json...) from Claude non-streaming responses to prevent Invalid JSON parse errors.

What we skipped:

  • The in-memory per-model rate limit locking (auth.js) — our fork already has a DB-based implementation for this.
  • The stream.js / claude-to-openai.js changes — not needed for our use case since non-streaming goes through a separate path.
  • Docs files.

Great find on the Accept header negotiation issue — clean and minimal fix. Appreciate the work! 🎉

decolua pushed a commit that referenced this pull request Mar 13, 2026
- Respect Accept: application/json header to return non-streaming JSON
  instead of SSE, fixing AI SDK generateObject/generateText compatibility
- Strip markdown code block markers (```json...```) from Claude
  non-streaming responses to prevent JSON parse errors

Cherry-picked and adapted from PR #290 by @rothnic
#290

Made-with: Cursor

@codeCraft-Ritik codeCraft-Ritik left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great fix! Respecting the Accept header makes the API behavior more predictable and standards-compliant.
Nice improvement for compatibility with AI SDK and existing clients 👍

idbara pushed a commit to idbara/9router that referenced this pull request Apr 23, 2026
- Respect Accept: application/json header to return non-streaming JSON
  instead of SSE, fixing AI SDK generateObject/generateText compatibility
- Strip markdown code block markers (```json...```) from Claude
  non-streaming responses to prevent JSON parse errors

Cherry-picked and adapted from PR decolua#290 by @rothnic
decolua#290

Made-with: Cursor
luckystart79-lang pushed a commit to luckystart79-lang/minirouter that referenced this pull request May 2, 2026
- Respect Accept: application/json header to return non-streaming JSON
  instead of SSE, fixing AI SDK generateObject/generateText compatibility
- Strip markdown code block markers (```json...```) from Claude
  non-streaming responses to prevent JSON parse errors

Cherry-picked and adapted from PR decolua#290 by @rothnic
decolua#290

Made-with: Cursor
caothu159 pushed a commit to diepxuan/9router that referenced this pull request May 14, 2026
- Respect Accept: application/json header to return non-streaming JSON
  instead of SSE, fixing AI SDK generateObject/generateText compatibility
- Strip markdown code block markers (```json...```) from Claude
  non-streaming responses to prevent JSON parse errors

Cherry-picked and adapted from PR decolua#290 by @rothnic
decolua#290

Made-with: Cursor
vibecoder11200 pushed a commit to vibecoder11200/9router that referenced this pull request Jun 13, 2026
- Respect Accept: application/json header to return non-streaming JSON
  instead of SSE, fixing AI SDK generateObject/generateText compatibility
- Strip markdown code block markers (```json...```) from Claude
  non-streaming responses to prevent JSON parse errors

Cherry-picked and adapted from PR #290 by @rothnic
decolua/9router#290

Made-with: Cursor
ikhbaaalll added a commit to ikhbaaalll/9router that referenced this pull request Oct 3, 2026
The account authenticates with an API key now, not the OAuth/WorkOS token, so the
plan and ledger endpoints behind api.cline.bot are not its business. The control
plane dropped its own reader in decolua#290; this is the fork half.

- delete open-sse/services/usage/clinepass.js (getClinepassUsage, buildClinepassQuotas)
- drop its import and its provider dispatch arm in open-sse/services/usage.js, so a
  clinepass account is answered like any provider with no reader: the dispatcher's
  own "Usage API not implemented for clinepass" line, with no request at all
- drop the clinepass parse arm in the usage page's ProviderLimits/utils.js
- delete the two clinepass-only test cases

Kept: the OAuth provider, the connect flow, and the transport, which the API-key
path still uses.

Refs the goal file /home/wdftr/goals/RINNAI-9ROUTER-CLINEPASS-QUOTA-GOAL.md
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Tracking: AI SDK Compatibility - Response Format, Streaming, and Accept Header Issues

3 participants