package realtime import ( "ai-operator/internal/ai" "encoding/base64" "encoding/json" "time" ) type GenericRealtimeEvent struct { Type string Raw map[string]any } func ParseServerEvent(data []byte) (ai.VoiceEvent, GenericRealtimeEvent, error) { var base map[string]any if err := json.Unmarshal(data, &base); err != nil { return ai.VoiceEvent{}, GenericRealtimeEvent{}, err } typ, _ := base["type"].(string) ev := ai.VoiceEvent{At: time.Now().UTC(), Metadata: map[string]any{"openai_type": typ}} switch typ { case "session.created": ev.Type = ai.VoiceEventSessionStarted return ev, GenericRealtimeEvent{Type: typ, Raw: base}, nil case "session.updated": ev.Type = ai.VoiceEventSessionUpdated return ev, GenericRealtimeEvent{Type: typ, Raw: base}, nil case "error": ev.Type = ai.VoiceEventError if e, ok := base["error"].(map[string]any); ok { ev.Error = toStr(e["message"]) } else { ev.Error = "openai error" } return ev, GenericRealtimeEvent{Type: typ, Raw: base}, nil case "input_audio_buffer.speech_started": ev.Type = ai.VoiceEventInterruption return ev, GenericRealtimeEvent{Type: typ, Raw: base}, nil case "response.output_audio.delta": ev.Type = ai.VoiceEventAssistantAudioDelta b, err := base64.StdEncoding.DecodeString(toStr(base["delta"])) if err != nil { return ai.VoiceEvent{}, GenericRealtimeEvent{}, err } ev.Audio = b return ev, GenericRealtimeEvent{Type: typ, Raw: base}, nil case "response.output_audio.done": ev.Type = ai.VoiceEventAssistantAudioDone return ev, GenericRealtimeEvent{Type: typ, Raw: base}, nil case "response.output_audio_transcript.delta", "response.output_text.delta": ev.Type = ai.VoiceEventAssistantTranscriptDelta ev.Text = toStr(base["delta"]) return ev, GenericRealtimeEvent{Type: typ, Raw: base}, nil case "response.output_audio_transcript.done", "response.output_text.done": ev.Type = ai.VoiceEventAssistantTranscriptDone ev.Text = toStr(base["transcript"]) + toStr(base["text"]) return ev, GenericRealtimeEvent{Type: typ, Raw: base}, nil case "rate_limits.updated": ev.Type = ai.VoiceEventRateLimitsUpdated ev.Metadata = base return ev, GenericRealtimeEvent{Type: typ, Raw: base}, nil case "response.done": if tc := extractToolCall(base); tc != nil { ev.Type = ai.VoiceEventToolCall ev.ToolCall = tc return ev, GenericRealtimeEvent{Type: typ, Raw: base}, nil } ev.Type = ai.VoiceEventAssistantAudioDone return ev, GenericRealtimeEvent{Type: typ, Raw: base}, nil default: return ai.VoiceEvent{}, GenericRealtimeEvent{Type: typ, Raw: base}, nil } } func extractToolCall(m map[string]any) *ai.ToolCall { resp, ok := m["response"].(map[string]any) if !ok { return nil } outs, ok := resp["output"].([]any) if !ok { return nil } for _, o := range outs { item, ok := o.(map[string]any) if !ok { continue } if toStr(item["type"]) == "function_call" { raw := toStr(item["arguments"]) args := map[string]any{} _ = json.Unmarshal([]byte(raw), &args) return &ai.ToolCall{ID: toStr(item["call_id"]), Name: toStr(item["name"]), Arguments: args, RawArguments: raw} } } return nil } func toStr(v any) string { if s, ok := v.(string); ok { return s } return "" }