mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-06-12 18:08:54 +00:00
Deduplicate ParseDataAudioURL function
This commit is contained in:
@@ -127,7 +127,7 @@ func SerializeMessages(messages []Message) []any {
|
||||
continue
|
||||
}
|
||||
|
||||
if format, data, ok := parseDataAudioURL(mediaURL); ok {
|
||||
if format, data, ok := ParseDataAudioURL(mediaURL); ok {
|
||||
parts = append(parts, map[string]any{
|
||||
"type": "input_audio",
|
||||
"input_audio": map[string]any{
|
||||
@@ -205,7 +205,8 @@ func serializeToolCalls(toolCalls []ToolCall) []openaiToolCall {
|
||||
return out
|
||||
}
|
||||
|
||||
func parseDataAudioURL(mediaURL string) (format, data string, ok bool) {
|
||||
// ParseDataAudioURL extracts the format and base64 data from a data:audio/... URL.
|
||||
func ParseDataAudioURL(mediaURL string) (format, data string, ok bool) {
|
||||
if !strings.HasPrefix(mediaURL, "data:audio/") {
|
||||
return "", "", false
|
||||
}
|
||||
@@ -478,69 +479,6 @@ func AsInt(v any) (int, bool) {
|
||||
}
|
||||
}
|
||||
|
||||
// ExtractProtocol extracts the effective protocol and model identifier from a
|
||||
// model configuration.
|
||||
//
|
||||
// The explicit Provider field takes precedence. When Provider is empty, the
|
||||
// protocol is inferred from Model. Plain model names default to "openai".
|
||||
// Provider-prefixed models strip the first slash-separated segment from the
|
||||
// returned model ID.
|
||||
//
|
||||
// The returned protocol is normalized to the provider's canonical spelling.
|
||||
// Examples:
|
||||
// - Model "openai/gpt-4o" -> ("openai", "gpt-4o")
|
||||
// - Model "nvidia/z-ai/glm-5.1" -> ("nvidia", "z-ai/glm-5.1")
|
||||
// - Provider "nvidia", Model "z-ai/glm-5.1" -> ("nvidia", "z-ai/glm-5.1")
|
||||
// - Provider "openai", Model "openai/gpt-4o" -> ("openai", "openai/gpt-4o")
|
||||
// - Model "gpt-4o" -> ("openai", "gpt-4o")
|
||||
func ExtractProtocol(model string) (protocol, modelID string) {
|
||||
if cfg == nil {
|
||||
return "", ""
|
||||
}
|
||||
|
||||
model := strings.TrimSpace(cfg.Model)
|
||||
if provider := strings.TrimSpace(cfg.Provider); provider != "" {
|
||||
return NormalizeProvider(provider), model
|
||||
}
|
||||
if model == "" {
|
||||
return "", ""
|
||||
}
|
||||
|
||||
protocol, rest, found := strings.Cut(model, "/")
|
||||
if !found {
|
||||
return "openai", model
|
||||
}
|
||||
protocol = strings.TrimSpace(protocol)
|
||||
if protocol == "" {
|
||||
return "", strings.TrimSpace(rest)
|
||||
}
|
||||
return NormalizeProvider(protocol), strings.TrimSpace(rest)
|
||||
}
|
||||
|
||||
// NormalizeAnthropicBaseURL ensures the Anthropic base URL is properly formatted.
|
||||
// It removes a trailing /v1 suffix if present (to avoid duplication), then
|
||||
// re-appends /v1 when appendV1Suffix is true. An empty apiBase falls back to
|
||||
// defaultBaseURL.
|
||||
func NormalizeAnthropicBaseURL(apiBase, defaultBaseURL string, appendV1Suffix bool) string {
|
||||
base := strings.TrimSpace(apiBase)
|
||||
if base == "" {
|
||||
return defaultBaseURL
|
||||
}
|
||||
|
||||
base = strings.TrimRight(base, "/")
|
||||
if before, ok := strings.CutSuffix(base, "/v1"); ok {
|
||||
base = before
|
||||
}
|
||||
if base == "" {
|
||||
return defaultBaseURL
|
||||
}
|
||||
|
||||
if appendV1Suffix {
|
||||
return base + "/v1"
|
||||
}
|
||||
return base
|
||||
}
|
||||
|
||||
// AsFloat converts various numeric types to float64.
|
||||
func AsFloat(v any) (float64, bool) {
|
||||
switch val := v.(type) {
|
||||
|
||||
@@ -691,6 +691,37 @@ func TestExtractProtocol(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// --- ParseDataAudioURL tests ---
|
||||
|
||||
func TestParseDataAudioURL(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
mediaURL string
|
||||
wantFormat string
|
||||
wantData string
|
||||
wantOK bool
|
||||
}{
|
||||
{"valid mp3", "data:audio/mp3;base64,SGVsbG8=", "mp3", "SGVsbG8=", true},
|
||||
{"valid wav", "data:audio/wav;base64,AAAA", "wav", "AAAA", true},
|
||||
{"not audio", "data:image/png;base64,abc", "", "", false},
|
||||
{"no comma", "data:audio/mp3;base64", "", "", false},
|
||||
{"empty data", "data:audio/mp3;base64,", "", "", false},
|
||||
{"empty string", "", "", "", false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
format, data, ok := ParseDataAudioURL(tt.mediaURL)
|
||||
if ok != tt.wantOK || format != tt.wantFormat || data != tt.wantData {
|
||||
t.Errorf(
|
||||
"ParseDataAudioURL(%q) = (%q, %q, %v), want (%q, %q, %v)",
|
||||
tt.mediaURL, format, data, ok,
|
||||
tt.wantFormat, tt.wantData, tt.wantOK,
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// --- NormalizeAnthropicBaseURL tests ---
|
||||
|
||||
func TestNormalizeAnthropicBaseURL(t *testing.T) {
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/openai/openai-go/v3"
|
||||
"github.com/openai/openai-go/v3/responses"
|
||||
|
||||
"github.com/sipeed/picoclaw/pkg/providers/common"
|
||||
"github.com/sipeed/picoclaw/pkg/providers/protocoltypes"
|
||||
)
|
||||
|
||||
@@ -118,7 +119,7 @@ func BuildMultipartContent(text string, media []string) responses.ResponseInputM
|
||||
},
|
||||
})
|
||||
} else if strings.HasPrefix(mediaURL, "data:audio/") {
|
||||
if format, data, ok := ParseDataAudioURL(mediaURL); ok {
|
||||
if format, data, ok := common.ParseDataAudioURL(mediaURL); ok {
|
||||
parts = append(parts, responses.ResponseInputContentUnionParam{
|
||||
OfInputFile: &responses.ResponseInputFileParam{
|
||||
FileData: openai.Opt(data),
|
||||
@@ -132,25 +133,6 @@ func BuildMultipartContent(text string, media []string) responses.ResponseInputM
|
||||
return parts
|
||||
}
|
||||
|
||||
// ParseDataAudioURL extracts the format and base64 data from a data:audio/... URL.
|
||||
func ParseDataAudioURL(mediaURL string) (format, data string, ok bool) {
|
||||
if !strings.HasPrefix(mediaURL, "data:audio/") {
|
||||
return "", "", false
|
||||
}
|
||||
payload := strings.TrimPrefix(mediaURL, "data:audio/")
|
||||
meta, data, found := strings.Cut(payload, ",")
|
||||
if !found {
|
||||
return "", "", false
|
||||
}
|
||||
format, _, _ = strings.Cut(meta, ";")
|
||||
format = strings.TrimSpace(format)
|
||||
data = strings.TrimSpace(data)
|
||||
if format == "" || data == "" {
|
||||
return "", "", false
|
||||
}
|
||||
return format, data, true
|
||||
}
|
||||
|
||||
// ResolveToolCall extracts the function name and JSON arguments string from a ToolCall.
|
||||
// Returns ok=false if the tool call has no name or if arguments fail to marshal.
|
||||
func ResolveToolCall(tc protocoltypes.ToolCall) (name string, arguments string, ok bool) {
|
||||
|
||||
@@ -506,42 +506,6 @@ func TestParseResponseBody_CanceledStatus(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// --- ParseDataAudioURL tests ---
|
||||
|
||||
func TestParseDataAudioURL_Valid(t *testing.T) {
|
||||
format, data, ok := ParseDataAudioURL("data:audio/mp3;base64,SGVsbG8=")
|
||||
if !ok {
|
||||
t.Fatal("expected ok=true")
|
||||
}
|
||||
if format != "mp3" {
|
||||
t.Errorf("format = %q, want %q", format, "mp3")
|
||||
}
|
||||
if data != "SGVsbG8=" {
|
||||
t.Errorf("data = %q, want %q", data, "SGVsbG8=")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDataAudioURL_NotAudio(t *testing.T) {
|
||||
_, _, ok := ParseDataAudioURL("data:image/png;base64,abc")
|
||||
if ok {
|
||||
t.Error("expected ok=false for non-audio URL")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDataAudioURL_MalformedNoComma(t *testing.T) {
|
||||
_, _, ok := ParseDataAudioURL("data:audio/mp3;base64")
|
||||
if ok {
|
||||
t.Error("expected ok=false for malformed URL")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDataAudioURL_EmptyData(t *testing.T) {
|
||||
_, _, ok := ParseDataAudioURL("data:audio/mp3;base64,")
|
||||
if ok {
|
||||
t.Error("expected ok=false for empty data")
|
||||
}
|
||||
}
|
||||
|
||||
// --- BuildMultipartContent tests ---
|
||||
|
||||
func TestBuildMultipartContent_TextOnly(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user