package voiceprovider import ( "bytes" "context" "encoding/json" "errors" "fmt" "io" "net/http" "strings" "time" ) type StartRequest struct { TaskRef string `json:"task_ref"` RoomID string `json:"room_id"` DeviceUserID string `json:"device_user_id"` AgentUserID string `json:"agent_user_id"` ConversationID string `json:"conversation_id"` EngineMode string `json:"engine_mode"` ContextSummary string `json:"context_summary,omitempty"` PromptBundleVersion string `json:"prompt_bundle_version,omitempty"` AgentBundleVersion string `json:"agent_bundle_version_id,omitempty"` PersonaVersion string `json:"persona_version_id,omitempty"` VoiceVersion string `json:"voice_version_id,omitempty"` ModelVersion string `json:"model_version_id,omitempty"` RoutingVersion string `json:"routing_version_id,omitempty"` SystemPrompt string `json:"system_prompt,omitempty"` PersonaTone string `json:"persona_tone,omitempty"` VoiceResourceRef string `json:"voice_resource_ref,omitempty"` ModelResourceRef string `json:"model_resource_ref,omitempty"` } type CallbackEvent struct { Provider string `json:"provider"` EventID string `json:"event_id"` TaskRef string `json:"task_ref"` ConversationID string `json:"conversation_id"` EventType string `json:"event_type"` Status string `json:"status"` OccurredAt time.Time `json:"occurred_at"` Turns []FinalTurn `json:"turns,omitempty"` Usage []UsageMetric `json:"usage,omitempty"` } type UsageMetric struct { UsageCategory string `json:"usage_category"` MetricCode string `json:"metric_code"` Quantity string `json:"quantity"` Unit string `json:"unit"` } type FinalTurn struct { TurnKey string `json:"turn_key"` SequenceNo int64 `json:"sequence_no"` Speaker string `json:"speaker"` StartAt time.Time `json:"start_at"` EndAt time.Time `json:"end_at"` FinalText string `json:"final_text"` Interrupted bool `json:"interrupted"` ProviderRef string `json:"provider_ref,omitempty"` } type StartResponse struct { TaskRef string `json:"task_ref"` Status string `json:"status"` } type StopRequest struct { TaskRef string `json:"task_ref"` RoomID string `json:"room_id,omitempty"` ConversationID string `json:"conversation_id"` Reason string `json:"reason"` } type StopResponse struct { TaskRef string `json:"task_ref"` Status string `json:"status"` } type QueryRequest struct { TaskRef string `json:"task_ref"` ConversationID string `json:"conversation_id"` } type QueryResponse struct { TaskRef string `json:"task_ref"` Status string `json:"status"` } type Client interface { Start(context.Context, StartRequest) (StartResponse, error) Query(context.Context, QueryRequest) (QueryResponse, error) Stop(context.Context, StopRequest) (StopResponse, error) } type Error struct { Operation string StatusCode int Code string Temporary bool Cause error } func (e *Error) Error() string { if e.Code != "" { return fmt.Sprintf("provider %s returned %s: %v", e.Operation, e.Code, e.Cause) } if e.StatusCode > 0 { return fmt.Sprintf("provider %s returned HTTP %d", e.Operation, e.StatusCode) } return fmt.Sprintf("provider %s failed: %v", e.Operation, e.Cause) } func (e *Error) Unwrap() error { return e.Cause } func IsTemporary(err error) bool { var providerError *Error return errors.As(err, &providerError) && providerError.Temporary } func IsNotFound(err error) bool { var providerError *Error return errors.As(err, &providerError) && providerError.StatusCode == http.StatusNotFound } type HTTPClient struct { baseURL string apiKey string client *http.Client } func NewHTTPClient(baseURL, apiKey string, timeout time.Duration) (*HTTPClient, error) { if strings.TrimSpace(baseURL) == "" || strings.TrimSpace(apiKey) == "" || timeout <= 0 { return nil, errors.New("provider base URL, API key and positive timeout are required") } return &HTTPClient{baseURL: strings.TrimRight(baseURL, "/"), apiKey: apiKey, client: &http.Client{Timeout: timeout}}, nil } func (c *HTTPClient) Start(ctx context.Context, input StartRequest) (StartResponse, error) { body, err := json.Marshal(input) if err != nil { return StartResponse{}, err } request, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+"/mock/v1/voice-chats:start", bytes.NewReader(body)) if err != nil { return StartResponse{}, err } request.Header.Set("Content-Type", "application/json") request.Header.Set("X-Mock-Provider-Key", c.apiKey) response, err := c.client.Do(request) if err != nil { return StartResponse{}, &Error{Operation: "start", Temporary: true, Cause: err} } defer response.Body.Close() responseBody, err := io.ReadAll(io.LimitReader(response.Body, 1<<20)) if err != nil { return StartResponse{}, &Error{Operation: "start", Temporary: true, Cause: err} } if response.StatusCode < 200 || response.StatusCode >= 300 { return StartResponse{}, &Error{Operation: "start", StatusCode: response.StatusCode, Temporary: response.StatusCode == http.StatusTooManyRequests || response.StatusCode >= 500} } var result StartResponse if err := json.Unmarshal(responseBody, &result); err != nil || result.TaskRef != input.TaskRef || result.Status == "" { return StartResponse{}, errors.New("provider start returned an invalid response") } return result, nil } func (c *HTTPClient) Stop(ctx context.Context, input StopRequest) (StopResponse, error) { body, err := json.Marshal(input) if err != nil { return StopResponse{}, err } request, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+"/mock/v1/voice-chats:stop", bytes.NewReader(body)) if err != nil { return StopResponse{}, err } request.Header.Set("Content-Type", "application/json") request.Header.Set("X-Mock-Provider-Key", c.apiKey) response, err := c.client.Do(request) if err != nil { return StopResponse{}, &Error{Operation: "stop", Temporary: true, Cause: err} } defer response.Body.Close() responseBody, err := io.ReadAll(io.LimitReader(response.Body, 1<<20)) if err != nil { return StopResponse{}, &Error{Operation: "stop", Temporary: true, Cause: err} } if response.StatusCode < 200 || response.StatusCode >= 300 { return StopResponse{}, &Error{Operation: "stop", StatusCode: response.StatusCode, Temporary: response.StatusCode == http.StatusTooManyRequests || response.StatusCode >= 500} } var result StopResponse if err := json.Unmarshal(responseBody, &result); err != nil || result.TaskRef != input.TaskRef || result.Status == "" { return StopResponse{}, errors.New("provider stop returned an invalid response") } return result, nil } func (c *HTTPClient) Query(ctx context.Context, input QueryRequest) (QueryResponse, error) { body, err := json.Marshal(input) if err != nil { return QueryResponse{}, err } request, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+"/mock/v1/voice-chats:query", bytes.NewReader(body)) if err != nil { return QueryResponse{}, err } request.Header.Set("Content-Type", "application/json") request.Header.Set("X-Mock-Provider-Key", c.apiKey) response, err := c.client.Do(request) if err != nil { return QueryResponse{}, &Error{Operation: "query", Temporary: true, Cause: err} } defer response.Body.Close() responseBody, err := io.ReadAll(io.LimitReader(response.Body, 1<<20)) if err != nil { return QueryResponse{}, &Error{Operation: "query", Temporary: true, Cause: err} } if response.StatusCode < 200 || response.StatusCode >= 300 { return QueryResponse{}, &Error{Operation: "query", StatusCode: response.StatusCode, Temporary: response.StatusCode == http.StatusTooManyRequests || response.StatusCode >= 500} } var result QueryResponse if err := json.Unmarshal(responseBody, &result); err != nil || result.TaskRef != input.TaskRef || result.Status == "" { return QueryResponse{}, errors.New("provider query returned an invalid response") } return result, nil }