diff --git a/package-lock.json b/package-lock.json index fbe74e2ac8..c47b7dc114 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "waveterm", - "version": "0.14.0", + "version": "0.14.1-beta.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "waveterm", - "version": "0.14.0", + "version": "0.14.1-beta.0", "hasInstallScript": true, "license": "Apache-2.0", "workspaces": [ diff --git a/pkg/aiusechat/aiutil/aiutil.go b/pkg/aiusechat/aiutil/aiutil.go index 8918d30037..075dd58e7b 100644 --- a/pkg/aiusechat/aiutil/aiutil.go +++ b/pkg/aiusechat/aiutil/aiutil.go @@ -11,6 +11,8 @@ import ( "encoding/hex" "encoding/json" "fmt" + "net/http" + "net/url" "strconv" "strings" "time" @@ -185,6 +187,24 @@ func JsonEncodeRequestBody(reqBody any) (bytes.Buffer, error) { return buf, nil } +func MakeHTTPClient(proxyURL string) (*http.Client, error) { + client := &http.Client{ + Timeout: 0, // rely on ctx; streaming can be long + } + if proxyURL == "" { + return client, nil + } + + pURL, err := url.Parse(proxyURL) + if err != nil { + return nil, fmt.Errorf("invalid proxy URL: %w", err) + } + client.Transport = &http.Transport{ + Proxy: http.ProxyURL(pURL), + } + return client, nil +} + func IsOpenAIReasoningModel(model string) bool { m := strings.ToLower(model) return CheckModelPrefix(m, "o1") || diff --git a/pkg/aiusechat/anthropic/anthropic-backend.go b/pkg/aiusechat/anthropic/anthropic-backend.go index 987b8c117e..b52b4a6797 100644 --- a/pkg/aiusechat/anthropic/anthropic-backend.go +++ b/pkg/aiusechat/anthropic/anthropic-backend.go @@ -12,12 +12,12 @@ import ( "io" "log" "net/http" - "net/url" "strings" "time" "github.com/google/uuid" "github.com/launchdarkly/eventsource" + "github.com/wavetermdev/waveterm/pkg/aiusechat/aiutil" "github.com/wavetermdev/waveterm/pkg/aiusechat/chatstore" "github.com/wavetermdev/waveterm/pkg/aiusechat/uctypes" "github.com/wavetermdev/waveterm/pkg/util/utilfn" @@ -454,18 +454,9 @@ func RunAnthropicChatStep( return nil, nil, nil, err } - httpClient := &http.Client{ - Timeout: 0, // rely on ctx; streaming can be long - } - // Proxy support - if chatOpts.Config.ProxyURL != "" { - pURL, perr := url.Parse(chatOpts.Config.ProxyURL) - if perr != nil { - return nil, nil, nil, fmt.Errorf("invalid proxy URL: %w", perr) - } - httpClient.Transport = &http.Transport{ - Proxy: http.ProxyURL(pURL), - } + httpClient, err := aiutil.MakeHTTPClient(chatOpts.Config.ProxyURL) + if err != nil { + return nil, nil, nil, err } resp, err := httpClient.Do(req) diff --git a/pkg/aiusechat/gemini/gemini-backend.go b/pkg/aiusechat/gemini/gemini-backend.go index 23a331dedb..728df59a4f 100644 --- a/pkg/aiusechat/gemini/gemini-backend.go +++ b/pkg/aiusechat/gemini/gemini-backend.go @@ -231,8 +231,9 @@ func RunGeminiChatStep( return nil, nil, nil, err } - httpClient := &http.Client{ - Timeout: 0, // rely on ctx; streaming can be long + httpClient, err := aiutil.MakeHTTPClient(chatOpts.Config.ProxyURL) + if err != nil { + return nil, nil, nil, err } resp, err := httpClient.Do(req) diff --git a/pkg/aiusechat/openai/openai-backend.go b/pkg/aiusechat/openai/openai-backend.go index dc91723417..dfb14b70df 100644 --- a/pkg/aiusechat/openai/openai-backend.go +++ b/pkg/aiusechat/openai/openai-backend.go @@ -528,18 +528,9 @@ func RunOpenAIChatStep( return nil, nil, nil, err } - httpClient := &http.Client{ - Timeout: 0, // rely on ctx; streaming can be long - } - // Proxy support - if chatOpts.Config.ProxyURL != "" { - pURL, perr := url.Parse(chatOpts.Config.ProxyURL) - if perr != nil { - return nil, nil, nil, fmt.Errorf("invalid proxy URL: %w", perr) - } - httpClient.Transport = &http.Transport{ - Proxy: http.ProxyURL(pURL), - } + httpClient, err := aiutil.MakeHTTPClient(chatOpts.Config.ProxyURL) + if err != nil { + return nil, nil, nil, err } resp, err := httpClient.Do(req) diff --git a/pkg/aiusechat/openaichat/openaichat-backend.go b/pkg/aiusechat/openaichat/openaichat-backend.go index 7b90aee674..635f334873 100644 --- a/pkg/aiusechat/openaichat/openaichat-backend.go +++ b/pkg/aiusechat/openaichat/openaichat-backend.go @@ -16,6 +16,7 @@ import ( "github.com/google/uuid" "github.com/launchdarkly/eventsource" + "github.com/wavetermdev/waveterm/pkg/aiusechat/aiutil" "github.com/wavetermdev/waveterm/pkg/aiusechat/chatstore" "github.com/wavetermdev/waveterm/pkg/aiusechat/uctypes" "github.com/wavetermdev/waveterm/pkg/web/sse" @@ -60,7 +61,10 @@ func RunChatStep( return nil, nil, nil, err } - client := &http.Client{} + client, err := aiutil.MakeHTTPClient(chatOpts.Config.ProxyURL) + if err != nil { + return nil, nil, nil, err + } resp, err := client.Do(req) if err != nil { return nil, nil, nil, fmt.Errorf("request failed: %w", err) diff --git a/pkg/aiusechat/uctypes/uctypes.go b/pkg/aiusechat/uctypes/uctypes.go index 05fe469fc5..d2b25bbc1b 100644 --- a/pkg/aiusechat/uctypes/uctypes.go +++ b/pkg/aiusechat/uctypes/uctypes.go @@ -189,29 +189,6 @@ const ( ApprovalCanceled = "canceled" ) -type AIModeConfig struct { - Mode string `json:"mode"` - DisplayName string `json:"display:name"` - DisplayOrder float64 `json:"display:order,omitempty"` - DisplayIcon string `json:"display:icon"` - Provider string `json:"provider,omitempty"` - APIType string `json:"apitype"` - Model string `json:"model"` - ThinkingLevel string `json:"thinkinglevel"` - BaseURL string `json:"baseurl,omitempty"` - WaveAICloud bool `json:"waveaicloud,omitempty"` - APIVersion string `json:"apiversion,omitempty"` - APIToken string `json:"apitoken,omitempty"` - APITokenSecretName string `json:"apitokensecretname,omitempty"` - Premium bool `json:"premium"` - Description string `json:"description"` - Capabilities []string `json:"capabilities,omitempty"` -} - -func (c *AIModeConfig) HasCapability(cap string) bool { - return slices.Contains(c.Capabilities, cap) -} - // when updating this struct, also modify frontend/app/aipanel/aitypes.ts WaveUIDataTypes.tooluse type UIMessageDataToolUse struct { ToolCallId string `json:"toolcallid"` diff --git a/pkg/aiusechat/usechat.go b/pkg/aiusechat/usechat.go index ca7587e339..a55a10060a 100644 --- a/pkg/aiusechat/usechat.go +++ b/pkg/aiusechat/usechat.go @@ -123,6 +123,7 @@ func getWaveAISettings(premium bool, builderMode bool, rtInfo waveobj.ObjRTInfo, Verbosity: verbosity, AIMode: aiMode, Endpoint: baseUrl, + ProxyURL: config.ProxyURL, Capabilities: config.Capabilities, WaveAIPremium: config.WaveAIPremium, } diff --git a/pkg/aiusechat/usechat_mode_test.go b/pkg/aiusechat/usechat_mode_test.go index 98ef4074c1..73959f9585 100644 --- a/pkg/aiusechat/usechat_mode_test.go +++ b/pkg/aiusechat/usechat_mode_test.go @@ -25,3 +25,15 @@ func TestApplyProviderDefaultsGroq(t *testing.T) { t.Fatalf("expected API token secret name %q, got %q", GroqAPITokenSecretName, config.APITokenSecretName) } } + +func TestApplyProviderDefaultsKeepsProxyURL(t *testing.T) { + config := wconfig.AIModeConfigType{ + Provider: uctypes.AIProvider_OpenAI, + Model: "gpt-5-mini", + ProxyURL: "http://localhost:8080", + } + applyProviderDefaults(&config) + if config.ProxyURL != "http://localhost:8080" { + t.Fatalf("expected proxy URL to be preserved, got %q", config.ProxyURL) + } +} diff --git a/pkg/wconfig/settingsconfig.go b/pkg/wconfig/settingsconfig.go index 39da4ac60a..387598e899 100644 --- a/pkg/wconfig/settingsconfig.go +++ b/pkg/wconfig/settingsconfig.go @@ -288,6 +288,7 @@ type AIModeConfigType struct { ThinkingLevel string `json:"ai:thinkinglevel,omitempty" jsonschema:"enum=low,enum=medium,enum=high"` Verbosity string `json:"ai:verbosity,omitempty" jsonschema:"enum=low,enum=medium,enum=high,description=Text verbosity level (OpenAI Responses API only)"` Endpoint string `json:"ai:endpoint,omitempty"` + ProxyURL string `json:"ai:proxyurl,omitempty"` AzureAPIVersion string `json:"ai:azureapiversion,omitempty"` APIToken string `json:"ai:apitoken,omitempty"` APITokenSecretName string `json:"ai:apitokensecretname,omitempty"` diff --git a/schema/waveai.json b/schema/waveai.json index d917cdcdae..5d90b86abd 100644 --- a/schema/waveai.json +++ b/schema/waveai.json @@ -59,6 +59,9 @@ "ai:endpoint": { "type": "string" }, + "ai:proxyurl": { + "type": "string" + }, "ai:azureapiversion": { "type": "string" }, @@ -109,4 +112,4 @@ "$ref": "#/$defs/AIModeConfigType" }, "type": "object" -} \ No newline at end of file +}