Skip to content

Support for conditional response types based on request body field (OpenResponses API pattern) #1377

@tristanz

Description

@tristanz

Summary

I'm trying to implement the OpenResponses API specification using oRPC, but I've hit a limitation: the API uses a single endpoint that returns different response types based on a stream field in the request body.

The OpenResponses Pattern

The spec defines a single endpoint POST /responses where:

  • stream: false (or absent) → Returns application/json with ResponseResource
  • stream: true → Returns text/event-stream with SSE streaming events

OpenAPI Spec (from official spec)

{
  "paths": {
    "/responses": {
      "post": {
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateResponseBody"
              }
            }
          }
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ResponseResource" }
              },
              "text/event-stream": {
                "schema": {
                  "oneOf": [
                    { "$ref": "#/components/schemas/ResponseCreatedStreamingEvent" },
                    { "$ref": "#/components/schemas/ResponseCompletedStreamingEvent" }
                  ]
                }
              }
            }
          }
        }
      }
    }
  }
}

Full spec: https://github.com/openresponses/openresponses/blob/main/public/openapi/openapi.json

The Challenge with oRPC

oRPC's contract model assumes one procedure → one output type:

// This works for separate endpoints
const nonStreaming = oc
  .route({ method: 'POST', path: '/responses' })
  .input(CreateResponseBody)
  .output(ResponseResource)

const streaming = oc
  .route({ method: 'POST', path: '/responses/stream' })
  .input(CreateResponseBody)
  .output(eventIterator(StreamingEvent))

But I can't define a single procedure that:

  1. Has one path (/responses)
  2. Returns ResponseResource when input.stream === false
  3. Returns AsyncIterator<StreamingEvent> when input.stream === true

Attempted Workarounds

  1. Two procedures, same path - oRPC doesn't allow this (path conflict)
  2. Union output type - Can't union a value type with an iterator
  3. Custom handler logic - Loses type safety on the output

Proposed Solutions

Option A: Conditional output based on input field

const createResponse = oc
  .route({ method: 'POST', path: '/responses' })
  .input(CreateResponseBody)
  .output(ResponseResource, { when: (input) => !input.stream })
  .output(eventIterator(StreamingEvent), { when: (input) => input.stream })

Option B: Multiple media type outputs

const createResponse = oc
  .route({ method: 'POST', path: '/responses' })
  .input(CreateResponseBody)
  .output({
    'application/json': ResponseResource,
    'text/event-stream': eventIterator(StreamingEvent),
  })

Option C: Output type that can be either value or iterator

const createResponse = oc
  .route({ method: 'POST', path: '/responses' })
  .input(CreateResponseBody)
  .output(z.union([ResponseResource, eventIterator(StreamingEvent)]))

Context

This pattern is common in LLM APIs:

  • OpenAI uses it for /chat/completions
  • Anthropic uses it for /messages
  • OpenResponses standardizes it

Being able to implement this pattern with oRPC would enable type-safe implementations of these popular API patterns.

Environment

  • oRPC version: 1.4.3
  • Target: Cloudflare Workers (fetch adapter)

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions