From 62f456f42023b266d6a22f5d85a0b6b847418425 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 28 Feb 2026 20:05:02 +0000 Subject: [PATCH 1/5] Initial plan From 1019c2ae934bcd37042f44fc35d3bf8778a8888b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 28 Feb 2026 20:08:12 +0000 Subject: [PATCH 2/5] Plan: add AI mode proxy URL support Co-authored-by: sawka <2722291+sawka@users.noreply.github.com> --- package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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": [ From 512691660a963284443caba652eaa3be4e1bb8db Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 28 Feb 2026 20:09:39 +0000 Subject: [PATCH 3/5] Add AI mode proxy URL plumbing and backend support Co-authored-by: sawka <2722291+sawka@users.noreply.github.com> --- pkg/aiusechat/gemini/gemini-backend.go | 23 +++++++++++++++++-- pkg/aiusechat/gemini/gemini-backend_test.go | 23 +++++++++++++++++++ .../openaichat/openaichat-backend.go | 22 +++++++++++++++++- .../openaichat/openaichat-backend_test.go | 23 +++++++++++++++++++ pkg/aiusechat/uctypes/uctypes.go | 23 ------------------- pkg/aiusechat/usechat.go | 1 + pkg/aiusechat/usechat_mode_test.go | 12 ++++++++++ pkg/wconfig/settingsconfig.go | 1 + schema/waveai.json | 5 +++- 9 files changed, 106 insertions(+), 27 deletions(-) create mode 100644 pkg/aiusechat/gemini/gemini-backend_test.go create mode 100644 pkg/aiusechat/openaichat/openaichat-backend_test.go diff --git a/pkg/aiusechat/gemini/gemini-backend.go b/pkg/aiusechat/gemini/gemini-backend.go index 23a331dedb..ea76e17887 100644 --- a/pkg/aiusechat/gemini/gemini-backend.go +++ b/pkg/aiusechat/gemini/gemini-backend.go @@ -54,6 +54,24 @@ func appendPartToLastUserMessage(contents []GeminiContent, text string) { } } +func makeHTTPClient(proxyURL string) (*http.Client, error) { + httpClient := &http.Client{ + Timeout: 0, // rely on ctx; streaming can be long + } + if proxyURL == "" { + return httpClient, nil + } + + pURL, err := url.Parse(proxyURL) + if err != nil { + return nil, fmt.Errorf("invalid proxy URL: %w", err) + } + httpClient.Transport = &http.Transport{ + Proxy: http.ProxyURL(pURL), + } + return httpClient, nil +} + // buildGeminiHTTPRequest creates an HTTP request for the Gemini API func buildGeminiHTTPRequest(ctx context.Context, contents []GeminiContent, chatOpts uctypes.WaveChatOpts) (*http.Request, error) { opts := chatOpts.Config @@ -231,8 +249,9 @@ func RunGeminiChatStep( return nil, nil, nil, err } - httpClient := &http.Client{ - Timeout: 0, // rely on ctx; streaming can be long + httpClient, err := 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_test.go b/pkg/aiusechat/gemini/gemini-backend_test.go new file mode 100644 index 0000000000..1225736029 --- /dev/null +++ b/pkg/aiusechat/gemini/gemini-backend_test.go @@ -0,0 +1,23 @@ +// Copyright 2026, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +package gemini + +import "testing" + +func TestMakeHTTPClientProxy(t *testing.T) { + client, err := makeHTTPClient("http://localhost:8080") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if client.Transport == nil { + t.Fatalf("expected proxy transport to be set") + } +} + +func TestMakeHTTPClientInvalidProxy(t *testing.T) { + _, err := makeHTTPClient("://bad-url") + if err == nil { + t.Fatalf("expected invalid proxy URL error") + } +} diff --git a/pkg/aiusechat/openaichat/openaichat-backend.go b/pkg/aiusechat/openaichat/openaichat-backend.go index 7b90aee674..8fd74dd0e7 100644 --- a/pkg/aiusechat/openaichat/openaichat-backend.go +++ b/pkg/aiusechat/openaichat/openaichat-backend.go @@ -11,6 +11,7 @@ import ( "io" "log" "net/http" + "net/url" "strings" "time" @@ -21,6 +22,22 @@ import ( "github.com/wavetermdev/waveterm/pkg/web/sse" ) +func makeHTTPClient(proxyURL string) (*http.Client, error) { + client := &http.Client{} + 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 +} + // RunChatStep executes a chat step using the chat completions API func RunChatStep( ctx context.Context, @@ -60,7 +77,10 @@ func RunChatStep( return nil, nil, nil, err } - client := &http.Client{} + client, err := 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/openaichat/openaichat-backend_test.go b/pkg/aiusechat/openaichat/openaichat-backend_test.go new file mode 100644 index 0000000000..318e2ad287 --- /dev/null +++ b/pkg/aiusechat/openaichat/openaichat-backend_test.go @@ -0,0 +1,23 @@ +// Copyright 2026, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +package openaichat + +import "testing" + +func TestMakeHTTPClientProxy(t *testing.T) { + client, err := makeHTTPClient("http://localhost:8080") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if client.Transport == nil { + t.Fatalf("expected proxy transport to be set") + } +} + +func TestMakeHTTPClientInvalidProxy(t *testing.T) { + _, err := makeHTTPClient("://bad-url") + if err == nil { + t.Fatalf("expected invalid proxy URL error") + } +} 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 +} From faae999e33f3e70a97165ff726d101cf7f84b61f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Mar 2026 18:44:37 +0000 Subject: [PATCH 4/5] Refactor shared proxy HTTP client helper into aiutil Co-authored-by: sawka <2722291+sawka@users.noreply.github.com> --- pkg/aiusechat/aiutil/aiutil.go | 20 +++++++++++++++++++ pkg/aiusechat/anthropic/anthropic-backend.go | 17 ++++------------ pkg/aiusechat/gemini/gemini-backend.go | 20 +------------------ pkg/aiusechat/gemini/gemini-backend_test.go | 10 +++++++--- pkg/aiusechat/openai/openai-backend.go | 15 +++----------- .../openaichat/openaichat-backend.go | 20 ++----------------- .../openaichat/openaichat-backend_test.go | 10 +++++++--- 7 files changed, 44 insertions(+), 68 deletions(-) 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 ea76e17887..728df59a4f 100644 --- a/pkg/aiusechat/gemini/gemini-backend.go +++ b/pkg/aiusechat/gemini/gemini-backend.go @@ -54,24 +54,6 @@ func appendPartToLastUserMessage(contents []GeminiContent, text string) { } } -func makeHTTPClient(proxyURL string) (*http.Client, error) { - httpClient := &http.Client{ - Timeout: 0, // rely on ctx; streaming can be long - } - if proxyURL == "" { - return httpClient, nil - } - - pURL, err := url.Parse(proxyURL) - if err != nil { - return nil, fmt.Errorf("invalid proxy URL: %w", err) - } - httpClient.Transport = &http.Transport{ - Proxy: http.ProxyURL(pURL), - } - return httpClient, nil -} - // buildGeminiHTTPRequest creates an HTTP request for the Gemini API func buildGeminiHTTPRequest(ctx context.Context, contents []GeminiContent, chatOpts uctypes.WaveChatOpts) (*http.Request, error) { opts := chatOpts.Config @@ -249,7 +231,7 @@ func RunGeminiChatStep( return nil, nil, nil, err } - httpClient, err := makeHTTPClient(chatOpts.Config.ProxyURL) + httpClient, err := aiutil.MakeHTTPClient(chatOpts.Config.ProxyURL) if err != nil { return nil, nil, nil, err } diff --git a/pkg/aiusechat/gemini/gemini-backend_test.go b/pkg/aiusechat/gemini/gemini-backend_test.go index 1225736029..70fb410f91 100644 --- a/pkg/aiusechat/gemini/gemini-backend_test.go +++ b/pkg/aiusechat/gemini/gemini-backend_test.go @@ -3,10 +3,14 @@ package gemini -import "testing" +import ( + "testing" + + "github.com/wavetermdev/waveterm/pkg/aiusechat/aiutil" +) func TestMakeHTTPClientProxy(t *testing.T) { - client, err := makeHTTPClient("http://localhost:8080") + client, err := aiutil.MakeHTTPClient("http://localhost:8080") if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -16,7 +20,7 @@ func TestMakeHTTPClientProxy(t *testing.T) { } func TestMakeHTTPClientInvalidProxy(t *testing.T) { - _, err := makeHTTPClient("://bad-url") + _, err := aiutil.MakeHTTPClient("://bad-url") if err == nil { t.Fatalf("expected invalid proxy URL error") } 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 8fd74dd0e7..635f334873 100644 --- a/pkg/aiusechat/openaichat/openaichat-backend.go +++ b/pkg/aiusechat/openaichat/openaichat-backend.go @@ -11,33 +11,17 @@ 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/web/sse" ) -func makeHTTPClient(proxyURL string) (*http.Client, error) { - client := &http.Client{} - 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 -} - // RunChatStep executes a chat step using the chat completions API func RunChatStep( ctx context.Context, @@ -77,7 +61,7 @@ func RunChatStep( return nil, nil, nil, err } - client, err := makeHTTPClient(chatOpts.Config.ProxyURL) + client, err := aiutil.MakeHTTPClient(chatOpts.Config.ProxyURL) if err != nil { return nil, nil, nil, err } diff --git a/pkg/aiusechat/openaichat/openaichat-backend_test.go b/pkg/aiusechat/openaichat/openaichat-backend_test.go index 318e2ad287..e23bfd3ce0 100644 --- a/pkg/aiusechat/openaichat/openaichat-backend_test.go +++ b/pkg/aiusechat/openaichat/openaichat-backend_test.go @@ -3,10 +3,14 @@ package openaichat -import "testing" +import ( + "testing" + + "github.com/wavetermdev/waveterm/pkg/aiusechat/aiutil" +) func TestMakeHTTPClientProxy(t *testing.T) { - client, err := makeHTTPClient("http://localhost:8080") + client, err := aiutil.MakeHTTPClient("http://localhost:8080") if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -16,7 +20,7 @@ func TestMakeHTTPClientProxy(t *testing.T) { } func TestMakeHTTPClientInvalidProxy(t *testing.T) { - _, err := makeHTTPClient("://bad-url") + _, err := aiutil.MakeHTTPClient("://bad-url") if err == nil { t.Fatalf("expected invalid proxy URL error") } From 69008221a9701f9829b66e449ec85a21d99b711e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Mar 2026 19:04:18 +0000 Subject: [PATCH 5/5] Remove redundant backend proxy client tests Co-authored-by: sawka <2722291+sawka@users.noreply.github.com> --- pkg/aiusechat/gemini/gemini-backend_test.go | 27 ------------------- .../openaichat/openaichat-backend_test.go | 27 ------------------- 2 files changed, 54 deletions(-) delete mode 100644 pkg/aiusechat/gemini/gemini-backend_test.go delete mode 100644 pkg/aiusechat/openaichat/openaichat-backend_test.go diff --git a/pkg/aiusechat/gemini/gemini-backend_test.go b/pkg/aiusechat/gemini/gemini-backend_test.go deleted file mode 100644 index 70fb410f91..0000000000 --- a/pkg/aiusechat/gemini/gemini-backend_test.go +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright 2026, Command Line Inc. -// SPDX-License-Identifier: Apache-2.0 - -package gemini - -import ( - "testing" - - "github.com/wavetermdev/waveterm/pkg/aiusechat/aiutil" -) - -func TestMakeHTTPClientProxy(t *testing.T) { - client, err := aiutil.MakeHTTPClient("http://localhost:8080") - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if client.Transport == nil { - t.Fatalf("expected proxy transport to be set") - } -} - -func TestMakeHTTPClientInvalidProxy(t *testing.T) { - _, err := aiutil.MakeHTTPClient("://bad-url") - if err == nil { - t.Fatalf("expected invalid proxy URL error") - } -} diff --git a/pkg/aiusechat/openaichat/openaichat-backend_test.go b/pkg/aiusechat/openaichat/openaichat-backend_test.go deleted file mode 100644 index e23bfd3ce0..0000000000 --- a/pkg/aiusechat/openaichat/openaichat-backend_test.go +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright 2026, Command Line Inc. -// SPDX-License-Identifier: Apache-2.0 - -package openaichat - -import ( - "testing" - - "github.com/wavetermdev/waveterm/pkg/aiusechat/aiutil" -) - -func TestMakeHTTPClientProxy(t *testing.T) { - client, err := aiutil.MakeHTTPClient("http://localhost:8080") - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if client.Transport == nil { - t.Fatalf("expected proxy transport to be set") - } -} - -func TestMakeHTTPClientInvalidProxy(t *testing.T) { - _, err := aiutil.MakeHTTPClient("://bad-url") - if err == nil { - t.Fatalf("expected invalid proxy URL error") - } -}