From bd1b35e2eec686b4c4046d1f139ae4f650bf3167 Mon Sep 17 00:00:00 2001 From: jinyisama Date: Sat, 11 Jul 2026 06:27:06 +0000 Subject: [PATCH] refactor: consolidate duplicated helpers into internal/util Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- internal/config/config.go | 21 ------- internal/config/config_loader.go | 52 ++++++++-------- internal/config/convert.go | 20 +++--- internal/extension/codex/catalog.go | 10 +-- internal/extension/deepseek_v4/deepseek_v4.go | 12 +--- internal/extension/deepseek_v4/state.go | 7 ++- internal/protocol/openai/adapter.go | 15 +---- internal/service/provider/manager.go | 7 --- internal/service/server/adapter_dispatch.go | 12 +--- internal/service/store/sqlite_store.go | 11 ++-- internal/util/util.go | 40 ++++++++++++ internal/util/util_test.go | 61 +++++++++++++++++++ 12 files changed, 154 insertions(+), 114 deletions(-) create mode 100644 internal/util/util.go create mode 100644 internal/util/util_test.go diff --git a/internal/config/config.go b/internal/config/config.go index ea8eb53c..caa72cdf 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -690,27 +690,6 @@ func (cfg CacheConfig) Validate() error { return nil } -func valueOrDefault(value string, fallback string) string { - if value == "" { - return fallback - } - return value -} - -func intOrDefault(value int, fallback int) int { - if value == 0 { - return fallback - } - return value -} - -func boolOrDefault(value *bool, fallback bool) bool { - if value == nil { - return fallback - } - return *value -} - func (cfg Config) validateExtensions() error { for _, spec := range cfg.extensionSpecs { if spec.Validate == nil { diff --git a/internal/config/config_loader.go b/internal/config/config_loader.go index 089427ba..1321e37c 100644 --- a/internal/config/config_loader.go +++ b/internal/config/config_loader.go @@ -9,6 +9,8 @@ import ( "path/filepath" "strings" + "moonbridge/internal/util" + "gopkg.in/yaml.v3" ) @@ -444,13 +446,13 @@ func FromFileConfigWithOptions(fileConfig FileConfig, opts LoadOptions) (Config, cfg := Config{ Mode: mode, - Addr: valueOrDefault(strings.TrimSpace(fileConfig.Server.Addr), DefaultAddr), + Addr: util.OrDefault(strings.TrimSpace(fileConfig.Server.Addr), DefaultAddr), AuthToken: strings.TrimSpace(fileConfig.Server.AuthToken), - MaxSessions: intOrDefault(fileConfig.Server.MaxSessions, 0), - SessionTTL: valueOrDefault(strings.TrimSpace(fileConfig.Server.SessionTTL), "24h"), + MaxSessions: util.OrDefault(fileConfig.Server.MaxSessions, 0), + SessionTTL: util.OrDefault(strings.TrimSpace(fileConfig.Server.SessionTTL), "24h"), TraceRequests: traceEnabled, - LogLevel: valueOrDefault(strings.TrimSpace(fileConfig.Log.Level), "info"), - LogFormat: valueOrDefault(strings.TrimSpace(fileConfig.Log.Format), "text"), + LogLevel: util.OrDefault(strings.TrimSpace(fileConfig.Log.Level), "info"), + LogFormat: util.OrDefault(strings.TrimSpace(fileConfig.Log.Format), "text"), SystemPrompt: defaults.SystemPrompt, DefaultModel: defaults.Model, Defaults: defaults, @@ -458,12 +460,12 @@ func FromFileConfigWithOptions(fileConfig FileConfig, opts LoadOptions) (Config, Routes: routes, ProviderDefs: providerDefs, WebSearchSupport: webSearchSupport, - WebSearchMaxUses: intOrDefault(fileConfig.WebSearch.MaxUses, 8), + WebSearchMaxUses: util.OrDefault(fileConfig.WebSearch.MaxUses, 8), TavilyAPIKey: strings.TrimSpace(fileConfig.WebSearch.TavilyAPIKey), FirecrawlAPIKey: strings.TrimSpace(fileConfig.WebSearch.FirecrawlAPIKey), - SearchMaxRounds: intOrDefault(fileConfig.WebSearch.SearchMaxRounds, 5), + SearchMaxRounds: util.OrDefault(fileConfig.WebSearch.SearchMaxRounds, 5), WebSearchExtra: cloneAnyMap(fileConfig.WebSearch.Extra), - DefaultMaxTokens: intOrDefault(defaults.MaxTokens, 1024), + DefaultMaxTokens: util.OrDefault(defaults.MaxTokens, 1024), Cache: fromCacheFileConfig(fileConfig.Cache), Persistence: FromPersistenceFileConfig(fileConfig.Persistence), ResponseProxy: responseProxy, @@ -514,11 +516,11 @@ func fromModelDefFileConfig(fileConfig map[string]ModelDefFileConfig, specs exte Description: strings.TrimSpace(p.Description), }) } - supportsReasoning := boolOrDefault( + supportsReasoning := util.Deref( m.SupportsReasoning, len(reasoningPresets) > 0 || strings.TrimSpace(m.DefaultReasoningLevel) != "" || - boolOrDefault(m.SupportsReasoningSummaries, false) || + util.Deref(m.SupportsReasoningSummaries, false) || strings.TrimSpace(m.DefaultReasoningSummary) != "", ) models[trimmedSlug] = ModelDef{ @@ -530,10 +532,10 @@ func fromModelDefFileConfig(fileConfig map[string]ModelDefFileConfig, specs exte SupportsReasoning: supportsReasoning, DefaultReasoningLevel: strings.TrimSpace(m.DefaultReasoningLevel), SupportedReasoningLevels: reasoningPresets, - SupportsReasoningSummaries: boolOrDefault(m.SupportsReasoningSummaries, false), + SupportsReasoningSummaries: util.Deref(m.SupportsReasoningSummaries, false), DefaultReasoningSummary: strings.TrimSpace(m.DefaultReasoningSummary), InputModalities: m.InputModalities, - SupportsImageDetailOriginal: boolOrDefault(m.SupportsImageDetailOriginal, false), + SupportsImageDetailOriginal: util.Deref(m.SupportsImageDetailOriginal, false), WebSearch: ws, Extensions: modelExtensions, } @@ -635,7 +637,7 @@ func fromProviderDefFileConfig(fileConfig map[string]ProviderDefFileConfig, spec pd := ProviderDef{ BaseURL: strings.TrimRight(strings.TrimSpace(def.BaseURL), "/"), APIKey: strings.TrimSpace(def.APIKey), - Version: valueOrDefault(strings.TrimSpace(def.Version), "2023-06-01"), + Version: util.OrDefault(strings.TrimSpace(def.Version), "2023-06-01"), UserAgent: strings.TrimSpace(def.UserAgent), Protocol: strings.TrimSpace(def.Protocol), WebSearchSupport: wsSupport, @@ -952,7 +954,7 @@ func FromAnthropicProxyFileConfig(fileConfig ProxyTargetFileConfig) AnthropicPro Model: strings.TrimSpace(fileConfig.Model), ProviderBaseURL: strings.TrimRight(strings.TrimSpace(fileConfig.BaseURL), "/"), ProviderAPIKey: strings.TrimSpace(fileConfig.APIKey), - ProviderVersion: valueOrDefault(strings.TrimSpace(fileConfig.Version), "2023-06-01"), + ProviderVersion: util.OrDefault(strings.TrimSpace(fileConfig.Version), "2023-06-01"), } } @@ -964,16 +966,16 @@ func FromPersistenceFileConfig(fileConfig PersistenceFileConfig) PersistenceConf func fromCacheFileConfig(fileConfig CacheFileConfig) CacheConfig { return CacheConfig{ - Mode: valueOrDefault(strings.TrimSpace(fileConfig.Mode), "automatic"), - TTL: valueOrDefault(strings.TrimSpace(fileConfig.TTL), "5m"), - PromptCaching: boolOrDefault(fileConfig.PromptCaching, true), - AutomaticPromptCache: boolOrDefault(fileConfig.AutomaticPromptCache, true), - ExplicitCacheBreakpoints: boolOrDefault(fileConfig.ExplicitCacheBreakpoints, true), - AllowRetentionDowngrade: boolOrDefault(fileConfig.AllowRetentionDowngrade, false), - MaxBreakpoints: intOrDefault(fileConfig.MaxBreakpoints, 4), - MinCacheTokens: intOrDefault(fileConfig.MinCacheTokens, 1024), - ExpectedReuse: intOrDefault(fileConfig.ExpectedReuse, 2), - MinimumValueScore: intOrDefault(fileConfig.MinimumValueScore, 2048), - MinBreakpointTokens: intOrDefault(fileConfig.MinBreakpointTokens, 1024), + Mode: util.OrDefault(strings.TrimSpace(fileConfig.Mode), "automatic"), + TTL: util.OrDefault(strings.TrimSpace(fileConfig.TTL), "5m"), + PromptCaching: util.Deref(fileConfig.PromptCaching, true), + AutomaticPromptCache: util.Deref(fileConfig.AutomaticPromptCache, true), + ExplicitCacheBreakpoints: util.Deref(fileConfig.ExplicitCacheBreakpoints, true), + AllowRetentionDowngrade: util.Deref(fileConfig.AllowRetentionDowngrade, false), + MaxBreakpoints: util.OrDefault(fileConfig.MaxBreakpoints, 4), + MinCacheTokens: util.OrDefault(fileConfig.MinCacheTokens, 1024), + ExpectedReuse: util.OrDefault(fileConfig.ExpectedReuse, 2), + MinimumValueScore: util.OrDefault(fileConfig.MinimumValueScore, 2048), + MinBreakpointTokens: util.OrDefault(fileConfig.MinBreakpointTokens, 1024), } } diff --git a/internal/config/convert.go b/internal/config/convert.go index 4f5e5118..4da65dc6 100644 --- a/internal/config/convert.go +++ b/internal/config/convert.go @@ -1,6 +1,8 @@ package config import ( + "moonbridge/internal/util" + "gopkg.in/yaml.v3" ) @@ -122,12 +124,12 @@ func toModelDefFileConfig(def ModelDef) ModelDefFileConfig { WebSearch: toWebSearchFileConfig(def.WebSearch), } - m.SupportsReasoning = boolPtr(def.SupportsReasoning) + m.SupportsReasoning = util.Ptr(def.SupportsReasoning) if def.SupportsReasoningSummaries { - m.SupportsReasoningSummaries = boolPtr(true) + m.SupportsReasoningSummaries = util.Ptr(true) } if def.SupportsImageDetailOriginal { - m.SupportsImageDetailOriginal = boolPtr(true) + m.SupportsImageDetailOriginal = util.Ptr(true) } if len(def.Extensions) > 0 { @@ -251,10 +253,10 @@ func toCacheFileConfig(c CacheConfig) CacheFileConfig { return CacheFileConfig{ Mode: c.Mode, TTL: c.TTL, - PromptCaching: boolPtr(c.PromptCaching), - AutomaticPromptCache: boolPtr(c.AutomaticPromptCache), - ExplicitCacheBreakpoints: boolPtr(c.ExplicitCacheBreakpoints), - AllowRetentionDowngrade: boolPtr(c.AllowRetentionDowngrade), + PromptCaching: util.Ptr(c.PromptCaching), + AutomaticPromptCache: util.Ptr(c.AutomaticPromptCache), + ExplicitCacheBreakpoints: util.Ptr(c.ExplicitCacheBreakpoints), + AllowRetentionDowngrade: util.Ptr(c.AllowRetentionDowngrade), MaxBreakpoints: c.MaxBreakpoints, MinCacheTokens: c.MinCacheTokens, ExpectedReuse: c.ExpectedReuse, @@ -262,7 +264,3 @@ func toCacheFileConfig(c CacheConfig) CacheFileConfig { MinBreakpointTokens: c.MinBreakpointTokens, } } - -func boolPtr(v bool) *bool { - return &v -} diff --git a/internal/extension/codex/catalog.go b/internal/extension/codex/catalog.go index 75ab8823..634f60c0 100644 --- a/internal/extension/codex/catalog.go +++ b/internal/extension/codex/catalog.go @@ -16,6 +16,7 @@ import ( "moonbridge/internal/config" "moonbridge/internal/extension/visual" "moonbridge/internal/modelref" + "moonbridge/internal/util" ) // ModelInfo represents a model entry in the OpenAI /v1/models response. @@ -469,13 +470,6 @@ func WriteModelsCatalog(path string, providerCfg config.ProviderConfig, pluginCf return os.WriteFile(path, data, 0644) } -func valueOrDefault(value string, fallback string) string { - if value == "" { - return fallback - } - return value -} - // routeFor resolves a model alias to a RouteEntry from a ProviderConfig. func routeFor(providerCfg config.ProviderConfig, modelAlias string) config.RouteEntry { if provider, upstream := modelref.Parse(modelAlias); provider != "" { @@ -547,7 +541,7 @@ func GenerateConfigToml(output io.Writer, modelAlias string, baseURL string, cod fmt.Fprintln(output) fmt.Fprintln(output, "[model_providers.moonbridge]") fmt.Fprintln(output, `name = "Moon Bridge"`) - fmt.Fprintf(output, "base_url = %q\n", valueOrDefault(baseURL, "http://"+config.DefaultAddr+"/v1")) + fmt.Fprintf(output, "base_url = %q\n", util.OrDefault(baseURL, "http://"+config.DefaultAddr+"/v1")) if serverCfg.AuthToken != "" { fmt.Fprintln(output, `requires_openai_auth = true`) } diff --git a/internal/extension/deepseek_v4/deepseek_v4.go b/internal/extension/deepseek_v4/deepseek_v4.go index 295c7ee3..815df082 100644 --- a/internal/extension/deepseek_v4/deepseek_v4.go +++ b/internal/extension/deepseek_v4/deepseek_v4.go @@ -8,6 +8,7 @@ import ( "moonbridge/internal/format" "moonbridge/internal/protocol/anthropic" "moonbridge/internal/protocol/openai" + "moonbridge/internal/util" ) // StripReasoningContent removes the reasoning_content field from message @@ -112,7 +113,7 @@ func StreamDeltaForReasoning(delta anthropic.StreamDelta) string { return delta.Text } if delta.Type == "thinking_delta" { - return firstNonEmpty(delta.Thinking, delta.Text) + return util.FirstNonEmpty(delta.Thinking, delta.Text) } return "" } @@ -126,15 +127,6 @@ func IsReasoningContentBlock(block *format.CoreContentBlock) bool { return block.Type == "reasoning" || block.Type == "reasoning_content" } -func firstNonEmpty(values ...string) string { - for _, value := range values { - if value != "" { - return value - } - } - return "" -} - // ToAnthropicRequest mutates an Anthropic request for DeepSeek V4 quirks. // DeepSeek-compatible providers may reject or mis-handle sampling knobs here. // Codex/OpenAI reasoning effort maps to DeepSeek's Anthropic-compatible diff --git a/internal/extension/deepseek_v4/state.go b/internal/extension/deepseek_v4/state.go index 2d9a828f..f801fcdd 100644 --- a/internal/extension/deepseek_v4/state.go +++ b/internal/extension/deepseek_v4/state.go @@ -10,6 +10,7 @@ import ( "moonbridge/internal/format" "moonbridge/internal/protocol/anthropic" + "moonbridge/internal/util" ) const persistedThinkingSummaryPrefix = "moonbridge:deepseek_v4_thinking:v1:" @@ -173,7 +174,7 @@ func (stream *StreamState) Start(index int, block *format.CoreContentBlock) bool if stream == nil || block == nil || !IsReasoningContentBlock(block) { return false } - stream.thinkingText[index] = firstNonEmpty(block.ReasoningText, block.Text) + stream.thinkingText[index] = util.FirstNonEmpty(block.ReasoningText, block.Text) stream.thinkingSignature[index] = block.ReasoningSignature return true } @@ -184,10 +185,10 @@ func (stream *StreamState) Delta(index int, delta anthropic.StreamDelta) bool { } switch delta.Type { case "thinking_delta", "reasoning_content_delta": - stream.thinkingText[index] += firstNonEmpty(delta.Thinking, delta.Text) + stream.thinkingText[index] += util.FirstNonEmpty(delta.Thinking, delta.Text) return true case "signature_delta": - stream.thinkingSignature[index] += firstNonEmpty(delta.Signature, delta.Text) + stream.thinkingSignature[index] += util.FirstNonEmpty(delta.Signature, delta.Text) return true default: return false diff --git a/internal/protocol/openai/adapter.go b/internal/protocol/openai/adapter.go index 2dab9d1a..162954a7 100644 --- a/internal/protocol/openai/adapter.go +++ b/internal/protocol/openai/adapter.go @@ -17,6 +17,7 @@ import ( "moonbridge/internal/extension/codextool" "moonbridge/internal/format" + "moonbridge/internal/util" ) // ============================================================================ @@ -1262,7 +1263,7 @@ func convertInput(raw json.RawMessage, model string) ([]format.CoreMessage, []fo } pendingFCBlocks = append(pendingFCBlocks, format.CoreContentBlock{ Type: "tool_use", - ToolUseID: firstNonEmpty(item.CallID, item.ID), + ToolUseID: util.FirstNonEmpty(item.CallID, item.ID), ToolName: item.Name, ToolNamespace: item.Namespace, ToolInput: toolInput, @@ -1287,7 +1288,7 @@ func convertInput(raw json.RawMessage, model string) ([]format.CoreMessage, []fo } pendingFCBlocks = append(pendingFCBlocks, format.CoreContentBlock{ Type: "tool_use", - ToolUseID: firstNonEmpty(item.CallID, item.ID), + ToolUseID: util.FirstNonEmpty(item.CallID, item.ID), ToolName: item.Name, ToolNamespace: item.Namespace, ToolInput: toolInput, @@ -1488,16 +1489,6 @@ func convertToolChoice(raw json.RawMessage) (*format.CoreToolChoice, error) { // Utility // ============================================================================ -// firstNonEmpty returns the first non-empty string from the list. -func firstNonEmpty(vals ...string) string { - for _, v := range vals { - if v != "" { - return v - } - } - return "" -} - // copyContentParts returns a shallow copy of a ContentPart slice. func copyContentParts(parts []ContentPart) []ContentPart { out := make([]ContentPart, len(parts)) diff --git a/internal/service/provider/manager.go b/internal/service/provider/manager.go index 01f7a84b..4d271a9c 100644 --- a/internal/service/provider/manager.go +++ b/internal/service/provider/manager.go @@ -466,13 +466,6 @@ func newHTTPClient(cfg HTTPConfig) *http.Client { } } -func valueOrDefault(value, fallback string) string { - if value == "" { - return fallback - } - return value -} - // ClientForKey returns the anthropic.Client for a given provider key. func (pm *ProviderManager) ClientForKey(key string) (ProviderClient, error) { pm.mu.RLock() diff --git a/internal/service/server/adapter_dispatch.go b/internal/service/server/adapter_dispatch.go index 49d702d2..00c70506 100644 --- a/internal/service/server/adapter_dispatch.go +++ b/internal/service/server/adapter_dispatch.go @@ -23,6 +23,7 @@ import ( "moonbridge/internal/service/stats" mbtrace "moonbridge/internal/service/trace" "moonbridge/internal/session" + "moonbridge/internal/util" ) // ============================================================================ @@ -827,7 +828,7 @@ func streamOutputItemToCoreBlocks(item openai.OutputItem) []format.CoreContentBl case "reasoning": return reasoningBlocksFromStreamOutput(item.Summary) case "function_call", "custom_tool_call", "local_shell_call": - toolUseID := firstNonEmptyString(item.CallID, item.ID) + toolUseID := util.FirstNonEmpty(item.CallID, item.ID) if toolUseID == "" { return nil } @@ -890,15 +891,6 @@ func streamOutputToolInput(item openai.OutputItem) json.RawMessage { return payload } -func firstNonEmptyString(vals ...string) string { - for _, v := range vals { - if v != "" { - return v - } - } - return "" -} - // handleAdapterStream handles the streaming path through adapter dispatch. func (s *Server) handleAdapterStream( w http.ResponseWriter, diff --git a/internal/service/store/sqlite_store.go b/internal/service/store/sqlite_store.go index fc5f4508..4b9321ca 100644 --- a/internal/service/store/sqlite_store.go +++ b/internal/service/store/sqlite_store.go @@ -15,6 +15,7 @@ import ( "moonbridge/internal/config" "moonbridge/internal/db" + "moonbridge/internal/util" ) // SQLiteConfigStore implements ConfigStore backed by a SQLite database. @@ -266,12 +267,12 @@ func toModelDefFileConfig(def config.ModelDef) config.ModelDefFileConfig { DefaultReasoningSummary: def.DefaultReasoningSummary, InputModalities: def.InputModalities, } - m.SupportsReasoning = boolPtr(def.SupportsReasoning) + m.SupportsReasoning = util.Ptr(def.SupportsReasoning) if def.SupportsReasoningSummaries { - m.SupportsReasoningSummaries = boolPtr(true) + m.SupportsReasoningSummaries = util.Ptr(true) } if def.SupportsImageDetailOriginal { - m.SupportsImageDetailOriginal = boolPtr(true) + m.SupportsImageDetailOriginal = util.Ptr(true) } if len(def.Extensions) > 0 { m.Extensions = make(map[string]config.ExtensionFileConfig, len(def.Extensions)) @@ -294,10 +295,6 @@ func toModelDefFileConfig(def config.ModelDef) config.ModelDefFileConfig { return m } -func boolPtr(v bool) *bool { - return &v -} - func cloneMap(m map[string]any) map[string]any { if m == nil { return nil diff --git a/internal/util/util.go b/internal/util/util.go new file mode 100644 index 00000000..250f0cad --- /dev/null +++ b/internal/util/util.go @@ -0,0 +1,40 @@ +// Package util provides small, generic helpers shared across the codebase. +// +// These consolidate value-fallback and pointer patterns that were previously +// duplicated as unexported helpers in multiple packages. +package util + +// Ptr returns a pointer to v. It is useful for populating optional +// pointer-typed struct fields from concrete values. +func Ptr[T any](v T) *T { + return &v +} + +// Deref returns *p when p is non-nil, otherwise fallback. +func Deref[T any](p *T, fallback T) T { + if p == nil { + return fallback + } + return *p +} + +// OrDefault returns value when it is not the zero value of T, otherwise +// fallback. It works for any comparable type (e.g. strings, ints). +func OrDefault[T comparable](value, fallback T) T { + var zero T + if value == zero { + return fallback + } + return value +} + +// FirstNonEmpty returns the first non-empty string in vals, or "" if all are +// empty. +func FirstNonEmpty(vals ...string) string { + for _, v := range vals { + if v != "" { + return v + } + } + return "" +} diff --git a/internal/util/util_test.go b/internal/util/util_test.go new file mode 100644 index 00000000..c749ccee --- /dev/null +++ b/internal/util/util_test.go @@ -0,0 +1,61 @@ +package util + +import "testing" + +func TestPtr(t *testing.T) { + p := Ptr(42) + if p == nil { + t.Fatal("expected non-nil pointer") + } + if *p != 42 { + t.Fatalf("expected 42, got %d", *p) + } + + bp := Ptr(true) + if !*bp { + t.Fatal("expected true") + } +} + +func TestDeref(t *testing.T) { + v := true + if got := Deref(&v, false); got != true { + t.Fatalf("expected true, got %v", got) + } + if got := Deref[bool](nil, true); got != true { + t.Fatalf("expected fallback true, got %v", got) + } + if got := Deref[int](nil, 7); got != 7 { + t.Fatalf("expected fallback 7, got %d", got) + } +} + +func TestOrDefault(t *testing.T) { + if got := OrDefault("value", "fallback"); got != "value" { + t.Fatalf("expected value, got %q", got) + } + if got := OrDefault("", "fallback"); got != "fallback" { + t.Fatalf("expected fallback, got %q", got) + } + if got := OrDefault(5, 10); got != 5 { + t.Fatalf("expected 5, got %d", got) + } + if got := OrDefault(0, 10); got != 10 { + t.Fatalf("expected fallback 10, got %d", got) + } +} + +func TestFirstNonEmpty(t *testing.T) { + if got := FirstNonEmpty("", "", "third"); got != "third" { + t.Fatalf("expected third, got %q", got) + } + if got := FirstNonEmpty("first", "second"); got != "first" { + t.Fatalf("expected first, got %q", got) + } + if got := FirstNonEmpty("", ""); got != "" { + t.Fatalf("expected empty, got %q", got) + } + if got := FirstNonEmpty(); got != "" { + t.Fatalf("expected empty, got %q", got) + } +}