Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions docs/reference/cli/ai-gateway_start.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions enterprise/cli/aigatewaystart.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
aibridgemetrics "github.com/coder/coder/v2/aibridge/metrics"
agpl "github.com/coder/coder/v2/cli"
"github.com/coder/coder/v2/cli/clilog"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/coderd/aibridged"
coderdtracing "github.com/coder/coder/v2/coderd/tracing"
"github.com/coder/coder/v2/codersdk"
Expand Down Expand Up @@ -69,6 +70,9 @@ var aiGatewayInheritedEnvs = map[string]struct{}{
"CODER_TRACE_HONEYCOMB_API_KEY": {},
"CODER_TRACE_LOGS": {},

// Config
"CODER_CONFIG_PATH": {},

// AI Gateway
"CODER_AI_GATEWAY_ALLOW_BYOK": {},
"CODER_AI_GATEWAY_CIRCUIT_BREAKER_ENABLED": {},
Expand Down Expand Up @@ -112,6 +116,10 @@ func (r *RootCmd) aiGatewayStart() *serpent.Command {
signalCtx, stop := inv.SignalNotifyContext(inv.Context(), agpl.StopSignals...)
defer stop()

if vals.Config != "" {
cliui.Warnf(inv.Stderr, "YAML support is experimental and offers no compatibility guarantees.")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this under an experimental flag? 🤔 or is it just to say that "we reserve the right to change the YAML schema without keeping backward compatibility"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is copied from coder server:

coder/cli/server.go

Lines 424 to 426 in 1fbd029

if vals.Config != "" {
cliui.Warnf(inv.Stderr, "YAML support is experimental and offers no compatibility guarantees.")
}

My understanding is that YAML configuration was added some time ago but never "matured" out of experimental status when it was added.

}

resolvedKey, err := resolveAIGatewayKey(key, keyFile)
if err != nil {
return err
Expand Down
1 change: 1 addition & 0 deletions enterprise/cli/aigatewaystart_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -592,6 +592,7 @@ func TestAIGatewayStart_InheritedOptions(t *testing.T) {

// Groups the gateway sources options from.
sourceGroups := map[string]struct{}{
"Config": {},
"Logging": {},
"Tracing": {},
"AI Gateway": {},
Expand Down
84 changes: 78 additions & 6 deletions enterprise/cli/aigatewaystart_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import (
"net/http/httptest"
"net/http/httputil"
"net/url"
"os"
"path/filepath"
"regexp"
"strings"
"sync"
Expand Down Expand Up @@ -239,6 +241,19 @@ func requireEventualAIGatewayStatus(ctx context.Context, t *testing.T, probeURL
}, testutil.WaitLong, testutil.IntervalFast, "%s never returned %d", probeURL, want)
}

// startUnreachableCoderd starts a coderd stub that answers 503 to every
// request, so the gateway daemon keeps retrying its control connection instead
// of completing. It returns the stub's URL.
func startUnreachableCoderd(t *testing.T) string {
t.Helper()

coderSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusServiceUnavailable)
}))
t.Cleanup(coderSrv.Close)
return coderSrv.URL
}

// TestAIGatewayStartE2E drives every part of the standalone gateway plumbing
// once through public surface only: the CLI starts with flags, connects to
// coderd with a gateway key, reports health and readiness, proxies an LLM
Expand Down Expand Up @@ -308,15 +323,12 @@ func TestAIGatewayStartE2E_InvalidKey(t *testing.T) {
func TestAIGatewayStart_HealthBeforeReady(t *testing.T) {
t.Parallel()

// Given: a coderd that always answers 503
coderSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusServiceUnavailable)
}))
t.Cleanup(coderSrv.Close)
// Given: a coderd that always answers 503, so the daemon keeps retrying.
coderURL := startUnreachableCoderd(t)

ctx := testutil.Context(t, testutil.WaitShort)
// When: the gateway starts and binds its listener.
baseURL, _ := startAIGatewayCommand(ctx, t, coderSrv.URL, "test-key")
baseURL, _ := startAIGatewayCommand(ctx, t, coderURL, "test-key")

// Then: healthz is already 200 while readyz stays 503.
// The startup log line is emitted after the bind, so no retry is needed.
Expand Down Expand Up @@ -679,3 +691,63 @@ func TestAIGatewayStartE2E_InFlightRequestSurvivesDisconnect(t *testing.T) {
// dropped, so this interception's usage rows are expected to be lost. Only
// the caller-visible outcome is asserted.
}

// TestAIGatewayStart_ConfigYAML verifies that a YAML file supplied via
// --config (CODER_CONFIG_PATH) configures the running standalone Gateway.
// It enables the inherited Prometheus listener through YAML and asserts that the
// listener comes up.
func TestAIGatewayStart_ConfigYAML(t *testing.T) {
t.Parallel()

// Fake coderd that answers 503 so the daemon keeps retrying to connect.
coderURL := startUnreachableCoderd(t)
configFile := filepath.Join(t.TempDir(), "config.yaml")
err := os.WriteFile(configFile, []byte(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: should we add a test where the config contains an invalid schema? I assume the CLI will fail in that case, saying it is an unknown field or something like that

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added.

"introspection:\n prometheus:\n enable: true\n address: 127.0.0.1:0\n",
), 0o600)
require.NoError(t, err)

ctx := testutil.Context(t, testutil.WaitShort)
inv, _ := newCLI(t,
"ai-gateway", "start",
"--url", coderURL,
"--key", "test-key",
"--http-address", "127.0.0.1:0",
"--config", configFile,
)
inv = inv.WithContext(ctx)
pty := ptytest.New(t).Attach(inv)
waiter := clitest.StartWithWaiter(t, inv)

// The Prometheus listener is only started when prometheus.enable
// option is set which is enabled through YAML.
promLine := pty.ExpectRegexMatch(ctx, `http server listening\s+addr=[0-9.]+:[0-9]+\s+name=prometheus`)
matches := regexp.MustCompile(`addr=([0-9.]+:[0-9]+)`).FindStringSubmatch(promLine)
require.Len(t, matches, 2, "prometheus address not found in startup log: %q", promLine)
promURL := "http://" + matches[1] + "/metrics"
requireAIGatewayStatus(ctx, t, promURL, http.StatusOK)

waiter.Cancel()
require.NoError(t, waiter.Wait())
}

// TestAIGatewayStart_ConfigYAML_Invalid verifies that a YAML file with an
// unknown option fails the command with a descriptive error.
func TestAIGatewayStart_ConfigYAML_Invalid(t *testing.T) {
t.Parallel()

configFile := filepath.Join(t.TempDir(), "config.yaml")
err := os.WriteFile(configFile, []byte(
"introspection:\n prometheus:\n unknown_field: true\n",
), 0o600)
require.NoError(t, err)

inv, _ := newCLI(t,
"ai-gateway", "start",
"--key", "test-key",
"--config", configFile,
)

err = inv.Run()
require.ErrorContains(t, err, `unknown option "introspection.prometheus.unknown_field"`)
}
6 changes: 6 additions & 0 deletions enterprise/cli/testdata/coder_ai-gateway_start_--help.golden
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@ AI GATEWAY OPTIONS:
making the request) and X-Ai-Bridge-Actor-Metadata-Username (their
username).

CONFIG OPTIONS:
Use a YAML configuration file when your server launch become unwieldy.

-c, --config yaml-config-path, $CODER_CONFIG_PATH
Specify a YAML file to load configuration from.

INTROSPECTION / LOGGING OPTIONS:
--log-human string, $CODER_LOGGING_HUMAN (default: /dev/stderr)
Output human-readable logs to a given file.
Expand Down
Loading