diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 0d63f842df3ed..e433a4cd48a7a 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -80,6 +80,7 @@ jobs: # Main repo directories for completeness in case other files are # touched: - "agent/**" + - "aibridge/**" - "cli/**" - "cmd/**" - "coderd/**" diff --git a/.github/workflows/typos.toml b/.github/workflows/typos.toml index 0f7f9a6cc9d39..fd962da6dc669 100644 --- a/.github/workflows/typos.toml +++ b/.github/workflows/typos.toml @@ -57,4 +57,6 @@ extend-exclude = [ # notifications' golden files confuse the detector because of quoted-printable encoding "coderd/notifications/testdata/**", "agent/agentcontainers/testdata/devcontainercli/**", + # aibridge fixtures contain truncated streaming chunks that look like typos + "aibridge/fixtures/**", ] diff --git a/.golangci.yaml b/.golangci.yaml index f03007f81e847..07c12dac4f0b8 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -6,6 +6,21 @@ linters-settings: # goal: 100 threshold: 412 + depguard: + rules: + aibridge_import_isolation: + list-mode: lax + files: + - "aibridge/*.go" + - "aibridge/**/*.go" + allow: + - $gostd + - github.com/coder/coder/v2/aibridge + - github.com/coder/coder/v2/buildinfo + deny: + - pkg: github.com/coder/coder/v2 + desc: aibridge code must not import coder packages outside aibridge; buildinfo is the only exception + exhaustruct: include: # Gradually extend to cover more of the codebase. @@ -227,6 +242,7 @@ linters: - asciicheck - bidichk - bodyclose + - depguard - dogsled - errcheck - errname diff --git a/aibridge/AGENTS.md b/aibridge/AGENTS.md index 54b7d41545697..0ee141a295557 100644 --- a/aibridge/AGENTS.md +++ b/aibridge/AGENTS.md @@ -7,10 +7,10 @@ over clever ones. Readability is a primary concern. ## Tone & Relationship -We're colleagues — push back on bad ideas and speak up when something +We're colleagues. Push back on bad ideas and speak up when something doesn't make sense. Honesty over agreeableness. -- Disagree when I'm wrong — act as a critical peer reviewer. +- Disagree when I'm wrong. Act as a critical peer reviewer. - Call out bad ideas, unreasonable expectations, and mistakes. - **Ask for clarification** instead of assuming. Say when you don't know something. - Architectural decisions require discussion; routine fixes do not. @@ -18,35 +18,35 @@ doesn't make sense. Honesty over agreeableness. ## Foundational Rules - Doing it right is better than doing it fast. -- YAGNI — don't add features we don't need right now. +- YAGNI. Don't add features we don't need right now. - Make the smallest reasonable changes to achieve the goal. - Reduce code duplication, even if it takes extra effort. -- Match the style of surrounding code — consistency within a file matters. +- Match the style of surrounding code. Consistency within a file matters. - Fix bugs immediately when you find them. ## Essential Commands -| Task | Command | Notes | -| ----------- | ---------------------- | --------------------------------- | -| Test | `make test` | All tests, no race detector | -| Test (race) | `make test-race` | CGO_ENABLED=1, use for CI | -| Coverage | `make coverage` | Prints summary to stdout | -| Format | `make fmt` | gofumpt; single file: `make fmt FILE=path` | -| Mocks | `make mocks` | Regenerate from `mcp/api.go` | +| Task | Command | Notes | +|-------------|------------------|--------------------------------------------| +| Test | `make test` | All tests, no race detector | +| Test (race) | `make test-race` | CGO_ENABLED=1, use for CI | +| Coverage | `make coverage` | Prints summary to stdout | +| Format | `make fmt` | gofumpt; single file: `make fmt FILE=path` | +| Mocks | `make mocks` | Regenerate from `mcp/api.go` | **Always use these commands** instead of running `go test` or `gofumpt` directly. ## Code Navigation Use LSP tools (go to definition, find references, hover) **before** resorting to grep. -This codebase has 90+ Go files across multiple packages — LSP is faster and more accurate. +This codebase has 90+ Go files across multiple packages, so LSP is faster and more accurate. ## Architecture Overview AI Bridge is a smart gateway that sits between AI clients (Claude Code, Cursor, etc.) and upstream providers (Anthropic, OpenAI). It intercepts all AI traffic to provide centralized authn/z, auditing, token attribution, and MCP tool -administration. It runs as part of `coderd` (the Coder control plane) — users +administration. It runs as part of `coderd` (the Coder control plane). Users authenticate with their Coder session tokens. ``` @@ -68,19 +68,19 @@ authenticate with their Coder session tokens. ``` Key packages: -- `intercept/` — request/response interception, per-provider subdirs (`messages/`, `responses/`, `chatcompletions/`) -- `provider/` — upstream provider definitions (Anthropic, OpenAI, Copilot) -- `mcp/` — MCP protocol integration -- `circuitbreaker/` — circuit breaker for upstream calls -- `context/` — request-scoped context helpers -- `internal/integrationtest/` — integration tests with mock upstreams +- `intercept/`: request/response interception, per-provider subdirs (`messages/`, `responses/`, `chatcompletions/`) +- `provider/`: upstream provider definitions (Anthropic, OpenAI, Copilot) +- `mcp/`: MCP protocol integration +- `circuitbreaker/`: circuit breaker for upstream calls +- `context/`: request-scoped context helpers +- `internal/integrationtest/`: integration tests with mock upstreams ## Go Patterns - Follow the [Uber Go Style Guide](https://github.com/uber-go/guide/blob/master/style.md). - Use `gofumpt` for formatting (enforced by `make fmt`). - Prefer table-driven tests. -- **Never use `time.Sleep` in tests** — use `github.com/coder/quartz` or channels/contexts for synchronization. +- **Never use `time.Sleep` in tests**. Use `github.com/coder/quartz` or channels/contexts for synchronization. - Use unique identifiers in tests: `fmt.Sprintf("test-%s-%d", t.Name(), time.Now().UnixNano())`. - Test observable behavior, not implementation details. @@ -88,8 +88,8 @@ Key packages: This codebase heavily uses SSE streaming. When modifying interceptors: - Always handle both blocking and streaming paths. -- Test with `*_test.go` files in the same package — they cover edge cases for chunked responses. -- Be careful with goroutine lifecycle — ensure proper cleanup on context cancellation. +- Test with `*_test.go` files in the same package. They cover edge cases for chunked responses. +- Be careful with goroutine lifecycle. Ensure proper cleanup on context cancellation. ## Commit Style @@ -110,9 +110,9 @@ type(scope): message ## Common Pitfalls -| Problem | Fix | -| ------- | --- | -| Race in streaming tests | Use `t.Cleanup()` and proper synchronization, never `time.Sleep` | -| Mock not updated | Run `make mocks` after changing `mcp/api.go` | -| Formatting failures | Run `make fmt` before committing | -| `retract` directive in go.mod | Don't remove — it's intentional (v1.0.8 conflict marker) | +| Problem | Fix | +|-------------------------------|------------------------------------------------------------------| +| Race in streaming tests | Use `t.Cleanup()` and proper synchronization, never `time.Sleep` | +| Mock not updated | Run `make mocks` after changing `mcp/api.go` | +| Formatting failures | Run `make fmt` before committing | +| `retract` directive in go.mod | Don't remove. It's intentional (v1.0.8 conflict marker) | diff --git a/aibridge/README.md b/aibridge/README.md new file mode 100644 index 0000000000000..ff76138518277 --- /dev/null +++ b/aibridge/README.md @@ -0,0 +1,93 @@ +# aibridge + +aibridge is an HTTP gateway that sits between AI clients and upstream AI providers (Anthropic, OpenAI). It intercepts requests to record token usage, prompts, and tool invocations per user. Optionally supports centralized [MCP](https://modelcontextprotocol.io/) tool injection with allowlist/denylist filtering. + +## Architecture + +``` +┌─────────────────┐ ┌───────────────────────────────────────────┐ +│ AI Client │ │ aibridge │ +│ (Claude Code, │────▶│ ┌─────────────────┐ ┌─────────────┐ │ +│ Cursor, etc.) │ │ │ RequestBridge │───▶│ Providers │ │ +└─────────────────┘ │ │ (http.Handler) │ │ (Anthropic │ │ + │ └─────────────────┘ │ OpenAI) │ │ + │ └──────┬──────┘ │ + │ │ │ + │ ▼ │ ┌─────────────┐ + │ ┌─────────────────┐ ┌─────────────┐ │ │ Upstream │ + │ │ Recorder │◀───│ Interceptor │─── ───▶│ API │ + │ │ (tokens, tools, │ │ (streaming/ │ │ │ (Anthropic │ + │ │ prompts) │ │ blocking) │ │ │ OpenAI) │ + │ └────────┬────────┘ └──────┬──────┘ │ └─────────────┘ + │ │ │ │ + │ ▼ ┌──────▼──────┐ │ + │ ┌ ─ ─ ─ ─ ─ ─ ─ ┐ │ MCP Proxy │ │ + │ │ Database │ │ (tools) │ │ + │ └ ─ ─ ─ ─ ─ ─ ─ ┘ └─────────────┘ │ + └───────────────────────────────────────────┘ +``` + +### Components + +- **RequestBridge**: The main `http.Handler` that routes requests to providers +- **Provider**: Defines bridged routes (intercepted) and passthrough routes (proxied) +- **Interceptor**: Handles request/response processing and streaming +- **Recorder**: Interface for capturing usage data (tokens, prompts, tools) +- **MCP Proxy** (optional): Connects to MCP servers to list tool, inject them into requests, and invoke them in an inner agentic loop + +## Request Flow + +1. Client sends request to `/anthropic/v1/messages` or `/openai/v1/chat/completions` +2. **Actor extraction**: Request must have an actor in context (via `AsActor()`). +3. **Upstream call**: Request forwarded to the AI provider +4. **Response relay**: Response streamed/sent to client +5. **Recording**: Token usage, prompts, and tool invocations recorded + +**With MCP enabled**: Tools from configured MCP servers are centrally defined and injected into requests (prefixed `bmcp_`). Allowlist/denylist regex patterns control which tools are available. When the model selects an injected tool, the gateway invokes it in an inner agentic loop, and continues the conversation loop until complete. + +Passthrough routes (`/v1/models`, `/v1/messages/count_tokens`) are reverse-proxied directly. + +## Observability + +### Prometheus Metrics + +Create metrics with `NewMetrics(prometheus.Registerer)`: + +| Metric | Type | Description | +|--------|------|-------------| +| `interceptions_total` | Counter | Intercepted request count | +| `interceptions_inflight` | Gauge | Currently processing requests | +| `interceptions_duration_seconds` | Histogram | Request duration | +| `tokens_total` | Counter | Token usage (input/output) | +| `prompts_total` | Counter | User prompt count | +| `injected_tool_invocations_total` | Counter | MCP tool invocations | +| `passthrough_total` | Counter | Non-intercepted requests | + +### Recorder Interface + +Implement `Recorder` to persist usage data to your database: + +- `aibridge_interceptions` - request metadata (provider, model, initiator, timestamps) +- `aibridge_token_usages` - input/output token counts per response +- `aibridge_user_prompts` - user prompts +- `aibridge_tool_usages` - tool invocations (injected and client-defined) + +```go +type Recorder interface { + RecordInterception(ctx context.Context, req *InterceptionRecord) error + RecordInterceptionEnded(ctx context.Context, req *InterceptionRecordEnded) error + RecordTokenUsage(ctx context.Context, req *TokenUsageRecord) error + RecordPromptUsage(ctx context.Context, req *PromptUsageRecord) error + RecordToolUsage(ctx context.Context, req *ToolUsageRecord) error +} +``` + +## Supported Routes + +| Provider | Route | Type | +|----------|-------|------| +| Anthropic | `/anthropic/v1/messages` | Bridged (intercepted) | +| Anthropic | `/anthropic/v1/models` | Passthrough | +| Anthropic | `/anthropic/v1/messages/count_tokens` | Passthrough | +| OpenAI | `/openai/v1/chat/completions` | Bridged (intercepted) | +| OpenAI | `/openai/v1/models` | Passthrough | diff --git a/aibridge/intercept/apidump/headers_test.go b/aibridge/intercept/apidump/headers_test.go index 1c5bb69723e7a..7c50b990cd12e 100644 --- a/aibridge/intercept/apidump/headers_test.go +++ b/aibridge/intercept/apidump/headers_test.go @@ -5,11 +5,10 @@ import ( "net/http" "testing" - "cdr.dev/slog/v3" - "github.com/google/uuid" "github.com/stretchr/testify/require" + "cdr.dev/slog/v3" "github.com/coder/quartz" ) diff --git a/aibridge/intercept/chatcompletions/base.go b/aibridge/intercept/chatcompletions/base.go index b479c2a1c27a8..6f6ca352618ab 100644 --- a/aibridge/intercept/chatcompletions/base.go +++ b/aibridge/intercept/chatcompletions/base.go @@ -15,6 +15,7 @@ import ( "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" + "cdr.dev/slog/v3" "github.com/coder/coder/v2/aibridge/config" aibcontext "github.com/coder/coder/v2/aibridge/context" "github.com/coder/coder/v2/aibridge/intercept" @@ -23,8 +24,6 @@ import ( "github.com/coder/coder/v2/aibridge/recorder" "github.com/coder/coder/v2/aibridge/tracing" "github.com/coder/quartz" - - "cdr.dev/slog/v3" ) type interceptionBase struct { diff --git a/aibridge/intercept/chatcompletions/blocking.go b/aibridge/intercept/chatcompletions/blocking.go index 71bbe74b9ea17..59c8bbb731bd5 100644 --- a/aibridge/intercept/chatcompletions/blocking.go +++ b/aibridge/intercept/chatcompletions/blocking.go @@ -14,6 +14,7 @@ import ( "go.opentelemetry.io/otel/trace" "golang.org/x/xerrors" + "cdr.dev/slog/v3" "github.com/coder/coder/v2/aibridge/config" aibcontext "github.com/coder/coder/v2/aibridge/context" "github.com/coder/coder/v2/aibridge/intercept" @@ -21,8 +22,6 @@ import ( "github.com/coder/coder/v2/aibridge/mcp" "github.com/coder/coder/v2/aibridge/recorder" "github.com/coder/coder/v2/aibridge/tracing" - - "cdr.dev/slog/v3" ) type BlockingInterception struct { diff --git a/aibridge/intercept/chatcompletions/streaming.go b/aibridge/intercept/chatcompletions/streaming.go index 8c58022195331..8dac47dddf5eb 100644 --- a/aibridge/intercept/chatcompletions/streaming.go +++ b/aibridge/intercept/chatcompletions/streaming.go @@ -19,6 +19,7 @@ import ( "go.opentelemetry.io/otel/trace" "golang.org/x/xerrors" + "cdr.dev/slog/v3" "github.com/coder/coder/v2/aibridge/config" aibcontext "github.com/coder/coder/v2/aibridge/context" "github.com/coder/coder/v2/aibridge/intercept" @@ -26,8 +27,6 @@ import ( "github.com/coder/coder/v2/aibridge/mcp" "github.com/coder/coder/v2/aibridge/recorder" "github.com/coder/coder/v2/aibridge/tracing" - - "cdr.dev/slog/v3" "github.com/coder/quartz" ) diff --git a/aibridge/intercept/eventstream/eventstream_test.go b/aibridge/intercept/eventstream/eventstream_test.go index 920234adca915..854b11eee0d7f 100644 --- a/aibridge/intercept/eventstream/eventstream_test.go +++ b/aibridge/intercept/eventstream/eventstream_test.go @@ -15,7 +15,6 @@ import ( "cdr.dev/slog/v3" "cdr.dev/slog/v3/sloggers/sloghuman" "cdr.dev/slog/v3/sloggers/slogtest" - "github.com/coder/coder/v2/aibridge/intercept/eventstream" "github.com/coder/quartz" ) @@ -86,7 +85,7 @@ func TestEventStream_NoWarning_WhenFlushIsFast(t *testing.T) { stream := eventstream.NewEventStream(ctx, logger, nil, clk) - // No clock advance — flush duration stays at 0, below threshold. + // No clock advance, flush duration stays at 0, below threshold. w := &clockAdvancingFlusher{ ResponseRecorder: httptest.NewRecorder(), clk: clk, diff --git a/aibridge/intercept/messages/base.go b/aibridge/intercept/messages/base.go index 83058b49f9d84..9853f7da5ee86 100644 --- a/aibridge/intercept/messages/base.go +++ b/aibridge/intercept/messages/base.go @@ -16,8 +16,12 @@ import ( "github.com/anthropics/anthropic-sdk-go/shared/constant" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/credentials" + "github.com/google/uuid" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" "golang.org/x/xerrors" + "cdr.dev/slog/v3" aibconfig "github.com/coder/coder/v2/aibridge/config" aibcontext "github.com/coder/coder/v2/aibridge/context" "github.com/coder/coder/v2/aibridge/intercept" @@ -27,12 +31,6 @@ import ( "github.com/coder/coder/v2/aibridge/tracing" "github.com/coder/coder/v2/aibridge/utils" "github.com/coder/quartz" - - "github.com/google/uuid" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/trace" - - "cdr.dev/slog/v3" ) // bedrockSupportedBetaFlags is the set of Anthropic-Beta flags that AWS Bedrock diff --git a/aibridge/intercept/messages/base_test.go b/aibridge/intercept/messages/base_test.go index 4b92b6df6524e..148c77c3fa7b6 100644 --- a/aibridge/intercept/messages/base_test.go +++ b/aibridge/intercept/messages/base_test.go @@ -257,12 +257,19 @@ func TestAWSBedrockCredentialChain(t *testing.T) { SmallFastModel: "test-small-model", }, envVars: map[string]string{ - "AWS_ACCESS_KEY_ID": "", - "AWS_SECRET_ACCESS_KEY": "", - "AWS_SESSION_TOKEN": "", - "AWS_PROFILE": "", - "AWS_SHARED_CREDENTIALS_FILE": "/dev/null", - "AWS_CONFIG_FILE": "/dev/null", + "AWS_ACCESS_KEY_ID": "", + "AWS_SECRET_ACCESS_KEY": "", + "AWS_SESSION_TOKEN": "", + "AWS_PROFILE": "", + "AWS_SHARED_CREDENTIALS_FILE": "/dev/null", + "AWS_CONFIG_FILE": "/dev/null", + "AWS_WEB_IDENTITY_TOKEN_FILE": "", + "AWS_ROLE_ARN": "", + "AWS_ROLE_SESSION_NAME": "", + "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI": "", + "AWS_CONTAINER_CREDENTIALS_FULL_URI": "", + "AWS_CONTAINER_AUTHORIZATION_TOKEN": "", + "AWS_EC2_METADATA_DISABLED": "true", }, expectError: true, errorMsg: "no AWS credentials found", diff --git a/aibridge/intercept/messages/blocking.go b/aibridge/intercept/messages/blocking.go index 04525a6c89477..610f93457841a 100644 --- a/aibridge/intercept/messages/blocking.go +++ b/aibridge/intercept/messages/blocking.go @@ -15,6 +15,7 @@ import ( "go.opentelemetry.io/otel/trace" "golang.org/x/xerrors" + "cdr.dev/slog/v3" "github.com/coder/coder/v2/aibridge/config" aibcontext "github.com/coder/coder/v2/aibridge/context" "github.com/coder/coder/v2/aibridge/intercept" @@ -22,8 +23,6 @@ import ( "github.com/coder/coder/v2/aibridge/mcp" "github.com/coder/coder/v2/aibridge/recorder" "github.com/coder/coder/v2/aibridge/tracing" - - "cdr.dev/slog/v3" ) type BlockingInterception struct { diff --git a/aibridge/intercept/messages/reqpayload.go b/aibridge/intercept/messages/reqpayload.go index dfe52fc80ccf7..293dca0c7f8f8 100644 --- a/aibridge/intercept/messages/reqpayload.go +++ b/aibridge/intercept/messages/reqpayload.go @@ -315,7 +315,7 @@ func (p RequestPayload) tools() ([]json.RawMessage, error) { func (RequestPayload) resultToRawMessage(items []gjson.Result) []json.RawMessage { // gjson.Result conversion to json.RawMessage is needed because - // gjson.Result does not implement json.Marshaler — would + // gjson.Result does not implement json.Marshaler. It would // serialize its struct fields instead of the raw JSON it represents. rawMessages := make([]json.RawMessage, 0, len(items)) for _, item := range items { diff --git a/aibridge/intercept/messages/streaming.go b/aibridge/intercept/messages/streaming.go index f45d1f2b07c26..881e62dad599d 100644 --- a/aibridge/intercept/messages/streaming.go +++ b/aibridge/intercept/messages/streaming.go @@ -20,6 +20,7 @@ import ( "go.opentelemetry.io/otel/trace" "golang.org/x/xerrors" + "cdr.dev/slog/v3" "github.com/coder/coder/v2/aibridge/config" aibcontext "github.com/coder/coder/v2/aibridge/context" "github.com/coder/coder/v2/aibridge/intercept" @@ -27,8 +28,6 @@ import ( "github.com/coder/coder/v2/aibridge/mcp" "github.com/coder/coder/v2/aibridge/recorder" "github.com/coder/coder/v2/aibridge/tracing" - - "cdr.dev/slog/v3" "github.com/coder/quartz" ) diff --git a/aibridge/mcp/mcp_test.go b/aibridge/mcp/mcp_test.go index 080653a5b9da5..aeea86e72d24b 100644 --- a/aibridge/mcp/mcp_test.go +++ b/aibridge/mcp/mcp_test.go @@ -10,19 +10,16 @@ import ( "strings" "testing" + mcplib "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" + "github.com/stretchr/testify/require" "go.opentelemetry.io/otel" "go.uber.org/goleak" "cdr.dev/slog/v3" "cdr.dev/slog/v3/sloggers/slogtest" - - "github.com/mark3labs/mcp-go/server" - "github.com/stretchr/testify/require" - "github.com/coder/coder/v2/aibridge/internal/testutil" "github.com/coder/coder/v2/aibridge/mcp" - - mcplib "github.com/mark3labs/mcp-go/mcp" ) func TestMain(m *testing.M) { diff --git a/aibridge/provider/anthropic_test.go b/aibridge/provider/anthropic_test.go index 7a76a34b893b8..75eef0e0d01df 100644 --- a/aibridge/provider/anthropic_test.go +++ b/aibridge/provider/anthropic_test.go @@ -10,7 +10,6 @@ import ( "github.com/stretchr/testify/require" "cdr.dev/slog/v3" - "github.com/coder/coder/v2/aibridge/config" "github.com/coder/coder/v2/aibridge/intercept" "github.com/coder/coder/v2/aibridge/internal/testutil" diff --git a/aibridge/provider/copilot_test.go b/aibridge/provider/copilot_test.go index 36dbb3a104f83..cd30a833500d8 100644 --- a/aibridge/provider/copilot_test.go +++ b/aibridge/provider/copilot_test.go @@ -11,7 +11,6 @@ import ( "go.opentelemetry.io/otel" "cdr.dev/slog/v3" - "github.com/coder/coder/v2/aibridge/config" "github.com/coder/coder/v2/aibridge/internal/testutil" ) diff --git a/aibridge/recorder/recorder.go b/aibridge/recorder/recorder.go index 25ecb5b4af314..26a9f24b5d0b8 100644 --- a/aibridge/recorder/recorder.go +++ b/aibridge/recorder/recorder.go @@ -5,12 +5,10 @@ import ( "sync" "time" + "go.opentelemetry.io/otel/trace" "golang.org/x/xerrors" "cdr.dev/slog/v3" - - "go.opentelemetry.io/otel/trace" - "github.com/coder/coder/v2/aibridge/metrics" "github.com/coder/coder/v2/aibridge/tracing" ) diff --git a/go.mod b/go.mod index 4c8564de9ec0a..a00d567c9e49d 100644 --- a/go.mod +++ b/go.mod @@ -281,7 +281,7 @@ require ( github.com/atotto/clipboard v0.1.4 // indirect github.com/aws/aws-sdk-go-v2 v1.41.5 github.com/aws/aws-sdk-go-v2/config v1.32.12 - github.com/aws/aws-sdk-go-v2/credentials v1.19.12 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.19.12 github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20 // indirect github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.6.14 github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 // indirect @@ -433,7 +433,7 @@ require ( github.com/tcnksm/go-httpstat v0.2.0 // indirect github.com/tdewolff/parse/v2 v2.8.11 // indirect github.com/tidwall/match v1.2.0 // indirect - github.com/tidwall/pretty v1.2.1 // indirect + github.com/tidwall/pretty v1.2.1 github.com/tinylib/msgp v1.2.5 // indirect github.com/tklauser/go-sysconf v0.3.16 // indirect github.com/tklauser/numcpus v0.11.0 // indirect @@ -509,7 +509,10 @@ require ( github.com/go-git/go-git/v5 v5.18.0 github.com/invopop/jsonschema v0.13.0 github.com/mark3labs/mcp-go v0.38.0 + github.com/openai/openai-go/v3 v3.28.0 github.com/shopspring/decimal v1.4.0 + github.com/sony/gobreaker/v2 v2.3.0 + github.com/tidwall/sjson v1.2.5 gonum.org/v1/gonum v0.17.0 ) @@ -604,7 +607,6 @@ require ( github.com/moby/sys/user v0.4.0 // indirect github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect github.com/openai/openai-go v1.12.0 // indirect - github.com/openai/openai-go/v3 v3.28.0 // indirect github.com/package-url/packageurl-go v0.1.3 // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect @@ -614,10 +616,8 @@ require ( github.com/segmentio/asm v1.2.1 // indirect github.com/sergeymakinen/go-bmp v1.0.0 // indirect github.com/sergeymakinen/go-ico v1.0.0-beta.0 // indirect - github.com/sony/gobreaker/v2 v2.3.0 // indirect github.com/spf13/cobra v1.10.2 // indirect github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect - github.com/tidwall/sjson v1.2.5 // indirect github.com/tmaxmax/go-sse v0.11.0 // indirect github.com/ulikunitz/xz v0.5.15 // indirect github.com/urfave/cli/v2 v2.27.5 // indirect diff --git a/scripts/check_emdash.sh b/scripts/check_emdash.sh index c41bf34111c1a..4001ac24087dd 100755 --- a/scripts/check_emdash.sh +++ b/scripts/check_emdash.sh @@ -20,9 +20,14 @@ emdash=$'\xE2\x80\x94' endash=$'\xE2\x80\x93' pattern="${emdash}|${endash}" +# Git exclude_pathspecs excluded from the check. Used in both ls-files and diff comparison. +exclude_pathspecs=( + ":(exclude)aibridge/fixtures/**/*.txtar" +) + scan_all_files() { local output - output=$(git ls-files -z | xargs -0 grep -IEn "$pattern" 2>/dev/null || true) + output=$(git ls-files -z -- "${exclude_pathspecs[@]}" | xargs -0 grep -IEn "$pattern" 2>/dev/null || true) if [[ -n "$output" ]]; then echo "$output" found=1 @@ -58,7 +63,7 @@ else fi found=0 - if ! diff_output=$(git diff "$base" -U0 -- . 2>&1); then + if ! diff_output=$(git diff "$base" -U0 -- . "${exclude_pathspecs[@]}" 2>&1); then echo "ERROR: git diff against $base failed:" echo "$diff_output" exit 1