Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 62 additions & 5 deletions generators/go-v2/base/src/asIs/core/stream.go_
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,16 @@ func WithFormat(format StreamFormat) StreamOption {
}
}

// WithEventDiscriminator configures the SSE stream reader to inject the
// SSE event field value as a JSON discriminator into the data payload.
// This is used for protocol-level discrimination where the union discriminant
// comes from the SSE event: field rather than from within the JSON data.
func WithEventDiscriminator(field string) StreamOption {
return func(opts *streamOptions) {
opts.eventDiscriminator = field
}
}

// NewStream constructs a new Stream from the given *http.Response.
func NewStream[T any](response *http.Response, opts ...StreamOption) *Stream[T] {
options := new(streamOptions)
Expand Down Expand Up @@ -227,11 +237,12 @@ func (s *ScannerStreamReader) isTerminated(bytes []byte) bool {
}

type streamOptions struct {
delimiter string
prefix string
terminator string
format StreamFormat
maxBufSize int
delimiter string
prefix string
terminator string
format StreamFormat
maxBufSize int
eventDiscriminator string
}

func (s *streamOptions) isEmpty() bool {
Expand Down Expand Up @@ -297,6 +308,11 @@ func (s *SseStreamReader) ReadFromStream() ([]byte, error) {
if s.isTerminated(event.data) {
return nil, io.EOF
}
// For protocol-level discrimination, inject the SSE event field value
// as the discriminator key into the JSON data payload.
if s.options.eventDiscriminator != "" && len(event.event) > 0 {
event.data = injectDiscriminator(event.data, s.options.eventDiscriminator, string(event.event))
}
return event.data, nil
}

Expand Down Expand Up @@ -363,6 +379,47 @@ func (event *SseEvent) String() string {
return fmt.Sprintf("SseEvent{id: %q, event: %q, data: %q, retry: %q}", event.id, event.event, event.data, event.retry)
}

// injectDiscriminator inserts a JSON key-value pair for the discriminator
// at the beginning of a JSON object. For example, given data `{"content":"Hello"}`,
// field "type", and value "completion", it produces `{"type":"completion","content":"Hello"}`.
//
// If the data already contains the discriminator key, it is returned unchanged
// to avoid duplicate keys.
func injectDiscriminator(data []byte, field string, value string) []byte {
trimmed := bytes.TrimSpace(data)
if len(trimmed) == 0 || trimmed[0] != '{' {
return data
}
// Skip injection if the key already exists in the data.
quotedField := fmt.Sprintf("%q", field)
if bytes.Contains(data, []byte(quotedField+":")) || bytes.Contains(data, []byte(quotedField+" :")) {
return data
}
// Build the injected key-value: "field":"value"
injected := quotedField + ":" + fmt.Sprintf("%q", value)
// Find the opening brace in the original data
openIdx := bytes.IndexByte(data, '{')
after := data[openIdx+1:]
// Check if the object has existing content (non-empty after trimming)
afterTrimmed := bytes.TrimSpace(after)
var result []byte
if len(afterTrimmed) == 0 || afterTrimmed[0] == '}' {
// Empty object: {"field":"value"}
result = make([]byte, 0, len(data)+len(injected))
result = append(result, data[:openIdx+1]...)
result = append(result, injected...)
result = append(result, after...)
} else {
// Non-empty object: {"field":"value",<existing>}
result = make([]byte, 0, len(data)+len(injected)+1)
result = append(result, data[:openIdx+1]...)
result = append(result, injected...)
result = append(result, ',')
result = append(result, after...)
}
return result
}

type SseEvent struct {
id []byte
data []byte
Expand Down
30 changes: 17 additions & 13 deletions generators/go-v2/base/src/asIs/internal/streamer.go_
Original file line number Diff line number Diff line change
Expand Up @@ -32,19 +32,20 @@ func NewStreamer[T any](caller *Caller) *Streamer[T] {

// StreamParams represents the parameters used to issue an API streaming call.
type StreamParams struct {
URL string
Method string
Prefix string
Delimiter string
Terminator string
MaxAttempts uint
Headers http.Header
BodyProperties map[string]interface{}
QueryParameters url.Values
Client HTTPClient
Request interface{}
ErrorDecoder ErrorDecoder
Format core.StreamFormat
URL string
Method string
Prefix string
Delimiter string
Terminator string
MaxAttempts uint
Headers http.Header
BodyProperties map[string]interface{}
QueryParameters url.Values
Client HTTPClient
Request interface{}
ErrorDecoder ErrorDecoder
Format core.StreamFormat
EventDiscriminator string
}

// Stream issues an API streaming call according to the given stream parameters.
Expand Down Expand Up @@ -113,6 +114,9 @@ func (s *Streamer[T]) Stream(ctx context.Context, params *StreamParams) (*core.S
if params.Format != core.StreamFormatEmpty {
opts = append(opts, core.WithFormat(params.Format))
}
if params.EventDiscriminator != "" {
opts = append(opts, core.WithEventDiscriminator(params.EventDiscriminator))
}

return core.NewStream[T](resp, opts...), nil
}
29 changes: 29 additions & 0 deletions generators/go-v2/sdk/src/internal/Streamer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,13 @@ export class Streamer {
value: format
});
}
const eventDiscriminator = this.getEventDiscriminator(args.streamingResponse);
if (eventDiscriminator != null) {
arguments_.push({
name: "EventDiscriminator",
value: eventDiscriminator
});
}
if (args.request != null) {
arguments_.push({
name: "Request",
Expand Down Expand Up @@ -239,4 +246,26 @@ export class Streamer {
importPath: this.context.getCoreImportPath()
});
}

private getEventDiscriminator(streamingResponse: FernIr.StreamingResponse): go.TypeInstantiation | undefined {
if (streamingResponse.type !== "sse") {
return undefined;
}
const payload = streamingResponse.payload;
if (payload.type !== "named") {
return undefined;
}
const typeDeclaration = this.context.getTypeDeclarationOrThrow(payload.typeId);
if (typeDeclaration.shape.type !== "union") {
return undefined;
}
const union = typeDeclaration.shape;
if (
!("discriminatorContext" in union) ||
(union as { discriminatorContext?: string }).discriminatorContext !== "protocol"
) {
return undefined;
}
return go.TypeInstantiation.string(union.discriminant.wireValue);
}
}
7 changes: 4 additions & 3 deletions generators/go/internal/fern/ir/types.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 14 additions & 0 deletions generators/go/sdk/versions.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,18 @@
# yaml-language-server: $schema=../../../fern-versions-yml.schema.json
- version: 1.24.0
changelogEntry:
- summary: |
Add support for protocol-level SSE discrimination via the new `discriminatorContext`
IR field. When a streaming endpoint returns a discriminated union with
`discriminatorContext: "protocol"`, the SSE `event:` field value is injected into
the JSON data payload as the discriminant key before unmarshaling, so the union's
existing `UnmarshalJSON` works unchanged. If the data already contains the
discriminant key, injection is skipped. Existing SSE endpoints using data-level
discrimination (the default) are unaffected.
type: feat
createdAt: "2026-02-13"
irVersion: 61

- version: 1.23.7
changelogEntry:
- summary: |
Expand Down
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
// service_completions
"/stream"
"/stream"
"/stream-events"
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"type": "object",
"properties": {
"content": {
"type": "string"
}
},
"required": [
"content"
],
"additionalProperties": false,
"definitions": {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"type": "object",
"properties": {
"error": {
"type": "string"
},
"code": {
"oneOf": [
{
"type": "integer"
},
{
"type": "null"
}
]
}
},
"required": [
"error"
],
"additionalProperties": false,
"definitions": {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
{
"type": "object",
"properties": {
"event": {
"type": "string",
"enum": [
"completion",
"error"
]
}
},
"oneOf": [
{
"properties": {
"event": {
"const": "completion"
},
"content": {
"type": "string"
}
},
"required": [
"event",
"content"
]
},
{
"properties": {
"event": {
"const": "error"
},
"error": {
"type": "string"
},
"code": {
"oneOf": [
{
"type": "integer"
},
{
"type": "null"
}
]
}
},
"required": [
"event",
"error"
]
}
],
"definitions": {}
}
Loading
Loading