mirror of
https://github.com/hanzoai/go-openai-realtime.git
synced 2026-09-30 21:54:42 +00:00
feat: add all client events & server events
This commit is contained in:
@@ -23,3 +23,6 @@ go.work.sum
|
||||
|
||||
# env file
|
||||
.env
|
||||
|
||||
.vscode
|
||||
.DS_Store
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
package openairt
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/coder/websocket"
|
||||
)
|
||||
|
||||
const (
|
||||
GPT4oRealtimePreview = "gpt-4o-realtime-preview"
|
||||
GPT4oRealtimePreview20241001 = "gpt-4o-realtime-preview-2024-10-01"
|
||||
)
|
||||
|
||||
// Client is OpenAI Realtime API client.
|
||||
type Client struct {
|
||||
config ClientConfig
|
||||
}
|
||||
|
||||
// NewClient creates new OpenAI Realtime API client.
|
||||
func NewClient(authToken string) *Client {
|
||||
config := DefaultConfig(authToken)
|
||||
return NewClientWithConfig(config)
|
||||
}
|
||||
|
||||
// NewClientWithConfig creates new OpenAI Realtime API client for specified config.
|
||||
func NewClientWithConfig(config ClientConfig) *Client {
|
||||
return &Client{
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) getUrl(model string) string {
|
||||
query := url.Values{}
|
||||
|
||||
if c.config.APIType == APITypeAzure {
|
||||
query.Set("api-version", c.config.APIVersion)
|
||||
query.Set("deployment", model)
|
||||
} else {
|
||||
query.Set("model", model)
|
||||
}
|
||||
|
||||
return c.config.BaseURL + "?" + query.Encode()
|
||||
}
|
||||
|
||||
func (c *Client) getHeaders() http.Header {
|
||||
headers := http.Header{}
|
||||
|
||||
if c.config.APIType == APITypeAzure {
|
||||
headers.Set("api-key", c.config.authToken)
|
||||
} else {
|
||||
headers.Set("Authorization", "Bearer "+c.config.authToken)
|
||||
headers.Set("OpenAI-Beta", "realtime=v1")
|
||||
}
|
||||
return headers
|
||||
}
|
||||
|
||||
type connectOption struct {
|
||||
model string
|
||||
dialOptions *websocket.DialOptions
|
||||
readLimit int64
|
||||
}
|
||||
|
||||
type ConnectOption func(*connectOption)
|
||||
|
||||
func WithModel(model string) ConnectOption {
|
||||
return func(opts *connectOption) {
|
||||
opts.model = model
|
||||
}
|
||||
}
|
||||
|
||||
func WithDialOptions(dialOptions *websocket.DialOptions) ConnectOption {
|
||||
return func(opts *connectOption) {
|
||||
opts.dialOptions = dialOptions
|
||||
}
|
||||
}
|
||||
|
||||
func WithReadLimit(limit int64) ConnectOption {
|
||||
return func(opts *connectOption) {
|
||||
opts.readLimit = limit
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) Connect(ctx context.Context, opts ...ConnectOption) (*Conn, error) {
|
||||
connectOpts := connectOption{
|
||||
model: GPT4oRealtimePreview,
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt(&connectOpts)
|
||||
}
|
||||
|
||||
// default headers
|
||||
headers := c.getHeaders()
|
||||
|
||||
// dialOptions
|
||||
if connectOpts.dialOptions != nil {
|
||||
for k, v := range connectOpts.dialOptions.HTTPHeader {
|
||||
headers[k] = append(headers[k], v...)
|
||||
}
|
||||
connectOpts.dialOptions.HTTPHeader = headers
|
||||
} else {
|
||||
connectOpts.dialOptions = &websocket.DialOptions{
|
||||
HTTPHeader: headers,
|
||||
}
|
||||
}
|
||||
|
||||
// get url by model
|
||||
url := c.getUrl(connectOpts.model)
|
||||
|
||||
// dial
|
||||
conn, _, err := websocket.Dial(ctx, url, connectOpts.dialOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// set readLimit
|
||||
if connectOpts.readLimit > 0 {
|
||||
conn.SetReadLimit(connectOpts.readLimit)
|
||||
}
|
||||
return &Conn{conn: conn}, nil
|
||||
}
|
||||
+243
@@ -0,0 +1,243 @@
|
||||
package openairt
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
type ClientEventType string
|
||||
|
||||
const (
|
||||
ClientEventTypeSessionUpdate ClientEventType = "session.update"
|
||||
ClientEventTypeInputAudioBufferAppend ClientEventType = "input_audio_buffer.append"
|
||||
ClientEventTypeInputAudioBufferCommit ClientEventType = "input_audio_buffer.commit"
|
||||
ClientEventTypeInputAudioBufferClear ClientEventType = "input_audio_buffer.clear"
|
||||
ClientEventTypeConversationItemCreate ClientEventType = "conversation.item.create"
|
||||
ClientEventTypeConversationItemTruncate ClientEventType = "conversation.item.truncate"
|
||||
ClientEventTypeConversationItemDelete ClientEventType = "conversation.item.delete"
|
||||
ClientEventTypeResponseCreate ClientEventType = "response.create"
|
||||
ClientEventTypeResponseCancel ClientEventType = "response.cancel"
|
||||
)
|
||||
|
||||
type ClientEvent interface {
|
||||
ClientEventType() ClientEventType
|
||||
}
|
||||
|
||||
type EventBase struct {
|
||||
EventID string `json:"event_id,omitempty"`
|
||||
}
|
||||
|
||||
type ClientSession struct {
|
||||
Modalities []Modality `json:"modalities,omitempty"`
|
||||
Instructions string `json:"instructions,omitempty"`
|
||||
Voice Voice `json:"voice,omitempty"`
|
||||
InputAudioFormat AudioFormat `json:"input_audio_format,omitempty"`
|
||||
OutputAudioFormat AudioFormat `json:"output_audio_format,omitempty"`
|
||||
InputAudioTranscription *InputAudioTranscription `json:"input_audio_transcription,omitempty"`
|
||||
TurnDetection *TurnDetection `json:"turn_detection,omitempty"`
|
||||
Tools []Tool `json:"tools,omitempty"`
|
||||
ToolChoice ToolChoiceInterface `json:"tool_choice,omitempty"`
|
||||
Temperature *float32 `json:"temperature,omitempty"`
|
||||
MaxOutputTokens int `json:"max_output_tokens,omitempty"`
|
||||
}
|
||||
|
||||
type SessionUpdateEvent struct {
|
||||
EventBase
|
||||
Session ClientSession `json:"session"`
|
||||
}
|
||||
|
||||
func (m SessionUpdateEvent) ClientEventType() ClientEventType {
|
||||
return ClientEventTypeSessionUpdate
|
||||
}
|
||||
|
||||
func (m SessionUpdateEvent) MarshalJSON() ([]byte, error) {
|
||||
type sessionUpdateEvent SessionUpdateEvent
|
||||
v := struct {
|
||||
*sessionUpdateEvent
|
||||
Type ClientEventType `json:"type"`
|
||||
}{
|
||||
sessionUpdateEvent: (*sessionUpdateEvent)(&m),
|
||||
Type: m.ClientEventType(),
|
||||
}
|
||||
return json.Marshal(v)
|
||||
}
|
||||
|
||||
type InputAudioBufferAppendEvent struct {
|
||||
EventBase
|
||||
Audio string `json:"audio"`
|
||||
}
|
||||
|
||||
func (m InputAudioBufferAppendEvent) ClientEventType() ClientEventType {
|
||||
return ClientEventTypeInputAudioBufferAppend
|
||||
}
|
||||
|
||||
func (m InputAudioBufferAppendEvent) MarshalJSON() ([]byte, error) {
|
||||
type inputAudioBufferAppendEvent InputAudioBufferAppendEvent
|
||||
v := struct {
|
||||
*inputAudioBufferAppendEvent
|
||||
Type ClientEventType `json:"type"`
|
||||
}{
|
||||
inputAudioBufferAppendEvent: (*inputAudioBufferAppendEvent)(&m),
|
||||
Type: m.ClientEventType(),
|
||||
}
|
||||
return json.Marshal(v)
|
||||
}
|
||||
|
||||
type InputAudioBufferCommitEvent struct {
|
||||
EventBase
|
||||
}
|
||||
|
||||
func (m InputAudioBufferCommitEvent) ClientEventType() ClientEventType {
|
||||
return ClientEventTypeInputAudioBufferCommit
|
||||
}
|
||||
|
||||
func (m InputAudioBufferCommitEvent) MarshalJSON() ([]byte, error) {
|
||||
type inputAudioBufferCommitEvent InputAudioBufferCommitEvent
|
||||
v := struct {
|
||||
*inputAudioBufferCommitEvent
|
||||
Type ClientEventType `json:"type"`
|
||||
}{
|
||||
inputAudioBufferCommitEvent: (*inputAudioBufferCommitEvent)(&m),
|
||||
Type: m.ClientEventType(),
|
||||
}
|
||||
return json.Marshal(v)
|
||||
}
|
||||
|
||||
type InputAudioBufferClearEvent struct {
|
||||
EventBase
|
||||
}
|
||||
|
||||
func (m InputAudioBufferClearEvent) ClientEventType() ClientEventType {
|
||||
return ClientEventTypeInputAudioBufferClear
|
||||
}
|
||||
|
||||
func (m InputAudioBufferClearEvent) MarshalJSON() ([]byte, error) {
|
||||
type inputAudioBufferClearEvent InputAudioBufferClearEvent
|
||||
v := struct {
|
||||
*inputAudioBufferClearEvent
|
||||
Type ClientEventType `json:"type"`
|
||||
}{
|
||||
inputAudioBufferClearEvent: (*inputAudioBufferClearEvent)(&m),
|
||||
Type: m.ClientEventType(),
|
||||
}
|
||||
return json.Marshal(v)
|
||||
}
|
||||
|
||||
type ConversationItemCreateEvent struct {
|
||||
EventBase
|
||||
PreviousItemID string `json:"previous_item_id,omitempty"`
|
||||
Item MessageItem `json:"item"`
|
||||
}
|
||||
|
||||
func (m ConversationItemCreateEvent) ClientEventType() ClientEventType {
|
||||
return ClientEventTypeConversationItemCreate
|
||||
}
|
||||
|
||||
func (m ConversationItemCreateEvent) MarshalJSON() ([]byte, error) {
|
||||
type conversationItemCreateEvent ConversationItemCreateEvent
|
||||
v := struct {
|
||||
*conversationItemCreateEvent
|
||||
Type ClientEventType `json:"type"`
|
||||
}{
|
||||
conversationItemCreateEvent: (*conversationItemCreateEvent)(&m),
|
||||
Type: m.ClientEventType(),
|
||||
}
|
||||
return json.Marshal(v)
|
||||
}
|
||||
|
||||
type ConversationItemTruncateEvent struct {
|
||||
EventBase
|
||||
ItemID string `json:"item_id"`
|
||||
ContentIndex int `json:"content_index"`
|
||||
AudioEndMs int `json:"audio_end_ms"`
|
||||
}
|
||||
|
||||
func (m ConversationItemTruncateEvent) ClientEventType() ClientEventType {
|
||||
return ClientEventTypeConversationItemTruncate
|
||||
}
|
||||
|
||||
func (m ConversationItemTruncateEvent) MarshalJSON() ([]byte, error) {
|
||||
type conversationItemTruncateEvent ConversationItemTruncateEvent
|
||||
v := struct {
|
||||
*conversationItemTruncateEvent
|
||||
Type ClientEventType `json:"type"`
|
||||
}{
|
||||
conversationItemTruncateEvent: (*conversationItemTruncateEvent)(&m),
|
||||
Type: m.ClientEventType(),
|
||||
}
|
||||
return json.Marshal(v)
|
||||
}
|
||||
|
||||
type ConversationItemDeleteEvent struct {
|
||||
EventBase
|
||||
ItemID string `json:"item_id"`
|
||||
}
|
||||
|
||||
func (m ConversationItemDeleteEvent) ClientEventType() ClientEventType {
|
||||
return ClientEventTypeConversationItemDelete
|
||||
}
|
||||
|
||||
func (m ConversationItemDeleteEvent) MarshalJSON() ([]byte, error) {
|
||||
type conversationItemDeleteEvent ConversationItemDeleteEvent
|
||||
v := struct {
|
||||
*conversationItemDeleteEvent
|
||||
Type ClientEventType `json:"type"`
|
||||
}{
|
||||
conversationItemDeleteEvent: (*conversationItemDeleteEvent)(&m),
|
||||
Type: m.ClientEventType(),
|
||||
}
|
||||
return json.Marshal(v)
|
||||
}
|
||||
|
||||
type ResponseCreateParams struct {
|
||||
Modalities []Modality `json:"modalities,omitempty"`
|
||||
Instructions string `json:"instructions,omitempty"`
|
||||
Voice Voice `json:"voice,omitempty"`
|
||||
OutputAudioFormat AudioFormat `json:"output_audio_format,omitempty"`
|
||||
Tools []Tool `json:"tools,omitempty"`
|
||||
ToolChoice ToolChoiceInterface `json:"tool_choice,omitempty"`
|
||||
Temperature *float32 `json:"temperature,omitempty"`
|
||||
MaxOutputTokens int `json:"max_output_tokens,omitempty"`
|
||||
}
|
||||
|
||||
type ResponseCreateEvent struct {
|
||||
EventBase
|
||||
Response ResponseCreateParams `json:"response"`
|
||||
}
|
||||
|
||||
func (m ResponseCreateEvent) ClientEventType() ClientEventType {
|
||||
return ClientEventTypeResponseCreate
|
||||
}
|
||||
|
||||
func (m ResponseCreateEvent) MarshalJSON() ([]byte, error) {
|
||||
type responseCreateEvent ResponseCreateEvent
|
||||
v := struct {
|
||||
*responseCreateEvent
|
||||
Type ClientEventType `json:"type"`
|
||||
}{
|
||||
responseCreateEvent: (*responseCreateEvent)(&m),
|
||||
Type: m.ClientEventType(),
|
||||
}
|
||||
return json.Marshal(v)
|
||||
}
|
||||
|
||||
type ResponseCancelEvent struct {
|
||||
EventBase
|
||||
}
|
||||
|
||||
func (m ResponseCancelEvent) ClientEventType() ClientEventType {
|
||||
return ClientEventTypeResponseCancel
|
||||
}
|
||||
|
||||
func (m ResponseCancelEvent) MarshalJSON() ([]byte, error) {
|
||||
type responseCancelEvent ResponseCancelEvent
|
||||
v := struct {
|
||||
*responseCancelEvent
|
||||
Type ClientEventType `json:"type"`
|
||||
}{
|
||||
responseCancelEvent: (*responseCancelEvent)(&m),
|
||||
Type: m.ClientEventType(),
|
||||
}
|
||||
return json.Marshal(v)
|
||||
}
|
||||
|
||||
func MarshalClientEvent(event ClientEvent) ([]byte, error) {
|
||||
return json.Marshal(event)
|
||||
}
|
||||
@@ -0,0 +1,356 @@
|
||||
package openairt_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
openairt "github.com/WqyJh/go-openai-realtime"
|
||||
"github.com/sashabaranov/go-openai"
|
||||
"github.com/sashabaranov/go-openai/jsonschema"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestSessionUpdateEvent(t *testing.T) {
|
||||
temperature := float32(0.5)
|
||||
message := openairt.SessionUpdateEvent{
|
||||
EventBase: openairt.EventBase{
|
||||
EventID: "test-id",
|
||||
},
|
||||
Session: openairt.ClientSession{
|
||||
Modalities: []openairt.Modality{openairt.ModalityText, openairt.ModalityAudio},
|
||||
Instructions: "test-instructions",
|
||||
Voice: openairt.VoiceAlloy,
|
||||
InputAudioFormat: openairt.AudioFormatPcm16,
|
||||
OutputAudioFormat: openairt.AudioFormatG711Ulaw,
|
||||
InputAudioTranscription: &openairt.InputAudioTranscription{
|
||||
Enabled: true,
|
||||
Model: openai.Whisper1,
|
||||
},
|
||||
TurnDetection: &openairt.TurnDetection{
|
||||
Type: openairt.TurnDetectionTypeServerVad,
|
||||
Threshold: 0.5,
|
||||
PrefixPaddingMs: 1000,
|
||||
SilenceDurationMs: 2000,
|
||||
},
|
||||
Tools: []openairt.Tool{
|
||||
{
|
||||
Type: openairt.ToolTypeFunction,
|
||||
Name: "test-tool",
|
||||
Parameters: jsonschema.Definition{
|
||||
Type: "object",
|
||||
Properties: map[string]jsonschema.Definition{
|
||||
"location": {
|
||||
Type: jsonschema.String,
|
||||
},
|
||||
},
|
||||
Required: []string{"location"},
|
||||
},
|
||||
},
|
||||
},
|
||||
ToolChoice: openairt.ToolChoice{
|
||||
Type: openairt.ToolTypeFunction,
|
||||
Function: openairt.ToolFunction{
|
||||
Name: "test-tool",
|
||||
},
|
||||
},
|
||||
Temperature: &temperature,
|
||||
MaxOutputTokens: 100,
|
||||
},
|
||||
}
|
||||
|
||||
// data, err := json.MarshalIndent(message, "", "\t")
|
||||
data, err := json.Marshal(message)
|
||||
assert.NoError(t, err)
|
||||
expected := `{
|
||||
"event_id": "test-id",
|
||||
"session": {
|
||||
"modalities": [
|
||||
"text",
|
||||
"audio"
|
||||
],
|
||||
"instructions": "test-instructions",
|
||||
"voice": "alloy",
|
||||
"input_audio_format": "pcm16",
|
||||
"output_audio_format": "g711-ulaw",
|
||||
"input_audio_transcription": {
|
||||
"enabled": true,
|
||||
"model": "whisper-1"
|
||||
},
|
||||
"turn_detection": {
|
||||
"type": "server_vad",
|
||||
"threshold": 0.5,
|
||||
"prefix_padding_ms": 1000,
|
||||
"silence_duration_ms": 2000
|
||||
},
|
||||
"tools": [
|
||||
{
|
||||
"type": "function",
|
||||
"name": "test-tool",
|
||||
"description": "",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"location": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"location"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"tool_choice": {
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "test-tool"
|
||||
}
|
||||
},
|
||||
"temperature": 0.5,
|
||||
"max_output_tokens": 100
|
||||
},
|
||||
"type": "session.update"
|
||||
}`
|
||||
assert.JSONEq(t, expected, string(data))
|
||||
}
|
||||
|
||||
func TestSessionUpdateEventSimple(t *testing.T) {
|
||||
temperature := float32(0.5)
|
||||
message := openairt.SessionUpdateEvent{
|
||||
Session: openairt.ClientSession{
|
||||
Modalities: []openairt.Modality{openairt.ModalityText},
|
||||
Instructions: "test-instructions",
|
||||
Voice: openairt.VoiceAlloy,
|
||||
InputAudioFormat: openairt.AudioFormatPcm16,
|
||||
OutputAudioFormat: openairt.AudioFormatG711Ulaw,
|
||||
InputAudioTranscription: nil,
|
||||
TurnDetection: nil,
|
||||
Tools: nil,
|
||||
ToolChoice: nil,
|
||||
Temperature: &temperature,
|
||||
MaxOutputTokens: 100,
|
||||
},
|
||||
}
|
||||
|
||||
// data, err := json.MarshalIndent(message, "", "\t")
|
||||
data, err := json.Marshal(message)
|
||||
assert.NoError(t, err)
|
||||
expected := `{
|
||||
"session": {
|
||||
"modalities": [
|
||||
"text"
|
||||
],
|
||||
"instructions": "test-instructions",
|
||||
"voice": "alloy",
|
||||
"input_audio_format": "pcm16",
|
||||
"output_audio_format": "g711-ulaw",
|
||||
"temperature": 0.5,
|
||||
"max_output_tokens": 100
|
||||
},
|
||||
"type": "session.update"
|
||||
}`
|
||||
assert.JSONEq(t, expected, string(data))
|
||||
}
|
||||
|
||||
func TestInputAudioBufferAppendEvent(t *testing.T) {
|
||||
message := openairt.InputAudioBufferAppendEvent{
|
||||
Audio: "test-audio",
|
||||
}
|
||||
data, err := json.Marshal(message)
|
||||
assert.NoError(t, err)
|
||||
expected := `{"audio":"test-audio","type":"input_audio_buffer.append"}`
|
||||
assert.JSONEq(t, expected, string(data))
|
||||
|
||||
message.EventBase.EventID = "test-id"
|
||||
data, err = json.Marshal(message)
|
||||
assert.NoError(t, err)
|
||||
expected = `{"event_id":"test-id","audio":"test-audio","type":"input_audio_buffer.append"}`
|
||||
assert.JSONEq(t, expected, string(data))
|
||||
}
|
||||
|
||||
func TestInputAudioBufferCommitEvent(t *testing.T) {
|
||||
message := openairt.InputAudioBufferCommitEvent{}
|
||||
data, err := json.Marshal(message)
|
||||
assert.NoError(t, err)
|
||||
expected := `{"type":"input_audio_buffer.commit"}`
|
||||
assert.JSONEq(t, expected, string(data))
|
||||
|
||||
message.EventBase.EventID = "test-id"
|
||||
data, err = json.Marshal(message)
|
||||
assert.NoError(t, err)
|
||||
expected = `{"event_id":"test-id","type":"input_audio_buffer.commit"}`
|
||||
assert.JSONEq(t, expected, string(data))
|
||||
}
|
||||
|
||||
func TestInputAudioBufferClearEvent(t *testing.T) {
|
||||
message := openairt.InputAudioBufferClearEvent{}
|
||||
data, err := json.Marshal(message)
|
||||
assert.NoError(t, err)
|
||||
expected := `{"type":"input_audio_buffer.clear"}`
|
||||
assert.JSONEq(t, expected, string(data))
|
||||
|
||||
message.EventBase.EventID = "test-id"
|
||||
data, err = json.Marshal(message)
|
||||
assert.NoError(t, err)
|
||||
expected = `{"event_id":"test-id","type":"input_audio_buffer.clear"}`
|
||||
assert.JSONEq(t, expected, string(data))
|
||||
}
|
||||
|
||||
func TestConversationItemCreateEvent(t *testing.T) {
|
||||
message := openairt.ConversationItemCreateEvent{
|
||||
PreviousItemID: "test-previous-item-id",
|
||||
Item: openairt.MessageItem{
|
||||
ID: "test-id",
|
||||
Type: openairt.MessageItemTypeMessage,
|
||||
Status: openairt.ItemStatusCompleted,
|
||||
Role: openairt.MessageRoleUser,
|
||||
Content: []openairt.MessageContentPart{
|
||||
{Type: openairt.MessageContentTypeText, Text: "test-content"},
|
||||
{Type: openairt.MessageContentTypeAudio, Audio: "test-audio"},
|
||||
{Type: openairt.MessageContentTypeTranscript, Transcript: "test-transcript"},
|
||||
},
|
||||
},
|
||||
}
|
||||
data, err := json.MarshalIndent(message, "", "\t")
|
||||
assert.NoError(t, err)
|
||||
expected := `{
|
||||
"previous_item_id": "test-previous-item-id",
|
||||
"item": {
|
||||
"id": "test-id",
|
||||
"type": "message",
|
||||
"status": "completed",
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "test-content"
|
||||
},
|
||||
{
|
||||
"type": "audio",
|
||||
"audio": "test-audio"
|
||||
},
|
||||
{
|
||||
"type": "transcript",
|
||||
"transcript": "test-transcript"
|
||||
}
|
||||
]
|
||||
},
|
||||
"type": "conversation.item.create"
|
||||
}`
|
||||
assert.JSONEq(t, expected, string(data))
|
||||
|
||||
message.EventBase.EventID = "test-id"
|
||||
data, err = json.Marshal(message)
|
||||
assert.NoError(t, err)
|
||||
expected = `{
|
||||
"event_id": "test-id",
|
||||
"previous_item_id": "test-previous-item-id",
|
||||
"item": {
|
||||
"id": "test-id",
|
||||
"type": "message",
|
||||
"status": "completed",
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "test-content"
|
||||
},
|
||||
{
|
||||
"type": "audio",
|
||||
"audio": "test-audio"
|
||||
},
|
||||
{
|
||||
"type": "transcript",
|
||||
"transcript": "test-transcript"
|
||||
}
|
||||
]
|
||||
},
|
||||
"type": "conversation.item.create"
|
||||
}`
|
||||
assert.JSONEq(t, expected, string(data))
|
||||
}
|
||||
|
||||
func TestConversationItemTruncateEvent(t *testing.T) {
|
||||
message := openairt.ConversationItemTruncateEvent{
|
||||
ItemID: "test-item-id",
|
||||
ContentIndex: 1,
|
||||
AudioEndMs: 2000,
|
||||
}
|
||||
data, err := json.Marshal(message)
|
||||
assert.NoError(t, err)
|
||||
expected := `{"item_id":"test-item-id","content_index":1,"audio_end_ms":2000,"type":"conversation.item.truncate"}`
|
||||
assert.JSONEq(t, expected, string(data))
|
||||
|
||||
message.EventBase.EventID = "test-id"
|
||||
data, err = json.Marshal(message)
|
||||
assert.NoError(t, err)
|
||||
expected = `{"event_id":"test-id","item_id":"test-item-id","content_index":1,"audio_end_ms":2000,"type":"conversation.item.truncate"}`
|
||||
assert.JSONEq(t, expected, string(data))
|
||||
}
|
||||
func TestConversationItemDeleteEvent(t *testing.T) {
|
||||
message := openairt.ConversationItemDeleteEvent{
|
||||
ItemID: "test-item-id",
|
||||
}
|
||||
data, err := json.Marshal(message)
|
||||
assert.NoError(t, err)
|
||||
expected := `{"item_id":"test-item-id","type":"conversation.item.delete"}`
|
||||
assert.JSONEq(t, expected, string(data))
|
||||
|
||||
message.EventBase.EventID = "test-id"
|
||||
data, err = json.Marshal(message)
|
||||
assert.NoError(t, err)
|
||||
expected = `{"event_id":"test-id","item_id":"test-item-id","type":"conversation.item.delete"}`
|
||||
assert.JSONEq(t, expected, string(data))
|
||||
}
|
||||
|
||||
func TestResponseCreateEvent(t *testing.T) {
|
||||
message := openairt.ResponseCreateEvent{
|
||||
Response: openairt.ResponseCreateParams{
|
||||
Modalities: []openairt.Modality{openairt.ModalityText, openairt.ModalityAudio},
|
||||
Instructions: "test-instructions",
|
||||
Voice: openairt.VoiceAlloy,
|
||||
OutputAudioFormat: openairt.AudioFormatG711Ulaw,
|
||||
Tools: nil,
|
||||
ToolChoice: openairt.ToolChoiceAuto,
|
||||
Temperature: nil,
|
||||
MaxOutputTokens: 100,
|
||||
},
|
||||
}
|
||||
data, err := json.MarshalIndent(message, "", "\t")
|
||||
assert.NoError(t, err)
|
||||
expected := `{
|
||||
"response": {
|
||||
"modalities": [
|
||||
"text",
|
||||
"audio"
|
||||
],
|
||||
"instructions": "test-instructions",
|
||||
"voice": "alloy",
|
||||
"output_audio_format": "g711-ulaw",
|
||||
"tool_choice": "auto",
|
||||
"max_output_tokens": 100
|
||||
},
|
||||
"type": "response.create"
|
||||
}`
|
||||
assert.JSONEq(t, expected, string(data))
|
||||
|
||||
message.EventBase.EventID = "test-id"
|
||||
data, err = json.Marshal(message)
|
||||
assert.NoError(t, err)
|
||||
expected = `{"event_id":"test-id","response":{"modalities":["text","audio"],"instructions":"test-instructions","voice":"alloy","output_audio_format":"g711-ulaw","tool_choice":"auto","max_output_tokens":100},"type":"response.create"}`
|
||||
assert.JSONEq(t, expected, string(data))
|
||||
}
|
||||
|
||||
func TestResponseCancelEvent(t *testing.T) {
|
||||
message := openairt.ResponseCancelEvent{}
|
||||
data, err := json.Marshal(message)
|
||||
assert.NoError(t, err)
|
||||
expected := `{"type":"response.cancel"}`
|
||||
assert.JSONEq(t, expected, string(data))
|
||||
|
||||
message.EventBase.EventID = "test-id"
|
||||
data, err = json.Marshal(message)
|
||||
assert.NoError(t, err)
|
||||
expected = `{"event_id":"test-id","type":"response.cancel"}`
|
||||
assert.JSONEq(t, expected, string(data))
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package openairt
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestClient(t *testing.T) {
|
||||
mockToken := "test"
|
||||
client := NewClient(mockToken)
|
||||
assert.Equal(t, client.config.authToken, mockToken)
|
||||
|
||||
config := DefaultConfig(mockToken)
|
||||
client = NewClientWithConfig(config)
|
||||
assert.Equal(t, client.config.authToken, mockToken)
|
||||
assert.Equal(t, client.config.BaseURL, openaiAPIURLv1)
|
||||
assert.Equal(t, client.config.APIType, APITypeOpenAI)
|
||||
url := client.getUrl("test-model")
|
||||
assert.Equal(t, url, openaiAPIURLv1+"?model=test-model")
|
||||
headers := client.getHeaders()
|
||||
assert.Equal(t, headers.Get("Authorization"), "Bearer "+mockToken)
|
||||
assert.Equal(t, headers.Get("OpenAI-Beta"), "realtime=v1")
|
||||
|
||||
azureUrl := "wss://my-eastus2-openai-resource.openai.azure.com/openai/realtime"
|
||||
config = DefaultAzureConfig(mockToken, azureUrl)
|
||||
client = NewClientWithConfig(config)
|
||||
assert.Equal(t, client.config.authToken, mockToken)
|
||||
assert.Equal(t, client.config.BaseURL, azureUrl)
|
||||
assert.Equal(t, client.config.APIType, APITypeAzure)
|
||||
assert.Equal(t, client.config.APIVersion, azureAPIVersion20241001Preview)
|
||||
url = client.getUrl("test-model")
|
||||
assert.Equal(t, url, azureUrl+"?api-version="+azureAPIVersion20241001Preview+"&deployment=test-model")
|
||||
headers = client.getHeaders()
|
||||
assert.Equal(t, headers.Get("api-key"), mockToken)
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package openairt
|
||||
|
||||
type APIType string
|
||||
|
||||
const (
|
||||
APITypeOpenAI APIType = "OPEN_AI"
|
||||
APITypeAzure APIType = "AZURE"
|
||||
)
|
||||
|
||||
const (
|
||||
openaiAPIURLv1 = "wss://api.openai.com/v1/realtime"
|
||||
)
|
||||
|
||||
const (
|
||||
azureAPIVersion20241001Preview = "2024-10-01-preview"
|
||||
)
|
||||
|
||||
type ClientConfig struct {
|
||||
authToken string
|
||||
|
||||
BaseURL string
|
||||
APIType APIType
|
||||
APIVersion string // required when APIType is APITypeAzure
|
||||
}
|
||||
|
||||
func DefaultConfig(authToken string) ClientConfig {
|
||||
return ClientConfig{
|
||||
authToken: authToken,
|
||||
BaseURL: openaiAPIURLv1,
|
||||
APIType: APITypeOpenAI,
|
||||
}
|
||||
}
|
||||
|
||||
func DefaultAzureConfig(apiKey, baseURL string) ClientConfig {
|
||||
return ClientConfig{
|
||||
authToken: apiKey,
|
||||
BaseURL: baseURL,
|
||||
APIType: APITypeAzure,
|
||||
APIVersion: azureAPIVersion20241001Preview,
|
||||
}
|
||||
}
|
||||
|
||||
func (ClientConfig) String() string {
|
||||
return "<OpenAI Realtime API ClientConfig>"
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package openairt
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"sync"
|
||||
|
||||
"github.com/coder/websocket"
|
||||
)
|
||||
|
||||
type ServerEventHandler func(ctx context.Context, event ServerEvent)
|
||||
|
||||
type Conn struct {
|
||||
conn *websocket.Conn
|
||||
}
|
||||
|
||||
func (c *Conn) Close() error {
|
||||
return c.conn.Close(websocket.StatusNormalClosure, "")
|
||||
}
|
||||
|
||||
func (c *Conn) SendMessage(ctx context.Context, msg ClientEvent) error {
|
||||
data, err := MarshalClientEvent(msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.conn.Write(ctx, websocket.MessageText, data)
|
||||
}
|
||||
|
||||
func (c *Conn) ReadMessage(ctx context.Context) (ServerEvent, error) {
|
||||
messageType, data, err := c.conn.Read(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if messageType != websocket.MessageText {
|
||||
return nil, fmt.Errorf("expected text message, got %d", messageType)
|
||||
}
|
||||
event, err := UnmarshalServerEvent(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return event, nil
|
||||
}
|
||||
|
||||
type ConnHandler struct {
|
||||
ctx context.Context
|
||||
conn *Conn
|
||||
handlers []ServerEventHandler
|
||||
cancel context.CancelFunc
|
||||
wg sync.WaitGroup
|
||||
err error
|
||||
}
|
||||
|
||||
func NewConnHandler(ctx context.Context, conn *Conn, handlers ...ServerEventHandler) *ConnHandler {
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
return &ConnHandler{
|
||||
ctx: ctx,
|
||||
conn: conn,
|
||||
handlers: handlers,
|
||||
cancel: cancel,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *ConnHandler) Start() {
|
||||
c.wg.Add(1)
|
||||
go func() {
|
||||
defer c.wg.Done()
|
||||
c.err = c.run()
|
||||
}()
|
||||
}
|
||||
|
||||
func (c *ConnHandler) run() error {
|
||||
for {
|
||||
select {
|
||||
case <-c.ctx.Done():
|
||||
return nil
|
||||
default:
|
||||
}
|
||||
|
||||
msg, err := c.conn.ReadMessage(c.ctx)
|
||||
if err != nil {
|
||||
log.Printf("ReadMessage: %v", err)
|
||||
continue
|
||||
}
|
||||
for _, handler := range c.handlers {
|
||||
handler(c.ctx, msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *ConnHandler) Stop() {
|
||||
c.cancel()
|
||||
c.wg.Wait()
|
||||
}
|
||||
|
||||
func (c *ConnHandler) Err() error {
|
||||
return c.err
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
module github.com/WqyJh/go-openai-realtime
|
||||
|
||||
go 1.19
|
||||
|
||||
require (
|
||||
github.com/coder/websocket v1.8.12
|
||||
github.com/sashabaranov/go-openai v1.32.0
|
||||
github.com/stretchr/testify v1.9.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
@@ -0,0 +1,14 @@
|
||||
github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo=
|
||||
github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/sashabaranov/go-openai v1.32.0 h1:Yk3iE9moX3RBXxrof3OBtUBrE7qZR0zF9ebsoO4zVzI=
|
||||
github.com/sashabaranov/go-openai v1.32.0/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
+372
@@ -0,0 +1,372 @@
|
||||
package openairt
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type ServerEventType string
|
||||
|
||||
const (
|
||||
ServerEventTypeError ServerEventType = "error"
|
||||
ServerEventTypeSessionCreated ServerEventType = "session.created"
|
||||
ServerEventTypeSessionUpdated ServerEventType = "session.updated"
|
||||
ServerEventTypeConversationCreated ServerEventType = "conversation.created"
|
||||
ServerEventTypeInputAudioBufferCommitted ServerEventType = "input_audio_buffer.committed"
|
||||
ServerEventTypeInputAudioBufferCleared ServerEventType = "input_audio_buffer.cleared"
|
||||
ServerEventTypeInputAudioBufferSpeechStarted ServerEventType = "input_audio_buffer.speech_started"
|
||||
ServerEventTypeInputAudioBufferSpeechStopped ServerEventType = "input_audio_buffer.speech_stopped"
|
||||
ServerEventTypeConversationItemCreated ServerEventType = "conversation.item.created"
|
||||
ServerEventTypeConversationItemInputAudioTranscriptionCompleted ServerEventType = "conversation.item.input_audio_transcription.completed"
|
||||
ServerEventTypeConversationItemInputAudioTranscriptionFailed ServerEventType = "conversation.item.input_audio_transcription.failed"
|
||||
ServerEventTypeConversationItemTruncated ServerEventType = "conversation.item.truncated"
|
||||
ServerEventTypeConversationItemDeleted ServerEventType = "conversation.item.deleted"
|
||||
ServerEventTypeResponseCreated ServerEventType = "response.created"
|
||||
ServerEventTypeResponseDone ServerEventType = "response.done"
|
||||
ServerEventTypeResponseOutputItemAdded ServerEventType = "response.output_item.added"
|
||||
ServerEventTypeResponseOutputItemDone ServerEventType = "response.output_item.done"
|
||||
ServerEventTypeResponseContentPartAdded ServerEventType = "response.content_part.added"
|
||||
ServerEventTypeResponseContentPartDone ServerEventType = "response.content_part.done"
|
||||
ServerEventTypeResponseTextDelta ServerEventType = "response.text.delta"
|
||||
ServerEventTypeResponseTextDone ServerEventType = "response.text.done"
|
||||
ServerEventTypeResponseAudioTranscriptDelta ServerEventType = "response.audio_transcript.delta"
|
||||
ServerEventTypeResponseAudioTranscriptDone ServerEventType = "response.audio_transcript.done"
|
||||
ServerEventTypeResponseAudioDelta ServerEventType = "response.audio.delta"
|
||||
ServerEventTypeResponseAudioDone ServerEventType = "response.audio.done"
|
||||
ServerEventTypeResponseFunctionCallArgumentsDelta ServerEventType = "response.function_call_arguments.delta"
|
||||
ServerEventTypeResponseFunctionCallArgumentsDone ServerEventType = "response.function_call_arguments.done"
|
||||
ServerEventTypeRateLimitsUpdated ServerEventType = "rate_limits.updated"
|
||||
)
|
||||
|
||||
type ServerEvent interface {
|
||||
ServerEventType() ServerEventType
|
||||
}
|
||||
|
||||
type ServerEventBase struct {
|
||||
EventID string `json:"event_id,omitempty"`
|
||||
Type ServerEventType `json:"type"`
|
||||
}
|
||||
|
||||
func (m ServerEventBase) ServerEventType() ServerEventType {
|
||||
return m.Type
|
||||
}
|
||||
|
||||
type ErrorEvent struct {
|
||||
ServerEventBase
|
||||
Error Error `json:"error"`
|
||||
}
|
||||
|
||||
type SessionCreatedEvent struct {
|
||||
ServerEventBase
|
||||
Session ServerSession `json:"session"`
|
||||
}
|
||||
|
||||
type SessionUpdatedEvent struct {
|
||||
ServerEventBase
|
||||
Session ServerSession `json:"session"`
|
||||
}
|
||||
|
||||
type ConversationCreatedEvent struct {
|
||||
ServerEventBase
|
||||
Conversation Conversation `json:"conversation"`
|
||||
}
|
||||
|
||||
type InputAudioBufferCommittedEvent struct {
|
||||
ServerEventBase
|
||||
PreviousItemID string `json:"previous_item_id,omitempty"`
|
||||
ItemID string `json:"item_id"`
|
||||
}
|
||||
|
||||
type InputAudioBufferClearedEvent struct {
|
||||
ServerEventBase
|
||||
}
|
||||
|
||||
type InputAudioBufferSpeechStartedEvent struct {
|
||||
ServerEventBase
|
||||
AudioStartMs int64 `json:"audio_start_ms"` // Milliseconds since the session started when speech was detected.
|
||||
ItemID string `json:"item_id"`
|
||||
}
|
||||
|
||||
type InputAudioBufferSpeechStoppedEvent struct {
|
||||
ServerEventBase
|
||||
AudioEndMs int64 `json:"audio_end_ms"` // Milliseconds since the session started when speech stopped.
|
||||
ItemID string `json:"item_id"`
|
||||
}
|
||||
|
||||
type ConversationItemCreatedEvent struct {
|
||||
ServerEventBase
|
||||
PreviousItemID string `json:"previous_item_id,omitempty"`
|
||||
Item ResponseMessageItem `json:"item"`
|
||||
}
|
||||
|
||||
type ConversationItemInputAudioTranscriptionCompletedEvent struct {
|
||||
ServerEventBase
|
||||
ItemID string `json:"item_id"`
|
||||
ContentIndex int `json:"content_index"`
|
||||
Transcript string `json:"transcript"`
|
||||
}
|
||||
|
||||
type ConversationItemInputAudioTranscriptionFailedEvent struct {
|
||||
ServerEventBase
|
||||
ItemID string `json:"item_id"`
|
||||
ContentIndex int `json:"content_index"`
|
||||
Error Error `json:"error"`
|
||||
}
|
||||
|
||||
type ConversationItemTruncatedEvent struct {
|
||||
ServerEventBase
|
||||
ItemID string `json:"item_id"` // The ID of the assistant message item that was truncated.
|
||||
ContentIndex int `json:"content_index"` // The index of the content part that was truncated.
|
||||
AudioEndMs int `json:"audio_end_ms"` // The duration up to which the audio was truncated, in milliseconds.
|
||||
}
|
||||
|
||||
type ConversationItemDeletedEvent struct {
|
||||
ServerEventBase
|
||||
ItemID string `json:"item_id"` // The ID of the item that was deleted.
|
||||
}
|
||||
|
||||
type ResponseCreatedEvent struct {
|
||||
ServerEventBase
|
||||
Response Response `json:"response"`
|
||||
}
|
||||
|
||||
type ResponseDoneEvent struct {
|
||||
ServerEventBase
|
||||
Response Response `json:"response"`
|
||||
}
|
||||
|
||||
type ResponseOutputItemAddedEvent struct {
|
||||
ServerEventBase
|
||||
ResponseID string `json:"response_id"`
|
||||
OutputIndex int `json:"output_index"`
|
||||
Item ResponseMessageItem `json:"item"`
|
||||
}
|
||||
|
||||
type ResponseOutputItemDoneEvent struct {
|
||||
ServerEventBase
|
||||
ResponseID string `json:"response_id"`
|
||||
OutputIndex int `json:"output_index"`
|
||||
Item ResponseMessageItem `json:"item"`
|
||||
}
|
||||
|
||||
type ResponseContentPartAddedEvent struct {
|
||||
ServerEventBase
|
||||
ResponseID string `json:"response_id"`
|
||||
ItemID string `json:"item_id"`
|
||||
OutputIndex int `json:"output_index"`
|
||||
ContentIndex int `json:"content_index"`
|
||||
Part MessageContentPart `json:"part"`
|
||||
}
|
||||
|
||||
type ResponseContentPartDoneEvent struct {
|
||||
ServerEventBase
|
||||
ResponseID string `json:"response_id"`
|
||||
ItemID string `json:"item_id"`
|
||||
OutputIndex int `json:"output_index"`
|
||||
ContentIndex int `json:"content_index"`
|
||||
Part MessageContentPart `json:"part"`
|
||||
}
|
||||
|
||||
type ResponseTextDeltaEvent struct {
|
||||
ServerEventBase
|
||||
ResponseID string `json:"response_id"`
|
||||
ItemID string `json:"item_id"`
|
||||
OutputIndex int `json:"output_index"`
|
||||
ContentIndex int `json:"content_index"`
|
||||
Delta string `json:"delta"`
|
||||
}
|
||||
|
||||
type ResponseTextDoneEvent struct {
|
||||
ServerEventBase
|
||||
ResponseID string `json:"response_id"`
|
||||
ItemID string `json:"item_id"`
|
||||
OutputIndex int `json:"output_index"`
|
||||
ContentIndex int `json:"content_index"`
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
type ResponseAudioTranscriptDeltaEvent struct {
|
||||
ServerEventBase
|
||||
ResponseID string `json:"response_id"`
|
||||
ItemID string `json:"item_id"`
|
||||
OutputIndex int `json:"output_index"`
|
||||
ContentIndex int `json:"content_index"`
|
||||
Delta string `json:"delta"`
|
||||
}
|
||||
|
||||
type ResponseAudioTranscriptDoneEvent struct {
|
||||
ServerEventBase
|
||||
ResponseID string `json:"response_id"`
|
||||
ItemID string `json:"item_id"`
|
||||
OutputIndex int `json:"output_index"`
|
||||
ContentIndex int `json:"content_index"`
|
||||
Transcript string `json:"transcript"`
|
||||
}
|
||||
|
||||
type ResponseAudioDeltaEvent struct {
|
||||
ServerEventBase
|
||||
ResponseID string `json:"response_id"`
|
||||
ItemID string `json:"item_id"`
|
||||
OutputIndex int `json:"output_index"`
|
||||
ContentIndex int `json:"content_index"`
|
||||
Delta string `json:"delta"`
|
||||
}
|
||||
|
||||
type ResponseAudioDoneEvent struct {
|
||||
ServerEventBase
|
||||
ResponseID string `json:"response_id"`
|
||||
ItemID string `json:"item_id"`
|
||||
OutputIndex int `json:"output_index"`
|
||||
ContentIndex int `json:"content_index"`
|
||||
}
|
||||
|
||||
type ResponseFunctionCallArgumentsDeltaEvent struct {
|
||||
ServerEventBase
|
||||
ResponseID string `json:"response_id"`
|
||||
ItemID string `json:"item_id"`
|
||||
OutputIndex int `json:"output_index"`
|
||||
CallID string `json:"call_id"`
|
||||
Delta string `json:"delta"`
|
||||
}
|
||||
|
||||
type ResponseFunctionCallArgumentsDoneEvent struct {
|
||||
ServerEventBase
|
||||
ResponseID string `json:"response_id"`
|
||||
ItemID string `json:"item_id"`
|
||||
OutputIndex int `json:"output_index"`
|
||||
CallID string `json:"call_id"`
|
||||
Name string `json:"name"`
|
||||
Arguments string `json:"arguments"`
|
||||
}
|
||||
|
||||
type RateLimitsUpdatedEvent struct {
|
||||
ServerEventBase
|
||||
RateLimits []RateLimit `json:"rate_limits"`
|
||||
}
|
||||
|
||||
func UnmarshalServerEvent(data []byte) (ServerEvent, error) {
|
||||
var eventType struct {
|
||||
Type ServerEventType `json:"type"`
|
||||
}
|
||||
err := json.Unmarshal(data, &eventType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch eventType.Type {
|
||||
case ServerEventTypeError:
|
||||
var event ErrorEvent
|
||||
err = json.Unmarshal(data, &event)
|
||||
return event, err
|
||||
case ServerEventTypeSessionCreated:
|
||||
var event SessionCreatedEvent
|
||||
err = json.Unmarshal(data, &event)
|
||||
return event, err
|
||||
case ServerEventTypeSessionUpdated:
|
||||
var event SessionUpdatedEvent
|
||||
err = json.Unmarshal(data, &event)
|
||||
return event, err
|
||||
case ServerEventTypeConversationCreated:
|
||||
var event ConversationCreatedEvent
|
||||
err = json.Unmarshal(data, &event)
|
||||
return event, err
|
||||
case ServerEventTypeInputAudioBufferCommitted:
|
||||
var event InputAudioBufferCommittedEvent
|
||||
err = json.Unmarshal(data, &event)
|
||||
return event, err
|
||||
case ServerEventTypeInputAudioBufferCleared:
|
||||
var event InputAudioBufferClearedEvent
|
||||
err = json.Unmarshal(data, &event)
|
||||
return event, err
|
||||
case ServerEventTypeInputAudioBufferSpeechStarted:
|
||||
var event InputAudioBufferSpeechStartedEvent
|
||||
err = json.Unmarshal(data, &event)
|
||||
return event, err
|
||||
case ServerEventTypeInputAudioBufferSpeechStopped:
|
||||
var event InputAudioBufferSpeechStoppedEvent
|
||||
err = json.Unmarshal(data, &event)
|
||||
return event, err
|
||||
case ServerEventTypeConversationItemCreated:
|
||||
var event ConversationItemCreatedEvent
|
||||
err = json.Unmarshal(data, &event)
|
||||
return event, err
|
||||
case ServerEventTypeConversationItemInputAudioTranscriptionCompleted:
|
||||
var event ConversationItemInputAudioTranscriptionCompletedEvent
|
||||
err = json.Unmarshal(data, &event)
|
||||
return event, err
|
||||
case ServerEventTypeConversationItemInputAudioTranscriptionFailed:
|
||||
var event ConversationItemInputAudioTranscriptionFailedEvent
|
||||
err = json.Unmarshal(data, &event)
|
||||
return event, err
|
||||
case ServerEventTypeConversationItemTruncated:
|
||||
var event ConversationItemTruncatedEvent
|
||||
err = json.Unmarshal(data, &event)
|
||||
return event, err
|
||||
case ServerEventTypeConversationItemDeleted:
|
||||
var event ConversationItemDeletedEvent
|
||||
err = json.Unmarshal(data, &event)
|
||||
return event, err
|
||||
case ServerEventTypeResponseCreated:
|
||||
var event ResponseCreatedEvent
|
||||
err = json.Unmarshal(data, &event)
|
||||
return event, err
|
||||
case ServerEventTypeResponseDone:
|
||||
var event ResponseDoneEvent
|
||||
err = json.Unmarshal(data, &event)
|
||||
return event, err
|
||||
case ServerEventTypeResponseOutputItemAdded:
|
||||
var event ResponseOutputItemAddedEvent
|
||||
err = json.Unmarshal(data, &event)
|
||||
return event, err
|
||||
case ServerEventTypeResponseOutputItemDone:
|
||||
var event ResponseOutputItemDoneEvent
|
||||
err = json.Unmarshal(data, &event)
|
||||
return event, err
|
||||
case ServerEventTypeResponseContentPartAdded:
|
||||
var event ResponseContentPartAddedEvent
|
||||
err = json.Unmarshal(data, &event)
|
||||
return event, err
|
||||
case ServerEventTypeResponseContentPartDone:
|
||||
var event ResponseContentPartDoneEvent
|
||||
err = json.Unmarshal(data, &event)
|
||||
return event, err
|
||||
case ServerEventTypeResponseTextDelta:
|
||||
var event ResponseTextDeltaEvent
|
||||
err = json.Unmarshal(data, &event)
|
||||
return event, err
|
||||
case ServerEventTypeResponseTextDone:
|
||||
var event ResponseTextDoneEvent
|
||||
err = json.Unmarshal(data, &event)
|
||||
return event, err
|
||||
case ServerEventTypeResponseAudioTranscriptDelta:
|
||||
var event ResponseAudioTranscriptDeltaEvent
|
||||
err = json.Unmarshal(data, &event)
|
||||
return event, err
|
||||
case ServerEventTypeResponseAudioTranscriptDone:
|
||||
var event ResponseAudioTranscriptDoneEvent
|
||||
err = json.Unmarshal(data, &event)
|
||||
return event, err
|
||||
case ServerEventTypeResponseAudioDelta:
|
||||
var event ResponseAudioDeltaEvent
|
||||
err = json.Unmarshal(data, &event)
|
||||
return event, err
|
||||
case ServerEventTypeResponseAudioDone:
|
||||
var event ResponseAudioDoneEvent
|
||||
err = json.Unmarshal(data, &event)
|
||||
return event, err
|
||||
case ServerEventTypeResponseFunctionCallArgumentsDelta:
|
||||
var event ResponseFunctionCallArgumentsDeltaEvent
|
||||
err = json.Unmarshal(data, &event)
|
||||
return event, err
|
||||
case ServerEventTypeResponseFunctionCallArgumentsDone:
|
||||
var event ResponseFunctionCallArgumentsDoneEvent
|
||||
err = json.Unmarshal(data, &event)
|
||||
return event, err
|
||||
case ServerEventTypeRateLimitsUpdated:
|
||||
var event RateLimitsUpdatedEvent
|
||||
err = json.Unmarshal(data, &event)
|
||||
return event, err
|
||||
default:
|
||||
// This should never happen.
|
||||
return nil, fmt.Errorf("unknown server event type: %s", eventType.Type)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,968 @@
|
||||
package openairt_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
openairt "github.com/WqyJh/go-openai-realtime"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestErrorEvent(t *testing.T) {
|
||||
data := `{
|
||||
"event_id": "event_890",
|
||||
"type": "error",
|
||||
"error": {
|
||||
"type": "invalid_request_error",
|
||||
"code": "invalid_event",
|
||||
"message": "The 'type' field is missing.",
|
||||
"param": null,
|
||||
"event_id": "event_567"
|
||||
}
|
||||
}`
|
||||
expected := openairt.ErrorEvent{
|
||||
ServerEventBase: openairt.ServerEventBase{
|
||||
Type: openairt.ServerEventTypeError,
|
||||
EventID: "event_890",
|
||||
},
|
||||
Error: openairt.Error{
|
||||
Type: "invalid_request_error",
|
||||
Code: "invalid_event",
|
||||
Message: "The 'type' field is missing.",
|
||||
Param: "",
|
||||
EventID: "event_567",
|
||||
},
|
||||
}
|
||||
actual, err := openairt.UnmarshalServerEvent([]byte(data))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, openairt.ServerEventTypeError, actual.ServerEventType())
|
||||
assert.Equal(t, expected, actual.(openairt.ErrorEvent))
|
||||
}
|
||||
|
||||
func TestServerToolChoice(t *testing.T) {
|
||||
data := `{"type": "function", "function": {"name": "get_current_weather"}}`
|
||||
expectedFunction := openairt.ToolChoice{
|
||||
Type: openairt.ToolTypeFunction,
|
||||
Function: openairt.ToolFunction{
|
||||
Name: "get_current_weather",
|
||||
},
|
||||
}
|
||||
expected := openairt.ServerToolChoice{Function: expectedFunction}
|
||||
actual := openairt.ServerToolChoice{}
|
||||
err := json.Unmarshal([]byte(data), &actual)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expected, actual)
|
||||
assert.Equal(t, expectedFunction, actual.Get())
|
||||
|
||||
data = `"auto"`
|
||||
expected = openairt.ServerToolChoice{String: openairt.ToolChoiceAuto}
|
||||
actual = openairt.ServerToolChoice{}
|
||||
err = json.Unmarshal([]byte(data), &actual)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expected, actual)
|
||||
assert.Equal(t, openairt.ToolChoiceAuto, actual.Get())
|
||||
|
||||
data = `"none"`
|
||||
expected = openairt.ServerToolChoice{String: openairt.ToolChoiceNone}
|
||||
actual = openairt.ServerToolChoice{}
|
||||
err = json.Unmarshal([]byte(data), &actual)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expected, actual)
|
||||
assert.Equal(t, openairt.ToolChoiceNone, actual.Get())
|
||||
|
||||
data = `"required"`
|
||||
expected = openairt.ServerToolChoice{String: openairt.ToolChoiceRequired}
|
||||
actual = openairt.ServerToolChoice{}
|
||||
err = json.Unmarshal([]byte(data), &actual)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expected, actual)
|
||||
assert.Equal(t, openairt.ToolChoiceRequired, actual.Get())
|
||||
}
|
||||
|
||||
func TestSessionCreatedEvent(t *testing.T) {
|
||||
data := `{
|
||||
"event_id": "event_1234",
|
||||
"type": "session.created",
|
||||
"session": {
|
||||
"id": "sess_001",
|
||||
"object": "realtime.session",
|
||||
"model": "gpt-4o-realtime-preview-2024-10-01",
|
||||
"modalities": ["text", "audio"],
|
||||
"instructions": "",
|
||||
"voice": "alloy",
|
||||
"input_audio_format": "pcm16",
|
||||
"output_audio_format": "pcm16",
|
||||
"input_audio_transcription": null,
|
||||
"turn_detection": {
|
||||
"type": "server_vad",
|
||||
"threshold": 0.5,
|
||||
"prefix_padding_ms": 300,
|
||||
"silence_duration_ms": 200
|
||||
},
|
||||
"tools": [],
|
||||
"tool_choice": "auto",
|
||||
"temperature": 0.8,
|
||||
"max_output_tokens": null
|
||||
}
|
||||
}`
|
||||
temperature := float32(0.8)
|
||||
expected := openairt.SessionCreatedEvent{
|
||||
ServerEventBase: openairt.ServerEventBase{
|
||||
EventID: "event_1234",
|
||||
Type: openairt.ServerEventTypeSessionCreated,
|
||||
},
|
||||
Session: openairt.ServerSession{
|
||||
ID: "sess_001",
|
||||
Object: "realtime.session",
|
||||
Model: openairt.GPT4oRealtimePreview20241001,
|
||||
Modalities: []openairt.Modality{openairt.ModalityText, openairt.ModalityAudio},
|
||||
Instructions: "",
|
||||
Voice: openairt.VoiceAlloy,
|
||||
InputAudioFormat: openairt.AudioFormatPcm16,
|
||||
OutputAudioFormat: openairt.AudioFormatPcm16,
|
||||
InputAudioTranscription: nil,
|
||||
TurnDetection: &openairt.TurnDetection{
|
||||
Type: openairt.TurnDetectionTypeServerVad,
|
||||
Threshold: 0.5,
|
||||
PrefixPaddingMs: 300,
|
||||
SilenceDurationMs: 200,
|
||||
},
|
||||
Tools: []openairt.Tool{},
|
||||
ToolChoice: openairt.ServerToolChoice{String: openairt.ToolChoiceAuto},
|
||||
Temperature: &temperature,
|
||||
MaxOutputTokens: 0,
|
||||
},
|
||||
}
|
||||
actual, err := openairt.UnmarshalServerEvent([]byte(data))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, openairt.ServerEventTypeSessionCreated, actual.ServerEventType())
|
||||
assert.Equal(t, expected, actual.(openairt.SessionCreatedEvent))
|
||||
}
|
||||
|
||||
func TestSessionUpdatedEvent(t *testing.T) {
|
||||
data := `{
|
||||
"event_id": "event_5678",
|
||||
"type": "session.updated",
|
||||
"session": {
|
||||
"id": "sess_001",
|
||||
"object": "realtime.session",
|
||||
"model": "gpt-4o-realtime-preview-2024-10-01",
|
||||
"modalities": ["text"],
|
||||
"instructions": "New instructions",
|
||||
"voice": "alloy",
|
||||
"input_audio_format": "pcm16",
|
||||
"output_audio_format": "pcm16",
|
||||
"input_audio_transcription": {
|
||||
"enabled": true,
|
||||
"model": "whisper-1"
|
||||
},
|
||||
"turn_detection": {
|
||||
"type": "none"
|
||||
},
|
||||
"tools": [],
|
||||
"tool_choice": "none",
|
||||
"temperature": 0.7,
|
||||
"max_output_tokens": 200
|
||||
}
|
||||
}`
|
||||
temperature := float32(0.7)
|
||||
expected := openairt.SessionUpdatedEvent{
|
||||
ServerEventBase: openairt.ServerEventBase{
|
||||
EventID: "event_5678",
|
||||
Type: openairt.ServerEventTypeSessionUpdated,
|
||||
},
|
||||
Session: openairt.ServerSession{
|
||||
ID: "sess_001",
|
||||
Object: "realtime.session",
|
||||
Model: openairt.GPT4oRealtimePreview20241001,
|
||||
Modalities: []openairt.Modality{openairt.ModalityText},
|
||||
Instructions: "New instructions",
|
||||
Voice: openairt.VoiceAlloy,
|
||||
InputAudioFormat: openairt.AudioFormatPcm16,
|
||||
OutputAudioFormat: openairt.AudioFormatPcm16,
|
||||
InputAudioTranscription: &openairt.InputAudioTranscription{
|
||||
Enabled: true,
|
||||
Model: "whisper-1",
|
||||
},
|
||||
TurnDetection: &openairt.TurnDetection{
|
||||
Type: openairt.TurnDetectionTypeNone,
|
||||
},
|
||||
Tools: []openairt.Tool{},
|
||||
ToolChoice: openairt.ServerToolChoice{String: openairt.ToolChoiceNone},
|
||||
Temperature: &temperature,
|
||||
MaxOutputTokens: 200,
|
||||
},
|
||||
}
|
||||
actual, err := openairt.UnmarshalServerEvent([]byte(data))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, openairt.ServerEventTypeSessionUpdated, actual.ServerEventType())
|
||||
assert.Equal(t, expected, actual.(openairt.SessionUpdatedEvent))
|
||||
}
|
||||
|
||||
func TestConversationCreatedEvent(t *testing.T) {
|
||||
data := `{
|
||||
"event_id": "event_9101",
|
||||
"type": "conversation.created",
|
||||
"conversation": {
|
||||
"id": "conv_001",
|
||||
"object": "realtime.conversation"
|
||||
}
|
||||
}`
|
||||
expected := openairt.ConversationCreatedEvent{
|
||||
ServerEventBase: openairt.ServerEventBase{
|
||||
EventID: "event_9101",
|
||||
Type: openairt.ServerEventTypeConversationCreated,
|
||||
},
|
||||
Conversation: openairt.Conversation{
|
||||
ID: "conv_001",
|
||||
Object: "realtime.conversation",
|
||||
},
|
||||
}
|
||||
actual, err := openairt.UnmarshalServerEvent([]byte(data))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, openairt.ServerEventTypeConversationCreated, actual.ServerEventType())
|
||||
assert.Equal(t, expected, actual.(openairt.ConversationCreatedEvent))
|
||||
}
|
||||
|
||||
func TestConversationItemCreatedEvent(t *testing.T) {
|
||||
data := `{
|
||||
"event_id": "event_1920",
|
||||
"type": "conversation.item.created",
|
||||
"previous_item_id": "msg_002",
|
||||
"item": {
|
||||
"id": "msg_003",
|
||||
"object": "realtime.item",
|
||||
"type": "message",
|
||||
"status": "completed",
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "input_audio",
|
||||
"transcript": null
|
||||
}
|
||||
]
|
||||
}
|
||||
}`
|
||||
expected := openairt.ConversationItemCreatedEvent{
|
||||
ServerEventBase: openairt.ServerEventBase{
|
||||
EventID: "event_1920",
|
||||
Type: openairt.ServerEventTypeConversationItemCreated,
|
||||
},
|
||||
PreviousItemID: "msg_002",
|
||||
Item: openairt.ResponseMessageItem{
|
||||
Object: "realtime.item",
|
||||
MessageItem: openairt.MessageItem{
|
||||
ID: "msg_003",
|
||||
Type: openairt.MessageItemTypeMessage,
|
||||
Status: openairt.ItemStatusCompleted,
|
||||
Role: openairt.MessageRoleUser,
|
||||
Content: []openairt.MessageContentPart{
|
||||
{
|
||||
Type: openairt.MessageContentTypeInputAudio,
|
||||
Transcript: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
actual, err := openairt.UnmarshalServerEvent([]byte(data))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, openairt.ServerEventTypeConversationItemCreated, actual.ServerEventType())
|
||||
assert.Equal(t, expected, actual.(openairt.ConversationItemCreatedEvent))
|
||||
}
|
||||
|
||||
func TestConversationItemInputAudioTranscriptionCompletedEvent(t *testing.T) {
|
||||
data := `{
|
||||
"event_id": "event_2122",
|
||||
"type": "conversation.item.input_audio_transcription.completed",
|
||||
"item_id": "msg_003",
|
||||
"content_index": 1,
|
||||
"transcript": "Hello, how are you?"
|
||||
}`
|
||||
expected := openairt.ConversationItemInputAudioTranscriptionCompletedEvent{
|
||||
ServerEventBase: openairt.ServerEventBase{
|
||||
EventID: "event_2122",
|
||||
Type: openairt.ServerEventTypeConversationItemInputAudioTranscriptionCompleted,
|
||||
},
|
||||
ItemID: "msg_003",
|
||||
ContentIndex: 1,
|
||||
Transcript: "Hello, how are you?",
|
||||
}
|
||||
actual, err := openairt.UnmarshalServerEvent([]byte(data))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, openairt.ServerEventTypeConversationItemInputAudioTranscriptionCompleted, actual.ServerEventType())
|
||||
assert.Equal(t, expected, actual.(openairt.ConversationItemInputAudioTranscriptionCompletedEvent))
|
||||
}
|
||||
|
||||
func TestConversationItemInputAudioTranscriptionFailedEvent(t *testing.T) {
|
||||
data := `{
|
||||
"event_id": "event_2324",
|
||||
"type": "conversation.item.input_audio_transcription.failed",
|
||||
"item_id": "msg_003",
|
||||
"content_index": 0,
|
||||
"error": {
|
||||
"type": "transcription_error",
|
||||
"code": "audio_unintelligible",
|
||||
"message": "The audio could not be transcribed.",
|
||||
"param": null
|
||||
}
|
||||
}`
|
||||
expected := openairt.ConversationItemInputAudioTranscriptionFailedEvent{
|
||||
ServerEventBase: openairt.ServerEventBase{
|
||||
EventID: "event_2324",
|
||||
Type: openairt.ServerEventTypeConversationItemInputAudioTranscriptionFailed,
|
||||
},
|
||||
ItemID: "msg_003",
|
||||
ContentIndex: 0,
|
||||
Error: openairt.Error{
|
||||
Type: "transcription_error",
|
||||
Code: "audio_unintelligible",
|
||||
Message: "The audio could not be transcribed.",
|
||||
Param: "",
|
||||
EventID: "",
|
||||
},
|
||||
}
|
||||
actual, err := openairt.UnmarshalServerEvent([]byte(data))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, openairt.ServerEventTypeConversationItemInputAudioTranscriptionFailed, actual.ServerEventType())
|
||||
assert.Equal(t, expected, actual.(openairt.ConversationItemInputAudioTranscriptionFailedEvent))
|
||||
}
|
||||
|
||||
func TestConversationItemTruncatedEvent(t *testing.T) {
|
||||
data := `{
|
||||
"event_id": "event_2526",
|
||||
"type": "conversation.item.truncated",
|
||||
"item_id": "msg_004",
|
||||
"content_index": 2,
|
||||
"audio_end_ms": 1500
|
||||
}`
|
||||
expected := openairt.ConversationItemTruncatedEvent{
|
||||
ServerEventBase: openairt.ServerEventBase{
|
||||
EventID: "event_2526",
|
||||
Type: openairt.ServerEventTypeConversationItemTruncated,
|
||||
},
|
||||
ItemID: "msg_004",
|
||||
ContentIndex: 2,
|
||||
AudioEndMs: 1500,
|
||||
}
|
||||
actual, err := openairt.UnmarshalServerEvent([]byte(data))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, openairt.ServerEventTypeConversationItemTruncated, actual.ServerEventType())
|
||||
assert.Equal(t, expected, actual.(openairt.ConversationItemTruncatedEvent))
|
||||
}
|
||||
|
||||
func TestConversationItemDeletedEvent(t *testing.T) {
|
||||
data := `{
|
||||
"event_id": "event_2728",
|
||||
"type": "conversation.item.deleted",
|
||||
"item_id": "msg_005"
|
||||
}`
|
||||
expected := openairt.ConversationItemDeletedEvent{
|
||||
ServerEventBase: openairt.ServerEventBase{
|
||||
EventID: "event_2728",
|
||||
Type: openairt.ServerEventTypeConversationItemDeleted,
|
||||
},
|
||||
ItemID: "msg_005",
|
||||
}
|
||||
actual, err := openairt.UnmarshalServerEvent([]byte(data))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, openairt.ServerEventTypeConversationItemDeleted, actual.ServerEventType())
|
||||
assert.Equal(t, expected, actual.(openairt.ConversationItemDeletedEvent))
|
||||
}
|
||||
|
||||
func TestInputAudioBufferCommittedEvent(t *testing.T) {
|
||||
data := `{
|
||||
"event_id": "event_1121",
|
||||
"type": "input_audio_buffer.committed",
|
||||
"previous_item_id": "msg_001",
|
||||
"item_id": "msg_002"
|
||||
}`
|
||||
expected := openairt.InputAudioBufferCommittedEvent{
|
||||
ServerEventBase: openairt.ServerEventBase{
|
||||
EventID: "event_1121",
|
||||
Type: openairt.ServerEventTypeInputAudioBufferCommitted,
|
||||
},
|
||||
PreviousItemID: "msg_001",
|
||||
ItemID: "msg_002",
|
||||
}
|
||||
actual, err := openairt.UnmarshalServerEvent([]byte(data))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, openairt.ServerEventTypeInputAudioBufferCommitted, actual.ServerEventType())
|
||||
assert.Equal(t, expected, actual.(openairt.InputAudioBufferCommittedEvent))
|
||||
}
|
||||
|
||||
func TestInputAudioBufferClearedEvent(t *testing.T) {
|
||||
data := `{
|
||||
"event_id": "event_1314",
|
||||
"type": "input_audio_buffer.cleared"
|
||||
}`
|
||||
expected := openairt.InputAudioBufferClearedEvent{
|
||||
ServerEventBase: openairt.ServerEventBase{
|
||||
EventID: "event_1314",
|
||||
Type: openairt.ServerEventTypeInputAudioBufferCleared,
|
||||
},
|
||||
}
|
||||
actual, err := openairt.UnmarshalServerEvent([]byte(data))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, openairt.ServerEventTypeInputAudioBufferCleared, actual.ServerEventType())
|
||||
assert.Equal(t, expected, actual.(openairt.InputAudioBufferClearedEvent))
|
||||
}
|
||||
|
||||
func TestInputAudioBufferSpeechStartedEvent(t *testing.T) {
|
||||
data := `{
|
||||
"event_id": "event_1516",
|
||||
"type": "input_audio_buffer.speech_started",
|
||||
"audio_start_ms": 1000,
|
||||
"item_id": "msg_003"
|
||||
}`
|
||||
expected := openairt.InputAudioBufferSpeechStartedEvent{
|
||||
ServerEventBase: openairt.ServerEventBase{
|
||||
EventID: "event_1516",
|
||||
Type: openairt.ServerEventTypeInputAudioBufferSpeechStarted,
|
||||
},
|
||||
AudioStartMs: 1000,
|
||||
ItemID: "msg_003",
|
||||
}
|
||||
actual, err := openairt.UnmarshalServerEvent([]byte(data))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, openairt.ServerEventTypeInputAudioBufferSpeechStarted, actual.ServerEventType())
|
||||
assert.Equal(t, expected, actual.(openairt.InputAudioBufferSpeechStartedEvent))
|
||||
}
|
||||
|
||||
func TestInputAudioBufferSpeechStoppedEvent(t *testing.T) {
|
||||
data := `{
|
||||
"event_id": "event_1718",
|
||||
"type": "input_audio_buffer.speech_stopped",
|
||||
"audio_end_ms": 2000,
|
||||
"item_id": "msg_003"
|
||||
}`
|
||||
expected := openairt.InputAudioBufferSpeechStoppedEvent{
|
||||
ServerEventBase: openairt.ServerEventBase{
|
||||
EventID: "event_1718",
|
||||
Type: openairt.ServerEventTypeInputAudioBufferSpeechStopped,
|
||||
},
|
||||
AudioEndMs: 2000,
|
||||
ItemID: "msg_003",
|
||||
}
|
||||
actual, err := openairt.UnmarshalServerEvent([]byte(data))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, openairt.ServerEventTypeInputAudioBufferSpeechStopped, actual.ServerEventType())
|
||||
assert.Equal(t, expected, actual.(openairt.InputAudioBufferSpeechStoppedEvent))
|
||||
}
|
||||
|
||||
func TestResponseCreatedEvent(t *testing.T) {
|
||||
data := `{
|
||||
"event_id": "event_2930",
|
||||
"type": "response.created",
|
||||
"response": {
|
||||
"id": "resp_001",
|
||||
"object": "realtime.response",
|
||||
"status": "in_progress",
|
||||
"status_details": null,
|
||||
"output": [],
|
||||
"usage": null
|
||||
}
|
||||
}`
|
||||
expected := openairt.ResponseCreatedEvent{
|
||||
ServerEventBase: openairt.ServerEventBase{
|
||||
EventID: "event_2930",
|
||||
Type: openairt.ServerEventTypeResponseCreated,
|
||||
},
|
||||
Response: openairt.Response{
|
||||
ID: "resp_001",
|
||||
Object: "realtime.response",
|
||||
Status: "in_progress",
|
||||
StatusDetails: nil,
|
||||
Output: []openairt.ResponseMessageItem{},
|
||||
Usage: nil,
|
||||
},
|
||||
}
|
||||
actual, err := openairt.UnmarshalServerEvent([]byte(data))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, openairt.ServerEventTypeResponseCreated, actual.ServerEventType())
|
||||
assert.Equal(t, expected, actual.(openairt.ResponseCreatedEvent))
|
||||
}
|
||||
|
||||
func TestResponseDoneEvent(t *testing.T) {
|
||||
data := `{
|
||||
"event_id": "event_3132",
|
||||
"type": "response.done",
|
||||
"response": {
|
||||
"id": "resp_001",
|
||||
"object": "realtime.response",
|
||||
"status": "completed",
|
||||
"status_details": null,
|
||||
"output": [
|
||||
{
|
||||
"id": "msg_006",
|
||||
"object": "realtime.item",
|
||||
"type": "message",
|
||||
"status": "completed",
|
||||
"role": "assistant",
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "Sure, how can I assist you today?"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"usage": {
|
||||
"total_tokens": 50,
|
||||
"input_tokens": 20,
|
||||
"output_tokens": 30
|
||||
}
|
||||
}
|
||||
}`
|
||||
expected := openairt.ResponseDoneEvent{
|
||||
ServerEventBase: openairt.ServerEventBase{
|
||||
EventID: "event_3132",
|
||||
Type: openairt.ServerEventTypeResponseDone,
|
||||
},
|
||||
Response: openairt.Response{
|
||||
ID: "resp_001",
|
||||
Object: "realtime.response",
|
||||
Status: openairt.ResponseStatusCompleted,
|
||||
StatusDetails: nil,
|
||||
Output: []openairt.ResponseMessageItem{
|
||||
{
|
||||
Object: "realtime.item",
|
||||
MessageItem: openairt.MessageItem{
|
||||
ID: "msg_006",
|
||||
Type: openairt.MessageItemTypeMessage,
|
||||
Status: openairt.ItemStatusCompleted,
|
||||
Role: openairt.MessageRoleAssistant,
|
||||
Content: []openairt.MessageContentPart{
|
||||
{
|
||||
Type: openairt.MessageContentTypeText,
|
||||
Text: "Sure, how can I assist you today?",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Usage: &openairt.Usage{
|
||||
TotalTokens: 50,
|
||||
InputTokens: 20,
|
||||
OutputTokens: 30,
|
||||
},
|
||||
},
|
||||
}
|
||||
actual, err := openairt.UnmarshalServerEvent([]byte(data))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, openairt.ServerEventTypeResponseDone, actual.ServerEventType())
|
||||
assert.Equal(t, expected, actual.(openairt.ResponseDoneEvent))
|
||||
}
|
||||
|
||||
func TestResponseOutputItemAddedEvent(t *testing.T) {
|
||||
data := `{
|
||||
"event_id": "event_3334",
|
||||
"type": "response.output_item.added",
|
||||
"response_id": "resp_001",
|
||||
"output_index": 10,
|
||||
"item": {
|
||||
"id": "msg_007",
|
||||
"object": "realtime.item",
|
||||
"type": "message",
|
||||
"status": "in_progress",
|
||||
"role": "assistant",
|
||||
"content": []
|
||||
}
|
||||
}`
|
||||
expected := openairt.ResponseOutputItemAddedEvent{
|
||||
ServerEventBase: openairt.ServerEventBase{
|
||||
EventID: "event_3334",
|
||||
Type: openairt.ServerEventTypeResponseOutputItemAdded,
|
||||
},
|
||||
ResponseID: "resp_001",
|
||||
OutputIndex: 10,
|
||||
Item: openairt.ResponseMessageItem{
|
||||
Object: "realtime.item",
|
||||
MessageItem: openairt.MessageItem{
|
||||
ID: "msg_007",
|
||||
Type: openairt.MessageItemTypeMessage,
|
||||
Status: openairt.ItemStatusInProgress,
|
||||
Role: openairt.MessageRoleAssistant,
|
||||
Content: []openairt.MessageContentPart{},
|
||||
},
|
||||
},
|
||||
}
|
||||
actual, err := openairt.UnmarshalServerEvent([]byte(data))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, openairt.ServerEventTypeResponseOutputItemAdded, actual.ServerEventType())
|
||||
assert.Equal(t, expected, actual.(openairt.ResponseOutputItemAddedEvent))
|
||||
}
|
||||
|
||||
func TestResponseOutputItemDoneEvent(t *testing.T) {
|
||||
data := `{
|
||||
"event_id": "event_3536",
|
||||
"type": "response.output_item.done",
|
||||
"response_id": "resp_001",
|
||||
"output_index": 0,
|
||||
"item": {
|
||||
"id": "msg_007",
|
||||
"object": "realtime.item",
|
||||
"type": "message",
|
||||
"status": "completed",
|
||||
"role": "assistant",
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "Sure, I can help with that."
|
||||
}
|
||||
]
|
||||
}
|
||||
}`
|
||||
expected := openairt.ResponseOutputItemDoneEvent{
|
||||
ServerEventBase: openairt.ServerEventBase{
|
||||
EventID: "event_3536",
|
||||
Type: openairt.ServerEventTypeResponseOutputItemDone,
|
||||
},
|
||||
ResponseID: "resp_001",
|
||||
OutputIndex: 0,
|
||||
Item: openairt.ResponseMessageItem{
|
||||
Object: "realtime.item",
|
||||
MessageItem: openairt.MessageItem{
|
||||
ID: "msg_007",
|
||||
Type: openairt.MessageItemTypeMessage,
|
||||
Status: openairt.ItemStatusCompleted,
|
||||
Role: openairt.MessageRoleAssistant,
|
||||
Content: []openairt.MessageContentPart{
|
||||
{
|
||||
Type: openairt.MessageContentTypeText,
|
||||
Text: "Sure, I can help with that.",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
actual, err := openairt.UnmarshalServerEvent([]byte(data))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, openairt.ServerEventTypeResponseOutputItemDone, actual.ServerEventType())
|
||||
assert.Equal(t, expected, actual.(openairt.ResponseOutputItemDoneEvent))
|
||||
}
|
||||
|
||||
func TestResponseContentPartAddedEvent(t *testing.T) {
|
||||
data := `{
|
||||
"event_id": "event_3738",
|
||||
"type": "response.content_part.added",
|
||||
"response_id": "resp_001",
|
||||
"item_id": "msg_007",
|
||||
"output_index": 1,
|
||||
"content_index": 2,
|
||||
"part": {
|
||||
"type": "text",
|
||||
"text": ""
|
||||
}
|
||||
}`
|
||||
expected := openairt.ResponseContentPartAddedEvent{
|
||||
ServerEventBase: openairt.ServerEventBase{
|
||||
EventID: "event_3738",
|
||||
Type: openairt.ServerEventTypeResponseContentPartAdded,
|
||||
},
|
||||
ResponseID: "resp_001",
|
||||
ItemID: "msg_007",
|
||||
OutputIndex: 1,
|
||||
ContentIndex: 2,
|
||||
Part: openairt.MessageContentPart{
|
||||
Type: openairt.MessageContentTypeText,
|
||||
Text: "",
|
||||
},
|
||||
}
|
||||
actual, err := openairt.UnmarshalServerEvent([]byte(data))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, openairt.ServerEventTypeResponseContentPartAdded, actual.ServerEventType())
|
||||
assert.Equal(t, expected, actual.(openairt.ResponseContentPartAddedEvent))
|
||||
}
|
||||
|
||||
func TestResponseContentPartDoneEvent(t *testing.T) {
|
||||
data := `{
|
||||
"event_id": "event_3940",
|
||||
"type": "response.content_part.done",
|
||||
"response_id": "resp_001",
|
||||
"item_id": "msg_007",
|
||||
"output_index": 0,
|
||||
"content_index": 0,
|
||||
"part": {
|
||||
"type": "text",
|
||||
"text": "Sure, I can help with that."
|
||||
}
|
||||
}`
|
||||
expected := openairt.ResponseContentPartDoneEvent{
|
||||
ServerEventBase: openairt.ServerEventBase{
|
||||
EventID: "event_3940",
|
||||
Type: openairt.ServerEventTypeResponseContentPartDone,
|
||||
},
|
||||
ResponseID: "resp_001",
|
||||
ItemID: "msg_007",
|
||||
OutputIndex: 0,
|
||||
ContentIndex: 0,
|
||||
Part: openairt.MessageContentPart{
|
||||
Type: openairt.MessageContentTypeText,
|
||||
Text: "Sure, I can help with that.",
|
||||
},
|
||||
}
|
||||
actual, err := openairt.UnmarshalServerEvent([]byte(data))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, openairt.ServerEventTypeResponseContentPartDone, actual.ServerEventType())
|
||||
assert.Equal(t, expected, actual.(openairt.ResponseContentPartDoneEvent))
|
||||
}
|
||||
|
||||
func TestResponseTextDelta(t *testing.T) {
|
||||
data := `{
|
||||
"event_id": "event_4142",
|
||||
"type": "response.text.delta",
|
||||
"response_id": "resp_001",
|
||||
"item_id": "msg_007",
|
||||
"output_index": 0,
|
||||
"content_index": 0,
|
||||
"delta": "Sure, I can h"
|
||||
}`
|
||||
expected := openairt.ResponseTextDeltaEvent{
|
||||
ServerEventBase: openairt.ServerEventBase{
|
||||
EventID: "event_4142",
|
||||
Type: openairt.ServerEventTypeResponseTextDelta,
|
||||
},
|
||||
ResponseID: "resp_001",
|
||||
ItemID: "msg_007",
|
||||
OutputIndex: 0,
|
||||
ContentIndex: 0,
|
||||
Delta: "Sure, I can h",
|
||||
}
|
||||
actual, err := openairt.UnmarshalServerEvent([]byte(data))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, openairt.ServerEventTypeResponseTextDelta, actual.ServerEventType())
|
||||
assert.Equal(t, expected, actual.(openairt.ResponseTextDeltaEvent))
|
||||
}
|
||||
|
||||
func TestResponseTextDoneEvent(t *testing.T) {
|
||||
data := `{
|
||||
"event_id": "event_4344",
|
||||
"type": "response.text.done",
|
||||
"response_id": "resp_001",
|
||||
"item_id": "msg_007",
|
||||
"output_index": 0,
|
||||
"content_index": 0,
|
||||
"text": "Sure, I can help with that."
|
||||
}`
|
||||
expected := openairt.ResponseTextDoneEvent{
|
||||
ServerEventBase: openairt.ServerEventBase{
|
||||
EventID: "event_4344",
|
||||
Type: openairt.ServerEventTypeResponseTextDone,
|
||||
},
|
||||
ResponseID: "resp_001",
|
||||
ItemID: "msg_007",
|
||||
OutputIndex: 0,
|
||||
ContentIndex: 0,
|
||||
Text: "Sure, I can help with that.",
|
||||
}
|
||||
actual, err := openairt.UnmarshalServerEvent([]byte(data))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, openairt.ServerEventTypeResponseTextDone, actual.ServerEventType())
|
||||
assert.Equal(t, expected, actual.(openairt.ResponseTextDoneEvent))
|
||||
}
|
||||
|
||||
func TestResponseAudioTranscriptDelta(t *testing.T) {
|
||||
data := `{
|
||||
"event_id": "event_4546",
|
||||
"type": "response.audio_transcript.delta",
|
||||
"response_id": "resp_001",
|
||||
"item_id": "msg_008",
|
||||
"output_index": 1,
|
||||
"content_index": 2,
|
||||
"delta": "Hello, how can I a"
|
||||
}`
|
||||
expected := openairt.ResponseAudioTranscriptDeltaEvent{
|
||||
ServerEventBase: openairt.ServerEventBase{
|
||||
EventID: "event_4546",
|
||||
Type: openairt.ServerEventTypeResponseAudioTranscriptDelta,
|
||||
},
|
||||
ResponseID: "resp_001",
|
||||
ItemID: "msg_008",
|
||||
OutputIndex: 1,
|
||||
ContentIndex: 2,
|
||||
Delta: "Hello, how can I a",
|
||||
}
|
||||
actual, err := openairt.UnmarshalServerEvent([]byte(data))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, openairt.ServerEventTypeResponseAudioTranscriptDelta, actual.ServerEventType())
|
||||
assert.Equal(t, expected, actual.(openairt.ResponseAudioTranscriptDeltaEvent))
|
||||
}
|
||||
|
||||
func TestResponseAudioTranscriptDoneEvent(t *testing.T) {
|
||||
data := `{
|
||||
"event_id": "event_4748",
|
||||
"type": "response.audio_transcript.done",
|
||||
"response_id": "resp_001",
|
||||
"item_id": "msg_008",
|
||||
"output_index": 0,
|
||||
"content_index": 0,
|
||||
"transcript": "Hello, how can I assist you today?"
|
||||
}`
|
||||
expected := openairt.ResponseAudioTranscriptDoneEvent{
|
||||
ServerEventBase: openairt.ServerEventBase{
|
||||
EventID: "event_4748",
|
||||
Type: openairt.ServerEventTypeResponseAudioTranscriptDone,
|
||||
},
|
||||
ResponseID: "resp_001",
|
||||
ItemID: "msg_008",
|
||||
OutputIndex: 0,
|
||||
ContentIndex: 0,
|
||||
Transcript: "Hello, how can I assist you today?",
|
||||
}
|
||||
actual, err := openairt.UnmarshalServerEvent([]byte(data))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, openairt.ServerEventTypeResponseAudioTranscriptDone, actual.ServerEventType())
|
||||
assert.Equal(t, expected, actual.(openairt.ResponseAudioTranscriptDoneEvent))
|
||||
}
|
||||
|
||||
func TestResponseAudioDeltaEvent(t *testing.T) {
|
||||
data := `{
|
||||
"event_id": "event_4950",
|
||||
"type": "response.audio.delta",
|
||||
"response_id": "resp_001",
|
||||
"item_id": "msg_008",
|
||||
"output_index": 0,
|
||||
"content_index": 0,
|
||||
"delta": "Base64EncodedAudioDelta"
|
||||
}`
|
||||
expected := openairt.ResponseAudioDeltaEvent{
|
||||
ServerEventBase: openairt.ServerEventBase{
|
||||
EventID: "event_4950",
|
||||
Type: openairt.ServerEventTypeResponseAudioDelta,
|
||||
},
|
||||
ResponseID: "resp_001",
|
||||
ItemID: "msg_008",
|
||||
OutputIndex: 0,
|
||||
ContentIndex: 0,
|
||||
Delta: "Base64EncodedAudioDelta",
|
||||
}
|
||||
actual, err := openairt.UnmarshalServerEvent([]byte(data))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, openairt.ServerEventTypeResponseAudioDelta, actual.ServerEventType())
|
||||
assert.Equal(t, expected, actual.(openairt.ResponseAudioDeltaEvent))
|
||||
}
|
||||
|
||||
func TestResponseAudioDoneEvent(t *testing.T) {
|
||||
data := `{
|
||||
"event_id": "event_5152",
|
||||
"type": "response.audio.done",
|
||||
"response_id": "resp_001",
|
||||
"item_id": "msg_008",
|
||||
"output_index": 0,
|
||||
"content_index": 0
|
||||
}`
|
||||
expected := openairt.ResponseAudioDoneEvent{
|
||||
ServerEventBase: openairt.ServerEventBase{
|
||||
EventID: "event_5152",
|
||||
Type: openairt.ServerEventTypeResponseAudioDone,
|
||||
},
|
||||
ResponseID: "resp_001",
|
||||
ItemID: "msg_008",
|
||||
OutputIndex: 0,
|
||||
ContentIndex: 0,
|
||||
}
|
||||
actual, err := openairt.UnmarshalServerEvent([]byte(data))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, openairt.ServerEventTypeResponseAudioDone, actual.ServerEventType())
|
||||
assert.Equal(t, expected, actual.(openairt.ResponseAudioDoneEvent))
|
||||
}
|
||||
|
||||
func TestResponseFunctionCallArgumentsDelta(t *testing.T) {
|
||||
data := `{
|
||||
"event_id": "event_5354",
|
||||
"type": "response.function_call_arguments.delta",
|
||||
"response_id": "resp_002",
|
||||
"item_id": "fc_001",
|
||||
"output_index": 0,
|
||||
"call_id": "call_001",
|
||||
"delta": "{\"location\": \"San\""
|
||||
}`
|
||||
expected := openairt.ResponseFunctionCallArgumentsDeltaEvent{
|
||||
ServerEventBase: openairt.ServerEventBase{
|
||||
EventID: "event_5354",
|
||||
Type: openairt.ServerEventTypeResponseFunctionCallArgumentsDelta,
|
||||
},
|
||||
ResponseID: "resp_002",
|
||||
ItemID: "fc_001",
|
||||
OutputIndex: 0,
|
||||
CallID: "call_001",
|
||||
Delta: "{\"location\": \"San\"",
|
||||
}
|
||||
actual, err := openairt.UnmarshalServerEvent([]byte(data))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, openairt.ServerEventTypeResponseFunctionCallArgumentsDelta, actual.ServerEventType())
|
||||
assert.Equal(t, expected, actual.(openairt.ResponseFunctionCallArgumentsDeltaEvent))
|
||||
}
|
||||
|
||||
func TestResponseFunctionCallArgumentsDoneEvent(t *testing.T) {
|
||||
data := `{
|
||||
"event_id": "event_5556",
|
||||
"type": "response.function_call_arguments.done",
|
||||
"response_id": "resp_002",
|
||||
"item_id": "fc_001",
|
||||
"output_index": 1,
|
||||
"call_id": "call_001",
|
||||
"arguments": "{\"location\": \"San Francisco\"}"
|
||||
}`
|
||||
expected := openairt.ResponseFunctionCallArgumentsDoneEvent{
|
||||
ServerEventBase: openairt.ServerEventBase{
|
||||
EventID: "event_5556",
|
||||
Type: openairt.ServerEventTypeResponseFunctionCallArgumentsDone,
|
||||
},
|
||||
ResponseID: "resp_002",
|
||||
ItemID: "fc_001",
|
||||
OutputIndex: 1,
|
||||
CallID: "call_001",
|
||||
Arguments: "{\"location\": \"San Francisco\"}",
|
||||
}
|
||||
actual, err := openairt.UnmarshalServerEvent([]byte(data))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, openairt.ServerEventTypeResponseFunctionCallArgumentsDone, actual.ServerEventType())
|
||||
assert.Equal(t, expected, actual.(openairt.ResponseFunctionCallArgumentsDoneEvent))
|
||||
}
|
||||
|
||||
func TestRateLimitsUpdatedEvent(t *testing.T) {
|
||||
data := `{
|
||||
"event_id": "event_5758",
|
||||
"type": "rate_limits.updated",
|
||||
"rate_limits": [
|
||||
{
|
||||
"name": "requests",
|
||||
"limit": 1000,
|
||||
"remaining": 999,
|
||||
"reset_seconds": 60.0
|
||||
},
|
||||
{
|
||||
"name": "tokens",
|
||||
"limit": 50000,
|
||||
"remaining": 49950,
|
||||
"reset_seconds": 60.0
|
||||
}
|
||||
]
|
||||
}`
|
||||
expected := openairt.RateLimitsUpdatedEvent{
|
||||
ServerEventBase: openairt.ServerEventBase{
|
||||
EventID: "event_5758",
|
||||
Type: openairt.ServerEventTypeRateLimitsUpdated,
|
||||
},
|
||||
RateLimits: []openairt.RateLimit{
|
||||
{
|
||||
Name: "requests",
|
||||
Limit: 1000,
|
||||
Remaining: 999,
|
||||
ResetSeconds: 60,
|
||||
},
|
||||
{
|
||||
Name: "tokens",
|
||||
Limit: 50000,
|
||||
Remaining: 49950,
|
||||
ResetSeconds: 60,
|
||||
},
|
||||
},
|
||||
}
|
||||
actual, err := openairt.UnmarshalServerEvent([]byte(data))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, openairt.ServerEventTypeRateLimitsUpdated, actual.ServerEventType())
|
||||
assert.Equal(t, expected, actual.(openairt.RateLimitsUpdatedEvent))
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
package openairt
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/sashabaranov/go-openai/jsonschema"
|
||||
)
|
||||
|
||||
type Voice string
|
||||
|
||||
const (
|
||||
VoiceAlloy Voice = "alloy"
|
||||
VoiceShimmer Voice = "shimmer"
|
||||
VoiceEcho Voice = "echo"
|
||||
)
|
||||
|
||||
type AudioFormat string
|
||||
|
||||
const (
|
||||
AudioFormatPcm16 AudioFormat = "pcm16"
|
||||
AudioFormatG711Ulaw AudioFormat = "g711-ulaw"
|
||||
AudioFormatG711Alaw AudioFormat = "g711-alaw"
|
||||
)
|
||||
|
||||
type Modality string
|
||||
|
||||
const (
|
||||
ModalityText Modality = "text"
|
||||
ModalityAudio Modality = "audio"
|
||||
)
|
||||
|
||||
type TurnDetectionType string
|
||||
|
||||
const (
|
||||
TurnDetectionTypeNone TurnDetectionType = "none"
|
||||
TurnDetectionTypeServerVad TurnDetectionType = "server_vad"
|
||||
)
|
||||
|
||||
type TurnDetection struct {
|
||||
Type TurnDetectionType `json:"type"`
|
||||
Threshold float64 `json:"threshold,omitempty"`
|
||||
PrefixPaddingMs int `json:"prefix_padding_ms,omitempty"`
|
||||
SilenceDurationMs int `json:"silence_duration_ms,omitempty"`
|
||||
}
|
||||
|
||||
type ToolType string
|
||||
|
||||
const (
|
||||
ToolTypeFunction ToolType = "function"
|
||||
)
|
||||
|
||||
type ToolChoiceInterface interface {
|
||||
ToolChoice()
|
||||
}
|
||||
|
||||
type ToolChoiceString string
|
||||
|
||||
func (ToolChoiceString) ToolChoice() {}
|
||||
|
||||
const (
|
||||
ToolChoiceAuto ToolChoiceString = "auto"
|
||||
ToolChoiceNone ToolChoiceString = "none"
|
||||
ToolChoiceRequired ToolChoiceString = "required"
|
||||
)
|
||||
|
||||
type ToolChoice struct {
|
||||
Type ToolType `json:"type"`
|
||||
Function ToolFunction `json:"function,omitempty"`
|
||||
}
|
||||
|
||||
func (t ToolChoice) ToolChoice() {}
|
||||
|
||||
type ToolFunction struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type MessageRole string
|
||||
|
||||
const (
|
||||
MessageRoleSystem MessageRole = "system"
|
||||
MessageRoleAssistant MessageRole = "assistant"
|
||||
MessageRoleUser MessageRole = "user"
|
||||
)
|
||||
|
||||
type InputAudioTranscription struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Model string `json:"model"`
|
||||
}
|
||||
|
||||
type Tool struct {
|
||||
Type ToolType `json:"type"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Parameters jsonschema.Definition `json:"parameters"`
|
||||
}
|
||||
|
||||
type MessageItemType string
|
||||
|
||||
const (
|
||||
MessageItemTypeMessage MessageItemType = "message"
|
||||
MessageItemTypeFunctionCall MessageItemType = "function_call"
|
||||
MessageItemTypeFunctionCallOutput MessageItemType = "function_call_output"
|
||||
)
|
||||
|
||||
type MessageContentType string
|
||||
|
||||
const (
|
||||
MessageContentTypeText MessageContentType = "text"
|
||||
MessageContentTypeAudio MessageContentType = "audio"
|
||||
MessageContentTypeTranscript MessageContentType = "transcript"
|
||||
MessageContentTypeInputText MessageContentType = "input_text"
|
||||
MessageContentTypeInputAudio MessageContentType = "input_audio"
|
||||
)
|
||||
|
||||
type MessageContentPart struct {
|
||||
Type MessageContentType `json:"type"`
|
||||
Text string `json:"text,omitempty"`
|
||||
Audio string `json:"audio,omitempty"`
|
||||
Transcript string `json:"transcript,omitempty"`
|
||||
}
|
||||
|
||||
type MessageItem struct {
|
||||
ID string `json:"id"`
|
||||
Type MessageItemType `json:"type"`
|
||||
Status ItemStatus `json:"status"`
|
||||
Role MessageRole `json:"role"`
|
||||
Content []MessageContentPart `json:"content"`
|
||||
}
|
||||
|
||||
type ResponseMessageItem struct {
|
||||
MessageItem
|
||||
Object string `json:"object,omitempty"`
|
||||
}
|
||||
|
||||
type Error struct {
|
||||
Message string `json:"message,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
Code string `json:"code,omitempty"`
|
||||
Param string `json:"param,omitempty"`
|
||||
EventID string `json:"event_id,omitempty"`
|
||||
}
|
||||
|
||||
// ServerToolChoice is a type that can be used to choose a tool response from the server.
|
||||
type ServerToolChoice struct {
|
||||
String ToolChoiceString
|
||||
Function ToolChoice
|
||||
}
|
||||
|
||||
// UnmarshalJSON is a custom unmarshaler for ServerToolChoice.
|
||||
func (m *ServerToolChoice) UnmarshalJSON(data []byte) error {
|
||||
err := json.Unmarshal(data, &m.Function)
|
||||
if err != nil {
|
||||
if data[0] == '"' {
|
||||
data = data[1:]
|
||||
}
|
||||
if data[len(data)-1] == '"' {
|
||||
data = data[:len(data)-1]
|
||||
}
|
||||
m.String = ToolChoiceString(data)
|
||||
m.Function = ToolChoice{}
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsFunction returns true if the tool choice is a function call.
|
||||
func (m *ServerToolChoice) IsFunction() bool {
|
||||
return m.Function.Type == ToolTypeFunction
|
||||
}
|
||||
|
||||
// Get returns the ToolChoiceInterface based on the type of tool choice.
|
||||
func (m ServerToolChoice) Get() ToolChoiceInterface {
|
||||
if m.IsFunction() {
|
||||
return m.Function
|
||||
}
|
||||
return m.String
|
||||
}
|
||||
|
||||
type ServerSession struct {
|
||||
ID string `json:"id"`
|
||||
Object string `json:"object"`
|
||||
Model string `json:"model"`
|
||||
Modalities []Modality `json:"modalities,omitempty"`
|
||||
Instructions string `json:"instructions,omitempty"`
|
||||
Voice Voice `json:"voice,omitempty"`
|
||||
InputAudioFormat AudioFormat `json:"input_audio_format,omitempty"`
|
||||
OutputAudioFormat AudioFormat `json:"output_audio_format,omitempty"`
|
||||
InputAudioTranscription *InputAudioTranscription `json:"input_audio_transcription,omitempty"`
|
||||
TurnDetection *TurnDetection `json:"turn_detection,omitempty"`
|
||||
Tools []Tool `json:"tools,omitempty"`
|
||||
ToolChoice ServerToolChoice `json:"tool_choice,omitempty"`
|
||||
Temperature *float32 `json:"temperature,omitempty"`
|
||||
MaxOutputTokens int `json:"max_output_tokens,omitempty"`
|
||||
}
|
||||
|
||||
type ItemStatus string
|
||||
|
||||
const (
|
||||
ItemStatusInProgress ItemStatus = "in_progress"
|
||||
ItemStatusCompleted ItemStatus = "completed"
|
||||
ItemStatusIncomplete ItemStatus = "incomplete"
|
||||
)
|
||||
|
||||
type Conversation struct {
|
||||
ID string `json:"id"`
|
||||
Object string `json:"object"`
|
||||
}
|
||||
|
||||
type ResponseStatus string
|
||||
|
||||
const (
|
||||
ResponseStatusInProgress ResponseStatus = "in_progress"
|
||||
ResponseStatusCompleted ResponseStatus = "completed"
|
||||
ResponseStatusCancelled ResponseStatus = "cancelled"
|
||||
ResponseStatusIncomplete ResponseStatus = "incomplete"
|
||||
ResponseStatusFailed ResponseStatus = "failed"
|
||||
)
|
||||
|
||||
type Usage struct {
|
||||
TotalTokens int `json:"total_tokens"`
|
||||
InputTokens int `json:"input_tokens"`
|
||||
OutputTokens int `json:"output_tokens"`
|
||||
}
|
||||
|
||||
type Response struct {
|
||||
ID string `json:"id"`
|
||||
Object string `json:"object"`
|
||||
Status ResponseStatus `json:"status"`
|
||||
StatusDetails any `json:"status_details,omitempty"`
|
||||
Output []ResponseMessageItem `json:"output"`
|
||||
Usage *Usage `json:"usage,omitempty"`
|
||||
}
|
||||
|
||||
type RateLimit struct {
|
||||
Name string `json:"name"`
|
||||
Limit int `json:"limit"`
|
||||
Remaining int `json:"remaining"`
|
||||
ResetSeconds float64 `json:"reset_seconds"`
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package openairt
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"math/big"
|
||||
)
|
||||
|
||||
func GenerateId(prefix string, length int) string {
|
||||
const chars = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
|
||||
if length <= len(prefix) {
|
||||
return prefix
|
||||
}
|
||||
|
||||
result := make([]byte, length-len(prefix))
|
||||
for i := range result {
|
||||
num, _ := rand.Int(rand.Reader, big.NewInt(int64(len(chars))))
|
||||
result[i] = chars[num.Int64()]
|
||||
}
|
||||
|
||||
return prefix + string(result)
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package openairt_test
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
openairt "github.com/WqyJh/go-openai-realtime"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGenerateId(t *testing.T) {
|
||||
generated := openairt.GenerateId("test_", 10)
|
||||
t.Logf("generated: %s", generated)
|
||||
assert.True(t, strings.HasPrefix(generated, "test_"))
|
||||
assert.Equal(t, 10, len(generated))
|
||||
|
||||
generated = openairt.GenerateId("test_", 20)
|
||||
t.Logf("generated: %s", generated)
|
||||
assert.True(t, strings.HasPrefix(generated, "test_"))
|
||||
assert.Equal(t, 20, len(generated))
|
||||
}
|
||||
Reference in New Issue
Block a user