feat: add /realtime/sessions api

This commit is contained in:
WqyJh
2025-02-08 19:51:57 +08:00
parent 4adcbdf285
commit 1236a47a1e
7 changed files with 230 additions and 3 deletions
+92
View File
@@ -0,0 +1,92 @@
package openairt
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
)
type CreateSessionRequest struct {
ClientSession
// The Realtime model used for this session.
Model string `json:"model"`
}
type ClientSecret struct {
// Ephemeral key usable in client environments to authenticate connections to the Realtime API. Use this in client-side environments rather than a standard API token, which should only be used server-side.
Value string `json:"value"`
// Timestamp for when the token expires. Currently, all tokens expire after one minute.
ExpiresAt int64 `json:"expires_at"`
}
type CreateSessionResponse struct {
ServerSession
// Ephemeral key returned by the API.
ClientSecret ClientSecret `json:"client_secret"`
}
type httpOption struct {
client *http.Client
headers http.Header
method string
}
type HttpOption func(*httpOption)
func WithHeaders(headers http.Header) HttpOption {
return func(o *httpOption) {
o.headers = headers
}
}
func WithClient(client *http.Client) HttpOption {
return func(o *httpOption) {
o.client = client
}
}
func WithMethod(method string) HttpOption {
return func(o *httpOption) {
o.method = method
}
}
func HttpDo[Q any, R any](ctx context.Context, url string, req *Q, opts ...HttpOption) (resp *R, err error) {
opt := httpOption{
client: http.DefaultClient,
headers: http.Header{},
method: http.MethodPost,
}
for _, o := range opts {
o(&opt)
}
data, err := json.Marshal(req)
if err != nil {
return nil, fmt.Errorf("failed to marshal request: %w", err)
}
request, err := http.NewRequestWithContext(ctx, opt.method, url, bytes.NewReader(data))
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
request.Header = opt.headers
response, err := opt.client.Do(request)
if err != nil {
return nil, fmt.Errorf("http failed: %w", err)
}
if response.StatusCode != http.StatusOK {
return nil, fmt.Errorf("http status code: %d", response.StatusCode)
}
err = json.NewDecoder(response.Body).Decode(&resp)
if err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
}
return resp, err
}
+97
View File
@@ -0,0 +1,97 @@
package openairt_test
import (
"encoding/json"
"testing"
openairt "github.com/WqyJh/go-openai-realtime"
"github.com/WqyJh/jsontools"
"github.com/sashabaranov/go-openai"
"github.com/stretchr/testify/require"
)
func TestCreateSessionRequest(t *testing.T) {
data := `{
"model": "gpt-4o-realtime-preview-2024-12-17",
"modalities": ["audio", "text"],
"instructions": "You are a friendly assistant."
}`
expected := openairt.CreateSessionRequest{
Model: openairt.GPT4oRealtimePreview20241217,
ClientSession: openairt.ClientSession{
Modalities: []openairt.Modality{
openairt.ModalityAudio,
openairt.ModalityText,
},
Instructions: "You are a friendly assistant.",
},
}
var actual openairt.CreateSessionRequest
err := json.Unmarshal([]byte(data), &actual)
require.NoError(t, err)
require.Equal(t, expected, actual)
actualBytes, err := json.Marshal(actual)
require.NoError(t, err)
jsontools.RequireJSONEq(t, data, string(actualBytes))
}
func TestCreateSessionResponse(t *testing.T) {
data := `{
"id": "sess_001",
"object": "realtime.session",
"model": "gpt-4o-realtime-preview-2024-12-17",
"modalities": ["audio", "text"],
"instructions": "You are a friendly assistant.",
"voice": "alloy",
"input_audio_format": "pcm16",
"output_audio_format": "pcm16",
"input_audio_transcription": {
"model": "whisper-1"
},
"turn_detection": null,
"tools": [],
"tool_choice": "none",
"temperature": 0.7,
"max_response_output_tokens": 200,
"client_secret": {
"value": "ek_abc123",
"expires_at": 1234567890
}
}
`
temperature := float32(0.7)
expected := openairt.CreateSessionResponse{
ClientSecret: openairt.ClientSecret{
Value: "ek_abc123",
ExpiresAt: 1234567890,
},
ServerSession: openairt.ServerSession{
ID: "sess_001",
Object: "realtime.session",
Model: openairt.GPT4oRealtimePreview20241217,
Modalities: []openairt.Modality{
openairt.ModalityAudio,
openairt.ModalityText,
},
Instructions: "You are a friendly assistant.",
Voice: openairt.VoiceAlloy,
InputAudioFormat: openairt.AudioFormatPcm16,
OutputAudioFormat: openairt.AudioFormatPcm16,
InputAudioTranscription: &openairt.InputAudioTranscription{
Model: openai.Whisper1,
},
TurnDetection: nil,
Tools: []openairt.Tool{},
ToolChoice: openairt.ServerToolChoice{String: openairt.ToolChoiceNone},
Temperature: &temperature,
MaxOutputTokens: openairt.IntOrInf(200),
},
}
var actual openairt.CreateSessionResponse
err := json.Unmarshal([]byte(data), &actual)
require.NoError(t, err)
require.Equal(t, expected, actual)
}
+23
View File
@@ -113,3 +113,26 @@ func (c *Client) Connect(ctx context.Context, opts ...ConnectOption) (*Conn, err
return &Conn{conn: conn, logger: connectOpts.logger}, nil
}
func (c *Client) getApiHeaders() 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("Content-Type", "application/json")
return headers
}
func (c *Client) CreateSession(ctx context.Context, req *CreateSessionRequest) (*CreateSessionResponse, error) {
return HttpDo[CreateSessionRequest, CreateSessionResponse](
ctx,
c.config.ApiBaseURL+"/realtime/sessions",
req,
WithClient(c.config.HTTPClient),
WithMethod(http.MethodPost),
WithHeaders(c.getApiHeaders()),
)
}
+13 -3
View File
@@ -1,5 +1,7 @@
package openairt
import "net/http"
// APIType is the type of API.
type APIType string
@@ -13,6 +15,9 @@ const (
const (
// OpenaiRealtimeAPIURLv1 is the base URL for the OpenAI Realtime API.
OpenaiRealtimeAPIURLv1 = "wss://api.openai.com/v1/realtime"
// OpenaiAPIURLv1 is the base URL for the OpenAI API.
OpenaiAPIURLv1 = "https://api.openai.com/v1"
)
const (
@@ -25,17 +30,21 @@ type ClientConfig struct {
authToken string
BaseURL string // Base URL for the API. Defaults to "wss://api.openai.com/v1/realtime"
ApiBaseURL string // Base URL for the API. Defaults to "https://api.openai.com/v1"
APIType APIType // API type. Defaults to APITypeOpenAI
APIVersion string // required when APIType is APITypeAzure
HTTPClient *http.Client
}
// DefaultConfig creates a new ClientConfig with the given auth token.
// Defaults to using the OpenAI Realtime API.
func DefaultConfig(authToken string) ClientConfig {
return ClientConfig{
authToken: authToken,
BaseURL: OpenaiRealtimeAPIURLv1,
APIType: APITypeOpenAI,
authToken: authToken,
BaseURL: OpenaiRealtimeAPIURLv1,
ApiBaseURL: OpenaiAPIURLv1,
APIType: APITypeOpenAI,
HTTPClient: &http.Client{},
}
}
@@ -47,6 +56,7 @@ func DefaultAzureConfig(apiKey, baseURL string) ClientConfig {
BaseURL: baseURL,
APIType: APITypeAzure,
APIVersion: azureAPIVersion20241001Preview,
HTTPClient: &http.Client{},
}
}
+1
View File
@@ -3,6 +3,7 @@ module github.com/WqyJh/go-openai-realtime
go 1.19
require (
github.com/WqyJh/jsontools v0.3.1
github.com/coder/websocket v1.8.12
github.com/sashabaranov/go-openai v1.32.0
github.com/stretchr/testify v1.9.0
+2
View File
@@ -1,3 +1,5 @@
github.com/WqyJh/jsontools v0.3.1 h1:zKT+DvxUSTji06ZcjsbQzZ48PycFZDI0OGATmmFhJ+U=
github.com/WqyJh/jsontools v0.3.1/go.mod h1:Gk2OlyXjAJmYNZ0aUbEXGHq4I5ihGRjXxVuUprWtkss=
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=
+2
View File
@@ -64,6 +64,8 @@ type TurnDetectionParams struct {
PrefixPaddingMs int `json:"prefix_padding_ms,omitempty"`
// Duration of silence to detect speech stop (in milliseconds).
SilenceDurationMs int `json:"silence_duration_ms,omitempty"`
// Whether or not to automatically generate a response when VAD is enabled. true by default.
CreateResponse *bool `json:"create_response,omitempty"`
}
type ClientTurnDetection struct {