mirror of
https://github.com/hanzoai/go-openai-realtime.git
synced 2026-09-30 21:54:42 +00:00
feat: add /realtime/sessions api
This commit is contained in:
@@ -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
@@ -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)
|
||||
}
|
||||
@@ -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()),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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,8 +30,10 @@ 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.
|
||||
@@ -35,7 +42,9 @@ func DefaultConfig(authToken string) ClientConfig {
|
||||
return ClientConfig{
|
||||
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{},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user