Skip to content

fix: Anthropic system content-block arrays, Responses API SSE spec compliance - #53

Merged
KochC merged 17 commits into
mainfrom
dev
Jul 4, 2026
Merged

fix: Anthropic system content-block arrays, Responses API SSE spec compliance#53
KochC merged 17 commits into
mainfrom
dev

Conversation

@KochC

@KochC KochC commented Jul 4, 2026

Copy link
Copy Markdown
Owner

Summary

Promotes dev to main. Includes:

Testing

  • npm test — all tests pass
  • npm run lint — clean

KochC and others added 17 commits March 27, 2026 15:44
- Add 17 new integration tests: CORS edge cases (disallowed origins,
  no-origin header, OPTIONS for disallowed origin), auth (401/pass-through),
  and error handling (400/502/404) for /v1/chat/completions
  Closes #14, closes #16
- Add ESLint with flat config, npm run lint script, and Lint job in CI
  Closes #15
- Improve README with quickstart section, npm install instructions, and
  corrected package name; add type column to env vars table
  Closes #17
- Implement streaming for POST /v1/chat/completions (issue #11):
  subscribe to opencode event stream, pipe message.part.updated deltas
  as SSE chat.completion.chunk events, finish on session.idle
- Implement streaming for POST /v1/responses (issue #11):
  emit response.created / output_text.delta / response.completed events
- Fix provider-agnostic system prompt hint (issue #12): remove
  'OpenAI-compatible' wording so non-OpenAI models are not confused
- Add TextEncoder and ReadableStream to ESLint globals
- Add streaming integration tests (happy path, unknown model, session.error)
- Extract createSseQueue() helper, eliminating duplicated SSE queue pattern
  in /v1/chat/completions and /v1/responses streaming branches (closes #34)
- Add tests for GET /v1/models happy path, empty providers, and error path (closes #33)
- Add tests for POST /v1/responses: happy path, validation, streaming, session.error (closes #32)
- Fix package.json description to be provider-agnostic (closes #35)
- Add engines field declaring bun >=1.0.0 requirement (closes #35)
- Line coverage: 55% -> 89%, function coverage: 83% -> 94%
- POST /v1/messages — Anthropic Messages API with streaming (SSE)
- POST /v1beta/models/:model:generateContent — Gemini non-streaming
- POST /v1beta/models/:model:streamGenerateContent — Gemini NDJSON streaming
- New helpers: normalizeAnthropicMessages, normalizeGeminiContents,
  extractGeminiSystemInstruction, mapFinishReasonToAnthropic/Gemini
- 35 new tests (77 -> 112 total, all passing)
- Update README to document all supported API formats

Closes #38, #39
- Lead with value proposition, ASCII diagram, and feature table
- Quickstart reduced to 4 steps; works in under 60 seconds
- SDK examples for OpenAI, Anthropic, Gemini (JS+Python), LangChain
- UI integration guides: Open WebUI, Chatbox, Continue, Zed
- Reference section kept concise; full prose docs moved inline
- package.json: sharper description, 20 keywords covering all search terms
  (openai-compatible, anthropic, gemini, ollama, langchain, open-webui,
   llm-proxy, ai-gateway, local-llm, github-copilot, model-router, …)
The Anthropic Messages API accepts the top-level `system` field as
either a string OR an array of content blocks (per
https://docs.anthropic.com/en/api/messages). The /v1/messages handler
at index.js:1044-1047 only checks `typeof body.system === "string"`
and silently drops the array form. Clients that follow the spec see
their system prompt ignored by the proxy.

Add and export a `normalizeAnthropicSystem` helper that accepts
either form: for the array form, concatenates `type: "text"` content
blocks (skipping falsy entries, non-text types, and non-string
texts); returns null when no usable text is present so the call site
can skip adding an empty system message. Use it at the call site in
place of the inline string check.

Adds 3 regression tests in index.test.js covering:
- array-form system reaches buildSystemPrompt (discriminating)
- multi-block text arrays are concatenated
- helper edge cases (null/undefined, empty strings, non-text blocks,
  non-string/non-array inputs)

Closes #46
…sponses API spec (#49)

The /v1/responses streaming handler violates the OpenAI Responses API
SSE lifecycle spec in two ways:

1. response.content_part.done is never emitted. Per the spec
   (https://platform.openai.com/docs/api-reference/responses-streaming),
   the event sequence for a text content part should be:
     content_part.added -> output_text.delta* -> output_text.done
     -> content_part.done -> output_item.done

2. response.output_text.done is emitted with text: "" instead of the
   accumulated output text. The spec requires the final content.

Accumulate delta tokens in a local variable at the streaming call
site, emit the missing response.content_part.done event with the
accumulated text in part.text, and populate output_text.done.text
with the same accumulated content. Gate the new content_part.done
event on at least one delta having been received, keeping the
content-part added/done lifecycle symmetric.

Adds one regression test in index.test.js that asserts:
- output_text.done.text equals the accumulated deltas
- content_part.done event is present with part.text populated
- correct ordering (output_text.done < content_part.done < output_item.done)

Closes #48
* feat: add tool/function calling support (closes #50)

Adds OpenAI-style function tools, Anthropic tools, and Gemini function
declarations across all four API surfaces (/v1/chat/completions,
/v1/responses, /v1/messages, /v1beta/models/:model:generateContent),
both streaming and non-streaming.

OpenCode's own agent loop always executes tools itself, server-side,
and has no concept of a 'client-executed' tool call to hand off to a
caller. To bridge that gap:

- When a request includes tools, the proxy dynamically registers a
  small local MCP server (mcp-tool-bridge.js) whose tool list is
  exactly the caller's declared tool schemas, reused from a small
  fixed-size pool of slot names (OpenCode's server API has no endpoint
  to deregister an MCP server once added).
- Only those tools are enabled for that one prompt call via the
  existing tools enable/disable map; every built-in OpenCode tool
  stays disabled, same as before.
- As soon as the model proposes calling one of the bridge tools, the
  full call (name + arguments) is already present on OpenCode's event
  stream (ToolStatePending includes the parsed input even before
  execution starts) - the proxy captures it and immediately aborts the
  session before the bridge's no-op tools/call handler would ever be
  consulted, then translates the call into the caller's expected
  tool_calls / tool_use / functionCall shape instead of a text answer.

Also extends the OpenAI/Anthropic/Gemini/Responses message normalizers
to render prior tool_calls/tool_use/functionCall and their
results/tool_result/functionResponse as descriptive text when replaying
conversation history, so multi-turn tool use works end-to-end even
though sessions are stateless per-request.

- index.js: parseOpenAITools/parseAnthropicTools/parseGeminiTools +
  applyOpenAIToolChoice/applyAnthropicToolChoice/applyGeminiToolChoice,
  sanitizeToolName, tool bridge pool + registerToolBridge, unified
  runAgentTurn (event-driven turn execution shared by executePrompt
  and executePromptStreaming when tools are present), tool-call
  branches in all four response builders and SSE emitters.
- mcp-tool-bridge.js: minimal MCP stdio JSON-RPC server exposing
  caller-supplied tool schemas; tools/call is a harmless no-op since
  the proxy aborts the session before it would ever be consulted.
- index.test.js: unit tests for the new parse/tool_choice helpers and
  history round-tripping, plus end-to-end tool-calling tests for all
  four API formats (stream + non-stream).
- README.md: documents the new tools/tool_choice/toolConfig request
  fields, how the bridge mechanism works, its current limitations, and
  the new OPENCODE_LLM_PROXY_TOOL_BRIDGE_POOL_SIZE env var.

Testing:
- npm test — 138 passed (116 existing + 22 new)
- npm run lint — clean

* docs: feature tool calling prominently in README, expand discoverability keywords

- Move the Tool calling section up (right after Configuration) and add a
  runnable curl request/response example, instead of burying it near the
  bottom after How it works.
- Add a Contents section now that the README has grown to 10+ sections.
- Call out tool calling in the top-level tagline, architecture diagram,
  supported-formats table, and Why section (coding agents are now a
  first-class use case, not just chat clients).
- Note in Install that copying just index.js doesn't get you tool calling
  (needs mcp-tool-bridge.js alongside it) - use the npm plugin instead.
- package.json: mention tool/function calling in the description and add
  tool-calling/function-calling/tools/mcp/model-context-protocol/
  coding-agent/ai-agent/agentic keywords for npm search discoverability.

---------

Co-authored-by: Framewrk CI <ci@framewrklabs.ai>
@KochC
KochC merged commit 0811e1e into main Jul 4, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants