Skip to content

feat: respect Accept header for response format negotiation#290

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

feat: respect Accept header for response format negotiation#290
rothnic wants to merge 6 commits intodecolua:masterfrom
rothnic:feat/accept-header-support

Conversation

@rothnic
Copy link
Contributor

@rothnic rothnic commented Mar 12, 2026

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
Copy link
Owner

decolua commented Mar 13, 2026

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
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

2 participants