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
1 change: 1 addition & 0 deletions packages/open_responses/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ Dart client for the **[OpenResponses specification](https://www.openresponses.or
| [Databricks](https://docs.databricks.com/aws/en/machine-learning/model-serving/score-model-serving-endpoints) | `https://<host>.databricks.com/serving-endpoints` | Bearer token | Enterprise model serving |
| [vLLM](https://docs.vllm.ai/en/latest/examples/online_serving/openai_responses_client/) | `http://localhost:8000/v1` | None by default | High-throughput local inference |
| LM Studio | `http://localhost:1234/v1` | None by default | Desktop local inference |
| [llama.cpp](https://github.com/ggml-org/llama.cpp/tree/master/tools/server) | `http://localhost:8080/v1` | None by default | Local GGUF inference via `llama-server` |

## Why choose this client?

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,9 @@ class OutputItemAddedEvent extends StreamingEvent {
final int sequenceNumber;

/// The index of the output item.
///
/// Defaults to 0 if not provided by the server, for compatibility with
/// providers that don't include this field in their responses.
final int outputIndex;

/// The added item.
Expand All @@ -494,7 +497,7 @@ class OutputItemAddedEvent extends StreamingEvent {
factory OutputItemAddedEvent.fromJson(Map<String, dynamic> json) {
return OutputItemAddedEvent(
sequenceNumber: json['sequence_number'] as int? ?? 0,
outputIndex: json['output_index'] as int,
outputIndex: json['output_index'] as int? ?? 0,
item: OutputItem.fromJson(json['item'] as Map<String, dynamic>),
);
}
Expand Down Expand Up @@ -563,7 +566,7 @@ class OutputItemDoneEvent extends StreamingEvent {
factory OutputItemDoneEvent.fromJson(Map<String, dynamic> json) {
return OutputItemDoneEvent(
sequenceNumber: json['sequence_number'] as int? ?? 0,
outputIndex: json['output_index'] as int,
outputIndex: json['output_index'] as int? ?? 0,
item: OutputItem.fromJson(json['item'] as Map<String, dynamic>),
);
}
Expand Down Expand Up @@ -626,6 +629,9 @@ class ContentPartAddedEvent extends StreamingEvent {
final int outputIndex;

/// The content index.
///
/// Defaults to 0 if not provided by the server, for compatibility with
/// providers that don't include this field in their responses.
final int contentIndex;

/// The added content part.
Expand All @@ -645,8 +651,8 @@ class ContentPartAddedEvent extends StreamingEvent {
return ContentPartAddedEvent(
sequenceNumber: json['sequence_number'] as int? ?? 0,
itemId: json['item_id'] as String,
outputIndex: json['output_index'] as int,
contentIndex: json['content_index'] as int,
outputIndex: json['output_index'] as int? ?? 0,
contentIndex: json['content_index'] as int? ?? 0,
part: OutputContent.fromJson(json['part'] as Map<String, dynamic>),
);
}
Expand Down Expand Up @@ -733,8 +739,8 @@ class ContentPartDoneEvent extends StreamingEvent {
return ContentPartDoneEvent(
sequenceNumber: json['sequence_number'] as int? ?? 0,
itemId: json['item_id'] as String,
outputIndex: json['output_index'] as int,
contentIndex: json['content_index'] as int,
outputIndex: json['output_index'] as int? ?? 0,
contentIndex: json['content_index'] as int? ?? 0,
part: OutputContent.fromJson(json['part'] as Map<String, dynamic>),
);
}
Expand Down Expand Up @@ -833,8 +839,8 @@ class OutputTextDeltaEvent extends StreamingEvent {
return OutputTextDeltaEvent(
sequenceNumber: json['sequence_number'] as int? ?? 0,
itemId: json['item_id'] as String,
outputIndex: json['output_index'] as int,
contentIndex: json['content_index'] as int,
outputIndex: json['output_index'] as int? ?? 0,
contentIndex: json['content_index'] as int? ?? 0,
delta: json['delta'] as String,
logprobs:
(json['logprobs'] as List?)
Expand Down Expand Up @@ -949,8 +955,8 @@ class OutputTextDoneEvent extends StreamingEvent {
return OutputTextDoneEvent(
sequenceNumber: json['sequence_number'] as int? ?? 0,
itemId: json['item_id'] as String,
outputIndex: json['output_index'] as int,
contentIndex: json['content_index'] as int,
outputIndex: json['output_index'] as int? ?? 0,
contentIndex: json['content_index'] as int? ?? 0,
text: json['text'] as String,
logprobs:
(json['logprobs'] as List?)
Expand Down Expand Up @@ -1037,6 +1043,9 @@ class OutputTextAnnotationAddedEvent extends StreamingEvent {
final int contentIndex;

/// The annotation index.
///
/// Defaults to 0 if not provided by the server, for compatibility with
/// providers that don't include this field in their responses.
final int annotationIndex;

/// The added annotation.
Expand All @@ -1057,9 +1066,9 @@ class OutputTextAnnotationAddedEvent extends StreamingEvent {
return OutputTextAnnotationAddedEvent(
sequenceNumber: json['sequence_number'] as int? ?? 0,
itemId: json['item_id'] as String,
outputIndex: json['output_index'] as int,
contentIndex: json['content_index'] as int,
annotationIndex: json['annotation_index'] as int,
outputIndex: json['output_index'] as int? ?? 0,
contentIndex: json['content_index'] as int? ?? 0,
annotationIndex: json['annotation_index'] as int? ?? 0,
annotation: Annotation.fromJson(
json['annotation'] as Map<String, dynamic>,
),
Expand Down Expand Up @@ -1162,8 +1171,8 @@ class RefusalDeltaEvent extends StreamingEvent {
return RefusalDeltaEvent(
sequenceNumber: json['sequence_number'] as int? ?? 0,
itemId: json['item_id'] as String,
outputIndex: json['output_index'] as int,
contentIndex: json['content_index'] as int,
outputIndex: json['output_index'] as int? ?? 0,
contentIndex: json['content_index'] as int? ?? 0,
delta: json['delta'] as String,
);
}
Expand Down Expand Up @@ -1250,8 +1259,8 @@ class RefusalDoneEvent extends StreamingEvent {
return RefusalDoneEvent(
sequenceNumber: json['sequence_number'] as int? ?? 0,
itemId: json['item_id'] as String,
outputIndex: json['output_index'] as int,
contentIndex: json['content_index'] as int,
outputIndex: json['output_index'] as int? ?? 0,
contentIndex: json['content_index'] as int? ?? 0,
refusal: json['refusal'] as String,
);
}
Expand Down Expand Up @@ -1342,7 +1351,7 @@ class FunctionCallArgumentsDeltaEvent extends StreamingEvent {
return FunctionCallArgumentsDeltaEvent(
sequenceNumber: json['sequence_number'] as int? ?? 0,
itemId: json['item_id'] as String,
outputIndex: json['output_index'] as int,
outputIndex: json['output_index'] as int? ?? 0,
delta: json['delta'] as String,
obfuscation: json['obfuscation'] as String?,
);
Expand Down Expand Up @@ -1428,7 +1437,7 @@ class FunctionCallArgumentsDoneEvent extends StreamingEvent {
return FunctionCallArgumentsDoneEvent(
sequenceNumber: json['sequence_number'] as int? ?? 0,
itemId: json['item_id'] as String,
outputIndex: json['output_index'] as int,
outputIndex: json['output_index'] as int? ?? 0,
arguments: json['arguments'] as String,
);
}
Expand Down Expand Up @@ -1519,8 +1528,8 @@ class ReasoningDeltaEvent extends StreamingEvent {
return ReasoningDeltaEvent(
sequenceNumber: json['sequence_number'] as int? ?? 0,
itemId: json['item_id'] as String,
outputIndex: json['output_index'] as int,
contentIndex: json['content_index'] as int,
outputIndex: json['output_index'] as int? ?? 0,
contentIndex: json['content_index'] as int? ?? 0,
delta: json['delta'] as String,
obfuscation: json['obfuscation'] as String?,
);
Expand Down Expand Up @@ -1620,8 +1629,8 @@ class ReasoningDoneEvent extends StreamingEvent {
return ReasoningDoneEvent(
sequenceNumber: json['sequence_number'] as int? ?? 0,
itemId: json['item_id'] as String,
outputIndex: json['output_index'] as int,
contentIndex: json['content_index'] as int,
outputIndex: json['output_index'] as int? ?? 0,
contentIndex: json['content_index'] as int? ?? 0,
text: json['text'] as String,
);
}
Expand Down Expand Up @@ -1689,6 +1698,9 @@ class ReasoningSummaryPartAddedEvent extends StreamingEvent {
final int outputIndex;

/// The summary index.
///
/// Defaults to 0 if not provided by the server, for compatibility with
/// providers that don't include this field in their responses.
final int summaryIndex;

/// The summary part, if available.
Expand All @@ -1708,8 +1720,8 @@ class ReasoningSummaryPartAddedEvent extends StreamingEvent {
return ReasoningSummaryPartAddedEvent(
sequenceNumber: json['sequence_number'] as int? ?? 0,
itemId: json['item_id'] as String,
outputIndex: json['output_index'] as int,
summaryIndex: json['summary_index'] as int,
outputIndex: json['output_index'] as int? ?? 0,
summaryIndex: json['summary_index'] as int? ?? 0,
part: json['part'] != null
? ReasoningSummaryContent.fromJson(
json['part'] as Map<String, dynamic>,
Expand Down Expand Up @@ -1802,8 +1814,8 @@ class ReasoningSummaryPartDoneEvent extends StreamingEvent {
return ReasoningSummaryPartDoneEvent(
sequenceNumber: json['sequence_number'] as int? ?? 0,
itemId: json['item_id'] as String,
outputIndex: json['output_index'] as int,
summaryIndex: json['summary_index'] as int,
outputIndex: json['output_index'] as int? ?? 0,
summaryIndex: json['summary_index'] as int? ?? 0,
part: ReasoningSummaryContent.fromJson(
json['part'] as Map<String, dynamic>,
),
Expand Down
100 changes: 100 additions & 0 deletions packages/open_responses/test/unit/models/streaming_event_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,106 @@ void main() {
expect(event.isFinal, isFalse);
});

group('llama.cpp / minimal-server compatibility', () {
// Some OpenAI-compatible servers (e.g. llama.cpp) omit the positional
// index fields that the spec marks as required. fromJson must tolerate
// their absence by defaulting to 0 instead of throwing. See issue #242.
test('output_text.delta without index fields defaults to 0', () {
final json = {
'type': 'response.output_text.delta',
'item_id': 'msg_001',
'delta': 'Hello',
};

final event = StreamingEvent.fromJson(json);

expect(event, isA<OutputTextDeltaEvent>());
final delta = event as OutputTextDeltaEvent;
expect(delta.sequenceNumber, 0);
expect(delta.outputIndex, 0);
expect(delta.contentIndex, 0);
expect(delta.delta, 'Hello');
});

test('content_part.added without index fields defaults to 0', () {
final json = {
'type': 'response.content_part.added',
'item_id': 'msg_001',
'part': {
'type': 'output_text',
'text': 'Hello',
'annotations': <dynamic>[],
},
};

final event = StreamingEvent.fromJson(json);

expect(event, isA<ContentPartAddedEvent>());
final part = event as ContentPartAddedEvent;
expect(part.outputIndex, 0);
expect(part.contentIndex, 0);
expect(part.itemId, 'msg_001');
});

test('output_item.added without output_index defaults to 0', () {
final json = {
'type': 'response.output_item.added',
'item': {'type': 'reasoning', 'id': 'rs_001', 'summary': <dynamic>[]},
};

final event = StreamingEvent.fromJson(json);

expect(event, isA<OutputItemAddedEvent>());
final item = event as OutputItemAddedEvent;
expect(item.outputIndex, 0);
expect(item.item, isA<ReasoningItem>());
});

test(
'output_text.annotation.added without index fields defaults to 0',
() {
final json = {
'type': 'response.output_text.annotation.added',
'item_id': 'msg_001',
'annotation': {
'type': 'url_citation',
'start_index': 0,
'end_index': 5,
'url': 'https://example.com',
'title': 'Example',
},
};

final event = StreamingEvent.fromJson(json);

expect(event, isA<OutputTextAnnotationAddedEvent>());
final annotation = event as OutputTextAnnotationAddedEvent;
expect(annotation.outputIndex, 0);
expect(annotation.contentIndex, 0);
expect(annotation.annotationIndex, 0);
expect(annotation.annotation, isA<UrlCitation>());
},
);

test(
'reasoning_summary_part.added without index fields defaults to 0',
() {
final json = {
'type': 'response.reasoning_summary_part.added',
'item_id': 'rs_001',
};

final event = StreamingEvent.fromJson(json);

expect(event, isA<ReasoningSummaryPartAddedEvent>());
final part = event as ReasoningSummaryPartAddedEvent;
expect(part.outputIndex, 0);
expect(part.summaryIndex, 0);
expect(part.itemId, 'rs_001');
},
);
});
Comment thread
davidmigloz marked this conversation as resolved.

group('reasoning_text aliases', () {
test('response.reasoning_text.delta parses to ReasoningDeltaEvent', () {
final json = {
Expand Down