From 10ce34d3a499be6c241e5cd3f338c1f9f1416a32 Mon Sep 17 00:00:00 2001 From: Koosha Paridehpour Date: Wed, 25 Feb 2026 20:00:33 -0700 Subject: [PATCH 1/3] ci: fix required check manifest and workflow names for feature/koosh-migrate-conflict-1302 --- .github/workflows/pr-path-guard.yml | 3 ++- .github/workflows/pr-test-build.yml | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pr-path-guard.yml b/.github/workflows/pr-path-guard.yml index 6dcbb1f3b0..d2922eca9a 100644 --- a/.github/workflows/pr-path-guard.yml +++ b/.github/workflows/pr-path-guard.yml @@ -9,6 +9,7 @@ on: jobs: ensure-no-translator-changes: + name: ensure-no-translator-changes runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -21,7 +22,7 @@ jobs: files: | internal/translator/** - name: Fail when restricted paths change - if: steps.changed-files.outputs.any_changed == 'true' && !(startsWith(github.head_ref, 'feature/koosh-migrate') || startsWith(github.head_ref, 'feature/migrate-') || startsWith(github.head_ref, 'migrated/')) + if: steps.changed-files.outputs.any_changed == 'true' && !(startsWith(github.head_ref, 'feature/koosh-migrate') || startsWith(github.head_ref, 'feature/migrate-') || startsWith(github.head_ref, 'migrated/')) && !(startsWith(github.head_ref, 'feature/koosh-migrate') || startsWith(github.head_ref, 'feature/migrate-') || startsWith(github.head_ref, 'migrated/')) run: | echo "Changes under internal/translator are not allowed in pull requests." echo "You need to create an issue for our maintenance team to make the necessary changes." diff --git a/.github/workflows/pr-test-build.yml b/.github/workflows/pr-test-build.yml index 477ff0498e..2fe1994b84 100644 --- a/.github/workflows/pr-test-build.yml +++ b/.github/workflows/pr-test-build.yml @@ -8,6 +8,7 @@ permissions: jobs: build: + name: build runs-on: ubuntu-latest steps: - name: Checkout From 3650e1b3d54b71190733effe5c64ba53825949cf Mon Sep 17 00:00:00 2001 From: Koosha Paridehpour Date: Wed, 25 Feb 2026 22:05:27 -0700 Subject: [PATCH 2/3] fix(ci): align sdk config types and include auto-merge workflow --- .github/workflows/auto-merge.yml | 33 ++++++++++++++ pkg/llmproxy/access/reconcile.go | 12 ++++- .../api/handlers/management/config_basic.go | 3 +- pkg/llmproxy/config/sdk_types.go | 45 +++---------------- 4 files changed, 50 insertions(+), 43 deletions(-) create mode 100644 .github/workflows/auto-merge.yml diff --git a/.github/workflows/auto-merge.yml b/.github/workflows/auto-merge.yml new file mode 100644 index 0000000000..008dd16f7c --- /dev/null +++ b/.github/workflows/auto-merge.yml @@ -0,0 +1,33 @@ +name: Auto Merge Gate + +on: + pull_request_target: + types: + - opened + - reopened + - ready_for_review + - synchronize + - labeled + pull_request_review: + types: + - submitted + +permissions: + contents: read + pull-requests: write + +jobs: + enable-automerge: + if: | + (github.event_name != 'pull_request_review') || + (github.event.review.state == 'APPROVED') + runs-on: ubuntu-latest + steps: + - name: Enable auto-merge for labeled PRs + if: | + contains(github.event.pull_request.labels.*.name, 'automerge') && + !contains(github.event.pull_request.labels.*.name, 'do-not-merge') + uses: peter-evans/enable-pull-request-automerge@v3 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + merge-method: squash diff --git a/pkg/llmproxy/access/reconcile.go b/pkg/llmproxy/access/reconcile.go index 290cac3e75..5b2d2eb830 100644 --- a/pkg/llmproxy/access/reconcile.go +++ b/pkg/llmproxy/access/reconcile.go @@ -9,6 +9,7 @@ import ( configaccess "github.com/router-for-me/CLIProxyAPI/v6/pkg/llmproxy/access/config_access" "github.com/router-for-me/CLIProxyAPI/v6/pkg/llmproxy/config" sdkaccess "github.com/router-for-me/CLIProxyAPI/v6/sdk/access" + sdkconfig "github.com/router-for-me/CLIProxyAPI/v6/sdk/config" log "github.com/sirupsen/logrus" ) @@ -85,7 +86,16 @@ func ApplyAccessProviders(manager *sdkaccess.Manager, oldCfg, newCfg *config.Con } existing := manager.Providers() - configaccess.Register((*config.SDKConfig)(&newCfg.SDKConfig)) + sdkCfg := sdkconfig.SDKConfig{ + ProxyURL: newCfg.SDKConfig.ProxyURL, + ForceModelPrefix: newCfg.SDKConfig.ForceModelPrefix, + RequestLog: newCfg.SDKConfig.RequestLog, + APIKeys: newCfg.SDKConfig.APIKeys, + PassthroughHeaders: newCfg.SDKConfig.PassthroughHeaders, + Streaming: sdkconfig.StreamingConfig(newCfg.SDKConfig.Streaming), + NonStreamKeepAliveInterval: newCfg.SDKConfig.NonStreamKeepAliveInterval, + } + configaccess.Register(&sdkCfg) providers, added, updated, removed, err := ReconcileProviders(oldCfg, newCfg, existing) if err != nil { log.Errorf("failed to reconcile request auth providers: %v", err) diff --git a/pkg/llmproxy/api/handlers/management/config_basic.go b/pkg/llmproxy/api/handlers/management/config_basic.go index 7222570dcf..b7bcfe4846 100644 --- a/pkg/llmproxy/api/handlers/management/config_basic.go +++ b/pkg/llmproxy/api/handlers/management/config_basic.go @@ -12,7 +12,6 @@ import ( "github.com/gin-gonic/gin" "github.com/router-for-me/CLIProxyAPI/v6/pkg/llmproxy/config" "github.com/router-for-me/CLIProxyAPI/v6/pkg/llmproxy/util" - sdkconfig "github.com/router-for-me/CLIProxyAPI/v6/sdk/config" log "github.com/sirupsen/logrus" "gopkg.in/yaml.v3" ) @@ -45,7 +44,7 @@ func (h *Handler) GetLatestVersion(c *gin.Context) { proxyURL = strings.TrimSpace(h.cfg.ProxyURL) } if proxyURL != "" { - sdkCfg := &sdkconfig.SDKConfig{ProxyURL: proxyURL} + sdkCfg := &config.SDKConfig{ProxyURL: proxyURL} util.SetProxy(sdkCfg, client) } diff --git a/pkg/llmproxy/config/sdk_types.go b/pkg/llmproxy/config/sdk_types.go index bf4fb90ecf..834d2aba6e 100644 --- a/pkg/llmproxy/config/sdk_types.go +++ b/pkg/llmproxy/config/sdk_types.go @@ -1,43 +1,8 @@ -// Package config provides configuration types for CLI Proxy API. -// This file contains SDK-specific config types that are used by internal/* packages. +// Package config provides configuration types for the llmproxy server. package config -// SDKConfig represents the SDK-level configuration embedded in Config. -type SDKConfig struct { - // ProxyURL is the URL of an optional proxy server to use for outbound requests. - ProxyURL string `yaml:"proxy-url" json:"proxy-url"` +import sdkconfig "github.com/router-for-me/CLIProxyAPI/v6/sdk/config" - // ForceModelPrefix requires explicit model prefixes (e.g., "teamA/gemini-3-pro-preview") - // to target prefixed credentials. When false, unprefixed model requests may use prefixed - // credentials as well. - ForceModelPrefix bool `yaml:"force-model-prefix" json:"force-model-prefix"` - - // RequestLog enables or disables detailed request logging functionality. - RequestLog bool `yaml:"request-log" json:"request-log"` - - // APIKeys is a list of keys for authenticating clients to this proxy server. - APIKeys []string `yaml:"api-keys" json:"api-keys"` - - // PassthroughHeaders controls whether upstream response headers are forwarded to downstream clients. - // Default is false (disabled). - PassthroughHeaders bool `yaml:"passthrough-headers" json:"passthrough-headers"` - - // Streaming configures server-side streaming behavior (keep-alives and safe bootstrap retries). - Streaming StreamingConfig `yaml:"streaming" json:"streaming"` - - // NonStreamKeepAliveInterval controls how often blank lines are emitted for non-streaming responses. - // <= 0 disables keep-alives. Value is in seconds. - NonStreamKeepAliveInterval int `yaml:"nonstream-keepalive-interval,omitempty" json:"nonstream-keepalive-interval,omitempty"` -} - -// StreamingConfig holds server streaming behavior configuration. -type StreamingConfig struct { - // KeepAliveSeconds controls how often the server emits SSE heartbeats (": keep-alive\n\n"). - // <= 0 disables keep-alives. Default is 0. - KeepAliveSeconds int `yaml:"keepalive-seconds,omitempty" json:"keepalive-seconds,omitempty"` - - // BootstrapRetries controls how many times the server may retry a streaming request before any bytes are sent, - // to allow auth rotation / transient recovery. - // <= 0 disables bootstrap retries. Default is 0. - BootstrapRetries int `yaml:"bootstrap-retries,omitempty" json:"bootstrap-retries,omitempty"` -} +// Keep SDK types aligned with public SDK config to avoid split-type regressions. +type SDKConfig = sdkconfig.SDKConfig +type StreamingConfig = sdkconfig.StreamingConfig From eb8d81fce184fb9df445934705716b5de9d48fd0 Mon Sep 17 00:00:00 2001 From: Koosha Paridehpour Date: Wed, 25 Feb 2026 22:08:59 -0700 Subject: [PATCH 3/3] fix(auth): adapt mixed stream path to StreamResult API --- sdk/cliproxy/auth/conductor.go | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/sdk/cliproxy/auth/conductor.go b/sdk/cliproxy/auth/conductor.go index d5b2ce5be9..3a4cfe1392 100644 --- a/sdk/cliproxy/auth/conductor.go +++ b/sdk/cliproxy/auth/conductor.go @@ -724,14 +724,17 @@ func (m *Manager) executeStreamMixedOnce(ctx context.Context, providers []string return nil, &Error{Code: "provider_not_found", Message: "no provider supplied"} } - var chunks <-chan cliproxyexecutor.StreamChunk + var result *cliproxyexecutor.StreamResult err := m.executeWithFallback(ctx, providers, req, opts, func(ctx context.Context, executor ProviderExecutor, auth *Auth, provider, routeModel string) error { return m.executeMixedAttempt(ctx, auth, provider, routeModel, req, opts, func(execCtx context.Context, execReq cliproxyexecutor.Request) error { var errExec error - chunks, errExec = executor.ExecuteStream(execCtx, auth, execReq, opts) + result, errExec = executor.ExecuteStream(execCtx, auth, execReq, opts) if errExec != nil { return errExec } + if result == nil { + return errors.New("empty stream result") + } out := make(chan cliproxyexecutor.StreamChunk) go func(streamCtx context.Context, streamAuth *Auth, streamProvider string, streamChunks <-chan cliproxyexecutor.StreamChunk) { @@ -764,12 +767,15 @@ func (m *Manager) executeStreamMixedOnce(ctx context.Context, providers []string if !failed { m.MarkResult(streamCtx, Result{AuthID: streamAuth.ID, Provider: streamProvider, Model: routeModel, Success: true}) } - }(execCtx, auth.Clone(), provider, chunks) - chunks = out + }(execCtx, auth.Clone(), provider, result.Chunks) + result = &cliproxyexecutor.StreamResult{ + Headers: result.Headers, + Chunks: out, + } return nil }) }) - return chunks, err + return result, err } func ensureRequestedModelMetadata(opts cliproxyexecutor.Options, requestedModel string) cliproxyexecutor.Options {