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

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions