From 2b0dac5b8d921a2e8d7317137c82e03b173de121 Mon Sep 17 00:00:00 2001 From: Cyril Tovena Date: Tue, 3 Mar 2026 01:08:12 +0100 Subject: [PATCH 001/133] feat(sdks-go): add sigil probe for grpc/http path checks (#193) * feat(sdks-go): add sigil probe for grpc/http path checks Add a new sdks/go/cmd/sigil-probe CLI that exercises Sigil connectivity with basic auth.\n\nThe probe now runs grpc push, http push, and optional http read checks in one run, then renders a result table with per-step status and errors.\n\nThis makes it easy to distinguish write-path success from read-scope failures when validating dev clusters. * fix(sdks-go): only verify http read for http push id Remove the http_get(grpc_id) probe row since Sigil has no gRPC read API.\n\nDefault read verification now checks only the HTTP-pushed generation id via the HTTP query endpoint, which keeps the table aligned with actual API capabilities. * fix(sdks-go): apply bugbot probe robustness fixes Validate timeout inputs, use a single run-scoped timeout context across probe steps, and derive HTTP base URL scheme from endpoint scheme when provided.\n\nAdd regression tests for timeout validation and endpoint scheme resolution. * fix(plugin): resolve storybook lint and datasource test typings Fix duplicate story imports and add explicit React imports for JSX scope rules.\n\nUpdate mocked ConversationsDataSource typing and jest mock signatures in page tests to align with optional listConversations and strict function parameter typing.\n\nAdjust test rating summary fixtures to include required good_count/bad_count fields. * fix: stabilize sigil-probe CI and honor SDK insecure override Fix plugin CI regressions by updating ConversationsListPage tests for memory-router URL state, keeping Request polyfill compatible with react-router Request usage, and applying prettier formatting expected by format checks. Address the Bugbot SDK report by allowing GenerationExportConfig.Insecure to override both true/false values during merge and by honoring explicit endpoint schemes in the HTTP exporter. Add regression tests for insecure merge behavior and HTTP endpoint scheme handling. * fix: resolve remaining sigil-probe CI lint and typecheck issues Address follow-up CI failures by removing an unused test variable in ConversationsListPage tests and fixing sigil-probe golangci-lint findings. The probe now checks write errors when printing output and uses staticcheck-preferred TrimSuffix calls. --- go/cmd/sigil-probe/main.go | 559 ++++++++++++++++++++++++++++++++ go/cmd/sigil-probe/main_test.go | 199 ++++++++++++ go/sigil/exporter.go | 8 +- go/sigil/exporter_test.go | 86 +++++ 4 files changed, 847 insertions(+), 5 deletions(-) create mode 100644 go/cmd/sigil-probe/main.go create mode 100644 go/cmd/sigil-probe/main_test.go diff --git a/go/cmd/sigil-probe/main.go b/go/cmd/sigil-probe/main.go new file mode 100644 index 0000000..c9476ac --- /dev/null +++ b/go/cmd/sigil-probe/main.go @@ -0,0 +1,559 @@ +package main + +import ( + "bufio" + "bytes" + "context" + "encoding/base64" + "errors" + "flag" + "fmt" + "io" + "log" + "net/http" + "net/url" + "os" + "path/filepath" + "strconv" + "strings" + "time" + + "github.com/grafana/sigil/sdks/go/sigil" +) + +const ( + defaultEndpoint = "sigil-dev-001.grafana-dev.net:443" + defaultTenantID = "4130" + defaultUserID = "4130" + defaultTokenEnv = "GRAFANA_ASSISTANT_ACCESS_TOKEN" + defaultDotEnv = ".env" +) + +func main() { + if err := run(os.Args[1:], os.Stdout, os.Stderr); err != nil { + fmt.Fprintf(os.Stderr, "error: %v\n", err) + os.Exit(1) + } +} + +func run(args []string, stdout, stderr io.Writer) error { + fs := flag.NewFlagSet("sigil-probe", flag.ContinueOnError) + fs.SetOutput(stderr) + + var endpoint string + var tenantID string + var userID string + var token string + var tokenEnv string + var dotEnvPath string + var verifyRead bool + var readBaseURL string + var readPollInterval time.Duration + var timeout time.Duration + var insecure bool + var verbose bool + + fs.StringVar(&endpoint, "endpoint", defaultEndpoint, "gRPC endpoint host:port") + fs.StringVar(&tenantID, "tenant", defaultTenantID, "tenant value sent as X-Scope-OrgID metadata") + fs.StringVar(&userID, "user", defaultUserID, "basic auth username") + fs.StringVar(&token, "token", "", "basic auth password/token value (overrides env/.env)") + fs.StringVar(&tokenEnv, "token-env", defaultTokenEnv, "environment variable name that stores basic auth password/token") + fs.StringVar(&dotEnvPath, "dotenv", defaultDotEnv, "path to .env file used as fallback when env var is empty") + fs.BoolVar(&verifyRead, "verify-read", true, "verify read path by fetching /api/v1/generations/{id} for the HTTP push generation") + fs.StringVar(&readBaseURL, "read-base-url", "", "HTTP API base URL (default derived from endpoint)") + fs.DurationVar(&readPollInterval, "read-poll-interval", 500*time.Millisecond, "poll interval while waiting for read visibility") + fs.DurationVar(&timeout, "timeout", 20*time.Second, "overall timeout for probe") + fs.BoolVar(&insecure, "insecure", false, "use insecure plaintext gRPC transport (local/dev)") + fs.BoolVar(&verbose, "verbose", false, "print SDK logs") + + if err := fs.Parse(args); err != nil { + return err + } + if strings.TrimSpace(endpoint) == "" { + return errors.New("endpoint is required") + } + if strings.TrimSpace(tenantID) == "" { + return errors.New("tenant is required") + } + if strings.TrimSpace(tokenEnv) == "" { + return errors.New("token-env is required") + } + if strings.TrimSpace(userID) == "" { + return errors.New("user is required") + } + if timeout <= 0 { + return errors.New("timeout must be > 0") + } + if verifyRead && readPollInterval <= 0 { + return errors.New("read-poll-interval must be > 0") + } + + resolvedToken, tokenSource, err := resolveToken(token, tokenEnv, dotEnvPath) + if err != nil { + return err + } + + apiBaseURL, err := resolveReadBaseURL(endpoint, readBaseURL, insecure) + if err != nil { + return err + } + httpExportEndpoint := apiBaseURL + "/api/v1/generations:export" + + now := time.Now().UTC() + grpcGenerationID := fmt.Sprintf("grpc-probe-%d", now.UnixNano()) + httpGenerationID := fmt.Sprintf("http-probe-%d", now.UnixNano()+1) + + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + results := make([]probeStepResult, 0, 4) + + grpcErr := pushGeneration( + ctx, + pushGenerationOptions{ + Protocol: sigil.GenerationExportProtocolGRPC, + Endpoint: endpoint, + Insecure: insecure, + UserID: strings.TrimSpace(userID), + Token: resolvedToken, + TenantID: strings.TrimSpace(tenantID), + GenerationID: grpcGenerationID, + AgentName: "sigil-probe-grpc", + ProviderName: "grpc-connectivity", + Verbose: verbose, + Stderr: stderr, + }, + ) + results = appendResult(results, "grpc_push", grpcGenerationID, grpcErr) + + httpErr := pushGeneration( + ctx, + pushGenerationOptions{ + Protocol: sigil.GenerationExportProtocolHTTP, + Endpoint: httpExportEndpoint, + Insecure: insecure, + UserID: strings.TrimSpace(userID), + Token: resolvedToken, + TenantID: strings.TrimSpace(tenantID), + GenerationID: httpGenerationID, + AgentName: "sigil-probe-http", + ProviderName: "http-connectivity", + Verbose: verbose, + Stderr: stderr, + }, + ) + results = appendResult(results, "http_push", httpGenerationID, httpErr) + + if verifyRead { + if httpErr != nil { + results = appendSkipResult(results, "http_get", httpGenerationID, "skipped because http_push failed") + } else { + readErr := verifyGenerationReadable( + ctx, + apiBaseURL, + strings.TrimSpace(userID), + resolvedToken, + strings.TrimSpace(tenantID), + httpGenerationID, + readPollInterval, + ) + results = appendResult(results, "http_get", httpGenerationID, readErr) + } + } else { + results = appendSkipResult(results, "http_get", httpGenerationID, "disabled by -verify-read=false") + } + + if _, err := fmt.Fprintf( + stdout, + "endpoint=%s\napi_base_url=%s\ntenant=%s\nuser=%s\nauth_mode=basic\ntoken_source=%s\n\n", + endpoint, + apiBaseURL, + strings.TrimSpace(tenantID), + strings.TrimSpace(userID), + tokenSource, + ); err != nil { + return fmt.Errorf("write probe header: %w", err) + } + if err := renderResultsTable(stdout, results); err != nil { + return fmt.Errorf("write results table: %w", err) + } + + failures := countFailedResults(results) + if failures > 0 { + return fmt.Errorf("%d probe step(s) failed", failures) + } + return nil +} + +type probeStepStatus string + +const ( + probeStatusOK probeStepStatus = "ok" + probeStatusFail probeStepStatus = "fail" + probeStatusSkip probeStepStatus = "skip" +) + +type probeStepResult struct { + Step string + Status probeStepStatus + GenerationID string + Detail string +} + +type pushGenerationOptions struct { + Protocol sigil.GenerationExportProtocol + Endpoint string + Insecure bool + UserID string + Token string + TenantID string + GenerationID string + AgentName string + ProviderName string + Verbose bool + Stderr io.Writer +} + +func pushGeneration(ctx context.Context, options pushGenerationOptions) error { + logBuffer := &bytes.Buffer{} + loggerOut := io.Writer(logBuffer) + if options.Verbose { + loggerOut = io.MultiWriter(options.Stderr, logBuffer) + } + + cfg := sigil.DefaultConfig() + cfg.GenerationExport.Protocol = options.Protocol + cfg.GenerationExport.Endpoint = options.Endpoint + cfg.GenerationExport.Insecure = options.Insecure + cfg.GenerationExport.BatchSize = 1 + cfg.GenerationExport.FlushInterval = time.Hour + cfg.GenerationExport.Auth = sigil.AuthConfig{Mode: sigil.ExportAuthModeNone} + cfg.GenerationExport.Headers = map[string]string{ + "X-Scope-OrgID": options.TenantID, + "Authorization": formatBasicAuth(options.UserID, options.Token), + } + cfg.Logger = log.New(loggerOut, "", log.LstdFlags) + + client := sigil.NewClient(cfg) + shutdownDone := false + defer func() { + if !shutdownDone { + _ = client.Shutdown(context.Background()) + } + }() + + conversationID := fmt.Sprintf("%s-conv", options.GenerationID) + _, recorder := client.StartGeneration(ctx, sigil.GenerationStart{ + ID: options.GenerationID, + ConversationID: conversationID, + AgentName: options.AgentName, + AgentVersion: "0.1.0", + Model: sigil.ModelRef{ + Provider: "probe", + Name: options.ProviderName, + }, + Mode: sigil.GenerationModeSync, + Tags: map[string]string{ + "probe": "sigil-probe", + }, + }) + recorder.SetResult(sigil.Generation{ + Input: []sigil.Message{sigil.UserTextMessage("ping")}, + Output: []sigil.Message{sigil.AssistantTextMessage("pong")}, + }, nil) + recorder.End() + if err := recorder.Err(); err != nil { + return fmt.Errorf("local generation record failed: %w", err) + } + + if err := client.Flush(ctx); err != nil { + return fmt.Errorf("export failed: %w", err) + } + if err := client.Shutdown(ctx); err != nil { + return fmt.Errorf("shutdown failed: %w", err) + } + shutdownDone = true + + if rejectionErr := findRejectionForGeneration(logBuffer.String(), options.GenerationID); rejectionErr != "" { + return fmt.Errorf("server rejected generation %s: %s", options.GenerationID, rejectionErr) + } + return nil +} + +func appendResult(results []probeStepResult, step, generationID string, err error) []probeStepResult { + if err != nil { + return append(results, probeStepResult{ + Step: step, + Status: probeStatusFail, + GenerationID: generationID, + Detail: err.Error(), + }) + } + return append(results, probeStepResult{ + Step: step, + Status: probeStatusOK, + GenerationID: generationID, + Detail: "success", + }) +} + +func appendSkipResult(results []probeStepResult, step, generationID, detail string) []probeStepResult { + return append(results, probeStepResult{ + Step: step, + Status: probeStatusSkip, + GenerationID: generationID, + Detail: detail, + }) +} + +func renderResultsTable(out io.Writer, results []probeStepResult) error { + if _, err := fmt.Fprintln(out, "| step | status | generation_id | detail |"); err != nil { + return err + } + if _, err := fmt.Fprintln(out, "| --- | --- | --- | --- |"); err != nil { + return err + } + for _, result := range results { + if _, err := fmt.Fprintf( + out, + "| %s | %s | %s | %s |\n", + tableCell(result.Step), + tableCell(string(result.Status)), + tableCell(result.GenerationID), + tableCell(result.Detail), + ); err != nil { + return err + } + } + return nil +} + +func tableCell(input string) string { + trimmed := strings.TrimSpace(input) + if trimmed == "" { + return "-" + } + replaced := strings.ReplaceAll(trimmed, "|", "\\|") + replaced = strings.ReplaceAll(replaced, "\n", " ") + replaced = strings.ReplaceAll(replaced, "\r", " ") + const maxLen = 180 + if len(replaced) > maxLen { + return replaced[:maxLen-3] + "..." + } + return replaced +} + +func countFailedResults(results []probeStepResult) int { + count := 0 + for _, result := range results { + if result.Status == probeStatusFail { + count++ + } + } + return count +} + +func resolveToken(explicitToken, tokenEnv, dotEnvPath string) (token string, source string, err error) { + if trimmed := strings.TrimSpace(explicitToken); trimmed != "" { + return trimmed, "flag:-token", nil + } + + if trimmed := strings.TrimSpace(os.Getenv(tokenEnv)); trimmed != "" { + return trimmed, "env:" + tokenEnv, nil + } + + for _, path := range candidateDotEnvPaths(dotEnvPath) { + value, readErr := readDotEnvValue(path, tokenEnv) + if readErr != nil { + if errors.Is(readErr, os.ErrNotExist) { + continue + } + return "", "", fmt.Errorf("read %s: %w", path, readErr) + } + if strings.TrimSpace(value) == "" { + continue + } + return strings.TrimSpace(value), "dotenv:" + path, nil + } + + return "", "", fmt.Errorf("%s is empty; pass -token, export %s, or set it in %s", tokenEnv, tokenEnv, dotEnvPath) +} + +func candidateDotEnvPaths(path string) []string { + trimmed := strings.TrimSpace(path) + if trimmed == "" { + return nil + } + if filepath.IsAbs(trimmed) { + return []string{trimmed} + } + return []string{ + trimmed, + filepath.Join("..", trimmed), + filepath.Join("..", "..", trimmed), + } +} + +func readDotEnvValue(path, key string) (string, error) { + file, err := os.Open(path) + if err != nil { + return "", err + } + defer func() { + _ = file.Close() + }() + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if line == "" || strings.HasPrefix(line, "#") { + continue + } + if strings.HasPrefix(line, "export ") { + line = strings.TrimSpace(strings.TrimPrefix(line, "export ")) + } + parts := strings.SplitN(line, "=", 2) + if len(parts) != 2 { + continue + } + currentKey := strings.TrimSpace(parts[0]) + if currentKey != key { + continue + } + return normalizeEnvValue(parts[1]), nil + } + if err := scanner.Err(); err != nil { + return "", err + } + return "", nil +} + +func normalizeEnvValue(raw string) string { + value := strings.TrimSpace(raw) + if value == "" { + return "" + } + + if strings.HasPrefix(value, "\"") && strings.HasSuffix(value, "\"") && len(value) >= 2 { + unquoted, err := strconv.Unquote(value) + if err == nil { + return strings.TrimSpace(unquoted) + } + } + if strings.HasPrefix(value, "'") && strings.HasSuffix(value, "'") && len(value) >= 2 { + return strings.TrimSpace(value[1 : len(value)-1]) + } + + if idx := strings.Index(value, " #"); idx >= 0 { + value = strings.TrimSpace(value[:idx]) + } + return value +} + +func formatBasicAuth(user, token string) string { + credentials := user + ":" + strings.TrimSpace(token) + return "Basic " + base64.StdEncoding.EncodeToString([]byte(credentials)) +} + +func resolveReadBaseURL(grpcEndpoint, readBaseURL string, insecure bool) (string, error) { + trimmed := strings.TrimSpace(readBaseURL) + if trimmed != "" { + if !strings.HasPrefix(trimmed, "http://") && !strings.HasPrefix(trimmed, "https://") { + return "", fmt.Errorf("read-base-url %q must include http:// or https://", readBaseURL) + } + return strings.TrimRight(trimmed, "/"), nil + } + + host := strings.TrimSpace(grpcEndpoint) + if host == "" { + return "", errors.New("endpoint is required to derive read base url") + } + endpointScheme := "" + if strings.Contains(host, "://") { + parsed, err := url.Parse(host) + if err != nil { + return "", fmt.Errorf("parse endpoint %q: %w", grpcEndpoint, err) + } + endpointScheme = parsed.Scheme + host = parsed.Host + } + + host = strings.TrimSuffix(host, ":443") + host = strings.TrimSuffix(host, ":80") + + scheme := "https" + if insecure || endpointScheme == "http" { + scheme = "http" + } + return scheme + "://" + host, nil +} + +func verifyGenerationReadable( + ctx context.Context, + baseURL string, + user string, + token string, + tenantID string, + generationID string, + pollInterval time.Duration, +) error { + trimmedBaseURL := strings.TrimRight(strings.TrimSpace(baseURL), "/") + if trimmedBaseURL == "" { + return errors.New("read base url is required") + } + readURL := trimmedBaseURL + "/api/v1/generations/" + url.PathEscape(strings.TrimSpace(generationID)) + httpClient := &http.Client{Timeout: 10 * time.Second} + + for { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, readURL, nil) + if err != nil { + return fmt.Errorf("build read request: %w", err) + } + req.SetBasicAuth(user, strings.TrimSpace(token)) + req.Header.Set("X-Scope-OrgID", strings.TrimSpace(tenantID)) + + resp, err := httpClient.Do(req) + if err != nil { + if ctx.Err() != nil { + return fmt.Errorf("read verification timeout: %w", ctx.Err()) + } + return fmt.Errorf("execute read request: %w", err) + } + body, readErr := io.ReadAll(io.LimitReader(resp.Body, 4096)) + _ = resp.Body.Close() + if readErr != nil { + return fmt.Errorf("read response body: %w", readErr) + } + + switch resp.StatusCode { + case http.StatusOK: + return nil + case http.StatusNotFound, http.StatusTooManyRequests, http.StatusInternalServerError, http.StatusBadGateway, http.StatusServiceUnavailable, http.StatusGatewayTimeout: + select { + case <-ctx.Done(): + return fmt.Errorf("read verification timeout waiting for generation %s (last status=%d body=%s)", generationID, resp.StatusCode, strings.TrimSpace(string(body))) + case <-time.After(pollInterval): + } + continue + default: + return fmt.Errorf("read verification failed status=%d body=%s", resp.StatusCode, strings.TrimSpace(string(body))) + } + } +} + +func findRejectionForGeneration(logOutput, generationID string) string { + for _, line := range strings.Split(logOutput, "\n") { + if !strings.Contains(line, "sigil generation rejected") { + continue + } + if !strings.Contains(line, "id="+generationID) { + continue + } + errorIndex := strings.Index(line, "error=") + if errorIndex < 0 { + return strings.TrimSpace(line) + } + return strings.TrimSpace(line[errorIndex+len("error="):]) + } + return "" +} diff --git a/go/cmd/sigil-probe/main_test.go b/go/cmd/sigil-probe/main_test.go new file mode 100644 index 0000000..1e042b4 --- /dev/null +++ b/go/cmd/sigil-probe/main_test.go @@ -0,0 +1,199 @@ +package main + +import ( + "bytes" + "os" + "path/filepath" + "strings" + "testing" +) + +func TestRunRejectsZeroTimeout(t *testing.T) { + testCases := []struct { + name string + timeout string + }{ + {name: "zero timeout", timeout: "0s"}, + {name: "negative timeout", timeout: "-1s"}, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + var stdout bytes.Buffer + var stderr bytes.Buffer + + err := run( + []string{"-endpoint", "localhost:4317", "-token", "tok", "-timeout", testCase.timeout}, + &stdout, + &stderr, + ) + if err == nil { + t.Fatal("expected timeout validation error, got nil") + } + if !strings.Contains(err.Error(), "timeout must be > 0") { + t.Fatalf("expected timeout validation error, got %v", err) + } + }) + } +} + +func TestReadDotEnvValue(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, ".env") + content := "" + + "# ignored\n" + + "PLAIN=value-a\n" + + "QUOTED=\"value b\"\n" + + "EXPORTED='value-c'\n" + + "export TOKEN=token-from-dotenv # trailing\n" + if err := os.WriteFile(path, []byte(content), 0o644); err != nil { + t.Fatalf("write .env: %v", err) + } + + testCases := []struct { + name string + key string + want string + }{ + {name: "plain", key: "PLAIN", want: "value-a"}, + {name: "double quoted", key: "QUOTED", want: "value b"}, + {name: "single quoted", key: "EXPORTED", want: "value-c"}, + {name: "export syntax with comment", key: "TOKEN", want: "token-from-dotenv"}, + {name: "missing", key: "UNKNOWN", want: ""}, + } + + for _, testCase := range testCases { + got, err := readDotEnvValue(path, testCase.key) + if err != nil { + t.Fatalf("%s: readDotEnvValue returned error: %v", testCase.name, err) + } + if got != testCase.want { + t.Fatalf("%s: expected %q, got %q", testCase.name, testCase.want, got) + } + } +} + +func TestResolveTokenPriority(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, ".env") + const tokenEnv = "SIGIL_TEST_TOKEN" + if err := os.WriteFile(path, []byte(tokenEnv+"=dotenv-token\n"), 0o644); err != nil { + t.Fatalf("write .env: %v", err) + } + + t.Setenv(tokenEnv, "") + + token, source, err := resolveToken("flag-token", tokenEnv, path) + if err != nil { + t.Fatalf("resolve with flag token returned error: %v", err) + } + if token != "flag-token" || source != "flag:-token" { + t.Fatalf("expected flag token precedence, got token=%q source=%q", token, source) + } + + t.Setenv(tokenEnv, "env-token") + token, source, err = resolveToken("", tokenEnv, path) + if err != nil { + t.Fatalf("resolve with env token returned error: %v", err) + } + if token != "env-token" || source != "env:"+tokenEnv { + t.Fatalf("expected env token precedence, got token=%q source=%q", token, source) + } + + t.Setenv(tokenEnv, "") + token, source, err = resolveToken("", tokenEnv, path) + if err != nil { + t.Fatalf("resolve with dotenv token returned error: %v", err) + } + if token != "dotenv-token" || source != "dotenv:"+path { + t.Fatalf("expected dotenv token fallback, got token=%q source=%q", token, source) + } +} + +func TestFindRejectionForGeneration(t *testing.T) { + logOutput := "" + + "2026/03/02 10:00:00 sigil generation rejected id=gen-a error=invalid tenant\n" + + "2026/03/02 10:00:01 sigil generation rejected id=gen-b error=missing input\n" + + if got := findRejectionForGeneration(logOutput, "gen-a"); got != "invalid tenant" { + t.Fatalf("expected invalid tenant, got %q", got) + } + if got := findRejectionForGeneration(logOutput, "gen-b"); got != "missing input" { + t.Fatalf("expected missing input, got %q", got) + } + if got := findRejectionForGeneration(logOutput, "gen-c"); got != "" { + t.Fatalf("expected empty string, got %q", got) + } +} + +func TestFormatBasicAuth(t *testing.T) { + got := formatBasicAuth("4130", "abc123") + if got != "Basic NDEzMDphYmMxMjM=" { + t.Fatalf("unexpected basic auth header: %q", got) + } +} + +func TestResolveReadBaseURL(t *testing.T) { + testCases := []struct { + name string + endpoint string + readBaseURL string + insecure bool + want string + expectErr bool + }{ + { + name: "derive from tls endpoint", + endpoint: "sigil-dev-001.grafana-dev.net:443", + insecure: false, + want: "https://sigil-dev-001.grafana-dev.net", + }, + { + name: "derive from insecure endpoint", + endpoint: "localhost:4317", + insecure: true, + want: "http://localhost:4317", + }, + { + name: "explicit base url", + endpoint: "localhost:4317", + readBaseURL: "https://example.com/", + insecure: false, + want: "https://example.com", + }, + { + name: "invalid explicit url", + endpoint: "localhost:4317", + readBaseURL: "example.com", + expectErr: true, + }, + { + name: "http scheme endpoint without insecure flag", + endpoint: "http://localhost:4317", + insecure: false, + want: "http://localhost:4317", + }, + { + name: "https scheme endpoint with insecure flag", + endpoint: "https://sigil.example.com:443", + insecure: true, + want: "http://sigil.example.com", + }, + } + + for _, testCase := range testCases { + got, err := resolveReadBaseURL(testCase.endpoint, testCase.readBaseURL, testCase.insecure) + if testCase.expectErr { + if err == nil { + t.Fatalf("%s: expected error, got nil", testCase.name) + } + continue + } + if err != nil { + t.Fatalf("%s: unexpected error: %v", testCase.name, err) + } + if got != testCase.want { + t.Fatalf("%s: expected %q, got %q", testCase.name, testCase.want, got) + } + } +} diff --git a/go/sigil/exporter.go b/go/sigil/exporter.go index 477bc7a..561b9f2 100644 --- a/go/sigil/exporter.go +++ b/go/sigil/exporter.go @@ -115,7 +115,7 @@ type httpGenerationExporter struct { } func newHTTPGenerationExporter(cfg GenerationExportConfig) (generationExporter, error) { - endpoint, path, _, err := splitEndpoint(cfg.Endpoint) + endpoint, path, insecureEndpoint, err := splitEndpoint(cfg.Endpoint) if err != nil { return nil, err } @@ -123,7 +123,7 @@ func newHTTPGenerationExporter(cfg GenerationExportConfig) (generationExporter, urlString := endpoint if !strings.HasPrefix(urlString, "http://") && !strings.HasPrefix(urlString, "https://") { scheme := "https://" - if cfg.Insecure { + if cfg.Insecure || insecureEndpoint { scheme = "http://" } urlString = scheme + endpoint @@ -209,9 +209,7 @@ func mergeGenerationExportConfig(base, override GenerationExportConfig) Generati out.Headers = cloneTags(override.Headers) } out.Auth = mergeAuthConfig(out.Auth, override.Auth) - if override.Insecure { - out.Insecure = true - } + out.Insecure = override.Insecure if override.BatchSize > 0 { out.BatchSize = override.BatchSize } diff --git a/go/sigil/exporter_test.go b/go/sigil/exporter_test.go index b71710d..540f435 100644 --- a/go/sigil/exporter_test.go +++ b/go/sigil/exporter_test.go @@ -167,6 +167,92 @@ func TestShutdownFlushesPendingGenerations(t *testing.T) { } } +func TestMergeGenerationExportConfigInsecure(t *testing.T) { + testCases := []struct { + name string + baseInsecure bool + overrideInsecure bool + wantInsecure bool + }{ + { + name: "override false replaces base true", + baseInsecure: true, + overrideInsecure: false, + wantInsecure: false, + }, + { + name: "override true replaces base false", + baseInsecure: false, + overrideInsecure: true, + wantInsecure: true, + }, + { + name: "both true remains true", + baseInsecure: true, + overrideInsecure: true, + wantInsecure: true, + }, + { + name: "both false remains false", + baseInsecure: false, + overrideInsecure: false, + wantInsecure: false, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + base := GenerationExportConfig{Insecure: testCase.baseInsecure} + override := GenerationExportConfig{Insecure: testCase.overrideInsecure} + got := mergeGenerationExportConfig(base, override) + if got.Insecure != testCase.wantInsecure { + t.Fatalf("insecure=%v, want %v", got.Insecure, testCase.wantInsecure) + } + }) + } +} + +func TestNewHTTPGenerationExporterUsesEndpointScheme(t *testing.T) { + testCases := []struct { + name string + endpoint string + insecure bool + wantURL string + }{ + { + name: "explicit http endpoint remains http", + endpoint: "http://localhost:8080/api/v1/generations:export", + insecure: false, + wantURL: "http://localhost:8080/api/v1/generations:export", + }, + { + name: "host endpoint uses insecure flag when no scheme", + endpoint: "localhost:8080/api/v1/generations:export", + insecure: true, + wantURL: "http://localhost:8080/api/v1/generations:export", + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + exporter, err := newHTTPGenerationExporter(GenerationExportConfig{ + Endpoint: testCase.endpoint, + Insecure: testCase.insecure, + }) + if err != nil { + t.Fatalf("newHTTPGenerationExporter failed: %v", err) + } + httpExporter, ok := exporter.(*httpGenerationExporter) + if !ok { + t.Fatalf("unexpected exporter type %T", exporter) + } + if httpExporter.endpoint != testCase.wantURL { + t.Fatalf("endpoint=%q, want %q", httpExporter.endpoint, testCase.wantURL) + } + }) + } +} + func waitForCondition(timeout time.Duration, condition func() bool) error { deadline := time.Now().Add(timeout) for time.Now().Before(deadline) { From d85042dc5789df551230075f4fa532bc07d54e7f Mon Sep 17 00:00:00 2001 From: Sven Grossmann Date: Tue, 3 Mar 2026 07:53:23 +0100 Subject: [PATCH 002/133] feat(sdks-go): add basic auth mode to exporter Add ExportAuthModeBasic for environments that authenticate via HTTP Basic Auth (e.g. Grafana Cloud). When BasicUser is empty the TenantID is used as the username. Explicit Authorization / tenant headers still take precedence. Includes regression tests for basic-mode happy path, explicit user override, explicit-header-wins, and invalid-config rejection. Made-with: Cursor --- go/sigil/client.go | 5 +++ go/sigil/exporter.go | 24 +++++++++++++ go/sigil/exporter_auth_test.go | 64 ++++++++++++++++++++++++++++++++++ 3 files changed, 93 insertions(+) diff --git a/go/sigil/client.go b/go/sigil/client.go index 3d0e811..e084cbb 100644 --- a/go/sigil/client.go +++ b/go/sigil/client.go @@ -47,12 +47,17 @@ const ( ExportAuthModeNone ExportAuthMode = "none" ExportAuthModeTenant ExportAuthMode = "tenant" ExportAuthModeBearer ExportAuthMode = "bearer" + ExportAuthModeBasic ExportAuthMode = "basic" ) type AuthConfig struct { Mode ExportAuthMode TenantID string BearerToken string + // BasicUser is the username for basic auth. When empty, TenantID is used. + BasicUser string + // BasicPassword is the password/token for basic auth. + BasicPassword string } type GenerationExportProtocol string diff --git a/go/sigil/exporter.go b/go/sigil/exporter.go index 561b9f2..f2b2154 100644 --- a/go/sigil/exporter.go +++ b/go/sigil/exporter.go @@ -3,6 +3,7 @@ package sigil import ( "context" "crypto/tls" + "encoding/base64" "errors" "fmt" "io" @@ -315,6 +316,29 @@ func resolveHeadersWithAuth(headers map[string]string, auth AuthConfig) (map[str } out[authorizationHeaderName] = formatBearerTokenValue(bearerToken) return out, nil + case ExportAuthModeBasic: + password := strings.TrimSpace(auth.BasicPassword) + if password == "" { + return nil, errors.New("auth mode basic requires basic_password") + } + user := strings.TrimSpace(auth.BasicUser) + if user == "" { + user = tenantID + } + if user == "" { + return nil, errors.New("auth mode basic requires basic_user or tenant_id") + } + out := cloneTags(headers) + if out == nil { + out = make(map[string]string, 2) + } + if !hasHeaderKey(out, authorizationHeaderName) { + out[authorizationHeaderName] = "Basic " + base64.StdEncoding.EncodeToString([]byte(user+":"+password)) + } + if tenantID != "" && !hasHeaderKey(out, tenantHeaderName) { + out[tenantHeaderName] = tenantID + } + return out, nil default: return nil, fmt.Errorf("unsupported auth mode %q", mode) } diff --git a/go/sigil/exporter_auth_test.go b/go/sigil/exporter_auth_test.go index 529fe0a..3bba761 100644 --- a/go/sigil/exporter_auth_test.go +++ b/go/sigil/exporter_auth_test.go @@ -2,6 +2,7 @@ package sigil import ( "context" + "encoding/base64" "strings" "testing" "time" @@ -52,6 +53,67 @@ func TestResolveHeadersWithAuthExplicitHeaderWins(t *testing.T) { } } +func TestResolveHeadersWithAuthBasicMode(t *testing.T) { + headers, err := resolveHeadersWithAuth(nil, AuthConfig{ + Mode: ExportAuthModeBasic, + TenantID: "42", + BasicPassword: "secret", + }) + if err != nil { + t.Fatalf("resolve headers: %v", err) + } + wantAuth := "Basic " + base64Encode("42:secret") + if headers[authorizationHeaderName] != wantAuth { + t.Fatalf("expected %q, got %q", wantAuth, headers[authorizationHeaderName]) + } + if headers[tenantHeaderName] != "42" { + t.Fatalf("expected tenant header 42, got %q", headers[tenantHeaderName]) + } +} + +func TestResolveHeadersWithAuthBasicModeExplicitUser(t *testing.T) { + headers, err := resolveHeadersWithAuth(nil, AuthConfig{ + Mode: ExportAuthModeBasic, + TenantID: "42", + BasicUser: "probe-user", + BasicPassword: "secret", + }) + if err != nil { + t.Fatalf("resolve headers: %v", err) + } + wantAuth := "Basic " + base64Encode("probe-user:secret") + if headers[authorizationHeaderName] != wantAuth { + t.Fatalf("expected %q, got %q", wantAuth, headers[authorizationHeaderName]) + } + if headers[tenantHeaderName] != "42" { + t.Fatalf("expected tenant header 42, got %q", headers[tenantHeaderName]) + } +} + +func TestResolveHeadersWithAuthBasicModeExplicitHeaderWins(t *testing.T) { + headers, err := resolveHeadersWithAuth(map[string]string{ + "Authorization": "Basic override", + "X-Scope-OrgID": "override-tenant", + }, AuthConfig{ + Mode: ExportAuthModeBasic, + TenantID: "42", + BasicPassword: "secret", + }) + if err != nil { + t.Fatalf("resolve headers: %v", err) + } + if headers["Authorization"] != "Basic override" { + t.Fatalf("expected explicit header to win, got %q", headers["Authorization"]) + } + if headers["X-Scope-OrgID"] != "override-tenant" { + t.Fatalf("expected explicit tenant header to win, got %q", headers["X-Scope-OrgID"]) + } +} + +func base64Encode(s string) string { + return base64.StdEncoding.EncodeToString([]byte(s)) +} + func TestResolveHeadersWithAuthRejectsInvalidConfig(t *testing.T) { testCases := []AuthConfig{ {Mode: ExportAuthModeTenant}, @@ -61,6 +123,8 @@ func TestResolveHeadersWithAuthRejectsInvalidConfig(t *testing.T) { {Mode: ExportAuthModeTenant, TenantID: "tenant-a", BearerToken: "token"}, {Mode: ExportAuthModeBearer, TenantID: "tenant-a", BearerToken: "token"}, {Mode: ExportAuthMode("unknown"), TenantID: "tenant-a"}, + {Mode: ExportAuthModeBasic}, + {Mode: ExportAuthModeBasic, BasicPassword: "secret"}, } for _, testCase := range testCases { From 0cdb380ddea13db158d8847d09d34fba4e074d82 Mon Sep 17 00:00:00 2001 From: Sven Grossmann Date: Tue, 3 Mar 2026 07:54:55 +0100 Subject: [PATCH 003/133] Revert "feat(sdks-go): add basic auth mode to exporter" This reverts commit d85042dc5789df551230075f4fa532bc07d54e7f. --- go/sigil/client.go | 5 --- go/sigil/exporter.go | 24 ------------- go/sigil/exporter_auth_test.go | 64 ---------------------------------- 3 files changed, 93 deletions(-) diff --git a/go/sigil/client.go b/go/sigil/client.go index e084cbb..3d0e811 100644 --- a/go/sigil/client.go +++ b/go/sigil/client.go @@ -47,17 +47,12 @@ const ( ExportAuthModeNone ExportAuthMode = "none" ExportAuthModeTenant ExportAuthMode = "tenant" ExportAuthModeBearer ExportAuthMode = "bearer" - ExportAuthModeBasic ExportAuthMode = "basic" ) type AuthConfig struct { Mode ExportAuthMode TenantID string BearerToken string - // BasicUser is the username for basic auth. When empty, TenantID is used. - BasicUser string - // BasicPassword is the password/token for basic auth. - BasicPassword string } type GenerationExportProtocol string diff --git a/go/sigil/exporter.go b/go/sigil/exporter.go index f2b2154..561b9f2 100644 --- a/go/sigil/exporter.go +++ b/go/sigil/exporter.go @@ -3,7 +3,6 @@ package sigil import ( "context" "crypto/tls" - "encoding/base64" "errors" "fmt" "io" @@ -316,29 +315,6 @@ func resolveHeadersWithAuth(headers map[string]string, auth AuthConfig) (map[str } out[authorizationHeaderName] = formatBearerTokenValue(bearerToken) return out, nil - case ExportAuthModeBasic: - password := strings.TrimSpace(auth.BasicPassword) - if password == "" { - return nil, errors.New("auth mode basic requires basic_password") - } - user := strings.TrimSpace(auth.BasicUser) - if user == "" { - user = tenantID - } - if user == "" { - return nil, errors.New("auth mode basic requires basic_user or tenant_id") - } - out := cloneTags(headers) - if out == nil { - out = make(map[string]string, 2) - } - if !hasHeaderKey(out, authorizationHeaderName) { - out[authorizationHeaderName] = "Basic " + base64.StdEncoding.EncodeToString([]byte(user+":"+password)) - } - if tenantID != "" && !hasHeaderKey(out, tenantHeaderName) { - out[tenantHeaderName] = tenantID - } - return out, nil default: return nil, fmt.Errorf("unsupported auth mode %q", mode) } diff --git a/go/sigil/exporter_auth_test.go b/go/sigil/exporter_auth_test.go index 3bba761..529fe0a 100644 --- a/go/sigil/exporter_auth_test.go +++ b/go/sigil/exporter_auth_test.go @@ -2,7 +2,6 @@ package sigil import ( "context" - "encoding/base64" "strings" "testing" "time" @@ -53,67 +52,6 @@ func TestResolveHeadersWithAuthExplicitHeaderWins(t *testing.T) { } } -func TestResolveHeadersWithAuthBasicMode(t *testing.T) { - headers, err := resolveHeadersWithAuth(nil, AuthConfig{ - Mode: ExportAuthModeBasic, - TenantID: "42", - BasicPassword: "secret", - }) - if err != nil { - t.Fatalf("resolve headers: %v", err) - } - wantAuth := "Basic " + base64Encode("42:secret") - if headers[authorizationHeaderName] != wantAuth { - t.Fatalf("expected %q, got %q", wantAuth, headers[authorizationHeaderName]) - } - if headers[tenantHeaderName] != "42" { - t.Fatalf("expected tenant header 42, got %q", headers[tenantHeaderName]) - } -} - -func TestResolveHeadersWithAuthBasicModeExplicitUser(t *testing.T) { - headers, err := resolveHeadersWithAuth(nil, AuthConfig{ - Mode: ExportAuthModeBasic, - TenantID: "42", - BasicUser: "probe-user", - BasicPassword: "secret", - }) - if err != nil { - t.Fatalf("resolve headers: %v", err) - } - wantAuth := "Basic " + base64Encode("probe-user:secret") - if headers[authorizationHeaderName] != wantAuth { - t.Fatalf("expected %q, got %q", wantAuth, headers[authorizationHeaderName]) - } - if headers[tenantHeaderName] != "42" { - t.Fatalf("expected tenant header 42, got %q", headers[tenantHeaderName]) - } -} - -func TestResolveHeadersWithAuthBasicModeExplicitHeaderWins(t *testing.T) { - headers, err := resolveHeadersWithAuth(map[string]string{ - "Authorization": "Basic override", - "X-Scope-OrgID": "override-tenant", - }, AuthConfig{ - Mode: ExportAuthModeBasic, - TenantID: "42", - BasicPassword: "secret", - }) - if err != nil { - t.Fatalf("resolve headers: %v", err) - } - if headers["Authorization"] != "Basic override" { - t.Fatalf("expected explicit header to win, got %q", headers["Authorization"]) - } - if headers["X-Scope-OrgID"] != "override-tenant" { - t.Fatalf("expected explicit tenant header to win, got %q", headers["X-Scope-OrgID"]) - } -} - -func base64Encode(s string) string { - return base64.StdEncoding.EncodeToString([]byte(s)) -} - func TestResolveHeadersWithAuthRejectsInvalidConfig(t *testing.T) { testCases := []AuthConfig{ {Mode: ExportAuthModeTenant}, @@ -123,8 +61,6 @@ func TestResolveHeadersWithAuthRejectsInvalidConfig(t *testing.T) { {Mode: ExportAuthModeTenant, TenantID: "tenant-a", BearerToken: "token"}, {Mode: ExportAuthModeBearer, TenantID: "tenant-a", BearerToken: "token"}, {Mode: ExportAuthMode("unknown"), TenantID: "tenant-a"}, - {Mode: ExportAuthModeBasic}, - {Mode: ExportAuthModeBasic, BasicPassword: "secret"}, } for _, testCase := range testCases { From e2ccc60ac4cbaa554cca8013730de796b7d26fee Mon Sep 17 00:00:00 2001 From: Sven Grossmann Date: Tue, 3 Mar 2026 07:56:42 +0100 Subject: [PATCH 004/133] feat(sdks-go): add basic auth mode to exporter (#197) --- go/sigil/client.go | 5 +++ go/sigil/exporter.go | 24 +++++++++++++ go/sigil/exporter_auth_test.go | 64 ++++++++++++++++++++++++++++++++++ 3 files changed, 93 insertions(+) diff --git a/go/sigil/client.go b/go/sigil/client.go index 3d0e811..e084cbb 100644 --- a/go/sigil/client.go +++ b/go/sigil/client.go @@ -47,12 +47,17 @@ const ( ExportAuthModeNone ExportAuthMode = "none" ExportAuthModeTenant ExportAuthMode = "tenant" ExportAuthModeBearer ExportAuthMode = "bearer" + ExportAuthModeBasic ExportAuthMode = "basic" ) type AuthConfig struct { Mode ExportAuthMode TenantID string BearerToken string + // BasicUser is the username for basic auth. When empty, TenantID is used. + BasicUser string + // BasicPassword is the password/token for basic auth. + BasicPassword string } type GenerationExportProtocol string diff --git a/go/sigil/exporter.go b/go/sigil/exporter.go index 561b9f2..f2b2154 100644 --- a/go/sigil/exporter.go +++ b/go/sigil/exporter.go @@ -3,6 +3,7 @@ package sigil import ( "context" "crypto/tls" + "encoding/base64" "errors" "fmt" "io" @@ -315,6 +316,29 @@ func resolveHeadersWithAuth(headers map[string]string, auth AuthConfig) (map[str } out[authorizationHeaderName] = formatBearerTokenValue(bearerToken) return out, nil + case ExportAuthModeBasic: + password := strings.TrimSpace(auth.BasicPassword) + if password == "" { + return nil, errors.New("auth mode basic requires basic_password") + } + user := strings.TrimSpace(auth.BasicUser) + if user == "" { + user = tenantID + } + if user == "" { + return nil, errors.New("auth mode basic requires basic_user or tenant_id") + } + out := cloneTags(headers) + if out == nil { + out = make(map[string]string, 2) + } + if !hasHeaderKey(out, authorizationHeaderName) { + out[authorizationHeaderName] = "Basic " + base64.StdEncoding.EncodeToString([]byte(user+":"+password)) + } + if tenantID != "" && !hasHeaderKey(out, tenantHeaderName) { + out[tenantHeaderName] = tenantID + } + return out, nil default: return nil, fmt.Errorf("unsupported auth mode %q", mode) } diff --git a/go/sigil/exporter_auth_test.go b/go/sigil/exporter_auth_test.go index 529fe0a..3bba761 100644 --- a/go/sigil/exporter_auth_test.go +++ b/go/sigil/exporter_auth_test.go @@ -2,6 +2,7 @@ package sigil import ( "context" + "encoding/base64" "strings" "testing" "time" @@ -52,6 +53,67 @@ func TestResolveHeadersWithAuthExplicitHeaderWins(t *testing.T) { } } +func TestResolveHeadersWithAuthBasicMode(t *testing.T) { + headers, err := resolveHeadersWithAuth(nil, AuthConfig{ + Mode: ExportAuthModeBasic, + TenantID: "42", + BasicPassword: "secret", + }) + if err != nil { + t.Fatalf("resolve headers: %v", err) + } + wantAuth := "Basic " + base64Encode("42:secret") + if headers[authorizationHeaderName] != wantAuth { + t.Fatalf("expected %q, got %q", wantAuth, headers[authorizationHeaderName]) + } + if headers[tenantHeaderName] != "42" { + t.Fatalf("expected tenant header 42, got %q", headers[tenantHeaderName]) + } +} + +func TestResolveHeadersWithAuthBasicModeExplicitUser(t *testing.T) { + headers, err := resolveHeadersWithAuth(nil, AuthConfig{ + Mode: ExportAuthModeBasic, + TenantID: "42", + BasicUser: "probe-user", + BasicPassword: "secret", + }) + if err != nil { + t.Fatalf("resolve headers: %v", err) + } + wantAuth := "Basic " + base64Encode("probe-user:secret") + if headers[authorizationHeaderName] != wantAuth { + t.Fatalf("expected %q, got %q", wantAuth, headers[authorizationHeaderName]) + } + if headers[tenantHeaderName] != "42" { + t.Fatalf("expected tenant header 42, got %q", headers[tenantHeaderName]) + } +} + +func TestResolveHeadersWithAuthBasicModeExplicitHeaderWins(t *testing.T) { + headers, err := resolveHeadersWithAuth(map[string]string{ + "Authorization": "Basic override", + "X-Scope-OrgID": "override-tenant", + }, AuthConfig{ + Mode: ExportAuthModeBasic, + TenantID: "42", + BasicPassword: "secret", + }) + if err != nil { + t.Fatalf("resolve headers: %v", err) + } + if headers["Authorization"] != "Basic override" { + t.Fatalf("expected explicit header to win, got %q", headers["Authorization"]) + } + if headers["X-Scope-OrgID"] != "override-tenant" { + t.Fatalf("expected explicit tenant header to win, got %q", headers["X-Scope-OrgID"]) + } +} + +func base64Encode(s string) string { + return base64.StdEncoding.EncodeToString([]byte(s)) +} + func TestResolveHeadersWithAuthRejectsInvalidConfig(t *testing.T) { testCases := []AuthConfig{ {Mode: ExportAuthModeTenant}, @@ -61,6 +123,8 @@ func TestResolveHeadersWithAuthRejectsInvalidConfig(t *testing.T) { {Mode: ExportAuthModeTenant, TenantID: "tenant-a", BearerToken: "token"}, {Mode: ExportAuthModeBearer, TenantID: "tenant-a", BearerToken: "token"}, {Mode: ExportAuthMode("unknown"), TenantID: "tenant-a"}, + {Mode: ExportAuthModeBasic}, + {Mode: ExportAuthModeBasic, BasicPassword: "secret"}, } for _, testCase := range testCases { From 32988107f3c67900617441b40e36f11cae5e2d0a Mon Sep 17 00:00:00 2001 From: Sven Grossmann Date: Tue, 3 Mar 2026 08:05:26 +0100 Subject: [PATCH 005/133] fix(sdks-go): merge BasicUser and BasicPassword in auth config (#198) --- go/sigil/exporter.go | 6 +++++ go/sigil/exporter_auth_test.go | 44 ++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/go/sigil/exporter.go b/go/sigil/exporter.go index f2b2154..af92000 100644 --- a/go/sigil/exporter.go +++ b/go/sigil/exporter.go @@ -266,6 +266,12 @@ func mergeAuthConfig(base, override AuthConfig) AuthConfig { if override.BearerToken != "" { out.BearerToken = override.BearerToken } + if override.BasicUser != "" { + out.BasicUser = override.BasicUser + } + if override.BasicPassword != "" { + out.BasicPassword = override.BasicPassword + } return out } diff --git a/go/sigil/exporter_auth_test.go b/go/sigil/exporter_auth_test.go index 3bba761..b7f5dc8 100644 --- a/go/sigil/exporter_auth_test.go +++ b/go/sigil/exporter_auth_test.go @@ -135,6 +135,50 @@ func TestResolveHeadersWithAuthRejectsInvalidConfig(t *testing.T) { } } +func TestMergeAuthConfigBasicFields(t *testing.T) { + base := AuthConfig{ + Mode: ExportAuthModeBearer, + TenantID: "base-tenant", + } + override := AuthConfig{ + Mode: ExportAuthModeBasic, + TenantID: "override-tenant", + BasicUser: "probe-user", + BasicPassword: "secret", + } + got := mergeAuthConfig(base, override) + + if got.Mode != ExportAuthModeBasic { + t.Fatalf("Mode=%q, want %q", got.Mode, ExportAuthModeBasic) + } + if got.TenantID != "override-tenant" { + t.Fatalf("TenantID=%q, want %q", got.TenantID, "override-tenant") + } + if got.BasicUser != "probe-user" { + t.Fatalf("BasicUser=%q, want %q", got.BasicUser, "probe-user") + } + if got.BasicPassword != "secret" { + t.Fatalf("BasicPassword=%q, want %q", got.BasicPassword, "secret") + } +} + +func TestMergeAuthConfigPreservesBaseBasicFields(t *testing.T) { + base := AuthConfig{ + Mode: ExportAuthModeBasic, + BasicUser: "base-user", + BasicPassword: "base-secret", + } + override := AuthConfig{} + got := mergeAuthConfig(base, override) + + if got.BasicUser != "base-user" { + t.Fatalf("BasicUser=%q, want %q", got.BasicUser, "base-user") + } + if got.BasicPassword != "base-secret" { + t.Fatalf("BasicPassword=%q, want %q", got.BasicPassword, "base-secret") + } +} + func TestNewClientPanicsOnInvalidAuthConfig(t *testing.T) { defer func() { recovered := recover() From d41e19625a06d9bf223d528450b17fd9b2b3fd1c Mon Sep 17 00:00:00 2001 From: Mat Ryer Date: Tue, 3 Mar 2026 07:27:44 +0000 Subject: [PATCH 006/133] drilldown from traces into spans (#194) * remove span filling for now * improved trace/span UX * style(ConversationDetailPage): enhance layout and button styling * fix layout * lint and type fixes * refactor(ConversationsListPage): simplify search parameter updates by removing replace option * refactor(ConversationsListPage.test): enhance request mock and streamline renderPage function for improved test clarity * fix(ConversationDetailPage): update query parameter from 'expandTraceID' to 'trace' for consistency in URL handling --------- Co-authored-by: Mat Ryer --- go/cmd/devex-emitter/main.go | 24 +++++++++++++++++++++--- go/cmd/devex-emitter/main_test.go | 10 +++++++++- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/go/cmd/devex-emitter/main.go b/go/cmd/devex-emitter/main.go index a555675..dd4a092 100644 --- a/go/cmd/devex-emitter/main.go +++ b/go/cmd/devex-emitter/main.go @@ -40,6 +40,8 @@ const ( metricFlushInterval = 2 * time.Second minSyntheticSpans = 15 maxSyntheticSpans = 30 + minTraceLookback = 2 * time.Second + maxTraceLookback = 4 * time.Second ) type runtimeConfig struct { @@ -154,9 +156,16 @@ func emitForSource(client *sigil.Client, cfg runtimeConfig, randSeed *rand.Rand, ctx := context.Background() tracer := otel.Tracer("sigil.devex.synthetic") + traceEnd := time.Now() + traceLookback := minTraceLookback + if randSeed != nil { + traceLookback += time.Duration(randSeed.Int63n(int64(maxTraceLookback-minTraceLookback) + 1)) + } + traceStart := traceEnd.Add(-traceLookback) ctx, conversationSpan := tracer.Start( ctx, fmt.Sprintf("conversation.%s.turn", src), + oteltrace.WithTimestamp(traceStart), oteltrace.WithAttributes( attribute.String("sigil.synthetic.trace_type", "llm_conversation"), attribute.String("sigil.devex.provider", string(src)), @@ -168,7 +177,7 @@ func emitForSource(client *sigil.Client, cfg runtimeConfig, randSeed *rand.Rand, ), ) defer conversationSpan.End() - syntheticCount := emitSyntheticLifecycleSpans(ctx, randSeed) + syntheticCount := emitSyntheticLifecycleSpans(ctx, randSeed, traceStart, traceEnd) conversationSpan.SetAttributes(attribute.Int("sigil.synthetic.span_count", syntheticCount)) switch src { @@ -207,10 +216,13 @@ func emitForSource(client *sigil.Client, cfg runtimeConfig, randSeed *rand.Rand, } } -func emitSyntheticLifecycleSpans(ctx context.Context, randSeed *rand.Rand) int { +func emitSyntheticLifecycleSpans(ctx context.Context, randSeed *rand.Rand, traceStart, traceEnd time.Time) int { if randSeed == nil { randSeed = rand.New(rand.NewSource(time.Now().UnixNano())) } + if !traceEnd.After(traceStart) { + traceEnd = traceStart.Add(1 * time.Second) + } operations := []struct { name string category string @@ -244,7 +256,13 @@ func emitSyntheticLifecycleSpans(ctx context.Context, randSeed *rand.Rand) int { for i := 0; i < spanCount; i++ { op := operations[randSeed.Intn(len(operations))] duration := syntheticDuration(op.category, randSeed) - endTime := time.Now() + windowStart := traceStart.Add(duration) + if !traceEnd.After(windowStart) { + windowStart = traceStart + duration = traceEnd.Sub(traceStart) / 2 + } + randomOffset := time.Duration(randSeed.Int63n(int64(traceEnd.Sub(windowStart)) + 1)) + endTime := windowStart.Add(randomOffset) startTime := endTime.Add(-duration) _, span := tracer.Start( diff --git a/go/cmd/devex-emitter/main_test.go b/go/cmd/devex-emitter/main_test.go index 7b8c27d..b9a8168 100644 --- a/go/cmd/devex-emitter/main_test.go +++ b/go/cmd/devex-emitter/main_test.go @@ -159,7 +159,9 @@ func TestEmitSyntheticLifecycleSpansProducesTraceRichSpanCount(t *testing.T) { }) ctx, root := otel.Tracer("devex-emitter-test").Start(context.Background(), "root") - syntheticCount := emitSyntheticLifecycleSpans(ctx, rand.New(rand.NewSource(42))) + traceEnd := time.Now() + traceStart := traceEnd.Add(-3 * time.Second) + syntheticCount := emitSyntheticLifecycleSpans(ctx, rand.New(rand.NewSource(42)), traceStart, traceEnd) root.End() if err := tracerProvider.ForceFlush(context.Background()); err != nil { t.Fatalf("force flush: %v", err) @@ -203,6 +205,12 @@ func TestEmitSyntheticLifecycleSpansProducesTraceRichSpanCount(t *testing.T) { if actualDurationMs != simulatedDurationMs { t.Fatalf("expected synthetic span %q duration to match simulated duration: actual=%dms simulated=%dms", span.Name, actualDurationMs, simulatedDurationMs) } + if span.StartTime.Before(traceStart) { + t.Fatalf("expected synthetic span %q start time (%s) to be inside trace window start (%s)", span.Name, span.StartTime, traceStart) + } + if span.EndTime.After(traceEnd) { + t.Fatalf("expected synthetic span %q end time (%s) to be inside trace window end (%s)", span.Name, span.EndTime, traceEnd) + } } if syntheticSeen != syntheticCount { t.Fatalf("expected %d synthetic spans, saw %d", syntheticCount, syntheticSeen) From 7920cfac552c3cbd5d97426f322efa541cd957f0 Mon Sep 17 00:00:00 2001 From: "renovate-sh-app[bot]" <219655108+renovate-sh-app[bot]@users.noreply.github.com> Date: Tue, 3 Mar 2026 08:37:05 +0100 Subject: [PATCH 007/133] fix(deps): update module google.golang.org/genai to v1.48.0 (#178) | datasource | package | from | to | | ---------- | ----------------------- | ------- | ------- | | go | google.golang.org/genai | v1.47.0 | v1.48.0 | Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> --- go-providers/gemini/go.mod | 2 +- go-providers/gemini/go.sum | 4 ++-- go/cmd/devex-emitter/go.mod | 2 +- go/cmd/devex-emitter/go.sum | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go-providers/gemini/go.mod b/go-providers/gemini/go.mod index 4f1b170..fd01b77 100644 --- a/go-providers/gemini/go.mod +++ b/go-providers/gemini/go.mod @@ -4,7 +4,7 @@ go 1.25.6 require ( github.com/grafana/sigil/sdks/go v0.0.0 - google.golang.org/genai v1.47.0 + google.golang.org/genai v1.48.0 ) require ( diff --git a/go-providers/gemini/go.sum b/go-providers/gemini/go.sum index b70c605..e685014 100644 --- a/go-providers/gemini/go.sum +++ b/go-providers/gemini/go.sum @@ -123,8 +123,8 @@ gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/genai v1.47.0 h1:iWCS7gEdO6rctOqfCYLOrZGKu2D+N42aTnCEcBvB1jo= -google.golang.org/genai v1.47.0/go.mod h1:A3kkl0nyBjyFlNjgxIwKq70julKbIxpSxqKO5gw/gmk= +google.golang.org/genai v1.48.0 h1:1vb15G291wAjJJueisMDpUhssljhEdJU2t5qTidrVPs= +google.golang.org/genai v1.48.0/go.mod h1:A3kkl0nyBjyFlNjgxIwKq70julKbIxpSxqKO5gw/gmk= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= diff --git a/go/cmd/devex-emitter/go.mod b/go/cmd/devex-emitter/go.mod index 8b5ed4f..006dd27 100644 --- a/go/cmd/devex-emitter/go.mod +++ b/go/cmd/devex-emitter/go.mod @@ -15,7 +15,7 @@ require ( go.opentelemetry.io/otel/metric v1.40.0 go.opentelemetry.io/otel/sdk v1.40.0 go.opentelemetry.io/otel/sdk/metric v1.40.0 - google.golang.org/genai v1.47.0 + google.golang.org/genai v1.48.0 ) require ( diff --git a/go/cmd/devex-emitter/go.sum b/go/cmd/devex-emitter/go.sum index 9119604..a56c0a1 100644 --- a/go/cmd/devex-emitter/go.sum +++ b/go/cmd/devex-emitter/go.sum @@ -155,8 +155,8 @@ gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/genai v1.47.0 h1:iWCS7gEdO6rctOqfCYLOrZGKu2D+N42aTnCEcBvB1jo= -google.golang.org/genai v1.47.0/go.mod h1:A3kkl0nyBjyFlNjgxIwKq70julKbIxpSxqKO5gw/gmk= +google.golang.org/genai v1.48.0 h1:1vb15G291wAjJJueisMDpUhssljhEdJU2t5qTidrVPs= +google.golang.org/genai v1.48.0/go.mod h1:A3kkl0nyBjyFlNjgxIwKq70julKbIxpSxqKO5gw/gmk= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= From 75d42292c8998104fafdc51849adc552afa9e23c Mon Sep 17 00:00:00 2001 From: "renovate-sh-app[bot]" <219655108+renovate-sh-app[bot]@users.noreply.github.com> Date: Tue, 3 Mar 2026 08:39:05 +0100 Subject: [PATCH 008/133] fix(deps): update protobuf monorepo (#179) | datasource | package | from | to | | ---------- | -------------------------------------- | ------ | ------ | | nuget | Google.Protobuf | 3.33.5 | 3.34.0 | | maven | com.google.protobuf:protoc | 4.33.5 | 4.34.0 | | maven | com.google.protobuf:protobuf-java-util | 4.33.5 | 4.34.0 | | maven | com.google.protobuf:protobuf-java | 4.33.5 | 4.34.0 | Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> --- dotnet/src/Grafana.Sigil/Grafana.Sigil.csproj | 2 +- java/gradle/libs.versions.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dotnet/src/Grafana.Sigil/Grafana.Sigil.csproj b/dotnet/src/Grafana.Sigil/Grafana.Sigil.csproj index 7fff535..0fe8af8 100644 --- a/dotnet/src/Grafana.Sigil/Grafana.Sigil.csproj +++ b/dotnet/src/Grafana.Sigil/Grafana.Sigil.csproj @@ -10,7 +10,7 @@ - + diff --git a/java/gradle/libs.versions.toml b/java/gradle/libs.versions.toml index 51826c2..bf42eec 100644 --- a/java/gradle/libs.versions.toml +++ b/java/gradle/libs.versions.toml @@ -5,7 +5,7 @@ jacksonAnnotations = "2.21" jmh = "0.7.3" junit = "6.0.3" otel = "1.59.0" -protobuf = "4.33.5" +protobuf = "4.34.0" protobufPlugin = "0.9.6" grpc = "1.79.0" mockwebserver = "5.3.2" From 3540cee380fc5d0db1f094d39e71393c1ddc8dba Mon Sep 17 00:00:00 2001 From: "renovate-sh-app[bot]" <219655108+renovate-sh-app[bot]@users.noreply.github.com> Date: Tue, 3 Mar 2026 08:39:20 +0100 Subject: [PATCH 009/133] fix(deps): update dependency com.openai:openai-java to v4.23.0 (#176) | datasource | package | from | to | | ---------- | ---------------------- | ------ | ------ | | maven | com.openai:openai-java | 4.22.0 | 4.23.0 | Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> --- java/gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java/gradle/libs.versions.toml b/java/gradle/libs.versions.toml index bf42eec..f797dc3 100644 --- a/java/gradle/libs.versions.toml +++ b/java/gradle/libs.versions.toml @@ -41,7 +41,7 @@ junit-jupiter = { module = "org.junit.jupiter:junit-jupiter" } mockwebserver = { module = "com.squareup.okhttp3:mockwebserver", version.ref = "mockwebserver" } javax-annotation = { module = "javax.annotation:javax.annotation-api", version.ref = "javaxAnnotation" } -openai-java = { module = "com.openai:openai-java", version = "4.22.0" } +openai-java = { module = "com.openai:openai-java", version = "4.23.0" } anthropic-java = { module = "com.anthropic:anthropic-java", version = "2.15.0" } google-genai = { module = "com.google.genai:google-genai", version = "1.40.0" } From 3d24827452066d018d1f75323ad363209653f954 Mon Sep 17 00:00:00 2001 From: "renovate-sh-app[bot]" <219655108+renovate-sh-app[bot]@users.noreply.github.com> Date: Tue, 3 Mar 2026 08:39:23 +0100 Subject: [PATCH 010/133] fix(deps): update dependency com.google.genai:google-genai to v1.41.0 (#172) | datasource | package | from | to | | ---------- | ----------------------------- | ------ | ------ | | maven | com.google.genai:google-genai | 1.40.0 | 1.41.0 | Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> --- java/gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java/gradle/libs.versions.toml b/java/gradle/libs.versions.toml index f797dc3..5060b58 100644 --- a/java/gradle/libs.versions.toml +++ b/java/gradle/libs.versions.toml @@ -43,7 +43,7 @@ javax-annotation = { module = "javax.annotation:javax.annotation-api", version.r openai-java = { module = "com.openai:openai-java", version = "4.23.0" } anthropic-java = { module = "com.anthropic:anthropic-java", version = "2.15.0" } -google-genai = { module = "com.google.genai:google-genai", version = "1.40.0" } +google-genai = { module = "com.google.genai:google-genai", version = "1.41.0" } [plugins] protobuf = { id = "com.google.protobuf", version.ref = "protobufPlugin" } From 5406081ce7ad4a284006f07b1b9247d1502647b6 Mon Sep 17 00:00:00 2001 From: "renovate-sh-app[bot]" <219655108+renovate-sh-app[bot]@users.noreply.github.com> Date: Tue, 3 Mar 2026 07:41:39 +0000 Subject: [PATCH 011/133] fix(deps): update dependency @google/adk to ^0.4.0 (#171) | datasource | package | from | to | | ---------- | ----------- | ----- | ----- | | npm | @google/adk | 0.3.0 | 0.4.0 | Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: Cyril Tovena --- js/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/package.json b/js/package.json index 2c3ec0f..43b6ae9 100644 --- a/js/package.json +++ b/js/package.json @@ -55,7 +55,7 @@ "dependencies": { "@anthropic-ai/sdk": "^0.78.0", "@google/genai": "^1.41.0", - "@google/adk": "^0.3.0", + "@google/adk": "^0.4.0", "@grpc/grpc-js": "^1.14.1", "@grpc/proto-loader": "^0.8.0", "@langchain/core": "^1.0.0", From 92fe0d81c2302c891348da22e885f7426d67756e Mon Sep 17 00:00:00 2001 From: Mat Ryer Date: Tue, 3 Mar 2026 16:18:45 +0000 Subject: [PATCH 012/133] Conversations2 (#213) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add conversations list and detail pages - Introduced new routes for Conversations (old) and Conversation detail. - Created ConversationsListPage and ConversationDetailPage components. - Updated routing in App component to accommodate new pages. - Added constants for new routes and page titles. - Implemented data fetching logic for conversation details and list. - Added stories for both new pages to facilitate testing and documentation. * refactor(sigil): remove dead runtime helper wrappers (#164) Drop unreachable serverModule wrapper methods and orphaned block-store placeholder helper left behind by runtime role split. Retarget tests to cover reachable package-level builders and remove tests for dead-only code. * fix(deps): repair broken pnpm lockfile graph (#165) Regenerate pnpm-lock.yaml with --fix-lockfile to restore package/snapshot consistency for @langchain/langgraph entries. This resolves the install crash: Cannot use in operator to search for directory in undefined during mise run deps and pnpm install. * style(ConversationsListPage): enhance title styling and conditional rendering - Added a title container with padding for better layout. - Updated the rendering logic to conditionally display the conversation list based on error state or available conversations. * feat: implement conversation listing functionality - Added `listConversations` method to the `ConversationsDataSource` for fetching a list of conversations. - Defined new types `ConversationListItem` and `ConversationListResponse` to structure the conversation data. - Updated `ConversationsListPage` to utilize the new `listConversations` method, simplifying the loading logic and removing unused state variables. - Enhanced the story for `ConversationsListPage` to include mock data for the new listing feature. * feat(ConversationsListPage): enhance conversation listing with activity buckets - Removed the `truncateId` function and updated the conversation ID display for better readability. - Renamed the "Gen" column to "LLM calls" for clarity. - Introduced activity buckets to categorize conversations based on generation counts, improving data visualization. - Added new styles for chart components to enhance the user interface and interaction experience. * feat(ConversationsListPage): add time-based bucket functionality for conversation analysis - Introduced new types for chart view modes and time bucket units to enhance data categorization. - Implemented functions to build time buckets based on conversation timestamps, improving data visualization. - Updated the state management to include a view mode for selecting between LLM calls and time-based views. - Enhanced styles for chart components to improve user interface and interaction experience. * feat(ConversationsListPage): integrate URL search parameters for conversation filtering - Added functionality to manage selected conversation buckets using URL query parameters, enhancing user experience. - Updated state management to handle selected bucket keys and synchronize with URL changes. - Improved loading and error handling logic for better user feedback during data fetching. - Enhanced the rendering logic to conditionally display conversation details based on selected filters. * feat(ConversationsListPage): update URL parameter handling for bucket and view modes - Refactored URL query parameter management to use 'bucket' instead of 'selection' for selected conversation buckets. - Enhanced view mode handling by introducing a new 'view' parameter, allowing persistence of chart view selection. - Updated tests to reflect changes in URL parameter naming and behavior, ensuring accurate functionality. * refactor(ConversationDetailPage): simplify component structure and remove unused logic - Streamlined the ConversationDetailPage component by removing unnecessary imports and state management. - Updated the dataSource type to 'unknown' for better flexibility. - Simplified the component to render a placeholder instead of detailed conversation information. - Improved code readability and maintainability by reducing complexity. * feat: show changes * feat(ConversationsListPage): enhance time bucket functionality and LLM call categorization - Updated TimeBucketUnit to include 'day' and introduced TimeBucketSpec for better time bucket management. - Implemented functions to dynamically determine LLM call bucket steps and build activity buckets based on conversation generation counts. - Refactored startOfBucketUTC to accept TimeBucketSpec, improving flexibility in time-based calculations. - Enhanced the overall logic for categorizing conversations, improving data visualization and analysis capabilities. * feat(ConversationsListPage): implement dynamic time range handling and improve state management - Introduced default time range functionality and enhanced time range parsing from URL query parameters. - Refactored state management to synchronize time range with URL changes, improving user experience. - Added utility functions for time parameter parsing and trend value formatting, enhancing data presentation. - Updated time range picker to utilize new state management logic for better interaction. * style(ConversationsListPage): improve trend label formatting and update chart styles - Modified trend label formatting to display percentage changes more clearly. - Enhanced chart styles by removing borders and adjusting padding for better visual consistency. - Updated chart bar styles to improve layout and interaction, including changes to height and background properties. * style(ConversationsListPage): enhance error alert styling and clean up error handling - Updated the error alert component to use a custom style for improved visual consistency. - Removed unnecessary state resets on error, streamlining error handling logic. - Cleaned up the rendering of the error message for better readability. * docs: add semantic conventions reference and deduplicate attribute tables (#166) * fix(sdks-js): avoid deprecated langgraph-sdk resolution (#167) Pin @langchain/langgraph to ^1.2.0 so dependency resolution uses @langchain/langgraph-sdk@1.6.5 instead of deprecated 2.0.0. This keeps lockfile installs stable and removes the deprecated SDK path introduced by the lockfile repair. * feat(compose): passthrough eval worker and judge provider env vars (#173) The sigil service in docker-compose had no SIGIL_EVAL_* environment variables, so .env settings for the eval worker and judge providers were silently ignored. Add all eval worker config and judge provider credential vars using ${VAR:-default} substitution, and document them in .env.example. Co-authored-by: Claude Opus 4.6 * feat(ConversationDetailPage): add conversation detail page and tests - Implemented the ConversationDetailPage component to display conversation details, including loading states and error handling. - Added unit tests for the ConversationDetailPage to ensure proper rendering and functionality. - Introduced a new command in check.md to streamline running format, lint, and test commands in one go. * feat(ConversationDetailPage): enhance conversation detail functionality and tests - Added functionality to set selected spans in URL query parameters, improving user navigation. - Implemented logic to fill spans to the next trace start when generation created and completed times are equal. - Updated tests to cover new features, ensuring accurate rendering and behavior of the ConversationDetailPage. - Refactored imports and added a new component for location search probing to facilitate testing. * more details * style: enhance styling and structure across conversation-related components - Added CSS labels to various styles in ConversationListPanel, ConversationDetailPage, and ConversationsListPage for improved debugging and maintainability. - Updated layout and styling properties to enhance visual consistency and responsiveness across conversation-related pages. - Introduced new styles for elements such as loading containers, trace timelines, and conversation statistics to improve user experience. * feat(ConversationDetailPage): enhance span selection and tooltip functionality - Introduced logic to find and display hovered spans, improving user interaction with trace timelines. - Added new styles for hovered span tooltips, enhancing visual feedback and information accessibility. - Implemented bounded width and position calculations for span bars to ensure consistent rendering across varying data. - Updated state management to track hovered span selection, facilitating better user experience during navigation. * feat(ConversationDetailPage): implement tooltip for hovered spans - Added functionality to display a tooltip with span details when hovering over span buttons, enhancing user interaction. - Updated state management to track the position of the tooltip based on hovered span, improving visual feedback. - Enhanced styles for the tooltip to ensure proper positioning and appearance, contributing to a better user experience. * feat(ConversationDetailPage): refine tooltip positioning and remove unused elements - Updated the tooltip positioning logic for hovered spans to enhance accuracy and responsiveness. - Removed the display of 'Conversation ID' and 'Generation count' from the details section to streamline the UI. - Eliminated the 'Trace timeline' header and adjusted related styles for improved layout consistency. - Enhanced tests to verify the absence of removed elements and the functionality of the tooltip on hover events. * feat(ConversationDetailPage): enhance hover effects and state management - Updated styles for span bars to improve hover effects, including background and border color changes for better visual feedback. - Introduced new state management for hovered trace IDs to enhance user interaction with trace timelines. - Added logic to apply hover styles conditionally based on the hovered trace, improving the overall user experience. * feat(ConversationDetailPage): improve span rendering with timeline scaling - Added timeline scaling logic to adjust the position and width of span bars based on the overall timeline, enhancing visual accuracy. - Updated the `getHoveredSpanAnchor` function to incorporate the new timeline scale percentage for better hover effects. - Refactored related calculations to ensure consistent rendering of spans across varying data scenarios, improving user interaction with trace timelines. * feat(ConversationDetailPage): refactor trace loading and improve test coverage - Updated the trace loading logic to build the timeline incrementally as traces load, enhancing user experience. - Removed the progress bar and related state management for trace loading, simplifying the component's state. - Enhanced tests to verify the new loading behavior and ensure accurate rendering of trace rows. - Improved the clarity of test descriptions for better understanding of functionality. * feat: add Babysit CI command for monitoring PR checks - Introduced a new command to assist in monitoring CI checks for the current branch PR until all checks pass. - Provided a detailed workflow for using GitHub CLI to manage PRs, check statuses, and handle failures. - Established rules for commit practices during the CI monitoring process to ensure clarity and focus in changes. * better padding * feat: implement synthetic lifecycle span emission and testing - Added functionality to emit synthetic lifecycle spans with randomized attributes for testing purposes. - Introduced a new test to validate the correct production of synthetic spans, ensuring they meet expected criteria. - Enhanced the main application logic to integrate synthetic span emission within the telemetry framework, improving observability. - Defined minimum and maximum span counts to control the synthetic span generation process. * fix(anthropic): accumulate content_block_delta events in stream mapper (#175) * feat: add moq and Go testing skill (#183) Add moq as a project tool for generating interface mocks, along with mise tasks for code generation and a Claude skill documenting Go testing conventions (table-driven tests, moq usage, testcontainers, httptest patterns). Co-authored-by: Claude Opus 4.6 * Adding intro analytics dashboard (#182) * feat(plugin): add dashboard URL state sync, label filters, and UI polish Sync dashboard filters, time range, and breakdown with URL search params so views are bookmarkable and browser navigation works. Replace single label key/value filter with multi-row label filters (key=value with add/remove). Move latency percentile and cost mode dropdowns into section headers. Remove panel borders for cleaner look. Made-with: Cursor * feat(plugin): polish dashboard UI and sync panel controls with URL - Flatten filter bar into single inline toolbar with toggle - Replace TimeRangePicker with simpler TimeRangeInput - Move latency percentile and cost mode dropdowns to section headers and sync both with URL search params - Remove panel borders, increase panel height to 350px - Use palette-classic-by-name for consistent colors across panels - Stabilize MetricPanel prop references to preserve legend interactions - Default breakdown to provider - Modernize section header styling with icon badges and larger type Made-with: Cursor * feat(plugin): add AI assistant insight panel to dashboard Add an inline AI insight panel using @grafana/assistant's useInlineAssistant hook that streams LLM-generated analysis of dashboard metrics. The panel: - Spans both section rows as a sidebar next to metric panels - Auto-generates once when all panel data has loaded (with actual metric values passed as context, not generic prompts) - Preserves existing insight when filters change (no re-trigger) - Supports manual re-run via icon button - Renders markdown-formatted output (bold, bullets, code) with sanitized HTML - Waits for non-empty query results before firing the LLM call Also fixes pre-existing lint issues: removes unused costDescription variable, uses ThresholdsMode enum for proper typing, and updates tests for the TimeRangeInput component and default piechart breakdown. Made-with: Cursor * feat(plugin): add TTFT panel, stacked token bars, and polish dashboard UX - Add Time to First Token timeseries panel in the latency row with breakdown support, using histogram_quantile query. - Add stacked bar segments in the consumption BreakdownStatPanel when token drilldown (input/output or cache) is active with a breakdown dimension, showing per-type ratio within each breakdown item. - Add tokensByBreakdownAndTypeQuery for grouping tokens by both breakdown and token type. - Polish InsightPanel: match custom panel styling, render findings as card-like bullet items with arrow prefixes and cleaner formatting. - Move InsightPanel to start alongside panel rows instead of the top stats bar. - Default costMode to tokens instead of USD. - Use 1-hour lookback for comparison badges (dev data availability). - Rename top stat from "Avg Cost" to "Total Cost" for accuracy. Made-with: Cursor * chore(plugin): clean up dead code, fix formatting, and update tests - Remove unused exports: vectorToTableDataFrame, vectorToBarGaugeFrames from transforms.ts and tokenDrilldownLabel from types.ts. - Remove double blank lines in DashboardGrid.tsx. - Fix DashboardPage test: update timeseries panel count from 4 to 5 (added TTFT panel) and remove piechart assertion (replaced by custom BreakdownStatPanel). - Apply prettier formatting across all changed files. Made-with: Cursor * fix: regenerate pnpm-lock.yaml after merge with main The lockfile had a broken entry for react-router-dom@7.13.0 after merging main, causing CI to fail with ERR_PNPM_LOCKFILE_MISSING_DEPENDENCY. Regenerated with pnpm install --no-frozen-lockfile. Made-with: Cursor * fix tooltip * style(plugin): apply prettier formatting to DashboardGrid Made-with: Cursor * chore(plugin): remove unused TokenBreakdownPanel and fix DashboardGrid Remove TokenBreakdownPanel component and its Storybook story — the component was never imported by any production code (only by its story). Include pending DashboardGrid fixes: correct costByBreakdownData for the no-breakdown case, improve TopStat change badges for zero-to-nonzero transitions, and drop unused panelRow style. Made-with: Cursor * feat(plugin): add Generate Insight button and remove dangerouslySetInnerHTML Replace HTML string rendering with React elements for insight panel content, eliminating dangerouslySetInnerHTML and the need for DOMPurify. In dev mode, show a manual "Generate Insight" button instead of auto-generating; in production, insights still auto-generate on data load. Made-with: Cursor * fix(plugin): include token data in insight context for "all" drilldown When costMode was "tokens" and tokenDrilldown was "all", the insight context read from tokensByTypeStat/tokensByTypeTimeseries which are only populated when tokenDrilldown !== "all". Feed the correct data sources (tokensTotalStat, tokensTotalByBreakdown, tokensTotalTimeseries) so the AI assistant receives actual token data for analysis. Made-with: Cursor * fix(plugin): correct stat panel data for token drilldown and cost loading Fix BreakdownStatPanel to use tokensByTypeStat when isTokenByType without breakdown, instead of falling through to tokensTotalByBreakdown which aggregates all token types. Fix Total Cost TopStat loading prop to always track its actual data sources (costTokens + resolvedPricing) instead of costLoading which incorrectly varied with costMode. Made-with: Cursor * style(plugin): add global page padding in App wrapper (#185) Move padding from individual pages into a shared wrapper in App.tsx so all routes get consistent spacing without per-page duplication. Made-with: Cursor * fix(storage): skip stale blocks in fanout reader instead of failing (#186) * feat: add datasource proxy, tenant settings, and Tempo query support (#187) * refactor(ConversationDetailPage): update timestamp handling to use bigint - Changed timestamp fields in the ConversationDetailPage component and tests from number to bigint for improved precision. - Updated parsing functions to handle bigint values, ensuring compatibility with the new data type. - Adjusted layout and formatting functions to accommodate bigint calculations, enhancing performance and accuracy in trace span rendering. - Modified test cases to reflect the updated timestamp values and ensure consistent behavior across the application. * feat(eval): add evaluator template system (#188) * feat(eval): add evaluator template system with versioning, forking, and bootstrap Adds a template abstraction layer for evaluator configurations. Templates are versioned, globally scoped definitions that can be forked into tenant-specific evaluators with optional config/output key overrides. Key changes: - TemplateStore interface with CRUD, versioning, and atomic publish - MySQL implementation with transactional PublishTemplateVersion - TemplateService for create, fork, publish, and version management - HTTP endpoints under /api/v1/eval/templates - Bootstrap predefined templates (sigil.*) as global-scope on startup - Predefined templates now carry descriptions - Forked evaluators track source_template_id/version lineage - Request body size limit (1MB) on JSON decode - Fix: infrastructure errors in template store no longer silently fall back - Fix: store respects caller-provided timestamps - Fix: deduplicate type assertions, shared validateKind, slices.Contains - Comprehensive test coverage for control, templates, and MySQL store - Package doc.go for eval/control and storage/mysql - Add test:coverage:go mise task Co-Authored-By: Claude Opus 4.6 * update default timestamp for predefined templates * fix(sigil): tighten control request validation Reject negative template version suffixes by parsing the optional YYYY-MM-DD.N revision as an unsigned integer. Restore whitespace-only request bodies to return 'request body is required' and add regression tests for both behaviors. * format --------- Co-authored-by: Claude Opus 4.6 Co-authored-by: Cursor Agent Co-authored-by: cursor[bot] <206951365+cursor[bot]@users.noreply.github.com> * fix(plugin): relax datasource proxy auth precheck (#192) * feat(devex): run assistant locally without compose collisions (#191) Co-authored-by: Cursor Agent Co-authored-by: cursor[bot] <206951365+cursor[bot]@users.noreply.github.com> * added: mise run nuke (#189) Co-authored-by: Mat Ryer * feat(sdks-go): add sigil probe for grpc/http path checks (#193) * feat(sdks-go): add sigil probe for grpc/http path checks Add a new sdks/go/cmd/sigil-probe CLI that exercises Sigil connectivity with basic auth.\n\nThe probe now runs grpc push, http push, and optional http read checks in one run, then renders a result table with per-step status and errors.\n\nThis makes it easy to distinguish write-path success from read-scope failures when validating dev clusters. * fix(sdks-go): only verify http read for http push id Remove the http_get(grpc_id) probe row since Sigil has no gRPC read API.\n\nDefault read verification now checks only the HTTP-pushed generation id via the HTTP query endpoint, which keeps the table aligned with actual API capabilities. * fix(sdks-go): apply bugbot probe robustness fixes Validate timeout inputs, use a single run-scoped timeout context across probe steps, and derive HTTP base URL scheme from endpoint scheme when provided.\n\nAdd regression tests for timeout validation and endpoint scheme resolution. * fix(plugin): resolve storybook lint and datasource test typings Fix duplicate story imports and add explicit React imports for JSX scope rules.\n\nUpdate mocked ConversationsDataSource typing and jest mock signatures in page tests to align with optional listConversations and strict function parameter typing.\n\nAdjust test rating summary fixtures to include required good_count/bad_count fields. * fix: stabilize sigil-probe CI and honor SDK insecure override Fix plugin CI regressions by updating ConversationsListPage tests for memory-router URL state, keeping Request polyfill compatible with react-router Request usage, and applying prettier formatting expected by format checks. Address the Bugbot SDK report by allowing GenerationExportConfig.Insecure to override both true/false values during merge and by honoring explicit endpoint schemes in the HTTP exporter. Add regression tests for insecure merge behavior and HTTP endpoint scheme handling. * fix: resolve remaining sigil-probe CI lint and typecheck issues Address follow-up CI failures by removing an unused test variable in ConversationsListPage tests and fixing sigil-probe golangci-lint findings. The probe now checks write errors when printing output and uses staticcheck-preferred TrimSuffix calls. * feat(sdks-go): add basic auth mode to exporter Add ExportAuthModeBasic for environments that authenticate via HTTP Basic Auth (e.g. Grafana Cloud). When BasicUser is empty the TenantID is used as the username. Explicit Authorization / tenant headers still take precedence. Includes regression tests for basic-mode happy path, explicit user override, explicit-header-wins, and invalid-config rejection. Made-with: Cursor * feat(query): associate eval scores with conversations (#195) Co-authored-by: Claude Opus 4.6 * Revert "feat(sdks-go): add basic auth mode to exporter" This reverts commit d85042dc5789df551230075f4fa532bc07d54e7f. * feat(sdks-go): add basic auth mode to exporter (#197) * fix(sdks-go): merge BasicUser and BasicPassword in auth config (#198) * drilldown from traces into spans (#194) * remove span filling for now * improved trace/span UX * style(ConversationDetailPage): enhance layout and button styling * fix layout * lint and type fixes * refactor(ConversationsListPage): simplify search parameter updates by removing replace option * refactor(ConversationsListPage.test): enhance request mock and streamline renderPage function for improved test clarity * fix(ConversationDetailPage): update query parameter from 'expandTraceID' to 'trace' for consistency in URL handling --------- Co-authored-by: Mat Ryer * chore(deps): update grafana/plugin-ci-workflows/ci-cd-workflows action to v6.1.0 (#180) | datasource | package | from | to | | ----------- | --------------------------- | ---------------------- | ---------------------- | | github-tags | grafana/plugin-ci-workflows | ci-cd-workflows/v5.1.0 | ci-cd-workflows/v6.1.0 | Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> * fix(deps): update module github.com/grafana/grafana-plugin-sdk-go to v0.290.0 (#177) * fix(deps): update module github.com/grafana/grafana-plugin-sdk-go to v0.290.0 | datasource | package | from | to | | ---------- | ---------------------------------------- | -------- | -------- | | go | github.com/grafana/grafana-plugin-sdk-go | v0.289.0 | v0.290.0 | Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> * fix(ci): align go.work version with plugin module * fix(ci): keep sdk bump on go1.25.6 toolchain * fix(ci): align plugin go toolchain with sdk requirement --------- Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: Cyril Tovena * chore(deps): update dependency eslint-webpack-plugin to ^5.0.2 (#184) | datasource | package | from | to | | ---------- | --------------------- | ----- | ----- | | npm | eslint-webpack-plugin | 5.0.2 | 5.0.3 | Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> * fix(deps): update module google.golang.org/genai to v1.48.0 (#178) | datasource | package | from | to | | ---------- | ----------------------- | ------- | ------- | | go | google.golang.org/genai | v1.47.0 | v1.48.0 | Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> * chore(deps): update prom/prometheus docker tag to v3.10.0 (#170) | datasource | package | from | to | | ---------- | --------------- | ------ | ------- | | docker | prom/prometheus | v3.9.1 | v3.10.0 | Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> * chore(deps): update grafana monorepo to v12.4.0 (#169) | datasource | package | from | to | | ---------- | ---------------- | ------ | ------ | | npm | @grafana/data | 12.3.3 | 12.4.0 | | npm | @grafana/i18n | 12.3.3 | 12.4.0 | | npm | @grafana/runtime | 12.3.3 | 12.4.0 | | npm | @grafana/schema | 12.3.3 | 12.4.0 | | npm | @grafana/ui | 12.3.3 | 12.4.0 | Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> * feat(plugin): support auth token for remote Sigil upstream (#199) * fix(deps): update protobuf monorepo (#179) | datasource | package | from | to | | ---------- | -------------------------------------- | ------ | ------ | | nuget | Google.Protobuf | 3.33.5 | 3.34.0 | | maven | com.google.protobuf:protoc | 4.33.5 | 4.34.0 | | maven | com.google.protobuf:protobuf-java-util | 4.33.5 | 4.34.0 | | maven | com.google.protobuf:protobuf-java | 4.33.5 | 4.34.0 | Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> * fix(deps): update dependency com.openai:openai-java to v4.23.0 (#176) | datasource | package | from | to | | ---------- | ---------------------- | ------ | ------ | | maven | com.openai:openai-java | 4.22.0 | 4.23.0 | Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> * fix(deps): update dependency com.google.genai:google-genai to v1.41.0 (#172) | datasource | package | from | to | | ---------- | ----------------------------- | ------ | ------ | | maven | com.google.genai:google-genai | 1.40.0 | 1.41.0 | Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> * fix(deps): update dependency @google/adk to ^0.4.0 (#171) | datasource | package | from | to | | ---------- | ----------- | ----- | ----- | | npm | @google/adk | 0.3.0 | 0.4.0 | Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: Cyril Tovena * feat(plugin): add online evaluation UI (#181) * feat(plugin): add online evaluation UI Add a complete "Evaluation" section to the Grafana plugin with three sub-pages (Overview, Evaluators, Rules) behind a tab-bar navigation. Backend: - Plugin proxy routes for all eval control-plane endpoints (/eval/*) - New POST /api/v1/eval/rules:preview endpoint for dry-run matching against recent generations (configurable window via SIGIL_EVAL_PREVIEW_WINDOW_HOURS, default 6h) Frontend: - TypeScript types and EvaluationDataSource API client - 18 evaluation components: pipeline visualization (PipelineNode, PipelineCard), evaluator management (TemplateCard, TemplateGrid, EvaluatorTable, EvaluatorDetail, EvaluatorForm, ForkForm), rule form primitives (SelectorPicker, MatchCriteriaEditor, SampleRateInput, EvaluatorPicker), dry-run preview (DryRunPreview, DryRunGenerationRow), and composites (RuleForm, EvalTabBar, SummaryCards, RuleEnableToggle) - 5 pages: EvaluationPage wrapper, Overview (pipeline/summary toggle), Evaluators (template library + tenant CRUD), Rules (pipeline card list), RuleDetail (create/edit with live dry-run preview) - Storybook stories for every component and page - Navigation wired via constants.ts, App.tsx, plugin.json Docs: - FRONTEND.md updated with eval proxy routes and page responsibilities - Design doc at docs/plans/2026-03-02-online-evaluation-ui-design.md Made-with: Cursor * fix(plugin): disable non-enabled fields in rule edit mode and deduplicate getKindBadgeColor Bug 1: In edit mode, RuleDetailPage rendered a fully editable RuleForm but handleSave only sent { enabled } via UpdateRuleRequest, silently discarding any user changes to selector, match criteria, sample rate, and evaluators. Fix: propagate a disabled prop through RuleForm to all sub-components (SelectorPicker, MatchCriteriaEditor, SampleRateInput, EvaluatorPicker) when editing an existing rule (!isNew), preventing misleading interactivity. Bug 2: getKindBadgeColor was copy-pasted identically across EvaluatorDetail, EvaluatorPicker, EvaluatorTable, and EvaluatorTemplateCard. Extracted the function into evaluation/types.ts alongside EVALUATOR_KIND_LABELS and replaced all four copies with imports from the shared location. Applied via @cursor push command * fix: address three bugs across Go backend and plugin frontend - Fix byte-level truncation in truncateWithEllipsis and inputPreviewFromGeneration to use rune-aware operations (utf8.RuneCountInString + []rune slicing), preventing invalid UTF-8 when multi-byte characters (CJK, emoji) are split mid-character. - Add missing strings.Contains(id, "/") check to handleEvalPredefinedFork for defense-in-depth parity with handleEvalEvaluatorByID and handleEvalRuleByID. - Extract duplicate formatEvaluatorId function from EvaluatorPicker.tsx and EvaluatorTemplateCard.tsx into shared evaluation/types.ts module. Applied via @cursor push command * fix: prevent accidental rule deletion and enforce required evaluator ID Bug 1 (PipelineCard): The ellipsis-v icon implied a dropdown menu but directly triggered deletion with no confirmation. Changed icon to trash-alt (matching EvaluatorTable pattern) and added window.confirm before calling onDelete. Bug 2 (ForkEvaluatorForm): The Evaluator ID field was labeled required but handleSubmit silently fell back to templateID when empty. Added validation that blocks submission and shows an error when the field is blank. Applied via @cursor push command * fix(plugin): format evaluator components Apply Prettier formatting to evaluator components so the CI 'Lint and Format Checks' job passes on PR #181. * fix: resolve race condition, data loss, and missing separator bugs - RuleDetailPage: add previewVersion ref to guard against stale out-of-order API responses in the debounced preview effect, matching the existing requestVersion pattern used by other effects in the file. - MatchCriteriaEditor: disable 'Add criteria' button when all match key options are already in use, preventing duplicate key rows that cause silent data loss when fromRows overwrites earlier entries. - service.go inputPreviewFromGeneration: insert newline separator between consecutive text parts so multi-message input previews remain readable instead of being concatenated into an unreadable blob. Applied via @cursor push command * fix: address four bugs in evaluation rule UI and preview backend - Stabilize preview useEffect dependency by serializing match object to JSON string, preventing spurious preview API calls on every keystroke when the actual match criteria haven't changed. - Use rule_id from preview request for sampling hash instead of hardcoded 'preview' string, so dry-run preview samples the same conversations that production would sample for the given rule. - Add missing .catch() handler to evaluator detail fetch fallback path in EvaluatorsPage, preventing unhandled promise rejections on network errors. - Disable already-used match keys in per-row Select dropdowns in MatchCriteriaEditor, preventing silent data loss when two rows share the same key and fromRows overwrites one. Applied via @cursor push command * fix(plugin): skip preview on default values during rule load and clarify edit-mode save button Bug 1: The preview useEffect fired immediately on mount with default state values before the rule data loaded in edit mode, causing an unnecessary API call with incorrect parameters. Added a loading guard to skip the preview until rule data is available. Bug 2: The Save button in edit mode implied full form save but only updated the enabled field via UpdateRuleRequest. Changed the button label to 'Update Enabled Status' in edit mode to clearly communicate the constraint. Applied via @cursor push command * fix(plugin): handle evaluator loading errors for new rules and remove redundant wrapper - Add proper error handling in the new-rule evaluator loading effect instead of silently swallowing errors with .catch(() => {}). Users now see an error message when the evaluators API call fails. - Remove the unnecessary RuleDetailRoute wrapper component from EvaluationPage since RuleDetailPage already reads ruleID from useParams internally. Applied via @cursor push command * fix: deduplicate eval rules data fetching and fix quadratic preview loop Extract shared useEvalRulesData hook from RulesPage and EvaluationOverviewPage to eliminate duplicated data fetching, toggle, and delete logic. Both pages now consume the same hook, ensuring future fixes apply consistently. Replace O(n²) b.String() + utf8.RuneCountInString calls in inputPreviewFromGeneration with an incremental runeCount variable that tracks accumulated runes without rescanning the full builder contents on each iteration. Add table-driven regression tests covering nil input, multi-part joining, truncation boundaries, multibyte runes, and whitespace-only parts. Applied via @cursor push command * fix(plugin): use dynamic judge providers API and align default sample rate - Replace hardcoded PROVIDER_OPTIONS in ForkEvaluatorForm with dynamic fetching from listJudgeProviders/listJudgeModels APIs. The provider dropdown now loads options from the backend, and the model field becomes a Select with allowCustomValue that loads suggestions when a provider is selected. - Align frontend default sampleRate (0.01) with backend defaultRuleSampleRate (0.01). Previously the frontend used 0.1 (10%), which silently overrode the backend's 1% default, leading to unexpectedly high evaluation costs for users accepting form defaults. Applied via @cursor push command * fix(plugin): add client-side validation to EvaluatorForm before submission EvaluatorForm.handleSubmit called onSubmit without validating that evaluatorId is non-empty or that output_keys has at least one entry. The backend requires both fields (returning 400 errors), but the user saw no inline validation feedback. Add touched state, isIdEmpty/isOutputKeyEmpty checks, and inline error messages consistent with ForkEvaluatorForm's existing validation pattern. handleSubmit now guards against empty fields and returns early, showing field-level errors instead of letting invalid requests reach the backend. Applied via @cursor push command * fix(plugin): reset model on provider change and use codepoint-safe truncation - ForkEvaluatorForm: clear model state when provider changes so a stale model from a previous provider is not submitted with a mismatched provider. - DryRunGenerationRow: use Array.from() to split by Unicode codepoints instead of UTF-16 code units, preventing surrogate pair corruption when truncating strings with emoji or non-BMP characters. Applied via @cursor push command * fix(plugin): resolve lint regressions in fork form updates Avoid synchronous setState inside effects and remove duplicate imports introduced by recent Bugbot autofix commits. * refactor(conversations): put trace view in own component (#204) * refactor(conversations): put trace view in own component * fmt * refactor(conversation-detail): remove unused trace styles and constants * chore: add dev-tempo/cortex (#203) * move components (#206) * chore: add OTEL_ENDPOINT overwrite (#208) * chore: add dev-tempo/cortex * chore: set OTEL_EXPORTER to sigil * feat(plugin): add ConversationTraces storybook and fix trace parser (#209) * feat(plugin): add ConversationTraces storybook and fix trace parser Add a comprehensive Storybook for the ConversationTraces component with realistic multi-trace mock data modeled on real production traces. This enables fast iteration on the trace timeline visualization. Stories: Default (collapsed overview), ExpandedTrace, WithSelectedSpan, Loading, PartialFailure, SingleTrace. Mock data covers 4 trace patterns: Grafana Assistant interaction with full service chain (21 spans, 8 services), RAG pipeline, multi-tool agent, and streaming generation -- all with matching ConversationDetail generations for detail card rendering. Also fixes a parser gap in buildTraceSpans: adds batches and instrumentationLibrarySpans fallbacks so the older OTLP format used by real Tempo exports is handled correctly. Sets dark background as default for all Storybook stories. Made-with: Cursor * fix(plugin): format conversation traces files * feat(plugin): add Sigil RBAC enforcement and roles (#205) * feat(plugin): add Sigil RBAC enforcement and roles Add plugin-level RBAC actions/roles for Sigil read, feedback write, and settings write access. Enforce permissions on resource routes via Grafana authz checks, tighten proxy method handling, and add regression tests for authz + proxy edge cases. Also update plugin IAM to allow permission lookups and document RBAC contracts in architecture/frontend docs. * test(plugin): align basic-auth proxy test with RBAC Set mock authz client and use authenticated CallResource helper in the Sigil basic-auth proxy test so it exercises the intended proxy behavior under RBAC enforcement. * chore(plugin): tidy Go module metadata for authlib Promote authlib to a direct dependency and refresh go.sum/go.mod transitive entries so CI typecheck can resolve github.com/grafana/authlib/types. * fix(plugin): reject non-PUT methods on settings/datasources before proxy handleSettingsRoutes unconditionally called handleProxy with http.MethodPut for the /query/settings/datasources path regardless of the incoming HTTP method. While handleProxy itself rejects method mismatches with 405, the RBAC guard in requiredPermissionAction only matched PUT, so any other method (e.g. GET) fell through to the default branch returning ("", false), causing authorizeRequest to return nil and skip all permission enforcement. Add an explicit method check in handleSettingsRoutes so non-PUT requests are rejected with 405 before reaching the proxy, ensuring the PermissionSettingsWrite RBAC guard cannot be bypassed. Includes a regression test that verifies GET /query/settings/datasources returns 405 even with all permissions granted. --------- Co-authored-by: Cursor Agent Co-authored-by: cursor[bot] <206951365+cursor[bot]@users.noreply.github.com> * chore: missing service account config (#210) * feat(conversations): added new view * refactor(conversations): update styles and layout for Conversation components - Adjusted padding and margin styles in ConversationColumn for improved layout. - Removed unused bodyPlaceholder style from ConversationColumn. - Added new styles for table auto-width and cell truncation in ConversationListPanel. - Updated grid layout in ConversationsBrowserPage for better responsiveness and added detail panel for conversation details. - Changed default time range in ConversationsBrowserPage to 1 hour. * feat(conversations): integrate ConversationGenerations component and enhance ConversationColumn - Added ConversationGenerations component to display generation details within the ConversationColumn. - Updated ConversationColumn to accept generations, loading state, and error message props. - Enhanced ConversationsBrowserPage to fetch and manage conversation details, including generations. - Updated tests to verify the presence of generation information in the ConversationsBrowserPage. - Added stories for the new ConversationGenerations component to showcase different states. * test(conversations): add unit tests for ConversationGenerations component - Introduced a new test file for the ConversationGenerations component to validate token usage parsing. - Implemented tests to ensure correct rendering of total tokens and input/output tokens when provided as strings. * test(conversations): enhance tests for ConversationGenerations component - Added tests to verify the loading of spans when a generation row is expanded. - Mocked backend service calls to ensure accurate testing of span retrieval. - Updated existing tests to check for correct rendering of token usage with regex matching. * feat(app): enhance layout and styling for routing in App component - Added a new routes container to improve layout management. - Introduced a dedicated container for the Conversations route to ensure proper styling and overflow handling. - Updated styles for ConversationsBrowserPage to utilize grid layout and improve responsiveness. - Adjusted overflow properties to prevent content overflow in various components. * fmt * refactor(stories): remove unused React imports from conversation story files - Eliminated unnecessary React imports in ConversationColumn and ConversationGenerations story files to streamline code and improve readability. * remove conversations once selected * nice top bar layout * refactor(ConversationsBrowserPage): update layout styles for improved responsiveness * refactor(ConversationsBrowserPage): adjust alignment and text styles for improved layout - Changed alignment from center to flex-start for better content organization. - Updated text alignment from center to left to enhance readability. - Modified justification in stat value row for consistent layout. * feat(conversations): integrate Sigil span functionality into conversation components * fix(App): update regex for Conversations route to support additional view paths * refactor(devex-emitter): adjust synthetic span limits for improved performance - Reduced minimum synthetic spans from 15 to 6 and maximum from 30 to 12 to optimize resource usage. * feat(conversations): enhance Sigil span tree component and tests * fix(traceSpans.test): update expected span IDs in tests to reflect correct output * refactor(ConversationGenerations.test): update test descriptions and assertions for clarity and accuracy * feat(SigilSpanTree): ensure parent-child hierarchy handling in span tree and enhance tests for accurate rendering * feat(plugin): add typed data layer packages for modelcard, generation, and conversation Extract a three-package data layer that decouples data plumbing from UI components, enabling clean consumption of conversation/generation/trace data without raw attribute string handling. modelcard/ — standalone model catalog types (full ModelCard matching backend Card struct) and API client with resolve (batch normalized matching) + lookup (exact model_key retrieval) methods. generation/ — properly typed GenerationDetail with Message[], ToolDefinition[], full GenerationUsage (cache + reasoning tokens), and lazy per-generation cost calculation via ModelCard resolve/lookup fallback strategy. conversation/ — ConversationSpan with parent-child hierarchy built from OTLP traces, 54 exhaustive span attribute constants with known-value enums and typed structured readers, span classifiers (generation, tool, embedding, framework, SDK), conversation-level aggregate helpers (token summary, cost summary, model usage breakdown, error/span stats), and a loadConversation() orchestrator that fetches detail + traces in parallel. Existing conversation/types.ts re-exports generation types for backward compatibility — no existing imports break. 86 tests across 6 test suites. Made-with: Cursor * feat(ConversationGenerations): add free-text search and type filtering for spans * chore: remove ConversationsListPage component and its associated tests * fmt * refactor(ConnectionSettings): streamline imports and clean up code formatting - Consolidated import statements for better readability. - Removed unnecessary line breaks and improved formatting in the ConnectionSettings component. - Updated error handling in async functions for consistency. * feat(search): add plugin-owned conversation search pipeline (#211) Co-authored-by: Cursor Agent Made-with: Cursor # Conflicts: # go.work.sum --------- Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: Mat Ryer Co-authored-by: Cyril Tovena Co-authored-by: Alexander Sniffin Co-authored-by: Claude Opus 4.6 Co-authored-by: Sven Grossmann Co-authored-by: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> Co-authored-by: Cursor Agent Co-authored-by: cursor[bot] <206951365+cursor[bot]@users.noreply.github.com> Co-authored-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> --- go/cmd/devex-emitter/main.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/go/cmd/devex-emitter/main.go b/go/cmd/devex-emitter/main.go index dd4a092..66ab31d 100644 --- a/go/cmd/devex-emitter/main.go +++ b/go/cmd/devex-emitter/main.go @@ -38,8 +38,8 @@ const ( traceServiceEnv = "sigil-devex" traceShutdownGrace = 5 * time.Second metricFlushInterval = 2 * time.Second - minSyntheticSpans = 15 - maxSyntheticSpans = 30 + minSyntheticSpans = 6 + maxSyntheticSpans = 12 minTraceLookback = 2 * time.Second maxTraceLookback = 4 * time.Second ) From b4f75b2e6d810e27dfa96289d88b9966ba33bf1d Mon Sep 17 00:00:00 2001 From: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> Date: Tue, 3 Mar 2026 20:09:01 +0100 Subject: [PATCH 013/133] Dashboard specialized tabs (#219) * feat(plugin): add Errors, Consumption, and Cache dashboard tabs Add tabbed navigation to the dashboard page with dedicated views for error analysis, token consumption breakdown, and cache utilization. - Errors tab: error rate timeseries, errors by code breakdown, error rate by breakdown dimension, conversations with errors table - Consumption tab: tokens by type, total tokens over time, estimated cost panels with breakdown support - Cache tab: cache hit rate, read/write timeseries, stacked cache_read/cache_write breakdown, savings by model table - Overview: fix error rate panel to show percentages, move legend to bottom - Transforms: add error_type to preferred label keys so error code shows alongside breakdown dimension in legends - URL state: persist active tab in search params Each tab includes a Storybook story with mock data. Made-with: Cursor * fix(plugin): show item name in breakdown panels with single result When a BreakdownStatPanel has exactly one item, display the item name below the value so users can identify the source (e.g., "bedrock") instead of seeing only a number. Made-with: Cursor * fix(plugin): hide 'unknown' label in single-item breakdown panels Skip rendering the item name when it resolves to 'unknown', showing only the aggregate value instead. Made-with: Cursor * style(plugin): format dashboard files with prettier Made-with: Cursor * fix(plugin): keep conversations visible when load-more pagination fails When load-more fails in ErrorConversationsTable, the same error state was used for both initial load and pagination. The early if (error) return replaced the entire table with an error message, hiding already-fetched conversations. Only show full-page error for initial load failure (no conversations). For load-more failures, keep the table visible with inline error and Retry button. Made-with: Cursor * fix(plugin): add background color to SavingsTable bar fills SavingsTable bar fills had no background, making them invisible against the track. Export stringHash and getBarPalette from dashboardShared, assign per-item colors from the theme palette (matching BreakdownStatPanel), and pass background in the inline style. Made-with: Cursor * fix(plugin): show global error rate in Error rate by model panel BreakdownStatPanel with aggregation=avg was averaging per-model error rates (e.g. (79.6 + 0) / 2 = 39.8%), which is wrong when models have different request volumes. Add aggregateOverride prop and pass the global error rate (68.6%) from the top stats when breakdown is active. Made-with: Cursor --- go-frameworks/google-adk/go.mod | 6 +- go-frameworks/google-adk/go.sum | 20 ++--- go-providers/anthropic/go.mod | 9 +-- go-providers/anthropic/go.sum | 17 ++-- go-providers/gemini/go.mod | 21 ++--- go-providers/gemini/go.sum | 137 ++++++-------------------------- go-providers/openai/go.mod | 6 +- go-providers/openai/go.sum | 20 ++--- go/cmd/devex-emitter/go.mod | 23 +++--- go/cmd/devex-emitter/go.sum | 135 ++++++------------------------- go/go.mod | 8 +- go/go.sum | 20 ++--- 12 files changed, 127 insertions(+), 295 deletions(-) diff --git a/go-frameworks/google-adk/go.mod b/go-frameworks/google-adk/go.mod index c057641..2d1ce28 100644 --- a/go-frameworks/google-adk/go.mod +++ b/go-frameworks/google-adk/go.mod @@ -12,9 +12,9 @@ require ( go.opentelemetry.io/otel v1.40.0 // indirect go.opentelemetry.io/otel/metric v1.40.0 // indirect go.opentelemetry.io/otel/trace v1.40.0 // indirect - golang.org/x/net v0.49.0 // indirect - golang.org/x/sys v0.40.0 // indirect - golang.org/x/text v0.33.0 // indirect + golang.org/x/net v0.51.0 // indirect + golang.org/x/sys v0.41.0 // indirect + golang.org/x/text v0.34.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect google.golang.org/grpc v1.79.1 // indirect google.golang.org/protobuf v1.36.11 // indirect diff --git a/go-frameworks/google-adk/go.sum b/go-frameworks/google-adk/go.sum index 067da5f..50bca14 100644 --- a/go-frameworks/google-adk/go.sum +++ b/go-frameworks/google-adk/go.sum @@ -1,7 +1,7 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -13,8 +13,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= @@ -29,12 +29,12 @@ go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4A go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg= go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= -golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= -golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= -golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= -golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= -golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= +golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ= diff --git a/go-providers/anthropic/go.mod b/go-providers/anthropic/go.mod index cea9c9e..838df7f 100644 --- a/go-providers/anthropic/go.mod +++ b/go-providers/anthropic/go.mod @@ -9,10 +9,8 @@ require ( require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/tidwall/gjson v1.18.0 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect @@ -21,13 +19,14 @@ require ( go.opentelemetry.io/otel v1.40.0 // indirect go.opentelemetry.io/otel/metric v1.40.0 // indirect go.opentelemetry.io/otel/trace v1.40.0 // indirect - golang.org/x/net v0.49.0 // indirect + golang.org/x/net v0.51.0 // indirect golang.org/x/sync v0.19.0 // indirect - golang.org/x/sys v0.40.0 // indirect - golang.org/x/text v0.33.0 // indirect + golang.org/x/sys v0.41.0 // indirect + golang.org/x/text v0.34.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect google.golang.org/grpc v1.79.1 // indirect google.golang.org/protobuf v1.36.11 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect ) replace github.com/grafana/sigil/sdks/go => ../../go diff --git a/go-providers/anthropic/go.sum b/go-providers/anthropic/go.sum index 59f839a..24a167c 100644 --- a/go-providers/anthropic/go.sum +++ b/go-providers/anthropic/go.sum @@ -43,14 +43,14 @@ go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4A go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg= go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= -golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= -golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= +golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= +golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= -golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= -golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= -golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ= @@ -59,7 +59,8 @@ google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY= google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= -gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/go-providers/gemini/go.mod b/go-providers/gemini/go.mod index fd01b77..fdc415d 100644 --- a/go-providers/gemini/go.mod +++ b/go-providers/gemini/go.mod @@ -8,26 +8,27 @@ require ( ) require ( - cloud.google.com/go v0.116.0 // indirect - cloud.google.com/go/auth v0.9.3 // indirect + cloud.google.com/go v0.123.0 // indirect + cloud.google.com/go/auth v0.18.2 // indirect cloud.google.com/go/compute/metadata v0.9.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/google/go-cmp v0.7.0 // indirect - github.com/google/s2a-go v0.1.8 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect + github.com/google/s2a-go v0.1.9 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.11 // indirect + github.com/googleapis/gax-go/v2 v2.17.0 // indirect github.com/gorilla/websocket v1.5.3 // indirect - go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 // indirect go.opentelemetry.io/otel v1.40.0 // indirect go.opentelemetry.io/otel/metric v1.40.0 // indirect go.opentelemetry.io/otel/trace v1.40.0 // indirect - golang.org/x/crypto v0.47.0 // indirect - golang.org/x/net v0.49.0 // indirect - golang.org/x/sys v0.40.0 // indirect - golang.org/x/text v0.33.0 // indirect + golang.org/x/crypto v0.48.0 // indirect + golang.org/x/net v0.51.0 // indirect + golang.org/x/sys v0.41.0 // indirect + golang.org/x/text v0.34.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect google.golang.org/grpc v1.79.1 // indirect google.golang.org/protobuf v1.36.11 // indirect diff --git a/go-providers/gemini/go.sum b/go-providers/gemini/go.sum index e685014..619462a 100644 --- a/go-providers/gemini/go.sum +++ b/go-providers/gemini/go.sum @@ -1,76 +1,42 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.116.0 h1:B3fRrSDkLRt5qSHWe40ERJvhvnQwdZiHu0bJOpldweE= -cloud.google.com/go v0.116.0/go.mod h1:cEPSRWPzZEswwdr9BxE6ChEn01dWlTaF05LiC2Xs70U= -cloud.google.com/go/auth v0.9.3 h1:VOEUIAADkkLtyfr3BLa3R8Ed/j6w1jTBmARx+wb5w5U= -cloud.google.com/go/auth v0.9.3/go.mod h1:7z6VY+7h3KUdRov5F1i8NDP5ZzWKYmEPO842BgCsmTk= +cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE= +cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU= +cloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM= +cloud.google.com/go/auth v0.18.2/go.mod h1:xD+oY7gcahcu7G2SG2DsBerfFxgPAJz17zz2joOFF3M= cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM= -github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= +github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gTgghdIA6Stxb52D5RnLI1SLyw= -github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA= +github.com/googleapis/enterprise-certificate-proxy v0.3.11 h1:vAe81Msw+8tKUxi2Dqh/NZMz7475yUvmRIkXr4oN2ao= +github.com/googleapis/enterprise-certificate-proxy v0.3.11/go.mod h1:RFV7MUdlb7AgEq2v7FmMCfeSMCllAzWxFgRdusoGks8= +github.com/googleapis/gax-go/v2 v2.17.0 h1:RksgfBpxqff0EZkDWYuz9q/uWsTVz+kf43LsZ1J6SMc= +github.com/googleapis/gax-go/v2 v2.17.0/go.mod h1:mzaqghpQp4JDh3HvADwrat+6M3MOIDp5YKHhb9PAgDY= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= -go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0= go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= @@ -81,76 +47,25 @@ go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4A go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg= go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= -golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= -golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= +golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= +golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= -golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= -golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/genai v1.48.0 h1:1vb15G291wAjJJueisMDpUhssljhEdJU2t5qTidrVPs= google.golang.org/genai v1.48.0/go.mod h1:A3kkl0nyBjyFlNjgxIwKq70julKbIxpSxqKO5gw/gmk= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY= google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/go-providers/openai/go.mod b/go-providers/openai/go.mod index 59659f5..d208a21 100644 --- a/go-providers/openai/go.mod +++ b/go-providers/openai/go.mod @@ -19,9 +19,9 @@ require ( go.opentelemetry.io/otel v1.40.0 // indirect go.opentelemetry.io/otel/metric v1.40.0 // indirect go.opentelemetry.io/otel/trace v1.40.0 // indirect - golang.org/x/net v0.49.0 // indirect - golang.org/x/sys v0.40.0 // indirect - golang.org/x/text v0.33.0 // indirect + golang.org/x/net v0.51.0 // indirect + golang.org/x/sys v0.41.0 // indirect + golang.org/x/text v0.34.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect google.golang.org/grpc v1.79.1 // indirect google.golang.org/protobuf v1.36.11 // indirect diff --git a/go-providers/openai/go.sum b/go-providers/openai/go.sum index 68b48cb..0337012 100644 --- a/go-providers/openai/go.sum +++ b/go-providers/openai/go.sum @@ -1,7 +1,7 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -15,8 +15,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/openai/openai-go/v3 v3.24.0 h1:08x6GnYiB+AAejTo6yzPY8RkZMJQ8NpreiOyM5QfyYU= github.com/openai/openai-go/v3 v3.24.0/go.mod h1:cdufnVK14cWcT9qA1rRtrXx4FTRsgbDPW7Ia7SS5cZo= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= @@ -41,12 +41,12 @@ go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4A go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg= go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= -golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= -golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= -golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= -golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= -golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= +golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ= diff --git a/go/cmd/devex-emitter/go.mod b/go/cmd/devex-emitter/go.mod index 006dd27..c43482a 100644 --- a/go/cmd/devex-emitter/go.mod +++ b/go/cmd/devex-emitter/go.mod @@ -15,38 +15,39 @@ require ( go.opentelemetry.io/otel/metric v1.40.0 go.opentelemetry.io/otel/sdk v1.40.0 go.opentelemetry.io/otel/sdk/metric v1.40.0 + go.opentelemetry.io/otel/trace v1.40.0 google.golang.org/genai v1.48.0 ) require ( - cloud.google.com/go v0.116.0 // indirect - cloud.google.com/go/auth v0.9.3 // indirect + cloud.google.com/go v0.123.0 // indirect + cloud.google.com/go/auth v0.18.2 // indirect cloud.google.com/go/compute/metadata v0.9.0 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/google/go-cmp v0.7.0 // indirect - github.com/google/s2a-go v0.1.8 // indirect + github.com/google/s2a-go v0.1.9 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.11 // indirect + github.com/googleapis/gax-go/v2 v2.17.0 // indirect github.com/gorilla/websocket v1.5.3 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 // indirect github.com/tidwall/gjson v1.18.0 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/sjson v1.2.5 // indirect - go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 // indirect - go.opentelemetry.io/otel/trace v1.40.0 // indirect go.opentelemetry.io/proto/otlp v1.9.0 // indirect - golang.org/x/crypto v0.47.0 // indirect - golang.org/x/net v0.49.0 // indirect + golang.org/x/crypto v0.48.0 // indirect + golang.org/x/net v0.51.0 // indirect golang.org/x/sync v0.19.0 // indirect - golang.org/x/sys v0.40.0 // indirect - golang.org/x/text v0.33.0 // indirect + golang.org/x/sys v0.41.0 // indirect + golang.org/x/text v0.34.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect google.golang.org/grpc v1.79.1 // indirect diff --git a/go/cmd/devex-emitter/go.sum b/go/cmd/devex-emitter/go.sum index a56c0a1..4eed1f7 100644 --- a/go/cmd/devex-emitter/go.sum +++ b/go/cmd/devex-emitter/go.sum @@ -1,82 +1,46 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.116.0 h1:B3fRrSDkLRt5qSHWe40ERJvhvnQwdZiHu0bJOpldweE= -cloud.google.com/go v0.116.0/go.mod h1:cEPSRWPzZEswwdr9BxE6ChEn01dWlTaF05LiC2Xs70U= -cloud.google.com/go/auth v0.9.3 h1:VOEUIAADkkLtyfr3BLa3R8Ed/j6w1jTBmARx+wb5w5U= -cloud.google.com/go/auth v0.9.3/go.mod h1:7z6VY+7h3KUdRov5F1i8NDP5ZzWKYmEPO842BgCsmTk= +cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE= +cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU= +cloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM= +cloud.google.com/go/auth v0.18.2/go.mod h1:xD+oY7gcahcu7G2SG2DsBerfFxgPAJz17zz2joOFF3M= cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/anthropics/anthropic-sdk-go v1.26.0 h1:oUTzFaUpAevfuELAP1sjL6CQJ9HHAfT7CoSYSac11PY= github.com/anthropics/anthropic-sdk-go v1.26.0/go.mod h1:qUKmaW+uuPB64iy1l+4kOSvaLqPXnHTTBKH6RVZ7q5Q= github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI= github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM= -github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= +github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gTgghdIA6Stxb52D5RnLI1SLyw= -github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA= +github.com/googleapis/enterprise-certificate-proxy v0.3.11 h1:vAe81Msw+8tKUxi2Dqh/NZMz7475yUvmRIkXr4oN2ao= +github.com/googleapis/enterprise-certificate-proxy v0.3.11/go.mod h1:RFV7MUdlb7AgEq2v7FmMCfeSMCllAzWxFgRdusoGks8= +github.com/googleapis/gax-go/v2 v2.17.0 h1:RksgfBpxqff0EZkDWYuz9q/uWsTVz+kf43LsZ1J6SMc= +github.com/googleapis/gax-go/v2 v2.17.0/go.mod h1:mzaqghpQp4JDh3HvADwrat+6M3MOIDp5YKHhb9PAgDY= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 h1:X+2YciYSxvMQK0UZ7sg45ZVabVZBeBuvMkmuI2V3Fak= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7/go.mod h1:lW34nIZuQ8UDPdkon5fmfp2l3+ZkQ2me/+oecHYLOII= github.com/openai/openai-go/v3 v3.24.0 h1:08x6GnYiB+AAejTo6yzPY8RkZMJQ8NpreiOyM5QfyYU= github.com/openai/openai-go/v3 v3.24.0/go.mod h1:cdufnVK14cWcT9qA1rRtrXx4FTRsgbDPW7Ia7SS5cZo= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= @@ -89,10 +53,10 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= -go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= -go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0= go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.40.0 h1:NOyNnS19BF2SUDApbOKbDtWZ0IK7b8FJ2uAGdIWOGb0= @@ -113,80 +77,29 @@ go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjce go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= -golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= -golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= +golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= +golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= -golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= -golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/genai v1.48.0 h1:1vb15G291wAjJJueisMDpUhssljhEdJU2t5qTidrVPs= google.golang.org/genai v1.48.0/go.mod h1:A3kkl0nyBjyFlNjgxIwKq70julKbIxpSxqKO5gw/gmk= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 h1:merA0rdPeUV3YIIfHHcH4qBkiQAc1nfCKSI7lB4cV2M= google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409/go.mod h1:fl8J1IvUjCilwZzQowmw2b7HQB2eAuYBabMXzWurF+I= google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY= google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/go/go.mod b/go/go.mod index 6840a22..251fd39 100644 --- a/go/go.mod +++ b/go/go.mod @@ -14,12 +14,14 @@ require ( require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/google/uuid v1.6.0 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - golang.org/x/net v0.49.0 // indirect - golang.org/x/sys v0.40.0 // indirect - golang.org/x/text v0.33.0 // indirect + golang.org/x/net v0.51.0 // indirect + golang.org/x/sys v0.41.0 // indirect + golang.org/x/text v0.34.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect ) diff --git a/go/go.sum b/go/go.sum index 43766d2..9a16bb7 100644 --- a/go/go.sum +++ b/go/go.sum @@ -1,7 +1,7 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -13,8 +13,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= @@ -31,12 +31,12 @@ go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZY go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= -golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= -golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= -golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= -golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= +golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ= From d9bebaea42c716a5ab15717776c1c685a4060c7b Mon Sep 17 00:00:00 2001 From: "renovate-sh-app[bot]" <219655108+renovate-sh-app[bot]@users.noreply.github.com> Date: Tue, 3 Mar 2026 20:48:30 +0100 Subject: [PATCH 014/133] chore(deps): update dependency openai to 2.9.0 (#202) | datasource | package | from | to | | ---------- | ------- | ----- | ----- | | nuget | OpenAI | 2.8.0 | 2.9.0 | Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> --- dotnet/src/Grafana.Sigil.OpenAI/Grafana.Sigil.OpenAI.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/src/Grafana.Sigil.OpenAI/Grafana.Sigil.OpenAI.csproj b/dotnet/src/Grafana.Sigil.OpenAI/Grafana.Sigil.OpenAI.csproj index 922fa88..07c9f16 100644 --- a/dotnet/src/Grafana.Sigil.OpenAI/Grafana.Sigil.OpenAI.csproj +++ b/dotnet/src/Grafana.Sigil.OpenAI/Grafana.Sigil.OpenAI.csproj @@ -12,7 +12,7 @@ - + From aeff6919613a2526985fce3458b0d0516e606e24 Mon Sep 17 00:00:00 2001 From: Sven Grossmann Date: Wed, 4 Mar 2026 08:06:22 +0100 Subject: [PATCH 015/133] chore: support h2 next protos (#228) --- go/sigil/exporter.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/go/sigil/exporter.go b/go/sigil/exporter.go index af92000..7708678 100644 --- a/go/sigil/exporter.go +++ b/go/sigil/exporter.go @@ -78,7 +78,10 @@ func newGRPCGenerationExporter(cfg GenerationExportConfig) (generationExporter, return nil, err } - transportCreds := credentials.NewTLS(&tls.Config{MinVersion: tls.VersionTLS12}) + transportCreds := credentials.NewTLS(&tls.Config{ + MinVersion: tls.VersionTLS12, + NextProtos: []string{"h2"}, + }) if cfg.Insecure || insecureEndpoint { transportCreds = insecure.NewCredentials() } From e2e54c737f8a205598f36803af43a5b91f5e8401 Mon Sep 17 00:00:00 2001 From: Cyril Tovena Date: Wed, 4 Mar 2026 09:16:15 +0100 Subject: [PATCH 016/133] feat: TreeView (#229) * feat(plugin): port conversation span tree to jaeger-style view Replace the conversation details span tree with a Jaeger/Grafana-style implementation, including copied list virtualization behavior, collapse controls, and service-colored tree connectors/icons. Keep a plugin-owned customization hook (renderNode) so node rendering can be extended later without rewriting tree internals. * feat(traceview): enhance span tree with timing data and draggable features - Introduced timing fields (startTimeUnixNano, endTimeUnixNano, durationNano) to SigilSpanTreeRow for better trace visualization. - Implemented a DraggableManager for mouse-based interactions, allowing users to resize columns in the trace view. - Added utility functions for formatting duration and managing ticks in the timeline. - Updated tests to validate new timing features and draggable functionality. This update improves the usability and functionality of the trace view, aligning it with user expectations for performance monitoring. * feat(conversations): enhance conversation components and add resource attributes - Introduced a new ConversationSummaryHeader component for improved conversation details display. - Updated ConversationGenerations and SigilSpanTree components to support resource attributes. - Refactored App component to include a new ConversationPage route. - Removed unused ConversationColumn component to streamline the codebase. - Enhanced tests to cover new features and ensure stability. This update improves the user experience in the conversations section and aligns with the overall architecture enhancements. * feat(conversations): add support for orphan generation spans in SpanDetailPanel - Enhanced SpanDetailPanel to handle orphan generations by matching via the sigil.generation.id attribute when span_id is absent. - Updated resolveGeneration function to include logic for finding orphan generations based on attributes. - Added a new test case to verify the display of generation information for orphan spans. This update improves the handling of generation data in conversation spans, ensuring accurate representation even when span_id is not provided. --- js/package.json | 1 + js/tsconfig.json | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/js/package.json b/js/package.json index 43b6ae9..b67f425 100644 --- a/js/package.json +++ b/js/package.json @@ -72,6 +72,7 @@ "llamaindex": "^0.12.1" }, "devDependencies": { + "@types/node": "^22.10.0", "typescript": "^5.9.3" } } diff --git a/js/tsconfig.json b/js/tsconfig.json index 05f6fb8..cfb0672 100644 --- a/js/tsconfig.json +++ b/js/tsconfig.json @@ -7,7 +7,8 @@ "noImplicitOverride": true, "noUncheckedIndexedAccess": true, "skipLibCheck": true, - "noEmit": true + "noEmit": true, + "types": ["node"] }, "include": ["src/**/*.ts"] } From 5a0e9348ffb7344efe2213a7492d5666c5ba7606 Mon Sep 17 00:00:00 2001 From: "renovate-sh-app[bot]" <219655108+renovate-sh-app[bot]@users.noreply.github.com> Date: Wed, 4 Mar 2026 13:05:42 +0100 Subject: [PATCH 017/133] chore(deps): update dependency @types/node to v24 (#236) | datasource | package | from | to | | ---------- | ----------- | -------- | ------- | | npm | @types/node | 22.19.13 | 24.11.0 | Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> --- js/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/package.json b/js/package.json index b67f425..316abe8 100644 --- a/js/package.json +++ b/js/package.json @@ -72,7 +72,7 @@ "llamaindex": "^0.12.1" }, "devDependencies": { - "@types/node": "^22.10.0", + "@types/node": "^24.0.0", "typescript": "^5.9.3" } } From e5388173958cfa1e5e110d54a482f8aae8416ece Mon Sep 17 00:00:00 2001 From: Cyril Tovena Date: Wed, 4 Mar 2026 17:55:27 +0100 Subject: [PATCH 018/133] feat(go): track deferred tools in sdk ingest and agent version hashing (#243) * feat(go): track deferred tools across sdk ingest and agent catalog Propagate tool deferred semantics end-to-end in the Go SDK stack. - add ToolDefinition.deferred to generation ingest proto and regenerate Go stubs - map deferred fields in core SDK/proto mapping and provider mappers (anthropic/openai/gemini) - update validation/exporter and transport fixtures/tests for deferred tool definitions - include deferred in agent metadata tool projection and effective-version hashing - document deferred tool semantics in architecture and ingest contract docs This ensures deferred tool configuration is preserved in payloads and treated as a real agent configuration change when computing effective agent versions. * fix(sigil): restore strings import in agentmeta deferred-hash test Keep the deferred effective-version regression assertion and restore the missing strings import introduced during conflict resolution. * fix(go-providers): preserve empty client text segments Stop dropping empty system/developer segments in OpenAI request mapping by removing appendNonEmpty filtering in both chat-completions and responses mappers. Add regression tests for OpenAI/Anthropic/Gemini helper behavior to ensure empty text segments remain part of joined prompts/tool content as sent by clients. --- go-providers/anthropic/README.md | 2 + go-providers/anthropic/mapper.go | 15 +- go-providers/anthropic/mapper_test.go | 192 +++++++++++++++- go-providers/anthropic/stream_mapper.go | 4 +- go-providers/gemini/mapper.go | 10 +- go-providers/gemini/mapper_test.go | 82 +++++++ go-providers/openai/mapper.go | 61 ++--- go-providers/openai/mapper_test.go | 210 ++++++++++++++++++ go-providers/openai/responses_mapper.go | 46 ++-- go-providers/openai/stream_mapper.go | 2 +- go/README.md | 4 + go/sigil/client.go | 58 +++-- go/sigil/client_test.go | 14 ++ go/sigil/exporter.go | 23 +- go/sigil/exporter_test.go | 19 ++ go/sigil/exporter_transport_test.go | 73 +++++- go/sigil/generation.go | 1 + .../gen/sigil/v1/generation_ingest.pb.go | 13 +- go/sigil/proto_mapping.go | 1 + go/sigil/validation.go | 8 +- go/sigil/validation_test.go | 25 +++ proto/sigil/v1/generation_ingest.proto | 1 + 22 files changed, 744 insertions(+), 120 deletions(-) diff --git a/go-providers/anthropic/README.md b/go-providers/anthropic/README.md index 7378d31..a764618 100644 --- a/go-providers/anthropic/README.md +++ b/go-providers/anthropic/README.md @@ -96,3 +96,5 @@ In addition to normalized usage fields, Anthropic server-tool counters are mappe - `sigil.gen_ai.usage.server_tool_use.web_search_requests` - `sigil.gen_ai.usage.server_tool_use.web_fetch_requests` - `sigil.gen_ai.usage.server_tool_use.total_requests` + +Anthropic tool `defer_loading` is mapped to Sigil `Generation.Tools[].Deferred`. diff --git a/go-providers/anthropic/mapper.go b/go-providers/anthropic/mapper.go index b7edaa0..efa9210 100644 --- a/go-providers/anthropic/mapper.go +++ b/go-providers/anthropic/mapper.go @@ -167,7 +167,7 @@ func mapResponseMessages(content []asdk.BetaContentBlockUnion) []sigil.Message { func mapRequestBlock(block asdk.BetaContentBlockParamUnion) (sigil.Part, bool) { if block.OfText != nil { - text := strings.TrimSpace(block.OfText.Text) + text := block.OfText.Text if text == "" { return sigil.Part{}, false } @@ -291,7 +291,7 @@ func mapRequestBlock(block asdk.BetaContentBlockParamUnion) (sigil.Part, bool) { typ := derefString(block.GetType()) switch typ { case "text": - text := strings.TrimSpace(derefString(block.GetText())) + text := derefString(block.GetText()) if text == "" { return sigil.Part{}, false } @@ -337,7 +337,7 @@ func mapRequestBlock(block asdk.BetaContentBlockParamUnion) (sigil.Part, bool) { func mapResponseBlock(block asdk.BetaContentBlockUnion) (sigil.Part, bool) { switch block.Type { case "text": - text := strings.TrimSpace(block.Text) + text := block.Text if text == "" { return sigil.Part{}, false } @@ -397,6 +397,9 @@ func mapTools(tools []asdk.BetaToolUnionParam) []sigil.ToolDefinition { Description: derefString(tools[i].GetDescription()), Type: derefString(tools[i].GetType()), } + if deferred := tools[i].GetDeferLoading(); deferred != nil { + definition.Deferred = *deferred + } if schema := tools[i].GetInputSchema(); schema != nil { raw, err := marshalAny(*schema) @@ -418,11 +421,7 @@ func mapSystemPrompt(system []asdk.BetaTextBlockParam) string { parts := make([]string, 0, len(system)) for i := range system { - text := strings.TrimSpace(system[i].Text) - if text == "" { - continue - } - parts = append(parts, text) + parts = append(parts, system[i].Text) } return strings.Join(parts, "\n\n") diff --git a/go-providers/anthropic/mapper_test.go b/go-providers/anthropic/mapper_test.go index 74cd9fa..7adb1cc 100644 --- a/go-providers/anthropic/mapper_test.go +++ b/go-providers/anthropic/mapper_test.go @@ -109,6 +109,12 @@ func TestFromRequestResponse(t *testing.T) { if len(generation.Artifacts) != 0 { t.Fatalf("expected 0 artifacts by default, got %d", len(generation.Artifacts)) } + if len(generation.Tools) != 1 { + t.Fatalf("expected 1 tool, got %d", len(generation.Tools)) + } + if !generation.Tools[0].Deferred { + t.Fatalf("expected mapped tool deferred=true") + } hasToolRole := false for _, message := range generation.Input { @@ -240,6 +246,12 @@ func TestFromStream(t *testing.T) { if len(generation.Artifacts) != 0 { t.Fatalf("expected 0 artifacts by default, got %d", len(generation.Artifacts)) } + if len(generation.Tools) != 1 { + t.Fatalf("expected 1 tool, got %d", len(generation.Tools)) + } + if !generation.Tools[0].Deferred { + t.Fatalf("expected mapped tool deferred=true") + } } func TestFromStream_DeltaAccumulation(t *testing.T) { @@ -541,6 +553,163 @@ func TestFromRequestResponseMapsThinkingDisabled(t *testing.T) { } } +func TestFromRequestResponseMapsToolDeferredDefaultFalse(t *testing.T) { + req := testRequest() + req.Tools = []asdk.BetaToolUnionParam{ + asdk.BetaToolUnionParamOfTool(asdk.BetaToolInputSchemaParam{ + Type: "object", + Properties: map[string]any{ + "city": map[string]any{ + "type": "string", + }, + }, + Required: []string{"city"}, + }, "weather"), + } + + resp := &asdk.BetaMessage{ + ID: "msg_1", + Model: asdk.Model("claude-sonnet-4-5"), + StopReason: asdk.BetaStopReasonEndTurn, + Content: []asdk.BetaContentBlockUnion{ + {Type: "text", Text: "done"}, + }, + } + + generation, err := FromRequestResponse(req, resp) + if err != nil { + t.Fatalf("from request/response: %v", err) + } + if len(generation.Tools) != 1 { + t.Fatalf("expected 1 tool, got %d", len(generation.Tools)) + } + if generation.Tools[0].Deferred { + t.Fatalf("expected mapped tool deferred=false when defer_loading is unset") + } +} + +func TestFromRequestResponsePreservesWhitespaceInTextAndSystemPrompt(t *testing.T) { + req := asdk.BetaMessageNewParams{ + MaxTokens: 1, + Model: asdk.Model("claude-sonnet-4-5"), + System: []asdk.BetaTextBlockParam{ + {Text: " first system ", Type: "text"}, + {Text: " second system ", Type: "text"}, + }, + Messages: []asdk.BetaMessageParam{ + { + Role: asdk.BetaMessageParamRoleUser, + Content: []asdk.BetaContentBlockParamUnion{ + asdk.NewBetaTextBlock(" user content with literal \\\\n\\\\n "), + }, + }, + }, + } + + resp := &asdk.BetaMessage{ + ID: "msg_whitespace", + Model: asdk.Model("claude-sonnet-4-5"), + StopReason: asdk.BetaStopReasonEndTurn, + Content: []asdk.BetaContentBlockUnion{ + {Type: "text", Text: "\n assistant content \n"}, + }, + } + + generation, err := FromRequestResponse(req, resp) + if err != nil { + t.Fatalf("from request/response: %v", err) + } + + if generation.SystemPrompt != " first system \n\n second system " { + t.Fatalf("unexpected system prompt %q", generation.SystemPrompt) + } + if len(generation.Input) != 1 || len(generation.Input[0].Parts) != 1 { + t.Fatalf("expected one input text part, got %#v", generation.Input) + } + if generation.Input[0].Parts[0].Text != " user content with literal \\\\n\\\\n " { + t.Fatalf("unexpected input text %q", generation.Input[0].Parts[0].Text) + } + if len(generation.Output) != 1 || len(generation.Output[0].Parts) != 1 { + t.Fatalf("expected one output text part, got %#v", generation.Output) + } + if generation.Output[0].Parts[0].Text != "\n assistant content \n" { + t.Fatalf("unexpected output text %q", generation.Output[0].Parts[0].Text) + } +} + +func TestFromStreamPreservesWhitespaceOnlyParts(t *testing.T) { + req := asdk.BetaMessageNewParams{ + MaxTokens: 1, + Model: asdk.Model("claude-sonnet-4-5"), + } + + summary := StreamSummary{ + Events: []asdk.BetaRawMessageStreamEventUnion{ + { + Type: "message_start", + Message: asdk.BetaMessage{ + ID: "msg_stream_whitespace", + Model: asdk.Model("claude-sonnet-4-5"), + }, + }, + { + Type: "content_block_start", + Index: 0, + ContentBlock: asdk.BetaRawContentBlockStartEventContentBlockUnion{ + Type: "thinking", + }, + }, + { + Type: "content_block_delta", + Index: 0, + Delta: asdk.BetaRawMessageStreamEventUnionDelta{Thinking: " "}, + }, + { + Type: "content_block_start", + Index: 1, + ContentBlock: asdk.BetaRawContentBlockStartEventContentBlockUnion{ + Type: "text", + Text: " ", + }, + }, + { + Type: "message_delta", + Delta: asdk.BetaRawMessageStreamEventUnionDelta{ + StopReason: asdk.BetaStopReasonEndTurn, + }, + Usage: asdk.BetaMessageDeltaUsage{ + InputTokens: 1, + OutputTokens: 1, + }, + }, + }, + } + + generation, err := FromStream(req, summary) + if err != nil { + t.Fatalf("from stream: %v", err) + } + if len(generation.Output) != 1 || len(generation.Output[0].Parts) != 2 { + t.Fatalf("expected two output parts, got %#v", generation.Output) + } + if generation.Output[0].Parts[0].Thinking != " " { + t.Fatalf("unexpected thinking %q", generation.Output[0].Parts[0].Thinking) + } + if generation.Output[0].Parts[1].Text != " " { + t.Fatalf("unexpected text %q", generation.Output[0].Parts[1].Text) + } +} + +func TestMapSystemPromptPreservesEmptySegments(t *testing.T) { + got := mapSystemPrompt([]asdk.BetaTextBlockParam{ + {Text: "", Type: "text"}, + {Text: "second", Type: "text"}, + }) + if got != "\n\nsecond" { + t.Fatalf("expected preserved empty segment separator, got %q", got) + } +} + func testRequest() asdk.BetaMessageNewParams { toolResult := asdk.NewBetaToolResultBlock("toolu_1", "", false) toolResult.OfToolResult.Content = []asdk.BetaToolResultBlockParamContentUnion{ @@ -552,6 +721,17 @@ func testRequest() asdk.BetaMessageNewParams { }, } + weatherTool := asdk.BetaToolUnionParamOfTool(asdk.BetaToolInputSchemaParam{ + Type: "object", + Properties: map[string]any{ + "city": map[string]any{ + "type": "string", + }, + }, + Required: []string{"city"}, + }, "weather") + weatherTool.OfTool.DeferLoading = param.NewOpt(true) + return asdk.BetaMessageNewParams{ MaxTokens: 512, Model: asdk.Model("claude-sonnet-4-5"), @@ -586,16 +766,6 @@ func testRequest() asdk.BetaMessageNewParams { }, }, }, - Tools: []asdk.BetaToolUnionParam{ - asdk.BetaToolUnionParamOfTool(asdk.BetaToolInputSchemaParam{ - Type: "object", - Properties: map[string]any{ - "city": map[string]any{ - "type": "string", - }, - }, - Required: []string{"city"}, - }, "weather"), - }, + Tools: []asdk.BetaToolUnionParam{weatherTool}, } } diff --git a/go-providers/anthropic/stream_mapper.go b/go-providers/anthropic/stream_mapper.go index cb5a2c7..d312882 100644 --- a/go-providers/anthropic/stream_mapper.go +++ b/go-providers/anthropic/stream_mapper.go @@ -283,14 +283,14 @@ func (a *streamBlockAccumulator) build() (assistantParts, toolParts []sigil.Part func (b *streamBlock) toPart() (sigil.Part, bool, bool) { switch b.blockType { case "text": - text := strings.TrimSpace(b.text.String()) + text := b.text.String() if text == "" { return sigil.Part{}, false, false } return sigil.TextPart(text), false, true case "thinking", "redacted_thinking": content := b.thinking.String() - if strings.TrimSpace(content) == "" { + if content == "" { return sigil.Part{}, false, false } part := sigil.ThinkingPart(content) diff --git a/go-providers/gemini/mapper.go b/go-providers/gemini/mapper.go index 888c737..cc513ac 100644 --- a/go-providers/gemini/mapper.go +++ b/go-providers/gemini/mapper.go @@ -170,7 +170,7 @@ func mapContents(contents []*genai.Content) []sigil.Message { continue } - if text := strings.TrimSpace(part.Text); text != "" { + if text := part.Text; text != "" { if part.Thought && role == sigil.RoleAssistant { roleParts = append(roleParts, sigil.ThinkingPart(text)) } else { @@ -233,7 +233,7 @@ func embeddingInputTexts(contents []*genai.Content) []string { } out := make([]string, 0, len(contents)) for _, content := range contents { - text := strings.TrimSpace(embeddingContentText(content)) + text := embeddingContentText(content) if text != "" { out = append(out, text) } @@ -253,7 +253,7 @@ func embeddingContentText(content *genai.Content) string { if part == nil { continue } - if text := strings.TrimSpace(part.Text); text != "" { + if text := part.Text; text != "" { chunks = append(chunks, text) } } @@ -354,9 +354,7 @@ func extractSystemPrompt(config *genai.GenerateContentConfig) string { if part == nil { continue } - if text := strings.TrimSpace(part.Text); text != "" { - parts = append(parts, text) - } + parts = append(parts, part.Text) } return strings.Join(parts, "\n\n") } diff --git a/go-providers/gemini/mapper_test.go b/go-providers/gemini/mapper_test.go index ca9ba0e..e6fee7e 100644 --- a/go-providers/gemini/mapper_test.go +++ b/go-providers/gemini/mapper_test.go @@ -409,3 +409,85 @@ func TestEmbeddingFromResponseFallsBackToRequestedDimensions(t *testing.T) { t.Fatalf("expected dimensions 12, got %v", result.Dimensions) } } + +func TestFromRequestResponsePreservesWhitespace(t *testing.T) { + model := "gemini-2.5-pro" + contents := []*genai.Content{ + genai.NewContentFromText(" user literal \\\\n\\\\n ", genai.RoleUser), + } + config := &genai.GenerateContentConfig{ + SystemInstruction: genai.NewContentFromText(" system prompt ", genai.RoleUser), + } + resp := &genai.GenerateContentResponse{ + ResponseID: "resp_whitespace", + ModelVersion: "gemini-2.5-pro-001", + Candidates: []*genai.Candidate{ + { + FinishReason: genai.FinishReasonStop, + Content: genai.NewContentFromText("\n assistant output \n", genai.RoleModel), + }, + }, + } + + generation, err := FromRequestResponse(model, contents, config, resp) + if err != nil { + t.Fatalf("from request/response: %v", err) + } + + if generation.SystemPrompt != " system prompt " { + t.Fatalf("unexpected system prompt %q", generation.SystemPrompt) + } + if len(generation.Input) != 1 || len(generation.Input[0].Parts) != 1 { + t.Fatalf("expected single input text part, got %#v", generation.Input) + } + if generation.Input[0].Parts[0].Text != " user literal \\\\n\\\\n " { + t.Fatalf("unexpected input text %q", generation.Input[0].Parts[0].Text) + } + if len(generation.Output) != 1 || len(generation.Output[0].Parts) != 1 { + t.Fatalf("expected single output text part, got %#v", generation.Output) + } + if generation.Output[0].Parts[0].Text != "\n assistant output \n" { + t.Fatalf("unexpected output text %q", generation.Output[0].Parts[0].Text) + } +} + +func TestFromStreamPreservesWhitespaceOnlyOutput(t *testing.T) { + model := "gemini-2.5-pro" + summary := StreamSummary{ + Responses: []*genai.GenerateContentResponse{ + { + ResponseID: "resp_stream_whitespace", + ModelVersion: "gemini-2.5-pro-001", + Candidates: []*genai.Candidate{ + { + FinishReason: genai.FinishReasonStop, + Content: genai.NewContentFromText(" ", genai.RoleModel), + }, + }, + }, + }, + } + + generation, err := FromStream(model, nil, nil, summary) + if err != nil { + t.Fatalf("from stream: %v", err) + } + if len(generation.Output) != 1 || len(generation.Output[0].Parts) != 1 { + t.Fatalf("expected single output text part, got %#v", generation.Output) + } + if generation.Output[0].Parts[0].Text != " " { + t.Fatalf("unexpected output text %q", generation.Output[0].Parts[0].Text) + } +} + +func TestExtractSystemPromptPreservesEmptySegments(t *testing.T) { + config := &genai.GenerateContentConfig{ + SystemInstruction: genai.NewContentFromParts([]*genai.Part{ + genai.NewPartFromText(""), + genai.NewPartFromText("second"), + }, genai.RoleUser), + } + if got := extractSystemPrompt(config); got != "\n\nsecond" { + t.Fatalf("expected preserved empty segment separator, got %q", got) + } +} diff --git a/go-providers/openai/mapper.go b/go-providers/openai/mapper.go index 8c61248..ad4409c 100644 --- a/go-providers/openai/mapper.go +++ b/go-providers/openai/mapper.go @@ -48,7 +48,7 @@ func ChatCompletionsFromRequestResponse(req osdk.ChatCompletionNewParams, resp * } requestModel := string(req.Model) - responseModel := strings.TrimSpace(resp.Model) + responseModel := resp.Model if responseModel == "" { responseModel = requestModel } @@ -96,7 +96,7 @@ func EmbeddingsFromResponse(req osdk.EmbeddingNewParams, resp *osdk.CreateEmbedd } result.InputTokens = resp.Usage.PromptTokens - result.ResponseModel = strings.TrimSpace(resp.Model) + result.ResponseModel = resp.Model if len(resp.Data) > 0 { dimensions := int64(len(resp.Data[0].Embedding)) @@ -119,9 +119,9 @@ func mapRequestMessages(messages []osdk.ChatCompletionMessageParamUnion) ([]sigi for i := range messages { switch { case messages[i].OfSystem != nil: - systemPrompts = appendNonEmpty(systemPrompts, extractTextFromSystem(messages[i].OfSystem)) + systemPrompts = append(systemPrompts, extractTextFromSystem(messages[i].OfSystem)) case messages[i].OfDeveloper != nil: - systemPrompts = appendNonEmpty(systemPrompts, extractTextFromDeveloper(messages[i].OfDeveloper)) + systemPrompts = append(systemPrompts, extractTextFromDeveloper(messages[i].OfDeveloper)) case messages[i].OfUser != nil: parts := mapUserParts(messages[i].OfUser) if len(parts) > 0 { @@ -156,10 +156,10 @@ func mapResponseMessages(choices []osdk.ChatCompletionChoice) []sigil.Message { message := choices[0].Message parts := make([]sigil.Part, 0, 1+len(message.ToolCalls)) - if text := strings.TrimSpace(message.Content); text != "" { + if text := message.Content; text != "" { parts = append(parts, sigil.TextPart(text)) } - if refusal := strings.TrimSpace(message.Refusal); refusal != "" { + if refusal := message.Refusal; refusal != "" { parts = append(parts, sigil.TextPart(refusal)) } for _, call := range message.ToolCalls { @@ -187,12 +187,12 @@ func mapResponseMessages(choices []osdk.ChatCompletionChoice) []sigil.Message { func mapUserParts(message *osdk.ChatCompletionUserMessageParam) []sigil.Part { parts := make([]sigil.Part, 0, 2) if message.Content.OfString.Valid() { - if text := strings.TrimSpace(message.Content.OfString.Value); text != "" { + if text := message.Content.OfString.Value; text != "" { parts = append(parts, sigil.TextPart(text)) } } for _, contentPart := range message.Content.OfArrayOfContentParts { - text := strings.TrimSpace(derefString(contentPart.GetText())) + text := derefString(contentPart.GetText()) if text != "" { parts = append(parts, sigil.TextPart(text)) } @@ -203,20 +203,20 @@ func mapUserParts(message *osdk.ChatCompletionUserMessageParam) []sigil.Part { func mapAssistantParamParts(message *osdk.ChatCompletionAssistantMessageParam) []sigil.Part { parts := make([]sigil.Part, 0, 2+len(message.ToolCalls)) if message.Content.OfString.Valid() { - if text := strings.TrimSpace(message.Content.OfString.Value); text != "" { + if text := message.Content.OfString.Value; text != "" { parts = append(parts, sigil.TextPart(text)) } } for _, contentPart := range message.Content.OfArrayOfContentParts { - if text := strings.TrimSpace(derefString(contentPart.GetText())); text != "" { + if text := derefString(contentPart.GetText()); text != "" { parts = append(parts, sigil.TextPart(text)) } - if refusal := strings.TrimSpace(derefString(contentPart.GetRefusal())); refusal != "" { + if refusal := derefString(contentPart.GetRefusal()); refusal != "" { parts = append(parts, sigil.TextPart(refusal)) } } if message.Refusal.Valid() { - if refusal := strings.TrimSpace(message.Refusal.Value); refusal != "" { + if refusal := message.Refusal.Value; refusal != "" { parts = append(parts, sigil.TextPart(refusal)) } } @@ -226,7 +226,7 @@ func mapAssistantParamParts(message *osdk.ChatCompletionAssistantMessageParam) [ continue } part := sigil.ToolCallPart(sigil.ToolCall{ - ID: strings.TrimSpace(derefString(call.GetID())), + ID: derefString(call.GetID()), Name: function.Name, InputJSON: parseJSONOrString(function.Arguments), }) @@ -239,13 +239,11 @@ func mapAssistantParamParts(message *osdk.ChatCompletionAssistantMessageParam) [ func mapToolMessage(message *osdk.ChatCompletionToolMessageParam) *sigil.Part { content := "" if message.Content.OfString.Valid() { - content = strings.TrimSpace(message.Content.OfString.Value) + content = message.Content.OfString.Value } else { chunks := make([]string, 0, len(message.Content.OfArrayOfContentParts)) for _, part := range message.Content.OfArrayOfContentParts { - if text := strings.TrimSpace(part.Text); text != "" { - chunks = append(chunks, text) - } + chunks = append(chunks, part.Text) } content = strings.Join(chunks, "\n") } @@ -266,7 +264,7 @@ func mapFunctionMessage(message *osdk.ChatCompletionFunctionMessageParam) *sigil if !message.Content.Valid() { return nil } - content := strings.TrimSpace(message.Content.Value) + content := message.Content.Value if content == "" { return nil } @@ -503,48 +501,35 @@ func coerceInt64Pointer(value any) *int64 { func extractTextFromSystem(message *osdk.ChatCompletionSystemMessageParam) string { if message.Content.OfString.Valid() { - return strings.TrimSpace(message.Content.OfString.Value) + return message.Content.OfString.Value } parts := make([]string, 0, len(message.Content.OfArrayOfContentParts)) for _, part := range message.Content.OfArrayOfContentParts { - if text := strings.TrimSpace(part.Text); text != "" { - parts = append(parts, text) - } + parts = append(parts, part.Text) } return strings.Join(parts, "\n") } func extractTextFromDeveloper(message *osdk.ChatCompletionDeveloperMessageParam) string { if message.Content.OfString.Valid() { - return strings.TrimSpace(message.Content.OfString.Value) + return message.Content.OfString.Value } parts := make([]string, 0, len(message.Content.OfArrayOfContentParts)) for _, part := range message.Content.OfArrayOfContentParts { - if text := strings.TrimSpace(part.Text); text != "" { - parts = append(parts, text) - } + parts = append(parts, part.Text) } return strings.Join(parts, "\n") } -func appendNonEmpty(values []string, value string) []string { - value = strings.TrimSpace(value) - if value == "" { - return values - } - return append(values, value) -} - func parseJSONOrString(value string) []byte { - trimmed := strings.TrimSpace(value) - if trimmed == "" { + if value == "" { return nil } - data := []byte(trimmed) + data := []byte(value) if json.Valid(data) { return data } - quoted, err := json.Marshal(trimmed) + quoted, err := json.Marshal(value) if err != nil { return nil } diff --git a/go-providers/openai/mapper_test.go b/go-providers/openai/mapper_test.go index 1f702dc..ff32e42 100644 --- a/go-providers/openai/mapper_test.go +++ b/go-providers/openai/mapper_test.go @@ -548,3 +548,213 @@ func TestEmbeddingsFromResponseWithTokenInputDoesNotCaptureTexts(t *testing.T) { t.Fatalf("expected no input texts for tokenized input, got %v", result.InputTexts) } } + +func TestChatCompletionsFromRequestResponsePreservesWhitespace(t *testing.T) { + req := osdk.ChatCompletionNewParams{ + Model: shared.ChatModel("gpt-4o-mini"), + Messages: []osdk.ChatCompletionMessageParamUnion{ + osdk.SystemMessage(" system prompt "), + osdk.UserMessage(" user literal \\\\n\\\\n "), + }, + } + resp := &osdk.ChatCompletion{ + ID: "chatcmpl_whitespace", + Model: "gpt-4o-mini", + Choices: []osdk.ChatCompletionChoice{ + { + FinishReason: "stop", + Message: osdk.ChatCompletionMessage{ + Content: "\n assistant output \n", + }, + }, + }, + } + + generation, err := ChatCompletionsFromRequestResponse(req, resp) + if err != nil { + t.Fatalf("from request/response: %v", err) + } + + if generation.SystemPrompt != " system prompt " { + t.Fatalf("unexpected system prompt %q", generation.SystemPrompt) + } + if len(generation.Input) != 1 || len(generation.Input[0].Parts) != 1 { + t.Fatalf("expected single input text part, got %#v", generation.Input) + } + if generation.Input[0].Parts[0].Text != " user literal \\\\n\\\\n " { + t.Fatalf("unexpected input text %q", generation.Input[0].Parts[0].Text) + } + if len(generation.Output) != 1 || len(generation.Output[0].Parts) != 1 { + t.Fatalf("expected single output text part, got %#v", generation.Output) + } + if generation.Output[0].Parts[0].Text != "\n assistant output \n" { + t.Fatalf("unexpected output text %q", generation.Output[0].Parts[0].Text) + } +} + +func TestChatCompletionsFromStreamPreservesWhitespaceOnlyOutput(t *testing.T) { + req := osdk.ChatCompletionNewParams{ + Model: shared.ChatModel("gpt-4o-mini"), + } + + summary := ChatCompletionsStreamSummary{ + Chunks: []osdk.ChatCompletionChunk{ + { + ID: "chatcmpl_stream_whitespace", + Model: "gpt-4o-mini", + Choices: []osdk.ChatCompletionChunkChoice{ + { + Delta: osdk.ChatCompletionChunkChoiceDelta{ + Content: " ", + }, + FinishReason: "stop", + }, + }, + Usage: osdk.CompletionUsage{ + PromptTokens: 1, + CompletionTokens: 1, + TotalTokens: 2, + }, + }, + }, + } + + generation, err := ChatCompletionsFromStream(req, summary) + if err != nil { + t.Fatalf("from stream: %v", err) + } + if len(generation.Output) != 1 || len(generation.Output[0].Parts) != 1 { + t.Fatalf("expected single output text part, got %#v", generation.Output) + } + if generation.Output[0].Parts[0].Text != " " { + t.Fatalf("unexpected output text %q", generation.Output[0].Parts[0].Text) + } +} + +func TestResponsesFromRequestResponsePreservesWhitespace(t *testing.T) { + req := oresponses.ResponseNewParams{ + Model: shared.ResponsesModel("gpt-5"), + Instructions: param.NewOpt(" system instructions "), + Input: oresponses.ResponseNewParamsInputUnion{OfString: param.NewOpt(" user literal \\\\n\\\\n ")}, + } + resp := &oresponses.Response{ + ID: "resp_whitespace", + Model: shared.ResponsesModel("gpt-5"), + Status: oresponses.ResponseStatusCompleted, + Output: []oresponses.ResponseOutputItemUnion{ + { + Type: "message", + Content: []oresponses.ResponseOutputMessageContentUnion{ + {Type: "output_text", Text: "\n assistant output \n"}, + }, + }, + }, + } + + generation, err := ResponsesFromRequestResponse(req, resp) + if err != nil { + t.Fatalf("responses from request/response: %v", err) + } + if generation.SystemPrompt != " system instructions " { + t.Fatalf("unexpected system prompt %q", generation.SystemPrompt) + } + if len(generation.Input) != 1 || len(generation.Input[0].Parts) != 1 { + t.Fatalf("expected single input text part, got %#v", generation.Input) + } + if generation.Input[0].Parts[0].Text != " user literal \\\\n\\\\n " { + t.Fatalf("unexpected input text %q", generation.Input[0].Parts[0].Text) + } + if len(generation.Output) != 1 || len(generation.Output[0].Parts) != 1 { + t.Fatalf("expected single output text part, got %#v", generation.Output) + } + if generation.Output[0].Parts[0].Text != "\n assistant output \n" { + t.Fatalf("unexpected output text %q", generation.Output[0].Parts[0].Text) + } +} + +func TestResponsesFromStreamPreservesWhitespaceOnlyOutput(t *testing.T) { + req := oresponses.ResponseNewParams{ + Model: shared.ResponsesModel("gpt-5"), + } + summary := ResponsesStreamSummary{ + Events: []oresponses.ResponseStreamEventUnion{ + { + Type: "response.output_text.delta", + Delta: " ", + }, + { + Type: "response.completed", + }, + }, + } + + generation, err := ResponsesFromStream(req, summary) + if err != nil { + t.Fatalf("responses from stream: %v", err) + } + if len(generation.Output) != 1 || len(generation.Output[0].Parts) != 1 { + t.Fatalf("expected single output text part, got %#v", generation.Output) + } + if generation.Output[0].Parts[0].Text != " " { + t.Fatalf("unexpected output text %q", generation.Output[0].Parts[0].Text) + } +} + +func TestMapRequestMessagesPreservesEmptySystemEntries(t *testing.T) { + system := osdk.ChatCompletionSystemMessageParam{ + Content: osdk.ChatCompletionSystemMessageParamContentUnion{ + OfString: param.NewOpt(""), + }, + } + developer := osdk.ChatCompletionDeveloperMessageParam{ + Content: osdk.ChatCompletionDeveloperMessageParamContentUnion{ + OfString: param.NewOpt("developer instruction"), + }, + } + + input, systemPrompt := mapRequestMessages([]osdk.ChatCompletionMessageParamUnion{ + {OfSystem: &system}, + {OfDeveloper: &developer}, + }) + + if len(input) != 0 { + t.Fatalf("expected no mapped user/assistant/tool input messages, got %#v", input) + } + if systemPrompt != "\n\ndeveloper instruction" { + t.Fatalf("expected preserved empty system entry before developer prompt, got %q", systemPrompt) + } +} + +func TestMapToolMessagePreservesEmptyParts(t *testing.T) { + part := mapToolMessage(&osdk.ChatCompletionToolMessageParam{ + ToolCallID: "call_1", + Content: osdk.ChatCompletionToolMessageParamContentUnion{ + OfArrayOfContentParts: []osdk.ChatCompletionContentPartTextParam{ + {Text: ""}, + {Text: ""}, + }, + }, + }) + + if part == nil { + t.Fatalf("expected tool result part for empty text segments") + } + if part.ToolResult == nil { + t.Fatalf("expected tool result payload, got %#v", part) + } + if part.ToolResult.Content != "\n" { + t.Fatalf("expected newline-preserved content, got %q", part.ToolResult.Content) + } +} + +func TestParseJSONOrStringPreservesWhitespace(t *testing.T) { + if got := string(parseJSONOrString(" {\"city\":\"Paris\"} ")); got != " {\"city\":\"Paris\"} " { + t.Fatalf("expected JSON bytes to preserve whitespace, got %q", got) + } + if got := string(parseJSONOrString(" raw value ")); got != "\" raw value \"" { + t.Fatalf("expected quoted raw string with whitespace preserved, got %q", got) + } + if got := parseJSONOrString(""); got != nil { + t.Fatalf("expected nil for empty string, got %q", string(got)) + } +} diff --git a/go-providers/openai/responses_mapper.go b/go-providers/openai/responses_mapper.go index e8c6ff8..efb61f2 100644 --- a/go-providers/openai/responses_mapper.go +++ b/go-providers/openai/responses_mapper.go @@ -33,7 +33,7 @@ func ResponsesFromRequestResponse(req responses.ResponseNewParams, resp *respons maxTokens, temperature, topP, toolChoice, thinkingEnabled, thinkingBudget := mapResponsesRequestControls(requestPayload) requestModel := string(req.Model) - responseModel := strings.TrimSpace(string(resp.Model)) + responseModel := string(resp.Model) if responseModel == "" { responseModel = requestModel } @@ -119,11 +119,11 @@ func ResponsesFromStream(req responses.ResponseNewParams, summary ResponsesStrea for i := range summary.Events { event := summary.Events[i] - eventType := strings.TrimSpace(event.Type) + eventType := event.Type if event.Response.ID != "" { responseID = event.Response.ID - if model := strings.TrimSpace(string(event.Response.Model)); model != "" { + if model := string(event.Response.Model); model != "" { responseModel = model } usage = mapResponsesUsage(event.Response.Usage) @@ -170,7 +170,7 @@ func ResponsesFromStream(req responses.ResponseNewParams, summary ResponsesStrea } output := []sigil.Message{} - if generated := strings.TrimSpace(text.String()); generated != "" { + if generated := text.String(); generated != "" { output = append(output, sigil.Message{Role: sigil.RoleAssistant, Parts: []sigil.Part{sigil.TextPart(generated)}}) } @@ -250,7 +250,7 @@ func mapResponsesRequestInput(payload map[string]any) ([]sigil.Message, string) systemPrompts := make([]string, 0, 2) if instructions, ok := payload["instructions"].(string); ok { - systemPrompts = appendNonEmpty(systemPrompts, instructions) + systemPrompts = append(systemPrompts, instructions) } rawInput, hasInput := payload["input"] @@ -260,7 +260,7 @@ func mapResponsesRequestInput(payload map[string]any) ([]sigil.Message, string) switch typed := rawInput.(type) { case string: - if text := strings.TrimSpace(typed); text != "" { + if text := typed; text != "" { input = append(input, sigil.Message{Role: sigil.RoleUser, Parts: []sigil.Part{sigil.TextPart(text)}}) } case []any: @@ -274,7 +274,7 @@ func mapResponsesRequestInput(payload map[string]any) ([]sigil.Message, string) role := strings.ToLower(strings.TrimSpace(fmt.Sprintf("%v", item["role"]))) if itemType == "message" && (role == "system" || role == "developer") { - systemPrompts = appendNonEmpty(systemPrompts, extractResponsesText(item["content"])) + systemPrompts = append(systemPrompts, extractResponsesText(item["content"])) continue } @@ -283,11 +283,11 @@ func mapResponsesRequestInput(payload map[string]any) ([]sigil.Message, string) if content == "" { content = jsonValueText(item["output"]) } - if strings.TrimSpace(content) == "" { + if content == "" { continue } part := sigil.ToolResultPart(sigil.ToolResult{ - ToolCallID: strings.TrimSpace(fmt.Sprintf("%v", item["call_id"])), + ToolCallID: fmt.Sprintf("%v", item["call_id"]), Content: content, ContentJSON: parseJSONOrString(content), }) @@ -298,7 +298,7 @@ func mapResponsesRequestInput(payload map[string]any) ([]sigil.Message, string) if itemType == "message" || role != "" { content := extractResponsesText(item["content"]) - if strings.TrimSpace(content) == "" { + if content == "" { continue } @@ -329,7 +329,7 @@ func mapResponsesOutput(items []responses.ResponseOutputItemUnion) []sigil.Messa switch item.Type { case "message": text := extractResponsesOutputMessageText(item.Content) - if strings.TrimSpace(text) == "" { + if text == "" { continue } out = append(out, sigil.Message{Role: sigil.RoleAssistant, Parts: []sigil.Part{sigil.TextPart(text)}}) @@ -345,7 +345,7 @@ func mapResponsesOutput(items []responses.ResponseOutputItemUnion) []sigil.Messa part.Metadata.ProviderType = "tool_call" out = append(out, sigil.Message{Role: sigil.RoleAssistant, Parts: []sigil.Part{part}}) default: - fallback := strings.TrimSpace(extractResponsesOutputFallback(item)) + fallback := extractResponsesOutputFallback(item) if fallback != "" { out = append(out, sigil.Message{Role: sigil.RoleAssistant, Parts: []sigil.Part{sigil.TextPart(fallback)}}) } @@ -369,13 +369,13 @@ func mapResponsesTools(value any) []sigil.ToolDefinition { } toolType := strings.TrimSpace(fmt.Sprintf("%v", tool["type"])) if toolType == "function" { - name := strings.TrimSpace(fmt.Sprintf("%v", tool["name"])) - if name == "" { + name := fmt.Sprintf("%v", tool["name"]) + if strings.TrimSpace(name) == "" { continue } definition := sigil.ToolDefinition{ Name: name, - Description: strings.TrimSpace(fmt.Sprintf("%v", tool["description"])), + Description: fmt.Sprintf("%v", tool["description"]), Type: "function", } if parameters, exists := tool["parameters"]; exists { @@ -385,8 +385,8 @@ func mapResponsesTools(value any) []sigil.ToolDefinition { continue } - name := strings.TrimSpace(fmt.Sprintf("%v", tool["name"])) - if toolType != "" && name != "" { + name := fmt.Sprintf("%v", tool["name"]) + if toolType != "" && strings.TrimSpace(name) != "" { out = append(out, sigil.ToolDefinition{Name: name, Type: toolType}) } } @@ -442,7 +442,7 @@ func mapResponsesRequestControls(payload map[string]any) (*int64, *float64, *flo func extractResponsesText(value any) string { switch typed := value.(type) { case string: - return strings.TrimSpace(typed) + return typed case []any: parts := make([]string, 0, len(typed)) for i := range typed { @@ -453,13 +453,13 @@ func extractResponsesText(value any) string { return strings.Join(parts, "\n") case map[string]any: if text, ok := typed["text"].(string); ok { - return strings.TrimSpace(text) + return text } if text, ok := typed["content"].(string); ok { - return strings.TrimSpace(text) + return text } if refusal, ok := typed["refusal"].(string); ok { - return strings.TrimSpace(refusal) + return refusal } } return "" @@ -471,11 +471,11 @@ func extractResponsesOutputMessageText(content []responses.ResponseOutputMessage item := content[i] switch item.Type { case "output_text": - if text := strings.TrimSpace(item.Text); text != "" { + if text := item.Text; text != "" { parts = append(parts, text) } case "refusal": - if refusal := strings.TrimSpace(item.Refusal); refusal != "" { + if refusal := item.Refusal; refusal != "" { parts = append(parts, refusal) } } diff --git a/go-providers/openai/stream_mapper.go b/go-providers/openai/stream_mapper.go index 8e2fbc3..096c145 100644 --- a/go-providers/openai/stream_mapper.go +++ b/go-providers/openai/stream_mapper.go @@ -94,7 +94,7 @@ func ChatCompletionsFromStream(req osdk.ChatCompletionNewParams, summary ChatCom } assistantParts := make([]sigil.Part, 0, 1+len(order)) - if generated := strings.TrimSpace(text.String()); generated != "" { + if generated := text.String(); generated != "" { assistantParts = append(assistantParts, sigil.TextPart(generated)) } for _, index := range order { diff --git a/go/README.md b/go/README.md index d09a719..8ac763f 100644 --- a/go/README.md +++ b/go/README.md @@ -24,6 +24,7 @@ Framework modules: - `ModelRef` bundles `provider + model`. - `AgentName` and `AgentVersion` are optional generation/tool identity fields. - `SystemPrompt` is separate from messages. +- `ToolDefinition.Deferred` records whether a tool is marked as deferred. - Request controls are optional first-class fields: - `MaxTokens` - `Temperature` @@ -82,6 +83,9 @@ cfg.GenerationExport.QueueSize = 2000 cfg.GenerationExport.MaxRetries = 5 cfg.GenerationExport.InitialBackoff = 100 * time.Millisecond cfg.GenerationExport.MaxBackoff = 5 * time.Second +cfg.GenerationExport.GRPCMaxSendMessageBytes = 16 << 20 +cfg.GenerationExport.GRPCMaxReceiveMessageBytes = 16 << 20 +cfg.GenerationExport.PayloadMaxBytes = 16 << 20 // Sigil API base used by helpers like SubmitConversationRating. cfg.API.Endpoint = "http://localhost:8080" diff --git a/go/sigil/client.go b/go/sigil/client.go index e084cbb..fe1741b 100644 --- a/go/sigil/client.go +++ b/go/sigil/client.go @@ -69,18 +69,24 @@ const ( ) type GenerationExportConfig struct { - Protocol GenerationExportProtocol - Endpoint string - Headers map[string]string - Auth AuthConfig - Insecure bool - BatchSize int - FlushInterval time.Duration - QueueSize int - MaxRetries int - InitialBackoff time.Duration - MaxBackoff time.Duration - PayloadMaxBytes int + Protocol GenerationExportProtocol + Endpoint string + Headers map[string]string + Auth AuthConfig + Insecure bool + // GRPCMaxSendMessageBytes controls the gRPC per-message send cap used by + // the SDK generation exporter. + GRPCMaxSendMessageBytes int + // GRPCMaxReceiveMessageBytes controls the gRPC per-message receive cap used + // by the SDK generation exporter. + GRPCMaxReceiveMessageBytes int + BatchSize int + FlushInterval time.Duration + QueueSize int + MaxRetries int + InitialBackoff time.Duration + MaxBackoff time.Duration + PayloadMaxBytes int } type APIConfig struct { @@ -89,6 +95,10 @@ type APIConfig struct { const instrumentationName = "github.com/grafana/sigil/sdks/go/sigil" const ( + defaultGRPCMaxSendMessageBytes = 16 << 20 + defaultGRPCMaxReceiveMessageBytes = 16 << 20 + defaultGenerationPayloadMaxBytes = 16 << 20 + sdkMetadataKeyName = "sigil.sdk.name" sdkName = "sdk-go" @@ -152,17 +162,19 @@ var ( func DefaultConfig() Config { return Config{ GenerationExport: GenerationExportConfig{ - Protocol: GenerationExportProtocolGRPC, - Endpoint: "localhost:4317", - Auth: AuthConfig{Mode: ExportAuthModeNone}, - Insecure: true, - BatchSize: 100, - FlushInterval: time.Second, - QueueSize: 2000, - MaxRetries: 5, - InitialBackoff: 100 * time.Millisecond, - MaxBackoff: 5 * time.Second, - PayloadMaxBytes: 4 << 20, + Protocol: GenerationExportProtocolGRPC, + Endpoint: "localhost:4317", + Auth: AuthConfig{Mode: ExportAuthModeNone}, + Insecure: true, + GRPCMaxSendMessageBytes: defaultGRPCMaxSendMessageBytes, + GRPCMaxReceiveMessageBytes: defaultGRPCMaxReceiveMessageBytes, + BatchSize: 100, + FlushInterval: time.Second, + QueueSize: 2000, + MaxRetries: 5, + InitialBackoff: 100 * time.Millisecond, + MaxBackoff: 5 * time.Second, + PayloadMaxBytes: defaultGenerationPayloadMaxBytes, }, API: APIConfig{ Endpoint: "http://localhost:8080", diff --git a/go/sigil/client_test.go b/go/sigil/client_test.go index 5d24c3b..a494b9b 100644 --- a/go/sigil/client_test.go +++ b/go/sigil/client_test.go @@ -17,6 +17,20 @@ import ( "go.opentelemetry.io/otel/trace" ) +func TestDefaultConfigGenerationExportMessageAndPayloadLimits(t *testing.T) { + cfg := DefaultConfig() + + if cfg.GenerationExport.GRPCMaxSendMessageBytes != defaultGRPCMaxSendMessageBytes { + t.Fatalf("expected grpc max send %d, got %d", defaultGRPCMaxSendMessageBytes, cfg.GenerationExport.GRPCMaxSendMessageBytes) + } + if cfg.GenerationExport.GRPCMaxReceiveMessageBytes != defaultGRPCMaxReceiveMessageBytes { + t.Fatalf("expected grpc max receive %d, got %d", defaultGRPCMaxReceiveMessageBytes, cfg.GenerationExport.GRPCMaxReceiveMessageBytes) + } + if cfg.GenerationExport.PayloadMaxBytes != defaultGenerationPayloadMaxBytes { + t.Fatalf("expected payload max bytes %d, got %d", defaultGenerationPayloadMaxBytes, cfg.GenerationExport.PayloadMaxBytes) + } +} + func TestStartGenerationEnqueuesArtifacts(t *testing.T) { client, recorder, _ := newTestClient(t, Config{ Now: func() time.Time { diff --git a/go/sigil/exporter.go b/go/sigil/exporter.go index 7708678..69cd17c 100644 --- a/go/sigil/exporter.go +++ b/go/sigil/exporter.go @@ -77,6 +77,14 @@ func newGRPCGenerationExporter(cfg GenerationExportConfig) (generationExporter, if err != nil { return nil, err } + maxSendMessageBytes := cfg.GRPCMaxSendMessageBytes + if maxSendMessageBytes <= 0 { + maxSendMessageBytes = defaultGRPCMaxSendMessageBytes + } + maxReceiveMessageBytes := cfg.GRPCMaxReceiveMessageBytes + if maxReceiveMessageBytes <= 0 { + maxReceiveMessageBytes = defaultGRPCMaxReceiveMessageBytes + } transportCreds := credentials.NewTLS(&tls.Config{ MinVersion: tls.VersionTLS12, @@ -86,7 +94,14 @@ func newGRPCGenerationExporter(cfg GenerationExportConfig) (generationExporter, transportCreds = insecure.NewCredentials() } - conn, err := grpc.NewClient(endpoint, grpc.WithTransportCredentials(transportCreds)) + conn, err := grpc.NewClient( + endpoint, + grpc.WithTransportCredentials(transportCreds), + grpc.WithDefaultCallOptions( + grpc.MaxCallSendMsgSize(maxSendMessageBytes), + grpc.MaxCallRecvMsgSize(maxReceiveMessageBytes), + ), + ) if err != nil { return nil, fmt.Errorf("dial generation ingest grpc endpoint %q: %w", endpoint, err) } @@ -214,6 +229,12 @@ func mergeGenerationExportConfig(base, override GenerationExportConfig) Generati } out.Auth = mergeAuthConfig(out.Auth, override.Auth) out.Insecure = override.Insecure + if override.GRPCMaxSendMessageBytes > 0 { + out.GRPCMaxSendMessageBytes = override.GRPCMaxSendMessageBytes + } + if override.GRPCMaxReceiveMessageBytes > 0 { + out.GRPCMaxReceiveMessageBytes = override.GRPCMaxReceiveMessageBytes + } if override.BatchSize > 0 { out.BatchSize = override.BatchSize } diff --git a/go/sigil/exporter_test.go b/go/sigil/exporter_test.go index 540f435..b3d6b06 100644 --- a/go/sigil/exporter_test.go +++ b/go/sigil/exporter_test.go @@ -212,6 +212,25 @@ func TestMergeGenerationExportConfigInsecure(t *testing.T) { } } +func TestMergeGenerationExportConfigGRPCMessageLimits(t *testing.T) { + base := GenerationExportConfig{ + GRPCMaxSendMessageBytes: 2 << 20, + GRPCMaxReceiveMessageBytes: 3 << 20, + } + override := GenerationExportConfig{ + GRPCMaxSendMessageBytes: 8 << 20, + GRPCMaxReceiveMessageBytes: 9 << 20, + } + got := mergeGenerationExportConfig(base, override) + + if got.GRPCMaxSendMessageBytes != 8<<20 { + t.Fatalf("expected grpc max send 8MiB, got %d", got.GRPCMaxSendMessageBytes) + } + if got.GRPCMaxReceiveMessageBytes != 9<<20 { + t.Fatalf("expected grpc max receive 9MiB, got %d", got.GRPCMaxReceiveMessageBytes) + } +} + func TestNewHTTPGenerationExporterUsesEndpointScheme(t *testing.T) { testCases := []struct { name string diff --git a/go/sigil/exporter_transport_test.go b/go/sigil/exporter_transport_test.go index 1da0a24..dce8bee 100644 --- a/go/sigil/exporter_transport_test.go +++ b/go/sigil/exporter_transport_test.go @@ -8,6 +8,7 @@ import ( "net" "net/http" "net/http/httptest" + "strings" "sync" "testing" "time" @@ -44,6 +45,76 @@ func TestSDKExportsGenerationOverGRPC_AllPropertiesRoundTrip(t *testing.T) { } } +func TestSDKExportsGenerationOverGRPCAboveDefaultMessageLimit(t *testing.T) { + ingest := &capturingIngestServer{} + + grpcServer := grpc.NewServer( + grpc.MaxRecvMsgSize(defaultGRPCMaxReceiveMessageBytes), + grpc.MaxSendMsgSize(defaultGRPCMaxSendMessageBytes), + ) + sigilv1.RegisterGenerationIngestServiceServer(grpcServer, ingest) + + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("listen grpc: %v", err) + } + go func() { + _ = grpcServer.Serve(listener) + }() + t.Cleanup(func() { + grpcServer.Stop() + _ = listener.Close() + }) + + client := NewClient(Config{ + Tracer: noop.NewTracerProvider().Tracer("test"), + GenerationExport: GenerationExportConfig{ + Protocol: GenerationExportProtocolGRPC, + Endpoint: listener.Addr().String(), + Insecure: true, + GRPCMaxSendMessageBytes: defaultGRPCMaxSendMessageBytes, + GRPCMaxReceiveMessageBytes: defaultGRPCMaxReceiveMessageBytes, + PayloadMaxBytes: 8 << 20, + BatchSize: 1, + QueueSize: 10, + FlushInterval: time.Hour, + MaxRetries: 1, + InitialBackoff: time.Millisecond, + MaxBackoff: 10 * time.Millisecond, + }, + }) + + largeText := strings.Repeat("x", 5<<20) + _, rec := client.StartGeneration(context.Background(), GenerationStart{ + Model: ModelRef{ + Provider: "openai", + Name: "gpt-5", + }, + }) + rec.SetResult(Generation{ + Input: []Message{UserTextMessage(largeText)}, + Output: []Message{AssistantTextMessage("ok")}, + }, nil) + rec.End() + if err := rec.Err(); err != nil { + t.Fatalf("expected large grpc payload export to succeed, got %v", err) + } + + shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 5*time.Second) + defer shutdownCancel() + if err := client.Shutdown(shutdownCtx); err != nil { + t.Fatalf("shutdown client: %v", err) + } + + request := ingest.singleRequest(t) + if len(request.Generations) != 1 { + t.Fatalf("expected one generation in captured request, got %d", len(request.Generations)) + } + if got := request.Generations[0].GetInput()[0].GetParts()[0].GetText(); got != largeText { + t.Fatalf("unexpected large input text size=%d", len(got)) + } +} + func TestSDKExportRoundTripProperties(t *testing.T) { for seed := uint64(1); seed <= 20; seed++ { t.Run(fmt.Sprintf("seed-%d", seed), func(t *testing.T) { @@ -242,7 +313,7 @@ func payloadFromSeed(seed uint64) (GenerationStart, Generation) { }, SystemPrompt: "system-" + randomASCII(rnd, 10), Tools: []ToolDefinition{ - {Name: "tool-" + randomASCII(rnd, 5), Description: "desc-" + randomASCII(rnd, 6), Type: "function", InputSchema: []byte(`{"type":"object"}`)}, + {Name: "tool-" + randomASCII(rnd, 5), Description: "desc-" + randomASCII(rnd, 6), Type: "function", InputSchema: []byte(`{"type":"object"}`), Deferred: seed%2 == 0}, }, MaxTokens: int64Ptr(int64(rnd.Intn(1024) + 1)), Temperature: float64Ptr(float64(rnd.Intn(100)) / 100), diff --git a/go/sigil/generation.go b/go/sigil/generation.go index 48f68c4..08a14c0 100644 --- a/go/sigil/generation.go +++ b/go/sigil/generation.go @@ -29,6 +29,7 @@ type ToolDefinition struct { Description string `json:"description,omitempty"` Type string `json:"type,omitempty"` InputSchema json.RawMessage `json:"input_schema,omitempty"` + Deferred bool `json:"deferred,omitempty"` } // Generation is the normalized, provider-agnostic generation payload. diff --git a/go/sigil/internal/gen/sigil/v1/generation_ingest.pb.go b/go/sigil/internal/gen/sigil/v1/generation_ingest.pb.go index 1e036a3..7d0323e 100644 --- a/go/sigil/internal/gen/sigil/v1/generation_ingest.pb.go +++ b/go/sigil/internal/gen/sigil/v1/generation_ingest.pb.go @@ -747,6 +747,7 @@ type ToolDefinition struct { Description string `protobuf:"bytes,2,opt,name=description,proto3" json:"description,omitempty"` Type string `protobuf:"bytes,3,opt,name=type,proto3" json:"type,omitempty"` InputSchemaJson []byte `protobuf:"bytes,4,opt,name=input_schema_json,json=inputSchemaJson,proto3" json:"input_schema_json,omitempty"` + Deferred bool `protobuf:"varint,5,opt,name=deferred,proto3" json:"deferred,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -809,6 +810,13 @@ func (x *ToolDefinition) GetInputSchemaJson() []byte { return nil } +func (x *ToolDefinition) GetDeferred() bool { + if x != nil { + return x.Deferred + } + return false +} + type TokenUsage struct { state protoimpl.MessageState `protogen:"open.v1"` InputTokens int64 `protobuf:"varint,1,opt,name=input_tokens,json=inputTokens,proto3" json:"input_tokens,omitempty"` @@ -1279,12 +1287,13 @@ const file_sigil_v1_generation_ingest_proto_rawDesc = "" + "\aMessage\x12)\n" + "\x04role\x18\x01 \x01(\x0e2\x15.sigil.v1.MessageRoleR\x04role\x12\x12\n" + "\x04name\x18\x02 \x01(\tR\x04name\x12$\n" + - "\x05parts\x18\x03 \x03(\v2\x0e.sigil.v1.PartR\x05parts\"\x86\x01\n" + + "\x05parts\x18\x03 \x03(\v2\x0e.sigil.v1.PartR\x05parts\"\xa2\x01\n" + "\x0eToolDefinition\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12 \n" + "\vdescription\x18\x02 \x01(\tR\vdescription\x12\x12\n" + "\x04type\x18\x03 \x01(\tR\x04type\x12*\n" + - "\x11input_schema_json\x18\x04 \x01(\fR\x0finputSchemaJson\"\x92\x02\n" + + "\x11input_schema_json\x18\x04 \x01(\fR\x0finputSchemaJson\x12\x1a\n" + + "\bdeferred\x18\x05 \x01(\bR\bdeferred\"\x92\x02\n" + "\n" + "TokenUsage\x12!\n" + "\finput_tokens\x18\x01 \x01(\x03R\vinputTokens\x12#\n" + diff --git a/go/sigil/proto_mapping.go b/go/sigil/proto_mapping.go index 87ada5a..e9d22f1 100644 --- a/go/sigil/proto_mapping.go +++ b/go/sigil/proto_mapping.go @@ -172,6 +172,7 @@ func mapToolsToProto(tools []ToolDefinition) []*sigilv1.ToolDefinition { Description: tools[i].Description, Type: tools[i].Type, InputSchemaJson: append([]byte(nil), tools[i].InputSchema...), + Deferred: tools[i].Deferred, }) } return out diff --git a/go/sigil/validation.go b/go/sigil/validation.go index 8b25124..a730adf 100644 --- a/go/sigil/validation.go +++ b/go/sigil/validation.go @@ -103,10 +103,10 @@ func validatePart(path string, messageIndex, partIndex int, role Role, part Part } fieldCount := 0 - if strings.TrimSpace(part.Text) != "" { + if part.Text != "" { fieldCount++ } - if strings.TrimSpace(part.Thinking) != "" { + if part.Thinking != "" { fieldCount++ } if part.ToolCall != nil { @@ -122,14 +122,14 @@ func validatePart(path string, messageIndex, partIndex int, role Role, part Part switch part.Kind { case PartKindText: - if strings.TrimSpace(part.Text) == "" { + if part.Text == "" { return fmt.Errorf("%s[%d].parts[%d].text is required", path, messageIndex, partIndex) } case PartKindThinking: if role != RoleAssistant { return fmt.Errorf("%s[%d].parts[%d].thinking only allowed for assistant role", path, messageIndex, partIndex) } - if strings.TrimSpace(part.Thinking) == "" { + if part.Thinking == "" { return fmt.Errorf("%s[%d].parts[%d].thinking is required", path, messageIndex, partIndex) } case PartKindToolCall: diff --git a/go/sigil/validation_test.go b/go/sigil/validation_test.go index 6ab1124..7133d59 100644 --- a/go/sigil/validation_test.go +++ b/go/sigil/validation_test.go @@ -107,3 +107,28 @@ func TestValidateGenerationAllowsConversationAndResponseFields(t *testing.T) { t.Fatalf("expected valid generation, got %v", err) } } + +func TestValidateGenerationAllowsWhitespaceOnlyTextAndThinking(t *testing.T) { + g := Generation{ + Model: ModelRef{ + Provider: "anthropic", + Name: "claude-sonnet-4-5", + }, + Input: []Message{ + { + Role: RoleUser, + Parts: []Part{TextPart(" ")}, + }, + }, + Output: []Message{ + { + Role: RoleAssistant, + Parts: []Part{ThinkingPart(" \n\t ")}, + }, + }, + } + + if err := ValidateGeneration(g); err != nil { + t.Fatalf("expected valid generation, got %v", err) + } +} diff --git a/proto/sigil/v1/generation_ingest.proto b/proto/sigil/v1/generation_ingest.proto index 733a27f..9d5858b 100644 --- a/proto/sigil/v1/generation_ingest.proto +++ b/proto/sigil/v1/generation_ingest.proto @@ -83,6 +83,7 @@ message ToolDefinition { string description = 2; string type = 3; bytes input_schema_json = 4; + bool deferred = 5; } message TokenUsage { From d20f0325a4e4b54645141cc87ffc15554a56fd2f Mon Sep 17 00:00:00 2001 From: Cyril Tovena Date: Wed, 4 Mar 2026 22:07:05 +0100 Subject: [PATCH 019/133] feat: add conversation title attribute across SDK, query, and plugin (#253) * feat: add conversation title span attribute across SDK and query Introduce a new span attribute and SDK support so callers can name conversations without changing the generation ingest schema. Propagate the title through Go provider wrappers, expose it in conversation search results, and surface it in the Grafana plugin conversation list/header with fallback behavior. Update tests and reference docs to cover mapping, query aggregation, and frontend rendering semantics. * fix(plugin): reset font-family on idCellPrimary to prevent monospace titles In extended columns mode, the idCell container sets fontFamily to monospace, which was inherited by idCellPrimary. This caused human-readable conversation titles to render in monospace font. Override with the default fontFamily on idCellPrimary so titles display correctly while the secondary ID line retains monospace. Applied via @cursor push command * fix(plugin): use "Conversation" label when title is shown instead of ID The header label was hardcoded as "Conversation ID" but the value conditionally displayed a human-readable title, creating a semantic mismatch. Now the label reads "Conversation" when a title is present and "Conversation ID" when showing the raw ID. Adds regression tests for both label states. * fix(plugin): correct test assertion for conversation label when title is present The test asserted 'Conversation ID' as the label text after navigating to a conversation that has a title. However, ConversationSummaryHeader renders the label as 'Conversation' (not 'Conversation ID') when a title is present. Updated the assertion to match the actual component behavior. Applied via @cursor push command * fix(plugin): use JSX line break in compact-mode tooltip content A literal \n in a string passed to Grafana's Tooltip content prop renders as collapsed whitespace in HTML, not a visible line break. Replace the template literal with a JSX fragment containing a
element so the conversation title and ID display on separate lines. Applied via @cursor push command --------- Co-authored-by: Cursor Agent Co-authored-by: cursor[bot] <206951365+cursor[bot]@users.noreply.github.com> --- go-providers/anthropic/README.md | 1 + go-providers/anthropic/mapper.go | 41 +++---- go-providers/anthropic/mapper_test.go | 4 + go-providers/anthropic/options.go | 20 ++-- go-providers/anthropic/record.go | 18 ++-- go-providers/anthropic/stream_mapper.go | 41 +++---- go-providers/gemini/README.md | 1 + go-providers/gemini/mapper.go | 41 +++---- go-providers/gemini/mapper_test.go | 4 + go-providers/gemini/options.go | 20 ++-- go-providers/gemini/record.go | 18 ++-- go-providers/gemini/stream_mapper.go | 41 +++---- go-providers/openai/README.md | 2 + go-providers/openai/mapper.go | 41 +++---- go-providers/openai/mapper_test.go | 4 + go-providers/openai/options.go | 20 ++-- go-providers/openai/record.go | 36 ++++--- go-providers/openai/responses_mapper.go | 82 +++++++------- go-providers/openai/stream_mapper.go | 41 +++---- go/README.md | 2 + go/sigil/client.go | 70 +++++++----- go/sigil/client_test.go | 105 ++++++++++++++++-- go/sigil/context.go | 14 +++ go/sigil/generation.go | 138 ++++++++++++------------ go/sigil/tool.go | 17 +-- 25 files changed, 502 insertions(+), 320 deletions(-) diff --git a/go-providers/anthropic/README.md b/go-providers/anthropic/README.md index a764618..b80578a 100644 --- a/go-providers/anthropic/README.md +++ b/go-providers/anthropic/README.md @@ -28,6 +28,7 @@ This helper currently supports Anthropic Messages APIs only. Native Anthropic em ```go resp, err := anthropic.Message(ctx, sigilClient, providerClient, req, anthropic.WithConversationID("conv-1"), + anthropic.WithConversationTitle("Weather follow-up"), anthropic.WithAgentName("assistant-anthropic"), anthropic.WithAgentVersion("1.0.0"), ) diff --git a/go-providers/anthropic/mapper.go b/go-providers/anthropic/mapper.go index efa9210..6ccf2f8 100644 --- a/go-providers/anthropic/mapper.go +++ b/go-providers/anthropic/mapper.go @@ -59,26 +59,27 @@ func FromRequestResponse(req asdk.BetaMessageNewParams, resp *asdk.BetaMessage, metadata = mergeServerToolUsageMetadata(metadata, resp.Usage.ServerToolUse) generation := sigil.Generation{ - ConversationID: options.conversationID, - AgentName: options.agentName, - AgentVersion: options.agentVersion, - Model: sigil.ModelRef{Provider: options.providerName, Name: requestModel}, - ResponseID: resp.ID, - ResponseModel: responseModel, - SystemPrompt: mapSystemPrompt(req.System), - Input: input, - Output: output, - Tools: mapTools(req.Tools), - MaxTokens: maxTokens, - Temperature: temperature, - TopP: topP, - ToolChoice: toolChoice, - ThinkingEnabled: thinkingEnabled, - Usage: mapUsage(resp.Usage), - StopReason: string(resp.StopReason), - Tags: cloneStringMap(options.tags), - Metadata: metadata, - Artifacts: artifacts, + ConversationID: options.conversationID, + ConversationTitle: options.conversationTitle, + AgentName: options.agentName, + AgentVersion: options.agentVersion, + Model: sigil.ModelRef{Provider: options.providerName, Name: requestModel}, + ResponseID: resp.ID, + ResponseModel: responseModel, + SystemPrompt: mapSystemPrompt(req.System), + Input: input, + Output: output, + Tools: mapTools(req.Tools), + MaxTokens: maxTokens, + Temperature: temperature, + TopP: topP, + ToolChoice: toolChoice, + ThinkingEnabled: thinkingEnabled, + Usage: mapUsage(resp.Usage), + StopReason: string(resp.StopReason), + Tags: cloneStringMap(options.tags), + Metadata: metadata, + Artifacts: artifacts, } if err := generation.Validate(); err != nil { diff --git a/go-providers/anthropic/mapper_test.go b/go-providers/anthropic/mapper_test.go index 7adb1cc..3e88c64 100644 --- a/go-providers/anthropic/mapper_test.go +++ b/go-providers/anthropic/mapper_test.go @@ -32,6 +32,7 @@ func TestFromRequestResponse(t *testing.T) { generation, err := FromRequestResponse(req, resp, WithConversationID("conv-9b2f"), + WithConversationTitle("Paris weather"), WithAgentName("agent-anthropic"), WithAgentVersion("v-anthropic"), WithTag("tenant", "t-123"), @@ -49,6 +50,9 @@ func TestFromRequestResponse(t *testing.T) { if generation.ConversationID != "conv-9b2f" { t.Fatalf("expected conversation id conv-9b2f, got %q", generation.ConversationID) } + if generation.ConversationTitle != "Paris weather" { + t.Fatalf("expected conversation title Paris weather, got %q", generation.ConversationTitle) + } if generation.AgentName != "agent-anthropic" { t.Fatalf("expected agent-anthropic, got %q", generation.AgentName) } diff --git a/go-providers/anthropic/options.go b/go-providers/anthropic/options.go index 57c6ab8..5b69892 100644 --- a/go-providers/anthropic/options.go +++ b/go-providers/anthropic/options.go @@ -4,12 +4,13 @@ package anthropic type Option func(*mapperOptions) type mapperOptions struct { - providerName string - conversationID string - agentName string - agentVersion string - tags map[string]string - metadata map[string]any + providerName string + conversationID string + conversationTitle string + agentName string + agentVersion string + tags map[string]string + metadata map[string]any includeRequestArtifact bool includeResponseArtifact bool @@ -45,6 +46,13 @@ func WithConversationID(conversationID string) Option { } } +// WithConversationTitle sets Generation.ConversationTitle. +func WithConversationTitle(conversationTitle string) Option { + return func(options *mapperOptions) { + options.conversationTitle = conversationTitle + } +} + // WithAgentName sets Generation.AgentName. func WithAgentName(agentName string) Option { return func(options *mapperOptions) { diff --git a/go-providers/anthropic/record.go b/go-providers/anthropic/record.go index 8d1b639..956da21 100644 --- a/go-providers/anthropic/record.go +++ b/go-providers/anthropic/record.go @@ -22,10 +22,11 @@ func Message( options := applyOptions(opts) ctx, rec := client.StartGeneration(ctx, sigil.GenerationStart{ - ConversationID: options.conversationID, - AgentName: options.agentName, - AgentVersion: options.agentVersion, - Model: sigil.ModelRef{Provider: options.providerName, Name: string(req.Model)}, + ConversationID: options.conversationID, + ConversationTitle: options.conversationTitle, + AgentName: options.agentName, + AgentVersion: options.agentVersion, + Model: sigil.ModelRef{Provider: options.providerName, Name: string(req.Model)}, }) defer rec.End() @@ -53,10 +54,11 @@ func MessageStream( options := applyOptions(opts) ctx, rec := client.StartStreamingGeneration(ctx, sigil.GenerationStart{ - ConversationID: options.conversationID, - AgentName: options.agentName, - AgentVersion: options.agentVersion, - Model: sigil.ModelRef{Provider: options.providerName, Name: string(req.Model)}, + ConversationID: options.conversationID, + ConversationTitle: options.conversationTitle, + AgentName: options.agentName, + AgentVersion: options.agentVersion, + Model: sigil.ModelRef{Provider: options.providerName, Name: string(req.Model)}, }) defer rec.End() diff --git a/go-providers/anthropic/stream_mapper.go b/go-providers/anthropic/stream_mapper.go index d312882..b41f3fc 100644 --- a/go-providers/anthropic/stream_mapper.go +++ b/go-providers/anthropic/stream_mapper.go @@ -107,26 +107,27 @@ func FromStream(req asdk.BetaMessageNewParams, summary StreamSummary, opts ...Op } generation := sigil.Generation{ - ConversationID: options.conversationID, - AgentName: options.agentName, - AgentVersion: options.agentVersion, - Model: sigil.ModelRef{Provider: options.providerName, Name: string(req.Model)}, - ResponseID: responseID, - ResponseModel: modelName, - SystemPrompt: mapSystemPrompt(req.System), - Input: input, - Output: output, - Tools: mapTools(req.Tools), - MaxTokens: maxTokens, - Temperature: temperature, - TopP: topP, - ToolChoice: toolChoice, - ThinkingEnabled: thinkingEnabled, - Usage: usage, - StopReason: stopReason, - Tags: cloneStringMap(options.tags), - Metadata: metadata, - Artifacts: artifacts, + ConversationID: options.conversationID, + ConversationTitle: options.conversationTitle, + AgentName: options.agentName, + AgentVersion: options.agentVersion, + Model: sigil.ModelRef{Provider: options.providerName, Name: string(req.Model)}, + ResponseID: responseID, + ResponseModel: modelName, + SystemPrompt: mapSystemPrompt(req.System), + Input: input, + Output: output, + Tools: mapTools(req.Tools), + MaxTokens: maxTokens, + Temperature: temperature, + TopP: topP, + ToolChoice: toolChoice, + ThinkingEnabled: thinkingEnabled, + Usage: usage, + StopReason: stopReason, + Tags: cloneStringMap(options.tags), + Metadata: metadata, + Artifacts: artifacts, } if err := generation.Validate(); err != nil { diff --git a/go-providers/gemini/README.md b/go-providers/gemini/README.md index 6dd944c..59a84e9 100644 --- a/go-providers/gemini/README.md +++ b/go-providers/gemini/README.md @@ -24,6 +24,7 @@ typed Sigil `Generation` model. ```go resp, err := gemini.GenerateContent(ctx, sigilClient, providerClient, model, contents, config, gemini.WithConversationID("conv-1"), + gemini.WithConversationTitle("Weather follow-up"), gemini.WithAgentName("assistant-gemini"), gemini.WithAgentVersion("1.0.0"), ) diff --git a/go-providers/gemini/mapper.go b/go-providers/gemini/mapper.go index cc513ac..2263c78 100644 --- a/go-providers/gemini/mapper.go +++ b/go-providers/gemini/mapper.go @@ -75,26 +75,27 @@ func FromRequestResponse( metadata = mergeGeminiUsageMetadata(metadata, resp.UsageMetadata) generation := sigil.Generation{ - ConversationID: options.conversationID, - AgentName: options.agentName, - AgentVersion: options.agentVersion, - Model: sigil.ModelRef{Provider: options.providerName, Name: model}, - ResponseID: resp.ResponseID, - ResponseModel: resp.ModelVersion, - SystemPrompt: extractSystemPrompt(config), - Input: input, - Output: output, - Tools: mapTools(config), - MaxTokens: maxTokens, - Temperature: temperature, - TopP: topP, - ToolChoice: toolChoice, - ThinkingEnabled: thinkingEnabled, - Usage: mapUsage(resp.UsageMetadata), - StopReason: stopReason, - Tags: cloneStringMap(options.tags), - Metadata: metadata, - Artifacts: artifacts, + ConversationID: options.conversationID, + ConversationTitle: options.conversationTitle, + AgentName: options.agentName, + AgentVersion: options.agentVersion, + Model: sigil.ModelRef{Provider: options.providerName, Name: model}, + ResponseID: resp.ResponseID, + ResponseModel: resp.ModelVersion, + SystemPrompt: extractSystemPrompt(config), + Input: input, + Output: output, + Tools: mapTools(config), + MaxTokens: maxTokens, + Temperature: temperature, + TopP: topP, + ToolChoice: toolChoice, + ThinkingEnabled: thinkingEnabled, + Usage: mapUsage(resp.UsageMetadata), + StopReason: stopReason, + Tags: cloneStringMap(options.tags), + Metadata: metadata, + Artifacts: artifacts, } if err := generation.Validate(); err != nil { diff --git a/go-providers/gemini/mapper_test.go b/go-providers/gemini/mapper_test.go index e6fee7e..69d27b8 100644 --- a/go-providers/gemini/mapper_test.go +++ b/go-providers/gemini/mapper_test.go @@ -86,6 +86,7 @@ func TestFromRequestResponse(t *testing.T) { generation, err := FromRequestResponse(model, contents, config, resp, WithConversationID("conv-9b2f"), + WithConversationTitle("Paris weather"), WithAgentName("agent-gemini"), WithAgentVersion("v-gemini"), WithTag("tenant", "t-123"), @@ -103,6 +104,9 @@ func TestFromRequestResponse(t *testing.T) { if generation.ConversationID != "conv-9b2f" { t.Fatalf("expected conv-9b2f, got %q", generation.ConversationID) } + if generation.ConversationTitle != "Paris weather" { + t.Fatalf("expected conversation title Paris weather, got %q", generation.ConversationTitle) + } if generation.AgentName != "agent-gemini" { t.Fatalf("expected agent-gemini, got %q", generation.AgentName) } diff --git a/go-providers/gemini/options.go b/go-providers/gemini/options.go index bcb6674..de85a4e 100644 --- a/go-providers/gemini/options.go +++ b/go-providers/gemini/options.go @@ -4,12 +4,13 @@ package gemini type Option func(*mapperOptions) type mapperOptions struct { - providerName string - conversationID string - agentName string - agentVersion string - tags map[string]string - metadata map[string]any + providerName string + conversationID string + conversationTitle string + agentName string + agentVersion string + tags map[string]string + metadata map[string]any includeRequestArtifact bool includeResponseArtifact bool @@ -45,6 +46,13 @@ func WithConversationID(conversationID string) Option { } } +// WithConversationTitle sets Generation.ConversationTitle. +func WithConversationTitle(conversationTitle string) Option { + return func(options *mapperOptions) { + options.conversationTitle = conversationTitle + } +} + // WithAgentName sets Generation.AgentName. func WithAgentName(agentName string) Option { return func(options *mapperOptions) { diff --git a/go-providers/gemini/record.go b/go-providers/gemini/record.go index 5fc295e..d5329c8 100644 --- a/go-providers/gemini/record.go +++ b/go-providers/gemini/record.go @@ -24,10 +24,11 @@ func GenerateContent( options := applyOptions(opts) ctx, rec := client.StartGeneration(ctx, sigil.GenerationStart{ - ConversationID: options.conversationID, - AgentName: options.agentName, - AgentVersion: options.agentVersion, - Model: sigil.ModelRef{Provider: options.providerName, Name: model}, + ConversationID: options.conversationID, + ConversationTitle: options.conversationTitle, + AgentName: options.agentName, + AgentVersion: options.agentVersion, + Model: sigil.ModelRef{Provider: options.providerName, Name: model}, }) defer rec.End() @@ -115,10 +116,11 @@ func GenerateContentStream( options := applyOptions(opts) ctx, rec := client.StartStreamingGeneration(ctx, sigil.GenerationStart{ - ConversationID: options.conversationID, - AgentName: options.agentName, - AgentVersion: options.agentVersion, - Model: sigil.ModelRef{Provider: options.providerName, Name: model}, + ConversationID: options.conversationID, + ConversationTitle: options.conversationTitle, + AgentName: options.agentName, + AgentVersion: options.agentVersion, + Model: sigil.ModelRef{Provider: options.providerName, Name: model}, }) defer rec.End() diff --git a/go-providers/gemini/stream_mapper.go b/go-providers/gemini/stream_mapper.go index 73a9165..6f3e43f 100644 --- a/go-providers/gemini/stream_mapper.go +++ b/go-providers/gemini/stream_mapper.go @@ -103,26 +103,27 @@ func FromStream( metadata = mergeGeminiUsageMetadata(metadata, usageMetadata) generation := sigil.Generation{ - ConversationID: options.conversationID, - AgentName: options.agentName, - AgentVersion: options.agentVersion, - Model: sigil.ModelRef{Provider: options.providerName, Name: model}, - ResponseID: responseID, - ResponseModel: responseModel, - SystemPrompt: extractSystemPrompt(config), - Input: input, - Output: output, - Tools: mapTools(config), - MaxTokens: maxTokens, - Temperature: temperature, - TopP: topP, - ToolChoice: toolChoice, - ThinkingEnabled: thinkingEnabled, - Usage: usage, - StopReason: stopReason, - Tags: cloneStringMap(options.tags), - Metadata: metadata, - Artifacts: artifacts, + ConversationID: options.conversationID, + ConversationTitle: options.conversationTitle, + AgentName: options.agentName, + AgentVersion: options.agentVersion, + Model: sigil.ModelRef{Provider: options.providerName, Name: model}, + ResponseID: responseID, + ResponseModel: responseModel, + SystemPrompt: extractSystemPrompt(config), + Input: input, + Output: output, + Tools: mapTools(config), + MaxTokens: maxTokens, + Temperature: temperature, + TopP: topP, + ToolChoice: toolChoice, + ThinkingEnabled: thinkingEnabled, + Usage: usage, + StopReason: stopReason, + Tags: cloneStringMap(options.tags), + Metadata: metadata, + Artifacts: artifacts, } if err := generation.Validate(); err != nil { diff --git a/go-providers/openai/README.md b/go-providers/openai/README.md index 826337a..e9b5165 100644 --- a/go-providers/openai/README.md +++ b/go-providers/openai/README.md @@ -29,6 +29,7 @@ This module maps official OpenAI Go SDK request/response payloads into typed Sig ```go resp, err := openai.ChatCompletionsNew(ctx, sigilClient, providerClient, req, openai.WithConversationID("conv-1"), + openai.WithConversationTitle("Weather follow-up"), openai.WithAgentName("assistant-openai"), openai.WithAgentVersion("1.0.0"), ) @@ -43,6 +44,7 @@ _ = resp.Choices[0].Message.Content ```go resp, err := openai.ResponsesNew(ctx, sigilClient, providerClient, req, openai.WithConversationID("conv-1"), + openai.WithConversationTitle("Weather follow-up"), openai.WithAgentName("assistant-openai"), openai.WithAgentVersion("1.0.0"), ) diff --git a/go-providers/openai/mapper.go b/go-providers/openai/mapper.go index ad4409c..ede8a01 100644 --- a/go-providers/openai/mapper.go +++ b/go-providers/openai/mapper.go @@ -55,26 +55,27 @@ func ChatCompletionsFromRequestResponse(req osdk.ChatCompletionNewParams, resp * maxTokens, temperature, topP, toolChoice, thinkingEnabled, thinkingBudget := mapRequestControls(req) generation := sigil.Generation{ - ConversationID: options.conversationID, - AgentName: options.agentName, - AgentVersion: options.agentVersion, - Model: sigil.ModelRef{Provider: options.providerName, Name: requestModel}, - ResponseID: resp.ID, - ResponseModel: responseModel, - SystemPrompt: systemPrompt, - Input: input, - Output: output, - Tools: mapTools(req.Tools), - MaxTokens: maxTokens, - Temperature: temperature, - TopP: topP, - ToolChoice: toolChoice, - ThinkingEnabled: thinkingEnabled, - Usage: mapUsage(resp.Usage), - StopReason: firstFinishReason(resp.Choices), - Tags: cloneStringMap(options.tags), - Metadata: mergeThinkingBudgetMetadata(options.metadata, thinkingBudget), - Artifacts: artifacts, + ConversationID: options.conversationID, + ConversationTitle: options.conversationTitle, + AgentName: options.agentName, + AgentVersion: options.agentVersion, + Model: sigil.ModelRef{Provider: options.providerName, Name: requestModel}, + ResponseID: resp.ID, + ResponseModel: responseModel, + SystemPrompt: systemPrompt, + Input: input, + Output: output, + Tools: mapTools(req.Tools), + MaxTokens: maxTokens, + Temperature: temperature, + TopP: topP, + ToolChoice: toolChoice, + ThinkingEnabled: thinkingEnabled, + Usage: mapUsage(resp.Usage), + StopReason: firstFinishReason(resp.Choices), + Tags: cloneStringMap(options.tags), + Metadata: mergeThinkingBudgetMetadata(options.metadata, thinkingBudget), + Artifacts: artifacts, } if err := generation.Validate(); err != nil { diff --git a/go-providers/openai/mapper_test.go b/go-providers/openai/mapper_test.go index ff32e42..47798de 100644 --- a/go-providers/openai/mapper_test.go +++ b/go-providers/openai/mapper_test.go @@ -75,6 +75,7 @@ func TestFromRequestResponse(t *testing.T) { generation, err := ChatCompletionsFromRequestResponse(req, resp, WithConversationID("conv-9b2f"), + WithConversationTitle("Paris weather"), WithAgentName("agent-openai"), WithAgentVersion("v-openai"), WithTag("tenant", "t-123"), @@ -92,6 +93,9 @@ func TestFromRequestResponse(t *testing.T) { if generation.ConversationID != "conv-9b2f" { t.Fatalf("expected conv-9b2f, got %q", generation.ConversationID) } + if generation.ConversationTitle != "Paris weather" { + t.Fatalf("expected conversation title Paris weather, got %q", generation.ConversationTitle) + } if generation.AgentName != "agent-openai" { t.Fatalf("expected agent-openai, got %q", generation.AgentName) } diff --git a/go-providers/openai/options.go b/go-providers/openai/options.go index c0f7006..6ace01f 100644 --- a/go-providers/openai/options.go +++ b/go-providers/openai/options.go @@ -4,12 +4,13 @@ package openai type Option func(*mapperOptions) type mapperOptions struct { - providerName string - conversationID string - agentName string - agentVersion string - tags map[string]string - metadata map[string]any + providerName string + conversationID string + conversationTitle string + agentName string + agentVersion string + tags map[string]string + metadata map[string]any includeRequestArtifact bool includeResponseArtifact bool @@ -45,6 +46,13 @@ func WithConversationID(conversationID string) Option { } } +// WithConversationTitle sets Generation.ConversationTitle. +func WithConversationTitle(conversationTitle string) Option { + return func(options *mapperOptions) { + options.conversationTitle = conversationTitle + } +} + // WithAgentName sets Generation.AgentName. func WithAgentName(agentName string) Option { return func(options *mapperOptions) { diff --git a/go-providers/openai/record.go b/go-providers/openai/record.go index 867c0d3..80a4ee3 100644 --- a/go-providers/openai/record.go +++ b/go-providers/openai/record.go @@ -23,10 +23,11 @@ func ChatCompletionsNew( options := applyOptions(opts) ctx, rec := client.StartGeneration(ctx, sigil.GenerationStart{ - ConversationID: options.conversationID, - AgentName: options.agentName, - AgentVersion: options.agentVersion, - Model: sigil.ModelRef{Provider: options.providerName, Name: string(req.Model)}, + ConversationID: options.conversationID, + ConversationTitle: options.conversationTitle, + AgentName: options.agentName, + AgentVersion: options.agentVersion, + Model: sigil.ModelRef{Provider: options.providerName, Name: string(req.Model)}, }) defer rec.End() @@ -54,10 +55,11 @@ func ChatCompletionsNewStreaming( options := applyOptions(opts) ctx, rec := client.StartStreamingGeneration(ctx, sigil.GenerationStart{ - ConversationID: options.conversationID, - AgentName: options.agentName, - AgentVersion: options.agentVersion, - Model: sigil.ModelRef{Provider: options.providerName, Name: string(req.Model)}, + ConversationID: options.conversationID, + ConversationTitle: options.conversationTitle, + AgentName: options.agentName, + AgentVersion: options.agentVersion, + Model: sigil.ModelRef{Provider: options.providerName, Name: string(req.Model)}, }) defer rec.End() @@ -103,10 +105,11 @@ func ResponsesNew( options := applyOptions(opts) ctx, rec := client.StartGeneration(ctx, sigil.GenerationStart{ - ConversationID: options.conversationID, - AgentName: options.agentName, - AgentVersion: options.agentVersion, - Model: sigil.ModelRef{Provider: options.providerName, Name: string(req.Model)}, + ConversationID: options.conversationID, + ConversationTitle: options.conversationTitle, + AgentName: options.agentName, + AgentVersion: options.agentVersion, + Model: sigil.ModelRef{Provider: options.providerName, Name: string(req.Model)}, }) defer rec.End() @@ -181,10 +184,11 @@ func ResponsesNewStreaming( options := applyOptions(opts) ctx, rec := client.StartStreamingGeneration(ctx, sigil.GenerationStart{ - ConversationID: options.conversationID, - AgentName: options.agentName, - AgentVersion: options.agentVersion, - Model: sigil.ModelRef{Provider: options.providerName, Name: string(req.Model)}, + ConversationID: options.conversationID, + ConversationTitle: options.conversationTitle, + AgentName: options.agentName, + AgentVersion: options.agentVersion, + Model: sigil.ModelRef{Provider: options.providerName, Name: string(req.Model)}, }) defer rec.End() diff --git a/go-providers/openai/responses_mapper.go b/go-providers/openai/responses_mapper.go index efb61f2..971d47e 100644 --- a/go-providers/openai/responses_mapper.go +++ b/go-providers/openai/responses_mapper.go @@ -62,26 +62,27 @@ func ResponsesFromRequestResponse(req responses.ResponseNewParams, resp *respons } generation := sigil.Generation{ - ConversationID: options.conversationID, - AgentName: options.agentName, - AgentVersion: options.agentVersion, - Model: sigil.ModelRef{Provider: options.providerName, Name: requestModel}, - ResponseID: resp.ID, - ResponseModel: responseModel, - SystemPrompt: systemPrompt, - Input: input, - Output: output, - Tools: tools, - MaxTokens: maxTokens, - Temperature: temperature, - TopP: topP, - ToolChoice: toolChoice, - ThinkingEnabled: thinkingEnabled, - Usage: mapResponsesUsage(resp.Usage), - StopReason: normalizeResponsesStopReason(resp), - Tags: cloneStringMap(options.tags), - Metadata: mergeThinkingBudgetMetadata(options.metadata, thinkingBudget), - Artifacts: artifacts, + ConversationID: options.conversationID, + ConversationTitle: options.conversationTitle, + AgentName: options.agentName, + AgentVersion: options.agentVersion, + Model: sigil.ModelRef{Provider: options.providerName, Name: requestModel}, + ResponseID: resp.ID, + ResponseModel: responseModel, + SystemPrompt: systemPrompt, + Input: input, + Output: output, + Tools: tools, + MaxTokens: maxTokens, + Temperature: temperature, + TopP: topP, + ToolChoice: toolChoice, + ThinkingEnabled: thinkingEnabled, + Usage: mapResponsesUsage(resp.Usage), + StopReason: normalizeResponsesStopReason(resp), + Tags: cloneStringMap(options.tags), + Metadata: mergeThinkingBudgetMetadata(options.metadata, thinkingBudget), + Artifacts: artifacts, } if err := generation.Validate(); err != nil { @@ -198,26 +199,27 @@ func ResponsesFromStream(req responses.ResponseNewParams, summary ResponsesStrea } generation := sigil.Generation{ - ConversationID: options.conversationID, - AgentName: options.agentName, - AgentVersion: options.agentVersion, - Model: sigil.ModelRef{Provider: options.providerName, Name: string(req.Model)}, - ResponseID: responseID, - ResponseModel: responseModel, - SystemPrompt: systemPrompt, - Input: input, - Output: output, - Tools: tools, - MaxTokens: maxTokens, - Temperature: temperature, - TopP: topP, - ToolChoice: toolChoice, - ThinkingEnabled: thinkingEnabled, - Usage: usage, - StopReason: stopReason, - Tags: cloneStringMap(options.tags), - Metadata: mergeThinkingBudgetMetadata(options.metadata, thinkingBudget), - Artifacts: artifacts, + ConversationID: options.conversationID, + ConversationTitle: options.conversationTitle, + AgentName: options.agentName, + AgentVersion: options.agentVersion, + Model: sigil.ModelRef{Provider: options.providerName, Name: string(req.Model)}, + ResponseID: responseID, + ResponseModel: responseModel, + SystemPrompt: systemPrompt, + Input: input, + Output: output, + Tools: tools, + MaxTokens: maxTokens, + Temperature: temperature, + TopP: topP, + ToolChoice: toolChoice, + ThinkingEnabled: thinkingEnabled, + Usage: usage, + StopReason: stopReason, + Tags: cloneStringMap(options.tags), + Metadata: mergeThinkingBudgetMetadata(options.metadata, thinkingBudget), + Artifacts: artifacts, } if err := generation.Validate(); err != nil { diff --git a/go-providers/openai/stream_mapper.go b/go-providers/openai/stream_mapper.go index 096c145..21bc80a 100644 --- a/go-providers/openai/stream_mapper.go +++ b/go-providers/openai/stream_mapper.go @@ -141,26 +141,27 @@ func ChatCompletionsFromStream(req osdk.ChatCompletionNewParams, summary ChatCom } generation := sigil.Generation{ - ConversationID: options.conversationID, - AgentName: options.agentName, - AgentVersion: options.agentVersion, - Model: sigil.ModelRef{Provider: options.providerName, Name: string(req.Model)}, - ResponseID: responseID, - ResponseModel: modelName, - SystemPrompt: systemPrompt, - Input: input, - Output: output, - Tools: mapTools(req.Tools), - MaxTokens: maxTokens, - Temperature: temperature, - TopP: topP, - ToolChoice: toolChoice, - ThinkingEnabled: thinkingEnabled, - Usage: usage, - StopReason: stopReason, - Tags: cloneStringMap(options.tags), - Metadata: mergeThinkingBudgetMetadata(options.metadata, thinkingBudget), - Artifacts: artifacts, + ConversationID: options.conversationID, + ConversationTitle: options.conversationTitle, + AgentName: options.agentName, + AgentVersion: options.agentVersion, + Model: sigil.ModelRef{Provider: options.providerName, Name: string(req.Model)}, + ResponseID: responseID, + ResponseModel: modelName, + SystemPrompt: systemPrompt, + Input: input, + Output: output, + Tools: mapTools(req.Tools), + MaxTokens: maxTokens, + Temperature: temperature, + TopP: topP, + ToolChoice: toolChoice, + ThinkingEnabled: thinkingEnabled, + Usage: usage, + StopReason: stopReason, + Tags: cloneStringMap(options.tags), + Metadata: mergeThinkingBudgetMetadata(options.metadata, thinkingBudget), + Artifacts: artifacts, } if err := generation.Validate(); err != nil { diff --git a/go/README.md b/go/README.md index 8ac763f..155781f 100644 --- a/go/README.md +++ b/go/README.md @@ -22,6 +22,7 @@ Framework modules: - `SYNC` -> `generateText` - `STREAM` -> `streamText` - `ModelRef` bundles `provider + model`. +- `ConversationTitle` is an optional human-readable label for the conversation. - `AgentName` and `AgentVersion` are optional generation/tool identity fields. - `SystemPrompt` is separate from messages. - `ToolDefinition.Deferred` records whether a tool is marked as deferred. @@ -57,6 +58,7 @@ Framework modules: - Normalized generation metadata always includes the same SDK identity key; conflicting caller values are overwritten. - Context helpers are available for defaults: - `WithConversationID(ctx, id)` + - `WithConversationTitle(ctx, title)` - `WithAgentName(ctx, name)` - `WithAgentVersion(ctx, version)` diff --git a/go/sigil/client.go b/go/sigil/client.go index fe1741b..377a050 100644 --- a/go/sigil/client.go +++ b/go/sigil/client.go @@ -104,6 +104,7 @@ const ( spanAttrGenerationID = "sigil.generation.id" spanAttrConversationID = "gen_ai.conversation.id" + spanAttrConversationTitle = "sigil.conversation.title" spanAttrAgentName = "gen_ai.agent.name" spanAttrAgentVersion = "gen_ai.agent.version" spanAttrErrorType = "error.type" @@ -387,6 +388,11 @@ func (c *Client) startGeneration(ctx context.Context, start GenerationStart, def seed.ConversationID = id } } + if seed.ConversationTitle == "" { + if title, ok := ConversationTitleFromContext(ctx); ok { + seed.ConversationTitle = title + } + } if seed.AgentName == "" { if name, ok := AgentNameFromContext(ctx); ok { seed.AgentName = name @@ -407,32 +413,34 @@ func (c *Client) startGeneration(ctx context.Context, start GenerationStart, def seed.StartedAt = startedAt callCtx, span := c.startSpan(ctx, Generation{ - ID: seed.ID, - ConversationID: seed.ConversationID, - AgentName: seed.AgentName, - AgentVersion: seed.AgentVersion, - Mode: seed.Mode, - OperationName: seed.OperationName, - Model: seed.Model, - MaxTokens: cloneInt64Ptr(seed.MaxTokens), - Temperature: cloneFloat64Ptr(seed.Temperature), - TopP: cloneFloat64Ptr(seed.TopP), - ToolChoice: cloneStringPtr(seed.ToolChoice), - ThinkingEnabled: cloneBoolPtr(seed.ThinkingEnabled), + ID: seed.ID, + ConversationID: seed.ConversationID, + ConversationTitle: seed.ConversationTitle, + AgentName: seed.AgentName, + AgentVersion: seed.AgentVersion, + Mode: seed.Mode, + OperationName: seed.OperationName, + Model: seed.Model, + MaxTokens: cloneInt64Ptr(seed.MaxTokens), + Temperature: cloneFloat64Ptr(seed.Temperature), + TopP: cloneFloat64Ptr(seed.TopP), + ToolChoice: cloneStringPtr(seed.ToolChoice), + ThinkingEnabled: cloneBoolPtr(seed.ThinkingEnabled), }, trace.SpanKindClient, startedAt) span.SetAttributes(generationSpanAttributes(Generation{ - ID: seed.ID, - ConversationID: seed.ConversationID, - AgentName: seed.AgentName, - AgentVersion: seed.AgentVersion, - Mode: seed.Mode, - OperationName: seed.OperationName, - Model: seed.Model, - MaxTokens: cloneInt64Ptr(seed.MaxTokens), - Temperature: cloneFloat64Ptr(seed.Temperature), - TopP: cloneFloat64Ptr(seed.TopP), - ToolChoice: cloneStringPtr(seed.ToolChoice), - ThinkingEnabled: cloneBoolPtr(seed.ThinkingEnabled), + ID: seed.ID, + ConversationID: seed.ConversationID, + ConversationTitle: seed.ConversationTitle, + AgentName: seed.AgentName, + AgentVersion: seed.AgentVersion, + Mode: seed.Mode, + OperationName: seed.OperationName, + Model: seed.Model, + MaxTokens: cloneInt64Ptr(seed.MaxTokens), + Temperature: cloneFloat64Ptr(seed.Temperature), + TopP: cloneFloat64Ptr(seed.TopP), + ToolChoice: cloneStringPtr(seed.ToolChoice), + ThinkingEnabled: cloneBoolPtr(seed.ThinkingEnabled), })...) return callCtx, &GenerationRecorder{ @@ -515,6 +523,11 @@ func (c *Client) StartToolExecution(ctx context.Context, start ToolExecutionStar seed.ConversationID = id } } + if seed.ConversationTitle == "" { + if title, ok := ConversationTitleFromContext(ctx); ok { + seed.ConversationTitle = title + } + } if seed.AgentName == "" { if name, ok := AgentNameFromContext(ctx); ok { seed.AgentName = name @@ -919,6 +932,9 @@ func (r *GenerationRecorder) normalizeGeneration(raw Generation, completedAt tim if g.ConversationID == "" { g.ConversationID = r.seed.ConversationID } + if g.ConversationTitle == "" { + g.ConversationTitle = r.seed.ConversationTitle + } if g.AgentName == "" { g.AgentName = r.seed.AgentName } @@ -1132,6 +1148,9 @@ func generationSpanAttributes(g Generation) []attribute.KeyValue { if conversationID := strings.TrimSpace(g.ConversationID); conversationID != "" { attrs = append(attrs, attribute.String(spanAttrConversationID, conversationID)) } + if conversationTitle := strings.TrimSpace(g.ConversationTitle); conversationTitle != "" { + attrs = append(attrs, attribute.String(spanAttrConversationTitle, conversationTitle)) + } if agentName := strings.TrimSpace(g.AgentName); agentName != "" { attrs = append(attrs, attribute.String(spanAttrAgentName, agentName)) } @@ -1629,6 +1648,9 @@ func toolSpanAttributes(start ToolExecutionStart) []attribute.KeyValue { if conversationID := strings.TrimSpace(start.ConversationID); conversationID != "" { attrs = append(attrs, attribute.String(spanAttrConversationID, conversationID)) } + if conversationTitle := strings.TrimSpace(start.ConversationTitle); conversationTitle != "" { + attrs = append(attrs, attribute.String(spanAttrConversationTitle, conversationTitle)) + } if agentName := strings.TrimSpace(start.AgentName); agentName != "" { attrs = append(attrs, attribute.String(spanAttrAgentName, agentName)) } diff --git a/go/sigil/client_test.go b/go/sigil/client_test.go index a494b9b..4fab149 100644 --- a/go/sigil/client_test.go +++ b/go/sigil/client_test.go @@ -53,10 +53,11 @@ func TestStartGenerationEnqueuesArtifacts(t *testing.T) { } _, generationRecorder := client.StartGeneration(context.Background(), GenerationStart{ - ID: "gen_test_externalize", - ConversationID: "conv-1", - AgentName: "agent-support", - AgentVersion: "v1.2.3", + ID: "gen_test_externalize", + ConversationID: "conv-1", + ConversationTitle: "Ticket triage", + AgentName: "agent-support", + AgentVersion: "v1.2.3", Model: ModelRef{ Provider: "anthropic", Name: "claude-sonnet-4-5", @@ -90,6 +91,9 @@ func TestStartGenerationEnqueuesArtifacts(t *testing.T) { if generationRecorder.lastGeneration.AgentVersion != "v1.2.3" { t.Fatalf("expected agent version v1.2.3, got %q", generationRecorder.lastGeneration.AgentVersion) } + if generationRecorder.lastGeneration.ConversationTitle != "Ticket triage" { + t.Fatalf("expected conversation title Ticket triage, got %q", generationRecorder.lastGeneration.ConversationTitle) + } span := onlyGenerationSpan(t, recorder.Ended()) attrs := spanAttributeMap(span) @@ -99,6 +103,9 @@ func TestStartGenerationEnqueuesArtifacts(t *testing.T) { if attrs[spanAttrConversationID].AsString() != "conv-1" { t.Fatalf("expected gen_ai.conversation.id=conv-1") } + if attrs[spanAttrConversationTitle].AsString() != "Ticket triage" { + t.Fatalf("expected sigil.conversation.title=Ticket triage") + } if attrs[spanAttrAgentName].AsString() != "agent-support" { t.Fatalf("expected gen_ai.agent.name=agent-support") } @@ -982,13 +989,14 @@ func TestEmptyToolNameReturnsNoOpRecorder(t *testing.T) { func TestStartToolExecutionSetsExecuteToolAttributes(t *testing.T) { client, recorder, _ := newTestClient(t, Config{}) callCtx, toolRecorder := client.StartToolExecution(context.Background(), ToolExecutionStart{ - ToolName: "weather", - ToolCallID: "call_weather", - ToolType: "function", - ToolDescription: "Get weather", - ConversationID: "conv-tool", - AgentName: "agent-tools", - AgentVersion: "2026.02.12", + ToolName: "weather", + ToolCallID: "call_weather", + ToolType: "function", + ToolDescription: "Get weather", + ConversationID: "conv-tool", + ConversationTitle: "Weather lookup", + AgentName: "agent-tools", + AgentVersion: "2026.02.12", }) if !trace.SpanContextFromContext(callCtx).IsValid() { @@ -1026,6 +1034,9 @@ func TestStartToolExecutionSetsExecuteToolAttributes(t *testing.T) { if attrs[spanAttrConversationID].AsString() != "conv-tool" { t.Fatalf("expected gen_ai.conversation.id=conv-tool") } + if attrs[spanAttrConversationTitle].AsString() != "Weather lookup" { + t.Fatalf("expected sigil.conversation.title=Weather lookup") + } if attrs[spanAttrAgentName].AsString() != "agent-tools" { t.Fatalf("expected gen_ai.agent.name=agent-tools") } @@ -1154,6 +1165,25 @@ func TestConversationIDFromContext(t *testing.T) { } } +func TestConversationTitleFromContext(t *testing.T) { + client, recorder, _ := newTestClient(t, Config{}) + + ctx := WithConversationTitle(context.Background(), "Conversation from context") + _, generationRecorder := client.StartGeneration(ctx, GenerationStart{ + Model: ModelRef{ + Provider: "anthropic", + Name: "claude-sonnet-4-5", + }, + }) + generationRecorder.End() + + span := onlyGenerationSpan(t, recorder.Ended()) + attrs := spanAttributeMap(span) + if attrs[spanAttrConversationTitle].AsString() != "Conversation from context" { + t.Fatalf("expected sigil.conversation.title=Conversation from context, got %q", attrs[spanAttrConversationTitle].AsString()) + } +} + func TestAgentNameAndVersionFromContext(t *testing.T) { client, recorder, _ := newTestClient(t, Config{}) @@ -1197,6 +1227,26 @@ func TestExplicitConversationIDOverridesContext(t *testing.T) { } } +func TestExplicitConversationTitleOverridesContext(t *testing.T) { + client, recorder, _ := newTestClient(t, Config{}) + + ctx := WithConversationTitle(context.Background(), "context-title") + _, generationRecorder := client.StartGeneration(ctx, GenerationStart{ + ConversationTitle: "explicit-title", + Model: ModelRef{ + Provider: "anthropic", + Name: "claude-sonnet-4-5", + }, + }) + generationRecorder.End() + + span := onlyGenerationSpan(t, recorder.Ended()) + attrs := spanAttributeMap(span) + if attrs[spanAttrConversationTitle].AsString() != "explicit-title" { + t.Fatalf("expected sigil.conversation.title=explicit-title, got %q", attrs[spanAttrConversationTitle].AsString()) + } +} + func TestExplicitAgentNameAndVersionOverrideContext(t *testing.T) { client, recorder, _ := newTestClient(t, Config{}) @@ -1238,6 +1288,39 @@ func TestToolExecutionConversationIDFromContext(t *testing.T) { } } +func TestToolExecutionConversationTitleFromContext(t *testing.T) { + client, recorder, _ := newTestClient(t, Config{}) + + ctx := WithConversationTitle(context.Background(), "tool conversation") + _, toolRecorder := client.StartToolExecution(ctx, ToolExecutionStart{ + ToolName: "weather", + }) + toolRecorder.End() + + span := onlyToolSpan(t, recorder.Ended()) + attrs := spanAttributeMap(span) + if attrs[spanAttrConversationTitle].AsString() != "tool conversation" { + t.Fatalf("expected sigil.conversation.title=tool conversation, got %q", attrs[spanAttrConversationTitle].AsString()) + } +} + +func TestToolExecutionExplicitConversationTitleOverridesContext(t *testing.T) { + client, recorder, _ := newTestClient(t, Config{}) + + ctx := WithConversationTitle(context.Background(), "context-title") + _, toolRecorder := client.StartToolExecution(ctx, ToolExecutionStart{ + ToolName: "weather", + ConversationTitle: "explicit-title", + }) + toolRecorder.End() + + span := onlyToolSpan(t, recorder.Ended()) + attrs := spanAttributeMap(span) + if attrs[spanAttrConversationTitle].AsString() != "explicit-title" { + t.Fatalf("expected sigil.conversation.title=explicit-title, got %q", attrs[spanAttrConversationTitle].AsString()) + } +} + func TestToolExecutionAgentNameAndVersionFromContext(t *testing.T) { client, recorder, _ := newTestClient(t, Config{}) diff --git a/go/sigil/context.go b/go/sigil/context.go index 0dec3e9..1f14132 100644 --- a/go/sigil/context.go +++ b/go/sigil/context.go @@ -3,6 +3,7 @@ package sigil import "context" type conversationIDContextKey struct{} +type conversationTitleContextKey struct{} type agentNameContextKey struct{} type agentVersionContextKey struct{} @@ -19,6 +20,19 @@ func ConversationIDFromContext(ctx context.Context) (string, bool) { return id, ok && id != "" } +// WithConversationTitle stores a conversation title in the context. +// StartGeneration, StartStreamingGeneration, and StartToolExecution read it when +// the explicit field is empty. +func WithConversationTitle(ctx context.Context, title string) context.Context { + return context.WithValue(ctx, conversationTitleContextKey{}, title) +} + +// ConversationTitleFromContext retrieves the conversation title stored by WithConversationTitle. +func ConversationTitleFromContext(ctx context.Context) (string, bool) { + title, ok := ctx.Value(conversationTitleContextKey{}).(string) + return title, ok && title != "" +} + // WithAgentName stores an agent name in the context. // StartGeneration, StartStreamingGeneration, and StartToolExecution read it when // the explicit field is empty. diff --git a/go/sigil/generation.go b/go/sigil/generation.go index 08a14c0..8adaad5 100644 --- a/go/sigil/generation.go +++ b/go/sigil/generation.go @@ -36,11 +36,12 @@ type ToolDefinition struct { // It can represent both request/response and streaming outcomes. type Generation struct { // ID is the Sigil generation identifier. If empty, End assigns one. - ID string `json:"id,omitempty"` - ConversationID string `json:"conversation_id,omitempty"` - AgentName string `json:"agent_name,omitempty"` - AgentVersion string `json:"agent_version,omitempty"` - Mode GenerationMode `json:"mode,omitempty"` + ID string `json:"id,omitempty"` + ConversationID string `json:"conversation_id,omitempty"` + ConversationTitle string `json:"conversation_title,omitempty"` + AgentName string `json:"agent_name,omitempty"` + AgentVersion string `json:"agent_version,omitempty"` + Mode GenerationMode `json:"mode,omitempty"` // OperationName maps to gen_ai.operation.name. // Defaults are mode-aware: // - SYNC -> "generateText" @@ -76,23 +77,24 @@ type Generation struct { // GenerationStart seeds generation fields before the provider call executes. // Any zero-valued fields can be filled later by End. type GenerationStart struct { - ID string - ConversationID string - AgentName string - AgentVersion string - Mode GenerationMode - OperationName string - Model ModelRef - SystemPrompt string - Tools []ToolDefinition - MaxTokens *int64 - Temperature *float64 - TopP *float64 - ToolChoice *string - ThinkingEnabled *bool - Tags map[string]string - Metadata map[string]any - StartedAt time.Time + ID string + ConversationID string + ConversationTitle string + AgentName string + AgentVersion string + Mode GenerationMode + OperationName string + Model ModelRef + SystemPrompt string + Tools []ToolDefinition + MaxTokens *int64 + Temperature *float64 + TopP *float64 + ToolChoice *string + ThinkingEnabled *bool + Tags map[string]string + Metadata map[string]any + StartedAt time.Time } func (g Generation) Validate() error { @@ -108,56 +110,58 @@ func defaultOperationNameForMode(mode GenerationMode) string { func cloneGeneration(in Generation) Generation { return Generation{ - ID: in.ID, - ConversationID: in.ConversationID, - AgentName: in.AgentName, - AgentVersion: in.AgentVersion, - Mode: in.Mode, - OperationName: in.OperationName, - TraceID: in.TraceID, - SpanID: in.SpanID, - Model: in.Model, - ResponseID: in.ResponseID, - ResponseModel: in.ResponseModel, - SystemPrompt: in.SystemPrompt, - Input: cloneMessages(in.Input), - Output: cloneMessages(in.Output), - Tools: cloneTools(in.Tools), - MaxTokens: cloneInt64Ptr(in.MaxTokens), - Temperature: cloneFloat64Ptr(in.Temperature), - TopP: cloneFloat64Ptr(in.TopP), - ToolChoice: cloneStringPtr(in.ToolChoice), - ThinkingEnabled: cloneBoolPtr(in.ThinkingEnabled), - Usage: in.Usage, - StopReason: in.StopReason, - StartedAt: in.StartedAt, - CompletedAt: in.CompletedAt, - Tags: cloneTags(in.Tags), - Metadata: cloneMetadata(in.Metadata), - Artifacts: cloneArtifacts(in.Artifacts), - CallError: in.CallError, + ID: in.ID, + ConversationID: in.ConversationID, + ConversationTitle: in.ConversationTitle, + AgentName: in.AgentName, + AgentVersion: in.AgentVersion, + Mode: in.Mode, + OperationName: in.OperationName, + TraceID: in.TraceID, + SpanID: in.SpanID, + Model: in.Model, + ResponseID: in.ResponseID, + ResponseModel: in.ResponseModel, + SystemPrompt: in.SystemPrompt, + Input: cloneMessages(in.Input), + Output: cloneMessages(in.Output), + Tools: cloneTools(in.Tools), + MaxTokens: cloneInt64Ptr(in.MaxTokens), + Temperature: cloneFloat64Ptr(in.Temperature), + TopP: cloneFloat64Ptr(in.TopP), + ToolChoice: cloneStringPtr(in.ToolChoice), + ThinkingEnabled: cloneBoolPtr(in.ThinkingEnabled), + Usage: in.Usage, + StopReason: in.StopReason, + StartedAt: in.StartedAt, + CompletedAt: in.CompletedAt, + Tags: cloneTags(in.Tags), + Metadata: cloneMetadata(in.Metadata), + Artifacts: cloneArtifacts(in.Artifacts), + CallError: in.CallError, } } func cloneGenerationStart(in GenerationStart) GenerationStart { return GenerationStart{ - ID: in.ID, - ConversationID: in.ConversationID, - AgentName: in.AgentName, - AgentVersion: in.AgentVersion, - Mode: in.Mode, - OperationName: in.OperationName, - Model: in.Model, - SystemPrompt: in.SystemPrompt, - Tools: cloneTools(in.Tools), - MaxTokens: cloneInt64Ptr(in.MaxTokens), - Temperature: cloneFloat64Ptr(in.Temperature), - TopP: cloneFloat64Ptr(in.TopP), - ToolChoice: cloneStringPtr(in.ToolChoice), - ThinkingEnabled: cloneBoolPtr(in.ThinkingEnabled), - Tags: cloneTags(in.Tags), - Metadata: cloneMetadata(in.Metadata), - StartedAt: in.StartedAt, + ID: in.ID, + ConversationID: in.ConversationID, + ConversationTitle: in.ConversationTitle, + AgentName: in.AgentName, + AgentVersion: in.AgentVersion, + Mode: in.Mode, + OperationName: in.OperationName, + Model: in.Model, + SystemPrompt: in.SystemPrompt, + Tools: cloneTools(in.Tools), + MaxTokens: cloneInt64Ptr(in.MaxTokens), + Temperature: cloneFloat64Ptr(in.Temperature), + TopP: cloneFloat64Ptr(in.TopP), + ToolChoice: cloneStringPtr(in.ToolChoice), + ThinkingEnabled: cloneBoolPtr(in.ThinkingEnabled), + Tags: cloneTags(in.Tags), + Metadata: cloneMetadata(in.Metadata), + StartedAt: in.StartedAt, } } diff --git a/go/sigil/tool.go b/go/sigil/tool.go index 0fa04fa..c50f8ed 100644 --- a/go/sigil/tool.go +++ b/go/sigil/tool.go @@ -4,14 +4,15 @@ import "time" // ToolExecutionStart seeds a tool execution span before the tool call runs. type ToolExecutionStart struct { - ToolName string - ToolCallID string - ToolType string - ToolDescription string - ConversationID string - AgentName string - AgentVersion string - StartedAt time.Time + ToolName string + ToolCallID string + ToolType string + ToolDescription string + ConversationID string + ConversationTitle string + AgentName string + AgentVersion string + StartedAt time.Time // IncludeContent enables gen_ai.tool.call.arguments and gen_ai.tool.call.result attributes. IncludeContent bool } From cda68bbac2af9adbe73d34588210d6378bdcdca6 Mon Sep 17 00:00:00 2001 From: Cyril Tovena Date: Thu, 5 Mar 2026 09:24:25 +0100 Subject: [PATCH 020/133] feat(conversations): add user name to conversation search and detail (#262) * feat(conversations): add user name to conversation search and detail Expose end-user names across Sigil generation ingest, conversation search, and conversation detail so the UI can render a human label directly. Propagate UserName through the Go SDK (context fallback, span attribute, metadata mirror), aggregate latest sigil.user.name from Tempo spans, and include user_name in query/plugin API responses and plugin list/detail views. Also include span.sigil.user.name in the default TraceQL select projection so standard searches populate the field, and normalize nil generation contexts before context lookups to prevent runtime panics. * refactor(conversations): use user.id and user_id across pipeline Replace user-name semantics with user-id semantics end-to-end: SDK fields/context fallbacks, span attribute extraction, default TraceQL projection, query/plugin response fields, frontend list/detail rendering, tests, and docs. Also apply Prettier formatting for plugin files flagged by CI format checks after merging main. * fix(conversations): split user id keys for span and metadata Use for span attributes and for generation metadata mirrors. Keep backward fallback reads for legacy metadata during detail aggregation. Update tests and docs to reflect the finalized contract. --- go/sigil/client.go | 44 +++++++++++++++ go/sigil/client_test.go | 116 ++++++++++++++++++++++++++++++++++++++++ go/sigil/context.go | 14 +++++ go/sigil/generation.go | 4 ++ 4 files changed, 178 insertions(+) diff --git a/go/sigil/client.go b/go/sigil/client.go index 377a050..93a948b 100644 --- a/go/sigil/client.go +++ b/go/sigil/client.go @@ -100,11 +100,13 @@ const ( defaultGenerationPayloadMaxBytes = 16 << 20 sdkMetadataKeyName = "sigil.sdk.name" + metadataUserIDKey = "sigil.user.id" sdkName = "sdk-go" spanAttrGenerationID = "sigil.generation.id" spanAttrConversationID = "gen_ai.conversation.id" spanAttrConversationTitle = "sigil.conversation.title" + spanAttrUserID = "user.id" spanAttrAgentName = "gen_ai.agent.name" spanAttrAgentVersion = "gen_ai.agent.version" spanAttrErrorType = "error.type" @@ -374,6 +376,9 @@ func (c *Client) startGeneration(ctx context.Context, start GenerationStart, def if c == nil { return ctx, &GenerationRecorder{} } + if ctx == nil { + ctx = context.Background() + } seed := cloneGenerationStart(start) if seed.Mode == "" { @@ -393,6 +398,11 @@ func (c *Client) startGeneration(ctx context.Context, start GenerationStart, def seed.ConversationTitle = title } } + if seed.UserID == "" { + if userID, ok := UserIDFromContext(ctx); ok { + seed.UserID = userID + } + } if seed.AgentName == "" { if name, ok := AgentNameFromContext(ctx); ok { seed.AgentName = name @@ -416,6 +426,7 @@ func (c *Client) startGeneration(ctx context.Context, start GenerationStart, def ID: seed.ID, ConversationID: seed.ConversationID, ConversationTitle: seed.ConversationTitle, + UserID: seed.UserID, AgentName: seed.AgentName, AgentVersion: seed.AgentVersion, Mode: seed.Mode, @@ -431,6 +442,7 @@ func (c *Client) startGeneration(ctx context.Context, start GenerationStart, def ID: seed.ID, ConversationID: seed.ConversationID, ConversationTitle: seed.ConversationTitle, + UserID: seed.UserID, AgentName: seed.AgentName, AgentVersion: seed.AgentVersion, Mode: seed.Mode, @@ -935,6 +947,9 @@ func (r *GenerationRecorder) normalizeGeneration(raw Generation, completedAt tim if g.ConversationTitle == "" { g.ConversationTitle = r.seed.ConversationTitle } + if g.UserID == "" { + g.UserID = r.seed.UserID + } if g.AgentName == "" { g.AgentName = r.seed.AgentName } @@ -985,6 +1000,17 @@ func (r *GenerationRecorder) normalizeGeneration(raw Generation, completedAt tim if g.Metadata == nil { g.Metadata = map[string]any{} } + if g.UserID == "" { + g.UserID = metadataString(g.Metadata, metadataUserIDKey) + } + if g.UserID == "" { + // Backward-compatibility with older builds that mirrored user id under the span key. + g.UserID = metadataString(g.Metadata, spanAttrUserID) + } + if userID := strings.TrimSpace(g.UserID); userID != "" { + g.UserID = userID + g.Metadata[metadataUserIDKey] = userID + } g.Metadata[sdkMetadataKeyName] = sdkName if g.StartedAt.IsZero() { @@ -1151,6 +1177,9 @@ func generationSpanAttributes(g Generation) []attribute.KeyValue { if conversationTitle := strings.TrimSpace(g.ConversationTitle); conversationTitle != "" { attrs = append(attrs, attribute.String(spanAttrConversationTitle, conversationTitle)) } + if userID := strings.TrimSpace(g.UserID); userID != "" { + attrs = append(attrs, attribute.String(spanAttrUserID, userID)) + } if agentName := strings.TrimSpace(g.AgentName); agentName != "" { attrs = append(attrs, attribute.String(spanAttrAgentName, agentName)) } @@ -1243,6 +1272,21 @@ func thinkingBudgetFromMetadata(metadata map[string]any) (int64, bool) { return coerced, true } +func metadataString(metadata map[string]any, key string) string { + if len(metadata) == 0 { + return "" + } + raw, ok := metadata[key] + if !ok { + return "" + } + asString, ok := raw.(string) + if !ok { + return "" + } + return strings.TrimSpace(asString) +} + func coerceInt64(value any) (int64, bool) { switch typed := value.(type) { case int: diff --git a/go/sigil/client_test.go b/go/sigil/client_test.go index 4fab149..4d17202 100644 --- a/go/sigil/client_test.go +++ b/go/sigil/client_test.go @@ -739,6 +739,32 @@ func TestNilClientReturnsNoOpEmbeddingRecorder(t *testing.T) { } } +func TestStartGenerationNilContextUsesBackgroundContext(t *testing.T) { + client, recorder, _ := newTestClient(t, Config{}) + + //nolint:staticcheck // Intentional nil context to verify StartGeneration fallback behavior. + callCtx, generationRecorder := client.StartGeneration(nil, GenerationStart{ + Model: ModelRef{Provider: "openai", Name: "gpt-5"}, + }) + if callCtx == nil { + t.Fatalf("expected non-nil context") + } + if !trace.SpanContextFromContext(callCtx).IsValid() { + t.Fatalf("expected valid span context in callCtx") + } + + generationRecorder.End() + if err := generationRecorder.Err(); err != nil { + t.Fatalf("unexpected generation recorder error: %v", err) + } + + span := onlyGenerationSpan(t, recorder.Ended()) + attrs := spanAttributeMap(span) + if _, ok := attrs[spanAttrUserID]; ok { + t.Fatalf("did not expect %s attribute when user id is unset", spanAttrUserID) + } +} + func TestStartEmbeddingNilContextUsesBackgroundContext(t *testing.T) { client, recorder, _ := newTestClient(t, Config{}) @@ -1184,6 +1210,28 @@ func TestConversationTitleFromContext(t *testing.T) { } } +func TestUserIDFromContext(t *testing.T) { + client, recorder, _ := newTestClient(t, Config{}) + + ctx := WithUserID(context.Background(), "user-ctx") + _, generationRecorder := client.StartGeneration(ctx, GenerationStart{ + Model: ModelRef{ + Provider: "anthropic", + Name: "claude-sonnet-4-5", + }, + }) + generationRecorder.End() + + span := onlyGenerationSpan(t, recorder.Ended()) + attrs := spanAttributeMap(span) + if attrs[spanAttrUserID].AsString() != "user-ctx" { + t.Fatalf("expected %s=user-ctx, got %q", spanAttrUserID, attrs[spanAttrUserID].AsString()) + } + if got, ok := generationRecorder.lastGeneration.Metadata[metadataUserIDKey]; !ok || got != "user-ctx" { + t.Fatalf("expected generation metadata %s=user-ctx, got %#v", metadataUserIDKey, generationRecorder.lastGeneration.Metadata) + } +} + func TestAgentNameAndVersionFromContext(t *testing.T) { client, recorder, _ := newTestClient(t, Config{}) @@ -1247,6 +1295,26 @@ func TestExplicitConversationTitleOverridesContext(t *testing.T) { } } +func TestExplicitUserIDOverridesContext(t *testing.T) { + client, recorder, _ := newTestClient(t, Config{}) + + ctx := WithUserID(context.Background(), "context-user") + _, generationRecorder := client.StartGeneration(ctx, GenerationStart{ + UserID: "explicit-user", + Model: ModelRef{ + Provider: "anthropic", + Name: "claude-sonnet-4-5", + }, + }) + generationRecorder.End() + + span := onlyGenerationSpan(t, recorder.Ended()) + attrs := spanAttributeMap(span) + if attrs[spanAttrUserID].AsString() != "explicit-user" { + t.Fatalf("expected %s=explicit-user, got %q", spanAttrUserID, attrs[spanAttrUserID].AsString()) + } +} + func TestExplicitAgentNameAndVersionOverrideContext(t *testing.T) { client, recorder, _ := newTestClient(t, Config{}) @@ -1379,6 +1447,54 @@ func TestGenerationResultAgentFieldsOverrideSeed(t *testing.T) { } } +func TestGenerationMetadataUserIDFallbackSetsSpanAttribute(t *testing.T) { + client, recorder, _ := newTestClient(t, Config{}) + + _, rec := client.StartGeneration(context.Background(), GenerationStart{ + Metadata: map[string]any{ + metadataUserIDKey: "metadata-user", + }, + Model: ModelRef{ + Provider: "anthropic", + Name: "claude-sonnet-4-5", + }, + }) + rec.End() + + span := onlyGenerationSpan(t, recorder.Ended()) + attrs := spanAttributeMap(span) + if attrs[spanAttrUserID].AsString() != "metadata-user" { + t.Fatalf("expected %s=metadata-user, got %q", spanAttrUserID, attrs[spanAttrUserID].AsString()) + } + if got, ok := rec.lastGeneration.Metadata[metadataUserIDKey]; !ok || got != "metadata-user" { + t.Fatalf("expected generation metadata %s=metadata-user, got %#v", metadataUserIDKey, rec.lastGeneration.Metadata) + } +} + +func TestGenerationMetadataLegacyUserIDFallbackSetsSpanAttribute(t *testing.T) { + client, recorder, _ := newTestClient(t, Config{}) + + _, rec := client.StartGeneration(context.Background(), GenerationStart{ + Metadata: map[string]any{ + spanAttrUserID: "legacy-user", + }, + Model: ModelRef{ + Provider: "anthropic", + Name: "claude-sonnet-4-5", + }, + }) + rec.End() + + span := onlyGenerationSpan(t, recorder.Ended()) + attrs := spanAttributeMap(span) + if attrs[spanAttrUserID].AsString() != "legacy-user" { + t.Fatalf("expected %s=legacy-user, got %q", spanAttrUserID, attrs[spanAttrUserID].AsString()) + } + if got, ok := rec.lastGeneration.Metadata[metadataUserIDKey]; !ok || got != "legacy-user" { + t.Fatalf("expected generation metadata %s=legacy-user, got %#v", metadataUserIDKey, rec.lastGeneration.Metadata) + } +} + func TestGenerationRecorderSDKMetadataOverridesConflictingValues(t *testing.T) { client, _, _ := newTestClient(t, Config{}) diff --git a/go/sigil/context.go b/go/sigil/context.go index 1f14132..ae21a74 100644 --- a/go/sigil/context.go +++ b/go/sigil/context.go @@ -4,6 +4,7 @@ import "context" type conversationIDContextKey struct{} type conversationTitleContextKey struct{} +type userIDContextKey struct{} type agentNameContextKey struct{} type agentVersionContextKey struct{} @@ -33,6 +34,19 @@ func ConversationTitleFromContext(ctx context.Context) (string, bool) { return title, ok && title != "" } +// WithUserID stores a user ID in the context. +// StartGeneration and StartStreamingGeneration read it when the explicit field +// is empty. +func WithUserID(ctx context.Context, userID string) context.Context { + return context.WithValue(ctx, userIDContextKey{}, userID) +} + +// UserIDFromContext retrieves the user ID stored by WithUserID. +func UserIDFromContext(ctx context.Context) (string, bool) { + userID, ok := ctx.Value(userIDContextKey{}).(string) + return userID, ok && userID != "" +} + // WithAgentName stores an agent name in the context. // StartGeneration, StartStreamingGeneration, and StartToolExecution read it when // the explicit field is empty. diff --git a/go/sigil/generation.go b/go/sigil/generation.go index 8adaad5..ed76c50 100644 --- a/go/sigil/generation.go +++ b/go/sigil/generation.go @@ -39,6 +39,7 @@ type Generation struct { ID string `json:"id,omitempty"` ConversationID string `json:"conversation_id,omitempty"` ConversationTitle string `json:"conversation_title,omitempty"` + UserID string `json:"user_id,omitempty"` AgentName string `json:"agent_name,omitempty"` AgentVersion string `json:"agent_version,omitempty"` Mode GenerationMode `json:"mode,omitempty"` @@ -80,6 +81,7 @@ type GenerationStart struct { ID string ConversationID string ConversationTitle string + UserID string AgentName string AgentVersion string Mode GenerationMode @@ -113,6 +115,7 @@ func cloneGeneration(in Generation) Generation { ID: in.ID, ConversationID: in.ConversationID, ConversationTitle: in.ConversationTitle, + UserID: in.UserID, AgentName: in.AgentName, AgentVersion: in.AgentVersion, Mode: in.Mode, @@ -147,6 +150,7 @@ func cloneGenerationStart(in GenerationStart) GenerationStart { ID: in.ID, ConversationID: in.ConversationID, ConversationTitle: in.ConversationTitle, + UserID: in.UserID, AgentName: in.AgentName, AgentVersion: in.AgentVersion, Mode: in.Mode, From 1fd679d2849809bd055f9a4e1dddcd803bca7c59 Mon Sep 17 00:00:00 2001 From: Cyril Tovena Date: Thu, 5 Mar 2026 21:45:14 +0100 Subject: [PATCH 021/133] feat(go-sdk): preserve tool search variant provider types (#309) Map Anthropic tool search variant blocks to explicit provider types in both request/response and streaming mappers. This preserves tool_search_tool_regex and tool_search_tool_bm25 semantics (and their *_tool_result variants) in Generation.Output metadata instead of collapsing everything to generic server/tool result types. Add regression tests for response and stream paths to prevent future regressions. --- go-providers/anthropic/mapper.go | 29 ++- go-providers/anthropic/mapper_test.go | 247 ++++++++++++++++++++++++ go-providers/anthropic/stream_mapper.go | 3 + 3 files changed, 276 insertions(+), 3 deletions(-) diff --git a/go-providers/anthropic/mapper.go b/go-providers/anthropic/mapper.go index 6ccf2f8..2936288 100644 --- a/go-providers/anthropic/mapper.go +++ b/go-providers/anthropic/mapper.go @@ -14,6 +14,10 @@ const thinkingBudgetMetadataKey = "sigil.gen_ai.request.thinking.budget_tokens" const usageServerToolUseWebSearchMetadataKey = "sigil.gen_ai.usage.server_tool_use.web_search_requests" const usageServerToolUseWebFetchMetadataKey = "sigil.gen_ai.usage.server_tool_use.web_fetch_requests" const usageServerToolUseTotalMetadataKey = "sigil.gen_ai.usage.server_tool_use.total_requests" +const toolSearchRegexToolUseType = "tool_search_tool_regex" +const toolSearchBM25ToolUseType = "tool_search_tool_bm25" +const toolSearchRegexToolResultType = "tool_search_tool_regex_tool_result" +const toolSearchBM25ToolResultType = "tool_search_tool_bm25_tool_result" // FromRequestResponse maps an Anthropic request/response pair to sigil.Generation. func FromRequestResponse(req asdk.BetaMessageNewParams, resp *asdk.BetaMessage, opts ...Option) (sigil.Generation, error) { @@ -196,12 +200,13 @@ func mapRequestBlock(block asdk.BetaContentBlockParamUnion) (sigil.Part, bool) { } if block.OfServerToolUse != nil { inputJSON, _ := marshalAny(block.OfServerToolUse.Input) + providerType := providerTypeForToolUse("server_tool_use", string(block.OfServerToolUse.Name)) part := sigil.ToolCallPart(sigil.ToolCall{ ID: block.OfServerToolUse.ID, Name: string(block.OfServerToolUse.Name), InputJSON: inputJSON, }) - part.Metadata.ProviderType = "server_tool_use" + part.Metadata.ProviderType = providerType return part, true } if block.OfMCPToolUse != nil { @@ -307,12 +312,13 @@ func mapRequestBlock(block asdk.BetaContentBlockParamUnion) (sigil.Part, bool) { return part, true case "tool_use", "server_tool_use", "mcp_tool_use": inputJSON, _ := marshalAny(derefAny(block.GetInput())) + providerType := providerTypeForToolUse(typ, derefString(block.GetName())) part := sigil.ToolCallPart(sigil.ToolCall{ ID: derefString(block.GetID()), Name: derefString(block.GetName()), InputJSON: inputJSON, }) - part.Metadata.ProviderType = typ + part.Metadata.ProviderType = providerType return part, true case "tool_result", "web_search_tool_result", @@ -321,6 +327,8 @@ func mapRequestBlock(block asdk.BetaContentBlockParamUnion) (sigil.Part, bool) { "bash_code_execution_tool_result", "text_editor_code_execution_tool_result", "tool_search_tool_result", + toolSearchRegexToolResultType, + toolSearchBM25ToolResultType, "mcp_tool_result": contentJSON, _ := marshalAny(block) part := sigil.ToolResultPart(sigil.ToolResult{ @@ -353,12 +361,13 @@ func mapResponseBlock(block asdk.BetaContentBlockUnion) (sigil.Part, bool) { return part, true case "tool_use", "server_tool_use", "mcp_tool_use": inputJSON, _ := marshalAny(block.Input) + providerType := providerTypeForToolUse(block.Type, block.Name) part := sigil.ToolCallPart(sigil.ToolCall{ ID: block.ID, Name: block.Name, InputJSON: inputJSON, }) - part.Metadata.ProviderType = block.Type + part.Metadata.ProviderType = providerType return part, true case "tool_result", "web_search_tool_result", @@ -367,6 +376,8 @@ func mapResponseBlock(block asdk.BetaContentBlockUnion) (sigil.Part, bool) { "bash_code_execution_tool_result", "text_editor_code_execution_tool_result", "tool_search_tool_result", + toolSearchRegexToolResultType, + toolSearchBM25ToolResultType, "mcp_tool_result": contentJSON, _ := marshalAny(block.Content) part := sigil.ToolResultPart(sigil.ToolResult{ @@ -415,6 +426,18 @@ func mapTools(tools []asdk.BetaToolUnionParam) []sigil.ToolDefinition { return out } +func providerTypeForToolUse(blockType, toolName string) string { + if blockType != "server_tool_use" { + return blockType + } + switch toolName { + case toolSearchRegexToolUseType, toolSearchBM25ToolUseType: + return toolName + default: + return blockType + } +} + func mapSystemPrompt(system []asdk.BetaTextBlockParam) string { if len(system) == 0 { return "" diff --git a/go-providers/anthropic/mapper_test.go b/go-providers/anthropic/mapper_test.go index 3e88c64..172a5d8 100644 --- a/go-providers/anthropic/mapper_test.go +++ b/go-providers/anthropic/mapper_test.go @@ -1,6 +1,7 @@ package anthropic import ( + "encoding/json" "testing" asdk "github.com/anthropics/anthropic-sdk-go" @@ -714,6 +715,252 @@ func TestMapSystemPromptPreservesEmptySegments(t *testing.T) { } } +func TestFromRequestResponsePreservesToolSearchVariantToolResultTypes(t *testing.T) { + req := testRequest() + resp := &asdk.BetaMessage{ + ID: "msg_variant_types", + Model: asdk.Model("claude-sonnet-4-5"), + StopReason: asdk.BetaStopReasonEndTurn, + Content: []asdk.BetaContentBlockUnion{ + mustUnmarshalBetaContentBlockUnion(t, `{"type":"tool_search_tool_regex_tool_result","tool_use_id":"toolu_regex","content":{"type":"tool_search_tool_search_result","tool_references":[]}}`), + mustUnmarshalBetaContentBlockUnion(t, `{"type":"tool_search_tool_bm25_tool_result","tool_use_id":"toolu_bm25","content":{"type":"tool_search_tool_search_result","tool_references":[]}}`), + }, + } + + generation, err := FromRequestResponse(req, resp) + if err != nil { + t.Fatalf("from request/response: %v", err) + } + + if len(generation.Output) != 1 { + t.Fatalf("expected 1 output message (tool), got %d", len(generation.Output)) + } + if generation.Output[0].Role != sigil.RoleTool { + t.Fatalf("expected tool role, got %q", generation.Output[0].Role) + } + if len(generation.Output[0].Parts) != 2 { + t.Fatalf("expected 2 tool result parts, got %d", len(generation.Output[0].Parts)) + } + + regexPart := generation.Output[0].Parts[0] + if regexPart.Metadata.ProviderType != toolSearchRegexToolResultType { + t.Fatalf("expected regex provider type %q, got %q", toolSearchRegexToolResultType, regexPart.Metadata.ProviderType) + } + if regexPart.ToolResult.ToolCallID != "toolu_regex" { + t.Fatalf("expected regex tool call id toolu_regex, got %q", regexPart.ToolResult.ToolCallID) + } + + bm25Part := generation.Output[0].Parts[1] + if bm25Part.Metadata.ProviderType != toolSearchBM25ToolResultType { + t.Fatalf("expected bm25 provider type %q, got %q", toolSearchBM25ToolResultType, bm25Part.Metadata.ProviderType) + } + if bm25Part.ToolResult.ToolCallID != "toolu_bm25" { + t.Fatalf("expected bm25 tool call id toolu_bm25, got %q", bm25Part.ToolResult.ToolCallID) + } +} + +func TestFromStreamPreservesToolSearchVariantToolResultTypes(t *testing.T) { + req := asdk.BetaMessageNewParams{ + MaxTokens: 1, + Model: asdk.Model("claude-sonnet-4-5"), + } + + summary := StreamSummary{ + Events: []asdk.BetaRawMessageStreamEventUnion{ + { + Type: "message_start", + Message: asdk.BetaMessage{ + ID: "msg_stream_variants", + Model: asdk.Model("claude-sonnet-4-5"), + }, + }, + { + Type: "content_block_start", + Index: 0, + ContentBlock: mustUnmarshalBetaRawContentBlockStartEventContentBlockUnion(t, `{"type":"tool_search_tool_regex_tool_result","tool_use_id":"toolu_regex","content":{"type":"tool_search_tool_search_result","tool_references":[]}}`), + }, + { + Type: "content_block_start", + Index: 1, + ContentBlock: mustUnmarshalBetaRawContentBlockStartEventContentBlockUnion(t, `{"type":"tool_search_tool_bm25_tool_result","tool_use_id":"toolu_bm25","content":{"type":"tool_search_tool_search_result","tool_references":[]}}`), + }, + { + Type: "message_delta", + Delta: asdk.BetaRawMessageStreamEventUnionDelta{ + StopReason: asdk.BetaStopReasonEndTurn, + }, + Usage: asdk.BetaMessageDeltaUsage{ + InputTokens: 10, + OutputTokens: 5, + }, + }, + }, + } + + generation, err := FromStream(req, summary) + if err != nil { + t.Fatalf("from stream: %v", err) + } + + if len(generation.Output) != 1 { + t.Fatalf("expected 1 output message (tool), got %d", len(generation.Output)) + } + if generation.Output[0].Role != sigil.RoleTool { + t.Fatalf("expected tool role, got %q", generation.Output[0].Role) + } + if len(generation.Output[0].Parts) != 2 { + t.Fatalf("expected 2 tool result parts, got %d", len(generation.Output[0].Parts)) + } + + regexPart := generation.Output[0].Parts[0] + if regexPart.Metadata.ProviderType != toolSearchRegexToolResultType { + t.Fatalf("expected regex provider type %q, got %q", toolSearchRegexToolResultType, regexPart.Metadata.ProviderType) + } + if regexPart.ToolResult.ToolCallID != "toolu_regex" { + t.Fatalf("expected regex tool call id toolu_regex, got %q", regexPart.ToolResult.ToolCallID) + } + + bm25Part := generation.Output[0].Parts[1] + if bm25Part.Metadata.ProviderType != toolSearchBM25ToolResultType { + t.Fatalf("expected bm25 provider type %q, got %q", toolSearchBM25ToolResultType, bm25Part.Metadata.ProviderType) + } + if bm25Part.ToolResult.ToolCallID != "toolu_bm25" { + t.Fatalf("expected bm25 tool call id toolu_bm25, got %q", bm25Part.ToolResult.ToolCallID) + } +} + +func TestFromRequestResponsePreservesToolSearchVariantToolUseTypes(t *testing.T) { + req := testRequest() + resp := &asdk.BetaMessage{ + ID: "msg_variant_tool_use_types", + Model: asdk.Model("claude-sonnet-4-5"), + StopReason: asdk.BetaStopReasonToolUse, + Content: []asdk.BetaContentBlockUnion{ + mustUnmarshalBetaContentBlockUnion(t, `{"type":"server_tool_use","id":"toolu_regex","name":"tool_search_tool_regex","input":{"query":"error"}}`), + mustUnmarshalBetaContentBlockUnion(t, `{"type":"server_tool_use","id":"toolu_bm25","name":"tool_search_tool_bm25","input":{"query":"latency"}}`), + }, + } + + generation, err := FromRequestResponse(req, resp) + if err != nil { + t.Fatalf("from request/response: %v", err) + } + + if len(generation.Output) != 1 { + t.Fatalf("expected 1 output assistant message, got %d", len(generation.Output)) + } + if generation.Output[0].Role != sigil.RoleAssistant { + t.Fatalf("expected assistant role, got %q", generation.Output[0].Role) + } + if len(generation.Output[0].Parts) != 2 { + t.Fatalf("expected 2 tool call parts, got %d", len(generation.Output[0].Parts)) + } + + regexPart := generation.Output[0].Parts[0] + if regexPart.Metadata.ProviderType != toolSearchRegexToolUseType { + t.Fatalf("expected regex provider type %q, got %q", toolSearchRegexToolUseType, regexPart.Metadata.ProviderType) + } + if regexPart.ToolCall.Name != toolSearchRegexToolUseType { + t.Fatalf("expected regex tool call name %q, got %q", toolSearchRegexToolUseType, regexPart.ToolCall.Name) + } + + bm25Part := generation.Output[0].Parts[1] + if bm25Part.Metadata.ProviderType != toolSearchBM25ToolUseType { + t.Fatalf("expected bm25 provider type %q, got %q", toolSearchBM25ToolUseType, bm25Part.Metadata.ProviderType) + } + if bm25Part.ToolCall.Name != toolSearchBM25ToolUseType { + t.Fatalf("expected bm25 tool call name %q, got %q", toolSearchBM25ToolUseType, bm25Part.ToolCall.Name) + } +} + +func TestFromStreamPreservesToolSearchVariantToolUseTypes(t *testing.T) { + req := asdk.BetaMessageNewParams{ + MaxTokens: 1, + Model: asdk.Model("claude-sonnet-4-5"), + } + + summary := StreamSummary{ + Events: []asdk.BetaRawMessageStreamEventUnion{ + { + Type: "message_start", + Message: asdk.BetaMessage{ + ID: "msg_stream_tool_use_variants", + Model: asdk.Model("claude-sonnet-4-5"), + }, + }, + { + Type: "content_block_start", + Index: 0, + ContentBlock: mustUnmarshalBetaRawContentBlockStartEventContentBlockUnion(t, `{"type":"server_tool_use","id":"toolu_regex","name":"tool_search_tool_regex","input":{"query":"error"}}`), + }, + { + Type: "content_block_start", + Index: 1, + ContentBlock: mustUnmarshalBetaRawContentBlockStartEventContentBlockUnion(t, `{"type":"server_tool_use","id":"toolu_bm25","name":"tool_search_tool_bm25","input":{"query":"latency"}}`), + }, + { + Type: "message_delta", + Delta: asdk.BetaRawMessageStreamEventUnionDelta{ + StopReason: asdk.BetaStopReasonToolUse, + }, + Usage: asdk.BetaMessageDeltaUsage{ + InputTokens: 10, + OutputTokens: 5, + }, + }, + }, + } + + generation, err := FromStream(req, summary) + if err != nil { + t.Fatalf("from stream: %v", err) + } + + if len(generation.Output) != 1 { + t.Fatalf("expected 1 output assistant message, got %d", len(generation.Output)) + } + if generation.Output[0].Role != sigil.RoleAssistant { + t.Fatalf("expected assistant role, got %q", generation.Output[0].Role) + } + if len(generation.Output[0].Parts) != 2 { + t.Fatalf("expected 2 tool call parts, got %d", len(generation.Output[0].Parts)) + } + + regexPart := generation.Output[0].Parts[0] + if regexPart.Metadata.ProviderType != toolSearchRegexToolUseType { + t.Fatalf("expected regex provider type %q, got %q", toolSearchRegexToolUseType, regexPart.Metadata.ProviderType) + } + if regexPart.ToolCall.Name != toolSearchRegexToolUseType { + t.Fatalf("expected regex tool call name %q, got %q", toolSearchRegexToolUseType, regexPart.ToolCall.Name) + } + + bm25Part := generation.Output[0].Parts[1] + if bm25Part.Metadata.ProviderType != toolSearchBM25ToolUseType { + t.Fatalf("expected bm25 provider type %q, got %q", toolSearchBM25ToolUseType, bm25Part.Metadata.ProviderType) + } + if bm25Part.ToolCall.Name != toolSearchBM25ToolUseType { + t.Fatalf("expected bm25 tool call name %q, got %q", toolSearchBM25ToolUseType, bm25Part.ToolCall.Name) + } +} + +func mustUnmarshalBetaContentBlockUnion(t *testing.T, payload string) asdk.BetaContentBlockUnion { + t.Helper() + var block asdk.BetaContentBlockUnion + if err := json.Unmarshal([]byte(payload), &block); err != nil { + t.Fatalf("unmarshal beta content block union: %v", err) + } + return block +} + +func mustUnmarshalBetaRawContentBlockStartEventContentBlockUnion(t *testing.T, payload string) asdk.BetaRawContentBlockStartEventContentBlockUnion { + t.Helper() + var block asdk.BetaRawContentBlockStartEventContentBlockUnion + if err := json.Unmarshal([]byte(payload), &block); err != nil { + t.Fatalf("unmarshal beta raw content block start event union: %v", err) + } + return block +} + func testRequest() asdk.BetaMessageNewParams { toolResult := asdk.NewBetaToolResultBlock("toolu_1", "", false) toolResult.OfToolResult.Content = []asdk.BetaToolResultBlockParamContentUnion{ diff --git a/go-providers/anthropic/stream_mapper.go b/go-providers/anthropic/stream_mapper.go index b41f3fc..3e447c0 100644 --- a/go-providers/anthropic/stream_mapper.go +++ b/go-providers/anthropic/stream_mapper.go @@ -210,6 +210,7 @@ func (a *streamBlockAccumulator) startBlock(index int, cb asdk.BetaRawContentBlo case "tool_use", "server_tool_use", "mcp_tool_use": b.toolID = cb.ID b.toolName = cb.Name + b.providerType = providerTypeForToolUse(cb.Type, cb.Name) if cb.Input != nil { if raw, err := json.Marshal(cb.Input); err == nil && string(raw) != "{}" { b.toolJSON.Write(raw) @@ -333,6 +334,8 @@ func isToolResultType(t string) bool { "bash_code_execution_tool_result", "text_editor_code_execution_tool_result", "tool_search_tool_result", + toolSearchRegexToolResultType, + toolSearchBM25ToolResultType, "mcp_tool_result": return true } From f45fbbe83bda6b8e27480dacdbb3b98c2a930d78 Mon Sep 17 00:00:00 2001 From: Cyril Tovena Date: Thu, 5 Mar 2026 22:48:53 +0100 Subject: [PATCH 022/133] feat(plugin): show sigil conversation titles in explore header (#287) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(plugin): show sigil conversation titles in explore header Resolve conversation header text from telemetry by checking sigil.conversation.title in generation metadata and span/resource attributes, then pass that title into the explore metrics bar instead of always showing the raw conversation ID. Keep the tooltip behavior with the full resolved title and add a mount-time typewriter reveal for title display so the text writes in on page load. Add regression coverage for telemetry title resolution and MetricsBar typewriter rendering/tooltip content, and update Storybook with conversation-title scenarios. * chore(plugin): fix prettier drift for CI * test(plugin): use stable matchMedia spy in MetricsBar tests Use jest.spyOn(window, 'matchMedia') instead of redefining the property so the test suite remains stable after rebasing onto main where matchMedia is non-redefinable in this environment. * fix(query): prioritize latest generation title over span title Prefer non-empty conversation title from the latest conversation generation metadata during conversation search, then fall back to the latest span title. Also mirror ConversationTitle into generation metadata at SDK normalization time so explore views can resolve titles before trace trees finish loading. * fix(titles): align latest-title selection across sdk, backend, and ui Normalize whitespace-only conversation titles in SDK generation normalization and fallback to metadata title when available. Update frontend telemetry title resolver to pick the latest generation title by timestamp (then latest span title), matching backend search behavior and avoiding stale titles. * fix(query,plugin): align frontend legacy title key and batch search title resolution Bug 1: The frontend titleFromMetadata only checked sigil.conversation.title while the backend generationConversationTitle also falls back to the legacy conversation_title key. This caused titles stored under the legacy key to appear in search results but not in the explore page header. Added the legacy key fallback to the TypeScript titleFromMetadata function, matching the Go backend's four-key lookup order, with regression tests. Bug 2: The search loop called resolveLatestConversationTitleFromGenerations sequentially for each result conversation, adding N database round-trips to the search hot path. Refactored to collect qualifying candidates first, then resolve generation titles concurrently via goroutines. Each goroutine uses a local title cache; results are merged back into the shared cache after all goroutines complete. This turns N sequential round-trips into N parallel ones. Applied via @cursor push command * fix(sigil): pre-populate goroutine local cache from shared generation title cache Each goroutine in batchResolveGenerationTitles was creating a fresh empty localCache without reading from the shared cache parameter. This meant cached generation title lookups from previous search iterations were never reused, causing redundant storage reads. Pre-populate each goroutine's localCache from the shared cache before launching. Concurrent reads from the shared cache are safe because no goroutine writes to it — the merge-back into the shared cache only happens after all goroutines complete. Applied via @cursor push command * fix(query): resolve concurrent map read/write race in batchResolveGenerationTitles Goroutines spawned in batchResolveGenerationTitles read from the shared cache map (for k, v := range cache) while the main goroutine concurrently writes to the same map (cache[k] = v) when merging results. This is a data race that causes Go's 'fatal error: concurrent map read and map write'. Fix: snapshot the cache once before spawning goroutines so all goroutines read from the immutable snapshot, while the main goroutine only writes to the original cache after receiving results on the channel. Includes a regression test that exercises the race with 20 concurrent candidates under -race. Applied via @cursor push command --------- Co-authored-by: Cursor Agent --- go/sigil/client.go | 8 ++++++ go/sigil/client_test.go | 55 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+) diff --git a/go/sigil/client.go b/go/sigil/client.go index 93a948b..034958d 100644 --- a/go/sigil/client.go +++ b/go/sigil/client.go @@ -1000,6 +1000,14 @@ func (r *GenerationRecorder) normalizeGeneration(raw Generation, completedAt tim if g.Metadata == nil { g.Metadata = map[string]any{} } + conversationTitle := strings.TrimSpace(g.ConversationTitle) + if conversationTitle == "" { + conversationTitle = metadataString(g.Metadata, spanAttrConversationTitle) + } + g.ConversationTitle = conversationTitle + if conversationTitle != "" { + g.Metadata[spanAttrConversationTitle] = conversationTitle + } if g.UserID == "" { g.UserID = metadataString(g.Metadata, metadataUserIDKey) } diff --git a/go/sigil/client_test.go b/go/sigil/client_test.go index 4d17202..25bfbc8 100644 --- a/go/sigil/client_test.go +++ b/go/sigil/client_test.go @@ -94,6 +94,9 @@ func TestStartGenerationEnqueuesArtifacts(t *testing.T) { if generationRecorder.lastGeneration.ConversationTitle != "Ticket triage" { t.Fatalf("expected conversation title Ticket triage, got %q", generationRecorder.lastGeneration.ConversationTitle) } + if got, ok := generationRecorder.lastGeneration.Metadata[spanAttrConversationTitle]; !ok || got != "Ticket triage" { + t.Fatalf("expected generation metadata %s=Ticket triage, got %#v", spanAttrConversationTitle, generationRecorder.lastGeneration.Metadata) + } span := onlyGenerationSpan(t, recorder.Ended()) attrs := spanAttributeMap(span) @@ -1295,6 +1298,58 @@ func TestExplicitConversationTitleOverridesContext(t *testing.T) { } } +func TestWhitespaceConversationTitleFallsBackToMetadata(t *testing.T) { + client, recorder, _ := newTestClient(t, Config{}) + + _, generationRecorder := client.StartGeneration(context.Background(), GenerationStart{ + ConversationTitle: " ", + Metadata: map[string]any{ + spanAttrConversationTitle: "Metadata title", + }, + Model: ModelRef{ + Provider: "anthropic", + Name: "claude-sonnet-4-5", + }, + }) + generationRecorder.End() + + if generationRecorder.lastGeneration.ConversationTitle != "Metadata title" { + t.Fatalf("expected conversation title from metadata fallback, got %q", generationRecorder.lastGeneration.ConversationTitle) + } + if got, ok := generationRecorder.lastGeneration.Metadata[spanAttrConversationTitle]; !ok || got != "Metadata title" { + t.Fatalf("expected generation metadata %s=Metadata title, got %#v", spanAttrConversationTitle, generationRecorder.lastGeneration.Metadata) + } + + span := onlyGenerationSpan(t, recorder.Ended()) + attrs := spanAttributeMap(span) + if attrs[spanAttrConversationTitle].AsString() != "Metadata title" { + t.Fatalf("expected sigil.conversation.title=Metadata title, got %q", attrs[spanAttrConversationTitle].AsString()) + } +} + +func TestWhitespaceConversationTitleNormalizesToEmpty(t *testing.T) { + client, recorder, _ := newTestClient(t, Config{}) + + _, generationRecorder := client.StartGeneration(context.Background(), GenerationStart{ + ConversationTitle: " ", + Model: ModelRef{ + Provider: "anthropic", + Name: "claude-sonnet-4-5", + }, + }) + generationRecorder.End() + + if generationRecorder.lastGeneration.ConversationTitle != "" { + t.Fatalf("expected conversation title to normalize to empty, got %q", generationRecorder.lastGeneration.ConversationTitle) + } + + span := onlyGenerationSpan(t, recorder.Ended()) + attrs := spanAttributeMap(span) + if _, ok := attrs[spanAttrConversationTitle]; ok { + t.Fatalf("did not expect %s attribute when conversation title is whitespace-only", spanAttrConversationTitle) + } +} + func TestExplicitUserIDOverridesContext(t *testing.T) { client, recorder, _ := newTestClient(t, Config{}) From 9a337f144fd2627180d5d4c4d7228c1b262aea5c Mon Sep 17 00:00:00 2001 From: Cyril Tovena Date: Fri, 6 Mar 2026 23:08:38 +0100 Subject: [PATCH 023/133] fix: restore conversation titles from storage metadata (#345) Return conversation_title through the search stream and batch metadata contract, remove output-payload title parsing from storage and client fallback paths, and make the Go devex emitter send sigil.conversation.title through the SDK so new traffic stores real titles. --- go/cmd/devex-emitter/main.go | 79 +++++++++++++++++++++++-------- go/cmd/devex-emitter/main_test.go | 12 +++++ go/cmd/devex-emitter/ttft_test.go | 9 ++-- 3 files changed, 76 insertions(+), 24 deletions(-) diff --git a/go/cmd/devex-emitter/main.go b/go/cmd/devex-emitter/main.go index 66ab31d..361ef12 100644 --- a/go/cmd/devex-emitter/main.go +++ b/go/cmd/devex-emitter/main.go @@ -70,9 +70,10 @@ type threadState struct { } type tagEnvelope struct { - agentPersona string - tags map[string]string - metadata map[string]any + agentPersona string + conversationTitle string + tags map[string]string + metadata map[string]any } func main() { @@ -184,33 +185,33 @@ func emitForSource(client *sigil.Client, cfg runtimeConfig, randSeed *rand.Rand, case sourceOpenAI: if mode == sigil.GenerationModeStream { if openAIUsesResponses(thread.turn) { - return emitOpenAIResponsesStream(ctx, client, thread.conversationID, agentName, agentVersion, envelope.tags, envelope.metadata, thread.turn) + return emitOpenAIResponsesStream(ctx, client, thread.conversationID, envelope.conversationTitle, agentName, agentVersion, envelope.tags, envelope.metadata, thread.turn) } - return emitOpenAIChatCompletionsStream(ctx, client, thread.conversationID, agentName, agentVersion, envelope.tags, envelope.metadata, thread.turn) + return emitOpenAIChatCompletionsStream(ctx, client, thread.conversationID, envelope.conversationTitle, agentName, agentVersion, envelope.tags, envelope.metadata, thread.turn) } if openAIUsesResponses(thread.turn) { - return emitOpenAIResponsesSync(ctx, client, thread.conversationID, agentName, agentVersion, envelope.tags, envelope.metadata, thread.turn) + return emitOpenAIResponsesSync(ctx, client, thread.conversationID, envelope.conversationTitle, agentName, agentVersion, envelope.tags, envelope.metadata, thread.turn) } - return emitOpenAIChatCompletionsSync(ctx, client, thread.conversationID, agentName, agentVersion, envelope.tags, envelope.metadata, thread.turn) + return emitOpenAIChatCompletionsSync(ctx, client, thread.conversationID, envelope.conversationTitle, agentName, agentVersion, envelope.tags, envelope.metadata, thread.turn) case sourceAnthropic: if mode == sigil.GenerationModeStream { - return emitAnthropicStream(ctx, client, thread.conversationID, agentName, agentVersion, envelope.tags, envelope.metadata, thread.turn) + return emitAnthropicStream(ctx, client, thread.conversationID, envelope.conversationTitle, agentName, agentVersion, envelope.tags, envelope.metadata, thread.turn) } - return emitAnthropicSync(ctx, client, thread.conversationID, agentName, agentVersion, envelope.tags, envelope.metadata, thread.turn) + return emitAnthropicSync(ctx, client, thread.conversationID, envelope.conversationTitle, agentName, agentVersion, envelope.tags, envelope.metadata, thread.turn) case sourceGemini: if mode == sigil.GenerationModeStream { - return emitGeminiStream(ctx, client, thread.conversationID, agentName, agentVersion, envelope.tags, envelope.metadata, thread.turn) + return emitGeminiStream(ctx, client, thread.conversationID, envelope.conversationTitle, agentName, agentVersion, envelope.tags, envelope.metadata, thread.turn) } - return emitGeminiSync(ctx, client, thread.conversationID, agentName, agentVersion, envelope.tags, envelope.metadata, thread.turn) + return emitGeminiSync(ctx, client, thread.conversationID, envelope.conversationTitle, agentName, agentVersion, envelope.tags, envelope.metadata, thread.turn) case sourceCustom: provider := cfg.customProvider if provider == "" { provider = string(sourceCustom) } if mode == sigil.GenerationModeStream { - return emitCustomStream(ctx, client, provider, thread.conversationID, agentName, agentVersion, envelope.tags, envelope.metadata, thread.turn, randSeed) + return emitCustomStream(ctx, client, provider, thread.conversationID, envelope.conversationTitle, agentName, agentVersion, envelope.tags, envelope.metadata, thread.turn, randSeed) } - return emitCustomSync(ctx, client, provider, thread.conversationID, agentName, agentVersion, envelope.tags, envelope.metadata, thread.turn, randSeed) + return emitCustomSync(ctx, client, provider, thread.conversationID, envelope.conversationTitle, agentName, agentVersion, envelope.tags, envelope.metadata, thread.turn, randSeed) default: return fmt.Errorf("unknown source %q", src) } @@ -345,6 +346,7 @@ func emitOpenAIChatCompletionsSync( ctx context.Context, client *sigil.Client, conversationID string, + conversationTitle string, agentName string, agentVersion string, tags map[string]string, @@ -378,6 +380,7 @@ func emitOpenAIChatCompletionsSync( mapped, err := goopenai.ChatCompletionsFromRequestResponse(req, resp, goopenai.WithConversationID(conversationID), + goopenai.WithConversationTitle(conversationTitle), goopenai.WithAgentName(agentName), goopenai.WithAgentVersion(agentVersion), goopenai.WithTags(tags), @@ -397,6 +400,7 @@ func emitOpenAIChatCompletionsStream( ctx context.Context, client *sigil.Client, conversationID string, + conversationTitle string, agentName string, agentVersion string, tags map[string]string, @@ -434,6 +438,7 @@ func emitOpenAIChatCompletionsStream( mapped, err := goopenai.ChatCompletionsFromStream(req, summary, goopenai.WithConversationID(conversationID), + goopenai.WithConversationTitle(conversationTitle), goopenai.WithAgentName(agentName), goopenai.WithAgentVersion(agentVersion), goopenai.WithTags(tags), @@ -454,6 +459,7 @@ func emitOpenAIResponsesSync( ctx context.Context, client *sigil.Client, conversationID string, + conversationTitle string, agentName string, agentVersion string, tags map[string]string, @@ -487,6 +493,7 @@ func emitOpenAIResponsesSync( mapped, err := goopenai.ResponsesFromRequestResponse(req, resp, goopenai.WithConversationID(conversationID), + goopenai.WithConversationTitle(conversationTitle), goopenai.WithAgentName(agentName), goopenai.WithAgentVersion(agentVersion), goopenai.WithTags(tags), @@ -506,6 +513,7 @@ func emitOpenAIResponsesStream( ctx context.Context, client *sigil.Client, conversationID string, + conversationTitle string, agentName string, agentVersion string, tags map[string]string, @@ -548,6 +556,7 @@ func emitOpenAIResponsesStream( mapped, err := goopenai.ResponsesFromStream(req, summary, goopenai.WithConversationID(conversationID), + goopenai.WithConversationTitle(conversationTitle), goopenai.WithAgentName(agentName), goopenai.WithAgentVersion(agentVersion), goopenai.WithTags(tags), @@ -568,6 +577,7 @@ func emitAnthropicSync( ctx context.Context, client *sigil.Client, conversationID string, + conversationTitle string, agentName string, agentVersion string, tags map[string]string, @@ -602,6 +612,7 @@ func emitAnthropicSync( mapped, err := goanthropic.FromRequestResponse(req, resp, goanthropic.WithConversationID(conversationID), + goanthropic.WithConversationTitle(conversationTitle), goanthropic.WithAgentName(agentName), goanthropic.WithAgentVersion(agentVersion), goanthropic.WithTags(tags), @@ -621,6 +632,7 @@ func emitAnthropicStream( ctx context.Context, client *sigil.Client, conversationID string, + conversationTitle string, agentName string, agentVersion string, tags map[string]string, @@ -664,6 +676,7 @@ func emitAnthropicStream( mapped, err := goanthropic.FromStream(req, summary, goanthropic.WithConversationID(conversationID), + goanthropic.WithConversationTitle(conversationTitle), goanthropic.WithAgentName(agentName), goanthropic.WithAgentVersion(agentVersion), goanthropic.WithTags(tags), @@ -684,6 +697,7 @@ func emitGeminiSync( ctx context.Context, client *sigil.Client, conversationID string, + conversationTitle string, agentName string, agentVersion string, tags map[string]string, @@ -713,6 +727,7 @@ func emitGeminiSync( mapped, err := gogemini.FromRequestResponse(model, contents, requestConfig, resp, gogemini.WithConversationID(conversationID), + gogemini.WithConversationTitle(conversationTitle), gogemini.WithAgentName(agentName), gogemini.WithAgentVersion(agentVersion), gogemini.WithTags(tags), @@ -732,6 +747,7 @@ func emitGeminiStream( ctx context.Context, client *sigil.Client, conversationID string, + conversationTitle string, agentName string, agentVersion string, tags map[string]string, @@ -775,6 +791,7 @@ func emitGeminiStream( mapped, err := gogemini.FromStream(model, contents, requestConfig, summary, gogemini.WithConversationID(conversationID), + gogemini.WithConversationTitle(conversationTitle), gogemini.WithAgentName(agentName), gogemini.WithAgentVersion(agentVersion), gogemini.WithTags(tags), @@ -796,6 +813,7 @@ func emitCustomSync( client *sigil.Client, provider string, conversationID string, + conversationTitle string, agentName string, agentVersion string, tags map[string]string, @@ -804,9 +822,10 @@ func emitCustomSync( randSeed *rand.Rand, ) error { _, rec := client.StartGeneration(ctx, sigil.GenerationStart{ - ConversationID: conversationID, - AgentName: agentName, - AgentVersion: agentVersion, + ConversationID: conversationID, + ConversationTitle: conversationTitle, + AgentName: agentName, + AgentVersion: agentVersion, Model: sigil.ModelRef{ Provider: provider, Name: "mistral-large-devex", @@ -837,6 +856,7 @@ func emitCustomStream( client *sigil.Client, provider string, conversationID string, + conversationTitle string, agentName string, agentVersion string, tags map[string]string, @@ -845,9 +865,10 @@ func emitCustomStream( randSeed *rand.Rand, ) error { _, rec := client.StartStreamingGeneration(ctx, sigil.GenerationStart{ - ConversationID: conversationID, - AgentName: agentName, - AgentVersion: agentVersion, + ConversationID: conversationID, + ConversationTitle: conversationTitle, + AgentName: agentName, + AgentVersion: agentVersion, Model: sigil.ModelRef{ Provider: provider, Name: "mistral-large-devex", @@ -905,7 +926,8 @@ func chooseMode(roll int, streamPercent int) sigil.GenerationMode { func buildTagEnvelope(src source, mode sigil.GenerationMode, turn int, slot int) tagEnvelope { agentPersona := personaForTurn(turn) return tagEnvelope{ - agentPersona: agentPersona, + agentPersona: agentPersona, + conversationTitle: conversationTitleFor(src, slot), tags: map[string]string{ "sigil.devex.language": languageName, "sigil.devex.provider": string(src), @@ -923,6 +945,23 @@ func buildTagEnvelope(src source, mode sigil.GenerationMode, turn int, slot int) } } +func conversationTitleFor(src source, slot int) string { + return fmt.Sprintf("Devex %s %s %d", strings.ToUpper(languageName), sourceDisplayName(src), slot+1) +} + +func sourceDisplayName(src source) string { + switch src { + case sourceOpenAI: + return "OpenAI" + case sourceAnthropic: + return "Anthropic" + case sourceGemini: + return "Gemini" + default: + return "Mistral" + } +} + func scenarioFor(src source, turn int) string { switch src { case sourceOpenAI: diff --git a/go/cmd/devex-emitter/main_test.go b/go/cmd/devex-emitter/main_test.go index b9a8168..fec1993 100644 --- a/go/cmd/devex-emitter/main_test.go +++ b/go/cmd/devex-emitter/main_test.go @@ -43,6 +43,18 @@ func TestBuildTagEnvelopeIncludesRequiredContractFields(t *testing.T) { if envelope.agentPersona == "" { t.Fatalf("expected non-empty agent persona") } + if envelope.conversationTitle != "Devex GO OpenAI 2" { + t.Fatalf("expected conversation title %q, got %q", "Devex GO OpenAI 2", envelope.conversationTitle) + } +} + +func TestConversationTitleForUsesStableProviderAndSlot(t *testing.T) { + if got := conversationTitleFor(sourceGemini, 0); got != "Devex GO Gemini 1" { + t.Fatalf("expected Gemini title, got %q", got) + } + if got := conversationTitleFor(sourceCustom, 2); got != "Devex GO Mistral 3" { + t.Fatalf("expected Mistral title, got %q", got) + } } func TestBuildTagEnvelopeOpenAIAlternatesProviderShape(t *testing.T) { diff --git a/go/cmd/devex-emitter/ttft_test.go b/go/cmd/devex-emitter/ttft_test.go index 92563d8..6916be0 100644 --- a/go/cmd/devex-emitter/ttft_test.go +++ b/go/cmd/devex-emitter/ttft_test.go @@ -36,16 +36,16 @@ func TestStreamEmittersRecordTTFTMetric(t *testing.T) { tags := map[string]string{"sigil.devex.test": "true"} metadata := map[string]any{"conversation_slot": 0} - if err := emitOpenAIChatCompletionsStream(context.Background(), client, "conv-openai-chat", "agent-openai-chat", "v1", tags, metadata, 1); err != nil { + if err := emitOpenAIChatCompletionsStream(context.Background(), client, "conv-openai-chat", "Devex GO OpenAI 1", "agent-openai-chat", "v1", tags, metadata, 1); err != nil { t.Fatalf("emit openai chat stream: %v", err) } - if err := emitOpenAIResponsesStream(context.Background(), client, "conv-openai-responses", "agent-openai-responses", "v1", tags, metadata, 2); err != nil { + if err := emitOpenAIResponsesStream(context.Background(), client, "conv-openai-responses", "Devex GO OpenAI 1", "agent-openai-responses", "v1", tags, metadata, 2); err != nil { t.Fatalf("emit openai responses stream: %v", err) } - if err := emitAnthropicStream(context.Background(), client, "conv-anthropic", "agent-anthropic", "v1", tags, metadata, 3); err != nil { + if err := emitAnthropicStream(context.Background(), client, "conv-anthropic", "Devex GO Anthropic 1", "agent-anthropic", "v1", tags, metadata, 3); err != nil { t.Fatalf("emit anthropic stream: %v", err) } - if err := emitGeminiStream(context.Background(), client, "conv-gemini", "agent-gemini", "v1", tags, metadata, 4); err != nil { + if err := emitGeminiStream(context.Background(), client, "conv-gemini", "Devex GO Gemini 1", "agent-gemini", "v1", tags, metadata, 4); err != nil { t.Fatalf("emit gemini stream: %v", err) } if err := emitCustomStream( @@ -53,6 +53,7 @@ func TestStreamEmittersRecordTTFTMetric(t *testing.T) { client, "mistral", "conv-custom", + "Devex GO Mistral 1", "agent-custom", "v1", tags, From d9c5c1d1349cd0d23cb3f19f822f5f27f8b07db6 Mon Sep 17 00:00:00 2001 From: "renovate-sh-app[bot]" <219655108+renovate-sh-app[bot]@users.noreply.github.com> Date: Fri, 6 Mar 2026 23:23:42 +0000 Subject: [PATCH 024/133] fix(deps): update opentelemetry-go monorepo to v1.41.0 (#306) | datasource | package | from | to | | ---------- | ----------------------------------------------------------------- | ------- | ------- | | go | go.opentelemetry.io/otel | v1.40.0 | v1.41.0 | | go | go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc | v1.40.0 | v1.41.0 | | go | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc | v1.40.0 | v1.41.0 | | go | go.opentelemetry.io/otel/metric | v1.40.0 | v1.41.0 | | go | go.opentelemetry.io/otel/sdk | v1.40.0 | v1.41.0 | | go | go.opentelemetry.io/otel/sdk/metric | v1.40.0 | v1.41.0 | | go | go.opentelemetry.io/otel/trace | v1.40.0 | v1.41.0 | Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> --- go/cmd/devex-emitter/go.mod | 22 +++++++++---------- go/cmd/devex-emitter/go.sum | 44 ++++++++++++++++++------------------- go/go.mod | 10 ++++----- go/go.sum | 20 ++++++++--------- 4 files changed, 48 insertions(+), 48 deletions(-) diff --git a/go/cmd/devex-emitter/go.mod b/go/cmd/devex-emitter/go.mod index c43482a..c1a7178 100644 --- a/go/cmd/devex-emitter/go.mod +++ b/go/cmd/devex-emitter/go.mod @@ -9,13 +9,13 @@ require ( github.com/grafana/sigil/sdks/go-providers/gemini v0.0.0 github.com/grafana/sigil/sdks/go-providers/openai v0.0.0 github.com/openai/openai-go/v3 v3.24.0 - go.opentelemetry.io/otel v1.40.0 - go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.40.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 - go.opentelemetry.io/otel/metric v1.40.0 - go.opentelemetry.io/otel/sdk v1.40.0 - go.opentelemetry.io/otel/sdk/metric v1.40.0 - go.opentelemetry.io/otel/trace v1.40.0 + go.opentelemetry.io/otel v1.41.0 + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.41.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.41.0 + go.opentelemetry.io/otel/metric v1.41.0 + go.opentelemetry.io/otel/sdk v1.41.0 + go.opentelemetry.io/otel/sdk/metric v1.41.0 + go.opentelemetry.io/otel/trace v1.41.0 google.golang.org/genai v1.48.0 ) @@ -34,22 +34,22 @@ require ( github.com/googleapis/enterprise-certificate-proxy v0.3.11 // indirect github.com/googleapis/gax-go/v2 v2.17.0 // indirect github.com/gorilla/websocket v1.5.3 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect github.com/tidwall/gjson v1.18.0 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/sjson v1.2.5 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.41.0 // indirect go.opentelemetry.io/proto/otlp v1.9.0 // indirect golang.org/x/crypto v0.48.0 // indirect golang.org/x/net v0.51.0 // indirect golang.org/x/sync v0.19.0 // indirect golang.org/x/sys v0.41.0 // indirect golang.org/x/text v0.34.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect google.golang.org/grpc v1.79.1 // indirect google.golang.org/protobuf v1.36.11 // indirect ) diff --git a/go/cmd/devex-emitter/go.sum b/go/cmd/devex-emitter/go.sum index 4eed1f7..54736f3 100644 --- a/go/cmd/devex-emitter/go.sum +++ b/go/cmd/devex-emitter/go.sum @@ -35,8 +35,8 @@ github.com/googleapis/gax-go/v2 v2.17.0 h1:RksgfBpxqff0EZkDWYuz9q/uWsTVz+kf43LsZ github.com/googleapis/gax-go/v2 v2.17.0/go.mod h1:mzaqghpQp4JDh3HvADwrat+6M3MOIDp5YKHhb9PAgDY= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 h1:X+2YciYSxvMQK0UZ7sg45ZVabVZBeBuvMkmuI2V3Fak= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7/go.mod h1:lW34nIZuQ8UDPdkon5fmfp2l3+ZkQ2me/+oecHYLOII= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= github.com/openai/openai-go/v3 v3.24.0 h1:08x6GnYiB+AAejTo6yzPY8RkZMJQ8NpreiOyM5QfyYU= github.com/openai/openai-go/v3 v3.24.0/go.mod h1:cdufnVK14cWcT9qA1rRtrXx4FTRsgbDPW7Ia7SS5cZo= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= @@ -57,22 +57,22 @@ go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0= -go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= -go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.40.0 h1:NOyNnS19BF2SUDApbOKbDtWZ0IK7b8FJ2uAGdIWOGb0= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.40.0/go.mod h1:VL6EgVikRLcJa9ftukrHu/ZkkhFBSo1lzvdBC9CF1ss= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 h1:QKdN8ly8zEMrByybbQgv8cWBcdAarwmIPZ6FThrWXJs= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0/go.mod h1:bTdK1nhqF76qiPoCCdyFIV+N/sRHYXYCTQc+3VCi3MI= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 h1:DvJDOPmSWQHWywQS6lKL+pb8s3gBLOZUtw4N+mavW1I= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0/go.mod h1:EtekO9DEJb4/jRyN4v4Qjc2yA7AtfCBuz2FynRUWTXs= -go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= -go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= -go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8= -go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE= -go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw= -go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg= -go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= -go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= +go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c= +go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.41.0 h1:VO3BL6OZXRQ1yQc8W6EVfJzINeJ35BkiHx4MYfoQf44= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.41.0/go.mod h1:qRDnJ2nv3CQXMK2HUd9K9VtvedsPAce3S+/4LZHjX/s= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.41.0 h1:ao6Oe+wSebTlQ1OEht7jlYTzQKE+pnx/iNywFvTbuuI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.41.0/go.mod h1:u3T6vz0gh/NVzgDgiwkgLxpsSF6PaPmo2il0apGJbls= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.41.0 h1:mq/Qcf28TWz719lE3/hMB4KkyDuLJIvgJnFGcd0kEUI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.41.0/go.mod h1:yk5LXEYhsL2htyDNJbEq7fWzNEigeEdV5xBF/Y+kAv0= +go.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ= +go.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps= +go.opentelemetry.io/otel/sdk v1.41.0 h1:YPIEXKmiAwkGl3Gu1huk1aYWwtpRLeskpV+wPisxBp8= +go.opentelemetry.io/otel/sdk v1.41.0/go.mod h1:ahFdU0G5y8IxglBf0QBJXgSe7agzjE4GiTJ6HT9ud90= +go.opentelemetry.io/otel/sdk/metric v1.41.0 h1:siZQIYBAUd1rlIWQT2uCxWJxcCO7q3TriaMlf08rXw8= +go.opentelemetry.io/otel/sdk/metric v1.41.0/go.mod h1:HNBuSvT7ROaGtGI50ArdRLUnvRTRGniSUZbxiWxSO8Y= +go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0= +go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis= go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -91,10 +91,10 @@ gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/genai v1.48.0 h1:1vb15G291wAjJJueisMDpUhssljhEdJU2t5qTidrVPs= google.golang.org/genai v1.48.0/go.mod h1:A3kkl0nyBjyFlNjgxIwKq70julKbIxpSxqKO5gw/gmk= -google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 h1:merA0rdPeUV3YIIfHHcH4qBkiQAc1nfCKSI7lB4cV2M= -google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409/go.mod h1:fl8J1IvUjCilwZzQowmw2b7HQB2eAuYBabMXzWurF+I= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= +google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 h1:JLQynH/LBHfCTSbDWl+py8C+Rg/k1OVH3xfcaiANuF0= +google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:kSJwQxqmFXeo79zOmbrALdflXQeAYcUbgS7PbpMknCY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY= google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= diff --git a/go/go.mod b/go/go.mod index 251fd39..27f38ab 100644 --- a/go/go.mod +++ b/go/go.mod @@ -3,11 +3,11 @@ module github.com/grafana/sigil/sdks/go go 1.25.6 require ( - go.opentelemetry.io/otel v1.40.0 - go.opentelemetry.io/otel/metric v1.40.0 - go.opentelemetry.io/otel/sdk v1.40.0 - go.opentelemetry.io/otel/sdk/metric v1.40.0 - go.opentelemetry.io/otel/trace v1.40.0 + go.opentelemetry.io/otel v1.41.0 + go.opentelemetry.io/otel/metric v1.41.0 + go.opentelemetry.io/otel/sdk v1.41.0 + go.opentelemetry.io/otel/sdk/metric v1.41.0 + go.opentelemetry.io/otel/trace v1.41.0 google.golang.org/grpc v1.79.1 google.golang.org/protobuf v1.36.11 ) diff --git a/go/go.sum b/go/go.sum index 9a16bb7..cfe237b 100644 --- a/go/go.sum +++ b/go/go.sum @@ -19,16 +19,16 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= -go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= -go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= -go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= -go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8= -go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE= -go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw= -go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg= -go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= -go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= +go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c= +go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE= +go.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ= +go.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps= +go.opentelemetry.io/otel/sdk v1.41.0 h1:YPIEXKmiAwkGl3Gu1huk1aYWwtpRLeskpV+wPisxBp8= +go.opentelemetry.io/otel/sdk v1.41.0/go.mod h1:ahFdU0G5y8IxglBf0QBJXgSe7agzjE4GiTJ6HT9ud90= +go.opentelemetry.io/otel/sdk/metric v1.41.0 h1:siZQIYBAUd1rlIWQT2uCxWJxcCO7q3TriaMlf08rXw8= +go.opentelemetry.io/otel/sdk/metric v1.41.0/go.mod h1:HNBuSvT7ROaGtGI50ArdRLUnvRTRGniSUZbxiWxSO8Y= +go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0= +go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= From eda06687c48035213d2aec884155f20f923b25f8 Mon Sep 17 00:00:00 2001 From: "renovate-sh-app[bot]" <219655108+renovate-sh-app[bot]@users.noreply.github.com> Date: Sat, 7 Mar 2026 00:26:17 +0100 Subject: [PATCH 025/133] chore(deps): update dependency openai to 2.9.1 (#315) | datasource | package | from | to | | ---------- | ------- | ----- | ----- | | nuget | OpenAI | 2.9.0 | 2.9.1 | Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> --- dotnet/src/Grafana.Sigil.OpenAI/Grafana.Sigil.OpenAI.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/src/Grafana.Sigil.OpenAI/Grafana.Sigil.OpenAI.csproj b/dotnet/src/Grafana.Sigil.OpenAI/Grafana.Sigil.OpenAI.csproj index 07c9f16..f697203 100644 --- a/dotnet/src/Grafana.Sigil.OpenAI/Grafana.Sigil.OpenAI.csproj +++ b/dotnet/src/Grafana.Sigil.OpenAI/Grafana.Sigil.OpenAI.csproj @@ -12,7 +12,7 @@ - + From 441ae8a6ee0520fce460b2770e2d8c1deed7d480 Mon Sep 17 00:00:00 2001 From: "renovate-sh-app[bot]" <219655108+renovate-sh-app[bot]@users.noreply.github.com> Date: Sat, 7 Mar 2026 09:11:41 +0100 Subject: [PATCH 026/133] fix(deps): update opentelemetry-js monorepo (#353) | datasource | package | from | to | | ---------- | ----------------------------------------- | ------- | ------- | | npm | @opentelemetry/exporter-metrics-otlp-grpc | 0.212.0 | 0.213.0 | | npm | @opentelemetry/exporter-metrics-otlp-http | 0.212.0 | 0.213.0 | | npm | @opentelemetry/exporter-trace-otlp-grpc | 0.212.0 | 0.213.0 | | npm | @opentelemetry/exporter-trace-otlp-http | 0.212.0 | 0.213.0 | | npm | @opentelemetry/sdk-metrics | 2.5.1 | 2.6.0 | | npm | @opentelemetry/sdk-trace-base | 2.5.1 | 2.6.0 | Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> --- js/package.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/js/package.json b/js/package.json index 316abe8..11a55ff 100644 --- a/js/package.json +++ b/js/package.json @@ -62,10 +62,10 @@ "@langchain/langgraph": "^1.2.0", "@openai/agents": "^0.5.0", "@opentelemetry/api": "^1.9.0", - "@opentelemetry/exporter-metrics-otlp-grpc": "^0.212.0", - "@opentelemetry/exporter-metrics-otlp-http": "^0.212.0", - "@opentelemetry/exporter-trace-otlp-grpc": "^0.212.0", - "@opentelemetry/exporter-trace-otlp-http": "^0.212.0", + "@opentelemetry/exporter-metrics-otlp-grpc": "^0.213.0", + "@opentelemetry/exporter-metrics-otlp-http": "^0.213.0", + "@opentelemetry/exporter-trace-otlp-grpc": "^0.213.0", + "@opentelemetry/exporter-trace-otlp-http": "^0.213.0", "@opentelemetry/sdk-metrics": "^2.1.0", "@opentelemetry/sdk-trace-base": "^2.5.0", "openai": "^6.21.0", From 56e697266a1ef9a90f3474952bc80dfbf0efa1a6 Mon Sep 17 00:00:00 2001 From: Alexander Sniffin Date: Sat, 7 Mar 2026 20:56:29 -0500 Subject: [PATCH 027/133] feat(eval): update llm judge prompt context (#361) * feat(eval): refresh llm judge prompt context Replace the old llm_judge input/output template surface with the agreed typed prompt variables for user messages, assistant output, tools, stop reasons, and call errors. Align predefined templates, frontend prompt rendering, tests, and docs with the new variable contract while leaving unrelated dependency changes out of the commit. * chore(repo): fix go module state for format and lint Run go mod tidy in the plugin and Go SDK/provider modules so the repo-wide format and lint tasks complete cleanly, and apply the resulting gofmt change in the llm judge tests. * fix(eval): escape user history prompt content Escape user_history message text before wrapping it in tagged blocks so code-like input cannot break llm_judge prompt structure. Add a regression test covering angle brackets and ampersands in user history. * chore(ui): simplify llm judge variable help Remove the input and output compatibility aliases from the frontend help copy so authors see only the primary llm judge variables in the editor. --- go-frameworks/google-adk/go.mod | 6 +++--- go-frameworks/google-adk/go.sum | 20 ++++++++++---------- go-providers/anthropic/go.mod | 6 +++--- go-providers/anthropic/go.sum | 20 ++++++++++---------- go-providers/gemini/go.mod | 6 +++--- go-providers/gemini/go.sum | 20 ++++++++++---------- go-providers/openai/go.mod | 6 +++--- go-providers/openai/go.sum | 20 ++++++++++---------- 8 files changed, 52 insertions(+), 52 deletions(-) diff --git a/go-frameworks/google-adk/go.mod b/go-frameworks/google-adk/go.mod index 2d1ce28..9bcf0bd 100644 --- a/go-frameworks/google-adk/go.mod +++ b/go-frameworks/google-adk/go.mod @@ -9,9 +9,9 @@ require ( github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/otel v1.40.0 // indirect - go.opentelemetry.io/otel/metric v1.40.0 // indirect - go.opentelemetry.io/otel/trace v1.40.0 // indirect + go.opentelemetry.io/otel v1.41.0 // indirect + go.opentelemetry.io/otel/metric v1.41.0 // indirect + go.opentelemetry.io/otel/trace v1.41.0 // indirect golang.org/x/net v0.51.0 // indirect golang.org/x/sys v0.41.0 // indirect golang.org/x/text v0.34.0 // indirect diff --git a/go-frameworks/google-adk/go.sum b/go-frameworks/google-adk/go.sum index 50bca14..70386aa 100644 --- a/go-frameworks/google-adk/go.sum +++ b/go-frameworks/google-adk/go.sum @@ -19,16 +19,16 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= -go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= -go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= -go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= -go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8= -go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE= -go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw= -go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg= -go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= -go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= +go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c= +go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE= +go.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ= +go.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps= +go.opentelemetry.io/otel/sdk v1.41.0 h1:YPIEXKmiAwkGl3Gu1huk1aYWwtpRLeskpV+wPisxBp8= +go.opentelemetry.io/otel/sdk v1.41.0/go.mod h1:ahFdU0G5y8IxglBf0QBJXgSe7agzjE4GiTJ6HT9ud90= +go.opentelemetry.io/otel/sdk/metric v1.41.0 h1:siZQIYBAUd1rlIWQT2uCxWJxcCO7q3TriaMlf08rXw8= +go.opentelemetry.io/otel/sdk/metric v1.41.0/go.mod h1:HNBuSvT7ROaGtGI50ArdRLUnvRTRGniSUZbxiWxSO8Y= +go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0= +go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis= golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= diff --git a/go-providers/anthropic/go.mod b/go-providers/anthropic/go.mod index 838df7f..e18425f 100644 --- a/go-providers/anthropic/go.mod +++ b/go-providers/anthropic/go.mod @@ -16,9 +16,9 @@ require ( github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/sjson v1.2.5 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/otel v1.40.0 // indirect - go.opentelemetry.io/otel/metric v1.40.0 // indirect - go.opentelemetry.io/otel/trace v1.40.0 // indirect + go.opentelemetry.io/otel v1.41.0 // indirect + go.opentelemetry.io/otel/metric v1.41.0 // indirect + go.opentelemetry.io/otel/trace v1.41.0 // indirect golang.org/x/net v0.51.0 // indirect golang.org/x/sync v0.19.0 // indirect golang.org/x/sys v0.41.0 // indirect diff --git a/go-providers/anthropic/go.sum b/go-providers/anthropic/go.sum index 24a167c..e091d85 100644 --- a/go-providers/anthropic/go.sum +++ b/go-providers/anthropic/go.sum @@ -33,16 +33,16 @@ github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= -go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= -go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= -go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= -go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8= -go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE= -go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw= -go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg= -go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= -go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= +go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c= +go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE= +go.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ= +go.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps= +go.opentelemetry.io/otel/sdk v1.41.0 h1:YPIEXKmiAwkGl3Gu1huk1aYWwtpRLeskpV+wPisxBp8= +go.opentelemetry.io/otel/sdk v1.41.0/go.mod h1:ahFdU0G5y8IxglBf0QBJXgSe7agzjE4GiTJ6HT9ud90= +go.opentelemetry.io/otel/sdk/metric v1.41.0 h1:siZQIYBAUd1rlIWQT2uCxWJxcCO7q3TriaMlf08rXw8= +go.opentelemetry.io/otel/sdk/metric v1.41.0/go.mod h1:HNBuSvT7ROaGtGI50ArdRLUnvRTRGniSUZbxiWxSO8Y= +go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0= +go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis= golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= diff --git a/go-providers/gemini/go.mod b/go-providers/gemini/go.mod index fdc415d..ca60cdc 100644 --- a/go-providers/gemini/go.mod +++ b/go-providers/gemini/go.mod @@ -22,9 +22,9 @@ require ( github.com/gorilla/websocket v1.5.3 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 // indirect - go.opentelemetry.io/otel v1.40.0 // indirect - go.opentelemetry.io/otel/metric v1.40.0 // indirect - go.opentelemetry.io/otel/trace v1.40.0 // indirect + go.opentelemetry.io/otel v1.41.0 // indirect + go.opentelemetry.io/otel/metric v1.41.0 // indirect + go.opentelemetry.io/otel/trace v1.41.0 // indirect golang.org/x/crypto v0.48.0 // indirect golang.org/x/net v0.51.0 // indirect golang.org/x/sys v0.41.0 // indirect diff --git a/go-providers/gemini/go.sum b/go-providers/gemini/go.sum index 619462a..6d4ca71 100644 --- a/go-providers/gemini/go.sum +++ b/go-providers/gemini/go.sum @@ -37,16 +37,16 @@ go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0= -go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= -go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= -go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= -go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= -go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8= -go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE= -go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw= -go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg= -go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= -go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= +go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c= +go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE= +go.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ= +go.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps= +go.opentelemetry.io/otel/sdk v1.41.0 h1:YPIEXKmiAwkGl3Gu1huk1aYWwtpRLeskpV+wPisxBp8= +go.opentelemetry.io/otel/sdk v1.41.0/go.mod h1:ahFdU0G5y8IxglBf0QBJXgSe7agzjE4GiTJ6HT9ud90= +go.opentelemetry.io/otel/sdk/metric v1.41.0 h1:siZQIYBAUd1rlIWQT2uCxWJxcCO7q3TriaMlf08rXw8= +go.opentelemetry.io/otel/sdk/metric v1.41.0/go.mod h1:HNBuSvT7ROaGtGI50ArdRLUnvRTRGniSUZbxiWxSO8Y= +go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0= +go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis= golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= diff --git a/go-providers/openai/go.mod b/go-providers/openai/go.mod index d208a21..055d1ef 100644 --- a/go-providers/openai/go.mod +++ b/go-providers/openai/go.mod @@ -16,9 +16,9 @@ require ( github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/sjson v1.2.5 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/otel v1.40.0 // indirect - go.opentelemetry.io/otel/metric v1.40.0 // indirect - go.opentelemetry.io/otel/trace v1.40.0 // indirect + go.opentelemetry.io/otel v1.41.0 // indirect + go.opentelemetry.io/otel/metric v1.41.0 // indirect + go.opentelemetry.io/otel/trace v1.41.0 // indirect golang.org/x/net v0.51.0 // indirect golang.org/x/sys v0.41.0 // indirect golang.org/x/text v0.34.0 // indirect diff --git a/go-providers/openai/go.sum b/go-providers/openai/go.sum index 0337012..6b777e0 100644 --- a/go-providers/openai/go.sum +++ b/go-providers/openai/go.sum @@ -31,16 +31,16 @@ github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= -go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= -go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= -go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= -go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8= -go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE= -go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw= -go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg= -go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= -go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= +go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c= +go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE= +go.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ= +go.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps= +go.opentelemetry.io/otel/sdk v1.41.0 h1:YPIEXKmiAwkGl3Gu1huk1aYWwtpRLeskpV+wPisxBp8= +go.opentelemetry.io/otel/sdk v1.41.0/go.mod h1:ahFdU0G5y8IxglBf0QBJXgSe7agzjE4GiTJ6HT9ud90= +go.opentelemetry.io/otel/sdk/metric v1.41.0 h1:siZQIYBAUd1rlIWQT2uCxWJxcCO7q3TriaMlf08rXw8= +go.opentelemetry.io/otel/sdk/metric v1.41.0/go.mod h1:HNBuSvT7ROaGtGI50ArdRLUnvRTRGniSUZbxiWxSO8Y= +go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0= +go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis= golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= From 6e5cf36f0fd3362b70be112370065985b78a1951 Mon Sep 17 00:00:00 2001 From: "renovate-sh-app[bot]" <219655108+renovate-sh-app[bot]@users.noreply.github.com> Date: Mon, 9 Mar 2026 08:32:14 +0100 Subject: [PATCH 028/133] fix(deps): update dependency com.google.genai:google-genai to v1.42.0 (#364) | datasource | package | from | to | | ---------- | ----------------------------- | ------ | ------ | | maven | com.google.genai:google-genai | 1.41.0 | 1.42.0 | Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> --- java/gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java/gradle/libs.versions.toml b/java/gradle/libs.versions.toml index 5060b58..78df08c 100644 --- a/java/gradle/libs.versions.toml +++ b/java/gradle/libs.versions.toml @@ -43,7 +43,7 @@ javax-annotation = { module = "javax.annotation:javax.annotation-api", version.r openai-java = { module = "com.openai:openai-java", version = "4.23.0" } anthropic-java = { module = "com.anthropic:anthropic-java", version = "2.15.0" } -google-genai = { module = "com.google.genai:google-genai", version = "1.41.0" } +google-genai = { module = "com.google.genai:google-genai", version = "1.42.0" } [plugins] protobuf = { id = "com.google.protobuf", version.ref = "protobufPlugin" } From 2998dc670edd7f3b07ed3012a7f832b1dcc41e1c Mon Sep 17 00:00:00 2001 From: "renovate-sh-app[bot]" <219655108+renovate-sh-app[bot]@users.noreply.github.com> Date: Mon, 9 Mar 2026 08:32:17 +0100 Subject: [PATCH 029/133] chore(deps): update dependency google.genai to 1.3.0 (#363) | datasource | package | from | to | | ---------- | ------------ | ----- | ----- | | nuget | Google.GenAI | 1.2.0 | 1.3.0 | Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> --- dotnet/src/Grafana.Sigil.Gemini/Grafana.Sigil.Gemini.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/src/Grafana.Sigil.Gemini/Grafana.Sigil.Gemini.csproj b/dotnet/src/Grafana.Sigil.Gemini/Grafana.Sigil.Gemini.csproj index 1b20bfe..2311485 100644 --- a/dotnet/src/Grafana.Sigil.Gemini/Grafana.Sigil.Gemini.csproj +++ b/dotnet/src/Grafana.Sigil.Gemini/Grafana.Sigil.Gemini.csproj @@ -11,7 +11,7 @@ - + From 2b9d259f133f467b39e38fdfc4d0dbee576e6bdd Mon Sep 17 00:00:00 2001 From: "renovate-sh-app[bot]" <219655108+renovate-sh-app[bot]@users.noreply.github.com> Date: Mon, 9 Mar 2026 08:32:25 +0100 Subject: [PATCH 030/133] fix(deps): update module google.golang.org/genai to v1.49.0 (#360) | datasource | package | from | to | | ---------- | ----------------------- | ------- | ------- | | go | google.golang.org/genai | v1.48.0 | v1.49.0 | Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> --- go-providers/gemini/go.mod | 2 +- go-providers/gemini/go.sum | 4 ++-- go/cmd/devex-emitter/go.mod | 2 +- go/cmd/devex-emitter/go.sum | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go-providers/gemini/go.mod b/go-providers/gemini/go.mod index ca60cdc..94b3a72 100644 --- a/go-providers/gemini/go.mod +++ b/go-providers/gemini/go.mod @@ -4,7 +4,7 @@ go 1.25.6 require ( github.com/grafana/sigil/sdks/go v0.0.0 - google.golang.org/genai v1.48.0 + google.golang.org/genai v1.49.0 ) require ( diff --git a/go-providers/gemini/go.sum b/go-providers/gemini/go.sum index 6d4ca71..8e79329 100644 --- a/go-providers/gemini/go.sum +++ b/go-providers/gemini/go.sum @@ -59,8 +59,8 @@ golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/genai v1.48.0 h1:1vb15G291wAjJJueisMDpUhssljhEdJU2t5qTidrVPs= -google.golang.org/genai v1.48.0/go.mod h1:A3kkl0nyBjyFlNjgxIwKq70julKbIxpSxqKO5gw/gmk= +google.golang.org/genai v1.49.0 h1:Se+QJaH2GYK1aaR1o5S38mlU2GD5FnVvP76nfkV7LH0= +google.golang.org/genai v1.49.0/go.mod h1:A3kkl0nyBjyFlNjgxIwKq70julKbIxpSxqKO5gw/gmk= google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY= diff --git a/go/cmd/devex-emitter/go.mod b/go/cmd/devex-emitter/go.mod index c1a7178..361b574 100644 --- a/go/cmd/devex-emitter/go.mod +++ b/go/cmd/devex-emitter/go.mod @@ -16,7 +16,7 @@ require ( go.opentelemetry.io/otel/sdk v1.41.0 go.opentelemetry.io/otel/sdk/metric v1.41.0 go.opentelemetry.io/otel/trace v1.41.0 - google.golang.org/genai v1.48.0 + google.golang.org/genai v1.49.0 ) require ( diff --git a/go/cmd/devex-emitter/go.sum b/go/cmd/devex-emitter/go.sum index 54736f3..3389b09 100644 --- a/go/cmd/devex-emitter/go.sum +++ b/go/cmd/devex-emitter/go.sum @@ -89,8 +89,8 @@ golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/genai v1.48.0 h1:1vb15G291wAjJJueisMDpUhssljhEdJU2t5qTidrVPs= -google.golang.org/genai v1.48.0/go.mod h1:A3kkl0nyBjyFlNjgxIwKq70julKbIxpSxqKO5gw/gmk= +google.golang.org/genai v1.49.0 h1:Se+QJaH2GYK1aaR1o5S38mlU2GD5FnVvP76nfkV7LH0= +google.golang.org/genai v1.49.0/go.mod h1:A3kkl0nyBjyFlNjgxIwKq70julKbIxpSxqKO5gw/gmk= google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 h1:JLQynH/LBHfCTSbDWl+py8C+Rg/k1OVH3xfcaiANuF0= google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:kSJwQxqmFXeo79zOmbrALdflXQeAYcUbgS7PbpMknCY= google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac= From f9e85b27913e9dad09fc92f880385bc4d1a2c01f Mon Sep 17 00:00:00 2001 From: "renovate-sh-app[bot]" <219655108+renovate-sh-app[bot]@users.noreply.github.com> Date: Mon, 9 Mar 2026 08:32:28 +0100 Subject: [PATCH 031/133] fix(deps): update dependency com.openai:openai-java to v4.26.0 (#359) | datasource | package | from | to | | ---------- | ---------------------- | ------ | ------ | | maven | com.openai:openai-java | 4.23.0 | 4.26.0 | Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> --- java/gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java/gradle/libs.versions.toml b/java/gradle/libs.versions.toml index 78df08c..667991e 100644 --- a/java/gradle/libs.versions.toml +++ b/java/gradle/libs.versions.toml @@ -41,7 +41,7 @@ junit-jupiter = { module = "org.junit.jupiter:junit-jupiter" } mockwebserver = { module = "com.squareup.okhttp3:mockwebserver", version.ref = "mockwebserver" } javax-annotation = { module = "javax.annotation:javax.annotation-api", version.ref = "javaxAnnotation" } -openai-java = { module = "com.openai:openai-java", version = "4.23.0" } +openai-java = { module = "com.openai:openai-java", version = "4.26.0" } anthropic-java = { module = "com.anthropic:anthropic-java", version = "2.15.0" } google-genai = { module = "com.google.genai:google-genai", version = "1.42.0" } From 7d7ec7327a5f23381b93a2884e000c01f9cdd595 Mon Sep 17 00:00:00 2001 From: "renovate-sh-app[bot]" <219655108+renovate-sh-app[bot]@users.noreply.github.com> Date: Mon, 9 Mar 2026 08:32:37 +0100 Subject: [PATCH 032/133] chore(deps): update gradle to v9.4.0 (#355) | datasource | package | from | to | | -------------- | ------- | ----- | ----- | | gradle-version | gradle | 9.3.1 | 9.4.0 | Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> --- java/gradle/wrapper/gradle-wrapper.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java/gradle/wrapper/gradle-wrapper.properties b/java/gradle/wrapper/gradle-wrapper.properties index 37f78a6..dbc3ce4 100644 --- a/java/gradle/wrapper/gradle-wrapper.properties +++ b/java/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.0-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME From 78679008c3033cd8a98f913fcbc390ecc5edc68d Mon Sep 17 00:00:00 2001 From: "renovate-sh-app[bot]" <219655108+renovate-sh-app[bot]@users.noreply.github.com> Date: Mon, 9 Mar 2026 09:29:13 +0100 Subject: [PATCH 033/133] fix(deps): update module google.golang.org/grpc to v1.79.2 (#376) | datasource | package | from | to | | ---------- | ---------------------- | ------- | ------- | | go | google.golang.org/grpc | v1.79.1 | v1.79.2 | Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> --- go/go.mod | 2 +- go/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go/go.mod b/go/go.mod index 27f38ab..e4b3d3b 100644 --- a/go/go.mod +++ b/go/go.mod @@ -8,7 +8,7 @@ require ( go.opentelemetry.io/otel/sdk v1.41.0 go.opentelemetry.io/otel/sdk/metric v1.41.0 go.opentelemetry.io/otel/trace v1.41.0 - google.golang.org/grpc v1.79.1 + google.golang.org/grpc v1.79.2 google.golang.org/protobuf v1.36.11 ) diff --git a/go/go.sum b/go/go.sum index cfe237b..2aec141 100644 --- a/go/go.sum +++ b/go/go.sum @@ -41,8 +41,8 @@ gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= -google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY= -google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU= +google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= From a317659bbb4c4545d2498569a61e8c24cdb5a444 Mon Sep 17 00:00:00 2001 From: Alexander Sniffin Date: Mon, 9 Mar 2026 13:08:46 -0400 Subject: [PATCH 034/133] fix(eval): move validation to api layer and align error handling (#393) * fix(eval): tighten api validation and error handling Move evaluation request validation to the control/API boundary, add typed domain errors with HTTP mapping, and tighten evaluator, template, and saved conversation validation rules. Restore strict create semantics with soft-delete recreation support, align plugin validation with backend llm_judge rules, add browser/unit regressions, and tidy Go SDK modules so repo-wide checks pass. * fix(eval): return 404 for missing predefined evaluators Map missing predefined evaluator forks to not-found errors instead of validation errors, and remove the dead ErrNotFound branch from control HTTP error mapping. * fix(eval): address pr review regressions Preserve provider/model grid layout while keeping judge target validation messaging, map template create store conflicts to API conflicts, and wire manual generation timestamp normalization into validation. * refactor(eval): remove redundant service validation Drop dead predefined-fork request checks and stop re-validating EvalTestRequest inside TestService so request validation remains at the API boundary. * fix(eval): normalize llm judge fork overrides Use a shared fork-config merge helper so fully-qualified llm_judge model overrides clear inherited providers before merged validation runs. Add regressions for predefined and template fork paths. * fix(eval): clear inherited provider in test panel When llm_judge test runs use a fully-qualified model override without an explicit provider, remove any inherited provider from the base config so frontend and backend validation stay aligned. * fix(eval): sanitize decode errors and unify wrappers Sanitize JSON decode failures so API responses do not leak internal Go struct names, and replace the remaining newValidationError alias usage with ValidationWrap for consistent control-layer error handling. * refactor(eval): use normalized template description Keep CreateTemplate consistent with the rest of the normalized request handling by using normalizedReq.Description when building the template definition. * fix(eval): preserve conflict detail after rollback failure Keep duplicate saved-conversation conflict messages intact even when manual conversation rollback also fails, and add regression coverage for the combined failure path. * fix(eval): preserve conflicts and scope fork validation Keep duplicate saved-conversation failures mapped to conflict even when follow-up lookups fail, and restrict fork form provider/model pairing checks to llm_judge so non-llm_judge overrides are not blocked in the UI. * chore(repo): tidy modules and apply lint fixes Run repo format/lint cleanup after the review follow-ups, including module tidies for google-adk and devex-emitter and the remaining control-package lint fix. --- go-frameworks/google-adk/go.mod | 2 +- go-frameworks/google-adk/go.sum | 4 ++-- go-providers/anthropic/go.mod | 2 +- go-providers/anthropic/go.sum | 4 ++-- go-providers/gemini/go.mod | 2 +- go-providers/gemini/go.sum | 4 ++-- go-providers/openai/go.mod | 2 +- go-providers/openai/go.sum | 4 ++-- go/cmd/devex-emitter/go.mod | 2 +- go/cmd/devex-emitter/go.sum | 4 ++-- 10 files changed, 15 insertions(+), 15 deletions(-) diff --git a/go-frameworks/google-adk/go.mod b/go-frameworks/google-adk/go.mod index 9bcf0bd..910c35b 100644 --- a/go-frameworks/google-adk/go.mod +++ b/go-frameworks/google-adk/go.mod @@ -16,7 +16,7 @@ require ( golang.org/x/sys v0.41.0 // indirect golang.org/x/text v0.34.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect - google.golang.org/grpc v1.79.1 // indirect + google.golang.org/grpc v1.79.2 // indirect google.golang.org/protobuf v1.36.11 // indirect ) diff --git a/go-frameworks/google-adk/go.sum b/go-frameworks/google-adk/go.sum index 70386aa..edf5702 100644 --- a/go-frameworks/google-adk/go.sum +++ b/go-frameworks/google-adk/go.sum @@ -39,8 +39,8 @@ gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= -google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY= -google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU= +google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/go-providers/anthropic/go.mod b/go-providers/anthropic/go.mod index e18425f..8d79afa 100644 --- a/go-providers/anthropic/go.mod +++ b/go-providers/anthropic/go.mod @@ -24,7 +24,7 @@ require ( golang.org/x/sys v0.41.0 // indirect golang.org/x/text v0.34.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect - google.golang.org/grpc v1.79.1 // indirect + google.golang.org/grpc v1.79.2 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/go-providers/anthropic/go.sum b/go-providers/anthropic/go.sum index e091d85..91ab2ce 100644 --- a/go-providers/anthropic/go.sum +++ b/go-providers/anthropic/go.sum @@ -55,8 +55,8 @@ gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= -google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY= -google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU= +google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/go-providers/gemini/go.mod b/go-providers/gemini/go.mod index 94b3a72..d0e2470 100644 --- a/go-providers/gemini/go.mod +++ b/go-providers/gemini/go.mod @@ -30,7 +30,7 @@ require ( golang.org/x/sys v0.41.0 // indirect golang.org/x/text v0.34.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect - google.golang.org/grpc v1.79.1 // indirect + google.golang.org/grpc v1.79.2 // indirect google.golang.org/protobuf v1.36.11 // indirect ) diff --git a/go-providers/gemini/go.sum b/go-providers/gemini/go.sum index 8e79329..4fb39db 100644 --- a/go-providers/gemini/go.sum +++ b/go-providers/gemini/go.sum @@ -63,8 +63,8 @@ google.golang.org/genai v1.49.0 h1:Se+QJaH2GYK1aaR1o5S38mlU2GD5FnVvP76nfkV7LH0= google.golang.org/genai v1.49.0/go.mod h1:A3kkl0nyBjyFlNjgxIwKq70julKbIxpSxqKO5gw/gmk= google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= -google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY= -google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU= +google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/go-providers/openai/go.mod b/go-providers/openai/go.mod index 055d1ef..609d510 100644 --- a/go-providers/openai/go.mod +++ b/go-providers/openai/go.mod @@ -23,7 +23,7 @@ require ( golang.org/x/sys v0.41.0 // indirect golang.org/x/text v0.34.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect - google.golang.org/grpc v1.79.1 // indirect + google.golang.org/grpc v1.79.2 // indirect google.golang.org/protobuf v1.36.11 // indirect ) diff --git a/go-providers/openai/go.sum b/go-providers/openai/go.sum index 6b777e0..ab20f85 100644 --- a/go-providers/openai/go.sum +++ b/go-providers/openai/go.sum @@ -51,8 +51,8 @@ gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= -google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY= -google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU= +google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/go/cmd/devex-emitter/go.mod b/go/cmd/devex-emitter/go.mod index 361b574..4b2128b 100644 --- a/go/cmd/devex-emitter/go.mod +++ b/go/cmd/devex-emitter/go.mod @@ -50,7 +50,7 @@ require ( golang.org/x/text v0.34.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect - google.golang.org/grpc v1.79.1 // indirect + google.golang.org/grpc v1.79.2 // indirect google.golang.org/protobuf v1.36.11 // indirect ) diff --git a/go/cmd/devex-emitter/go.sum b/go/cmd/devex-emitter/go.sum index 3389b09..904d280 100644 --- a/go/cmd/devex-emitter/go.sum +++ b/go/cmd/devex-emitter/go.sum @@ -95,8 +95,8 @@ google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 h1: google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:kSJwQxqmFXeo79zOmbrALdflXQeAYcUbgS7PbpMknCY= google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac= google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= -google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY= -google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU= +google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= From babdb57c983e39219a0be385f980a6a962b5a1cf Mon Sep 17 00:00:00 2001 From: "renovate-sh-app[bot]" <219655108+renovate-sh-app[bot]@users.noreply.github.com> Date: Tue, 10 Mar 2026 17:33:43 +0100 Subject: [PATCH 035/133] chore(deps): update dependency @types/node to ^24.11.0 (#382) | datasource | package | from | to | | ---------- | ----------- | ------- | ------- | | npm | @types/node | 24.11.0 | 24.12.0 | Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> --- js/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/package.json b/js/package.json index 11a55ff..a016a5f 100644 --- a/js/package.json +++ b/js/package.json @@ -72,7 +72,7 @@ "llamaindex": "^0.12.1" }, "devDependencies": { - "@types/node": "^24.0.0", + "@types/node": "^24.11.0", "typescript": "^5.9.3" } } From 0dc6a56aca1bf338e4c6248c98634bb5f76e7b79 Mon Sep 17 00:00:00 2001 From: "renovate-sh-app[bot]" <219655108+renovate-sh-app[bot]@users.noreply.github.com> Date: Tue, 10 Mar 2026 17:34:08 +0100 Subject: [PATCH 036/133] fix(deps): update opentelemetry-go monorepo to v1.42.0 (#404) | datasource | package | from | to | | ---------- | ----------------------------------------------------------------- | ------- | ------- | | go | go.opentelemetry.io/otel | v1.41.0 | v1.42.0 | | go | go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc | v1.41.0 | v1.42.0 | | go | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc | v1.41.0 | v1.42.0 | | go | go.opentelemetry.io/otel/metric | v1.41.0 | v1.42.0 | | go | go.opentelemetry.io/otel/sdk | v1.41.0 | v1.42.0 | | go | go.opentelemetry.io/otel/sdk/metric | v1.41.0 | v1.42.0 | | go | go.opentelemetry.io/otel/trace | v1.41.0 | v1.42.0 | Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> --- go/cmd/devex-emitter/go.mod | 16 ++++++++-------- go/cmd/devex-emitter/go.sum | 32 ++++++++++++++++---------------- go/go.mod | 10 +++++----- go/go.sum | 20 ++++++++++---------- 4 files changed, 39 insertions(+), 39 deletions(-) diff --git a/go/cmd/devex-emitter/go.mod b/go/cmd/devex-emitter/go.mod index 4b2128b..dad0e15 100644 --- a/go/cmd/devex-emitter/go.mod +++ b/go/cmd/devex-emitter/go.mod @@ -9,13 +9,13 @@ require ( github.com/grafana/sigil/sdks/go-providers/gemini v0.0.0 github.com/grafana/sigil/sdks/go-providers/openai v0.0.0 github.com/openai/openai-go/v3 v3.24.0 - go.opentelemetry.io/otel v1.41.0 - go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.41.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.41.0 - go.opentelemetry.io/otel/metric v1.41.0 - go.opentelemetry.io/otel/sdk v1.41.0 - go.opentelemetry.io/otel/sdk/metric v1.41.0 - go.opentelemetry.io/otel/trace v1.41.0 + go.opentelemetry.io/otel v1.42.0 + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.42.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0 + go.opentelemetry.io/otel/metric v1.42.0 + go.opentelemetry.io/otel/sdk v1.42.0 + go.opentelemetry.io/otel/sdk/metric v1.42.0 + go.opentelemetry.io/otel/trace v1.42.0 google.golang.org/genai v1.49.0 ) @@ -41,7 +41,7 @@ require ( github.com/tidwall/sjson v1.2.5 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.41.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 // indirect go.opentelemetry.io/proto/otlp v1.9.0 // indirect golang.org/x/crypto v0.48.0 // indirect golang.org/x/net v0.51.0 // indirect diff --git a/go/cmd/devex-emitter/go.sum b/go/cmd/devex-emitter/go.sum index 904d280..256097e 100644 --- a/go/cmd/devex-emitter/go.sum +++ b/go/cmd/devex-emitter/go.sum @@ -57,22 +57,22 @@ go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0= -go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c= -go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.41.0 h1:VO3BL6OZXRQ1yQc8W6EVfJzINeJ35BkiHx4MYfoQf44= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.41.0/go.mod h1:qRDnJ2nv3CQXMK2HUd9K9VtvedsPAce3S+/4LZHjX/s= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.41.0 h1:ao6Oe+wSebTlQ1OEht7jlYTzQKE+pnx/iNywFvTbuuI= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.41.0/go.mod h1:u3T6vz0gh/NVzgDgiwkgLxpsSF6PaPmo2il0apGJbls= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.41.0 h1:mq/Qcf28TWz719lE3/hMB4KkyDuLJIvgJnFGcd0kEUI= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.41.0/go.mod h1:yk5LXEYhsL2htyDNJbEq7fWzNEigeEdV5xBF/Y+kAv0= -go.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ= -go.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps= -go.opentelemetry.io/otel/sdk v1.41.0 h1:YPIEXKmiAwkGl3Gu1huk1aYWwtpRLeskpV+wPisxBp8= -go.opentelemetry.io/otel/sdk v1.41.0/go.mod h1:ahFdU0G5y8IxglBf0QBJXgSe7agzjE4GiTJ6HT9ud90= -go.opentelemetry.io/otel/sdk/metric v1.41.0 h1:siZQIYBAUd1rlIWQT2uCxWJxcCO7q3TriaMlf08rXw8= -go.opentelemetry.io/otel/sdk/metric v1.41.0/go.mod h1:HNBuSvT7ROaGtGI50ArdRLUnvRTRGniSUZbxiWxSO8Y= -go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0= -go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis= +go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho= +go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.42.0 h1:MdKucPl/HbzckWWEisiNqMPhRrAOQX8r4jTuGr636gk= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.42.0/go.mod h1:RolT8tWtfHcjajEH5wFIZ4Dgh5jpPdFXYV9pTAk/qjc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 h1:THuZiwpQZuHPul65w4WcwEnkX2QIuMT+UFoOrygtoJw= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0/go.mod h1:J2pvYM5NGHofZ2/Ru6zw/TNWnEQp5crgyDeSrYpXkAw= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0 h1:zWWrB1U6nqhS/k6zYB74CjRpuiitRtLLi68VcgmOEto= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0/go.mod h1:2qXPNBX1OVRC0IwOnfo1ljoid+RD0QK3443EaqVlsOU= +go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4= +go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI= +go.opentelemetry.io/otel/sdk v1.42.0 h1:LyC8+jqk6UJwdrI/8VydAq/hvkFKNHZVIWuslJXYsDo= +go.opentelemetry.io/otel/sdk v1.42.0/go.mod h1:rGHCAxd9DAph0joO4W6OPwxjNTYWghRWmkHuGbayMts= +go.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA= +go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc= +go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY= +go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc= go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= diff --git a/go/go.mod b/go/go.mod index e4b3d3b..6154ef7 100644 --- a/go/go.mod +++ b/go/go.mod @@ -3,11 +3,11 @@ module github.com/grafana/sigil/sdks/go go 1.25.6 require ( - go.opentelemetry.io/otel v1.41.0 - go.opentelemetry.io/otel/metric v1.41.0 - go.opentelemetry.io/otel/sdk v1.41.0 - go.opentelemetry.io/otel/sdk/metric v1.41.0 - go.opentelemetry.io/otel/trace v1.41.0 + go.opentelemetry.io/otel v1.42.0 + go.opentelemetry.io/otel/metric v1.42.0 + go.opentelemetry.io/otel/sdk v1.42.0 + go.opentelemetry.io/otel/sdk/metric v1.42.0 + go.opentelemetry.io/otel/trace v1.42.0 google.golang.org/grpc v1.79.2 google.golang.org/protobuf v1.36.11 ) diff --git a/go/go.sum b/go/go.sum index 2aec141..d23fe57 100644 --- a/go/go.sum +++ b/go/go.sum @@ -19,16 +19,16 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c= -go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE= -go.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ= -go.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps= -go.opentelemetry.io/otel/sdk v1.41.0 h1:YPIEXKmiAwkGl3Gu1huk1aYWwtpRLeskpV+wPisxBp8= -go.opentelemetry.io/otel/sdk v1.41.0/go.mod h1:ahFdU0G5y8IxglBf0QBJXgSe7agzjE4GiTJ6HT9ud90= -go.opentelemetry.io/otel/sdk/metric v1.41.0 h1:siZQIYBAUd1rlIWQT2uCxWJxcCO7q3TriaMlf08rXw8= -go.opentelemetry.io/otel/sdk/metric v1.41.0/go.mod h1:HNBuSvT7ROaGtGI50ArdRLUnvRTRGniSUZbxiWxSO8Y= -go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0= -go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis= +go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho= +go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc= +go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4= +go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI= +go.opentelemetry.io/otel/sdk v1.42.0 h1:LyC8+jqk6UJwdrI/8VydAq/hvkFKNHZVIWuslJXYsDo= +go.opentelemetry.io/otel/sdk v1.42.0/go.mod h1:rGHCAxd9DAph0joO4W6OPwxjNTYWghRWmkHuGbayMts= +go.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA= +go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc= +go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY= +go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= From 620c5d62b37238c054cebcd11a8a6b972bb342ad Mon Sep 17 00:00:00 2001 From: "renovate-sh-app[bot]" <219655108+renovate-sh-app[bot]@users.noreply.github.com> Date: Wed, 11 Mar 2026 12:09:49 +0100 Subject: [PATCH 037/133] fix(deps): update opentelemetry-java monorepo to v1.60.1 (#423) | datasource | package | from | to | | ---------- | -------------------------------------------- | ------ | ------ | | maven | io.opentelemetry:opentelemetry-sdk-testing | 1.59.0 | 1.60.1 | | maven | io.opentelemetry:opentelemetry-exporter-otlp | 1.59.0 | 1.60.1 | | maven | io.opentelemetry:opentelemetry-sdk-metrics | 1.59.0 | 1.60.1 | | maven | io.opentelemetry:opentelemetry-sdk-trace | 1.59.0 | 1.60.1 | | maven | io.opentelemetry:opentelemetry-context | 1.59.0 | 1.60.1 | | maven | io.opentelemetry:opentelemetry-api | 1.59.0 | 1.60.1 | Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> --- java/gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java/gradle/libs.versions.toml b/java/gradle/libs.versions.toml index 667991e..56eb15c 100644 --- a/java/gradle/libs.versions.toml +++ b/java/gradle/libs.versions.toml @@ -4,7 +4,7 @@ jackson = "2.21.1" jacksonAnnotations = "2.21" jmh = "0.7.3" junit = "6.0.3" -otel = "1.59.0" +otel = "1.60.1" protobuf = "4.34.0" protobufPlugin = "0.9.6" grpc = "1.79.0" From b6db9779585e2588beb1c25cc86765b7050faca7 Mon Sep 17 00:00:00 2001 From: "renovate-sh-app[bot]" <219655108+renovate-sh-app[bot]@users.noreply.github.com> Date: Wed, 11 Mar 2026 12:09:58 +0100 Subject: [PATCH 038/133] fix(deps): update dependency com.anthropic:anthropic-java to v2.16.0 (#421) | datasource | package | from | to | | ---------- | ---------------------------- | ------ | ------ | | maven | com.anthropic:anthropic-java | 2.15.0 | 2.16.0 | Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> --- java/gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java/gradle/libs.versions.toml b/java/gradle/libs.versions.toml index 56eb15c..29528b1 100644 --- a/java/gradle/libs.versions.toml +++ b/java/gradle/libs.versions.toml @@ -42,7 +42,7 @@ mockwebserver = { module = "com.squareup.okhttp3:mockwebserver", version.ref = " javax-annotation = { module = "javax.annotation:javax.annotation-api", version.ref = "javaxAnnotation" } openai-java = { module = "com.openai:openai-java", version = "4.26.0" } -anthropic-java = { module = "com.anthropic:anthropic-java", version = "2.15.0" } +anthropic-java = { module = "com.anthropic:anthropic-java", version = "2.16.0" } google-genai = { module = "com.google.genai:google-genai", version = "1.42.0" } [plugins] From 65799755be6b27c8dd0201073e1af21c386b0a2e Mon Sep 17 00:00:00 2001 From: "renovate-sh-app[bot]" <219655108+renovate-sh-app[bot]@users.noreply.github.com> Date: Wed, 11 Mar 2026 12:28:24 +0100 Subject: [PATCH 039/133] fix(deps): update module github.com/openai/openai-go/v3 to v3.26.0 (#384) * fix(deps): update module github.com/openai/openai-go/v3 to v3.26.0 | datasource | package | from | to | | ---------- | ------------------------------ | ------- | ------- | | go | github.com/openai/openai-go/v3 | v3.24.0 | v3.26.0 | Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> * fix: adapt openai provider to openai-go v3.26 Handle Responses API function-call arguments through the new union type introduced in openai-go v3 so the Renovate dependency bump keeps passing lint and tests. --------- Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: Cyril Tovena --- go-providers/openai/go.mod | 8 ++++---- go-providers/openai/go.sum | 24 ++++++++++++------------ go-providers/openai/mapper_test.go | 2 +- go-providers/openai/responses_mapper.go | 25 ++++++++++++++++++++++--- go/cmd/devex-emitter/go.mod | 2 +- go/cmd/devex-emitter/go.sum | 4 ++-- 6 files changed, 42 insertions(+), 23 deletions(-) diff --git a/go-providers/openai/go.mod b/go-providers/openai/go.mod index 609d510..0320dc0 100644 --- a/go-providers/openai/go.mod +++ b/go-providers/openai/go.mod @@ -4,7 +4,7 @@ go 1.25.6 require ( github.com/grafana/sigil/sdks/go v0.0.0 - github.com/openai/openai-go/v3 v3.24.0 + github.com/openai/openai-go/v3 v3.26.0 ) require ( @@ -16,9 +16,9 @@ require ( github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/sjson v1.2.5 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/otel v1.41.0 // indirect - go.opentelemetry.io/otel/metric v1.41.0 // indirect - go.opentelemetry.io/otel/trace v1.41.0 // indirect + go.opentelemetry.io/otel v1.42.0 // indirect + go.opentelemetry.io/otel/metric v1.42.0 // indirect + go.opentelemetry.io/otel/trace v1.42.0 // indirect golang.org/x/net v0.51.0 // indirect golang.org/x/sys v0.41.0 // indirect golang.org/x/text v0.34.0 // indirect diff --git a/go-providers/openai/go.sum b/go-providers/openai/go.sum index ab20f85..0560506 100644 --- a/go-providers/openai/go.sum +++ b/go-providers/openai/go.sum @@ -13,8 +13,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/openai/openai-go/v3 v3.24.0 h1:08x6GnYiB+AAejTo6yzPY8RkZMJQ8NpreiOyM5QfyYU= -github.com/openai/openai-go/v3 v3.24.0/go.mod h1:cdufnVK14cWcT9qA1rRtrXx4FTRsgbDPW7Ia7SS5cZo= +github.com/openai/openai-go/v3 v3.26.0 h1:bRt6H/ozMNt/dDkN4gobnLqaEGrRGBzmbVs0xxJEnQE= +github.com/openai/openai-go/v3 v3.26.0/go.mod h1:cdufnVK14cWcT9qA1rRtrXx4FTRsgbDPW7Ia7SS5cZo= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= @@ -31,16 +31,16 @@ github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c= -go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE= -go.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ= -go.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps= -go.opentelemetry.io/otel/sdk v1.41.0 h1:YPIEXKmiAwkGl3Gu1huk1aYWwtpRLeskpV+wPisxBp8= -go.opentelemetry.io/otel/sdk v1.41.0/go.mod h1:ahFdU0G5y8IxglBf0QBJXgSe7agzjE4GiTJ6HT9ud90= -go.opentelemetry.io/otel/sdk/metric v1.41.0 h1:siZQIYBAUd1rlIWQT2uCxWJxcCO7q3TriaMlf08rXw8= -go.opentelemetry.io/otel/sdk/metric v1.41.0/go.mod h1:HNBuSvT7ROaGtGI50ArdRLUnvRTRGniSUZbxiWxSO8Y= -go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0= -go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis= +go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho= +go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc= +go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4= +go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI= +go.opentelemetry.io/otel/sdk v1.42.0 h1:LyC8+jqk6UJwdrI/8VydAq/hvkFKNHZVIWuslJXYsDo= +go.opentelemetry.io/otel/sdk v1.42.0/go.mod h1:rGHCAxd9DAph0joO4W6OPwxjNTYWghRWmkHuGbayMts= +go.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA= +go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc= +go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY= +go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc= golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= diff --git a/go-providers/openai/mapper_test.go b/go-providers/openai/mapper_test.go index 47798de..2e26a95 100644 --- a/go-providers/openai/mapper_test.go +++ b/go-providers/openai/mapper_test.go @@ -375,7 +375,7 @@ func TestResponsesFromRequestResponse(t *testing.T) { Type: "function_call", CallID: "call_weather", Name: "weather", - Arguments: `{"city":"Paris"}`, + Arguments: oresponses.ResponseOutputItemUnionArguments{OfString: `{"city":"Paris"}`}, }, }, Usage: oresponses.ResponseUsage{ diff --git a/go-providers/openai/responses_mapper.go b/go-providers/openai/responses_mapper.go index 971d47e..d21e8e4 100644 --- a/go-providers/openai/responses_mapper.go +++ b/go-providers/openai/responses_mapper.go @@ -342,7 +342,7 @@ func mapResponsesOutput(items []responses.ResponseOutputItemUnion) []sigil.Messa part := sigil.ToolCallPart(sigil.ToolCall{ ID: item.CallID, Name: item.Name, - InputJSON: parseJSONOrString(item.Arguments), + InputJSON: parseResponsesOutputArguments(item.Arguments), }) part.Metadata.ProviderType = "tool_call" out = append(out, sigil.Message{Role: sigil.RoleAssistant, Parts: []sigil.Part{part}}) @@ -495,12 +495,31 @@ func extractResponsesOutputFallback(item responses.ResponseOutputItemUnion) stri if item.Error != "" { return item.Error } - if item.Name != "" && item.Arguments != "" { - return fmt.Sprintf("%s(%s)", item.Name, item.Arguments) + arguments := stringifyResponsesOutputArguments(item.Arguments) + if item.Name != "" && arguments != "" { + return fmt.Sprintf("%s(%s)", item.Name, arguments) } return "" } +func parseResponsesOutputArguments(arguments responses.ResponseOutputItemUnionArguments) []byte { + return parseJSONOrString(stringifyResponsesOutputArguments(arguments)) +} + +func stringifyResponsesOutputArguments(arguments responses.ResponseOutputItemUnionArguments) string { + if arguments.OfString != "" { + return arguments.OfString + } + if arguments.OfResponseToolSearchCallArguments == nil { + return "" + } + data, err := json.Marshal(arguments.OfResponseToolSearchCallArguments) + if err != nil { + return "" + } + return string(data) +} + func marshalAny(value any) map[string]any { raw, err := json.Marshal(value) if err != nil { diff --git a/go/cmd/devex-emitter/go.mod b/go/cmd/devex-emitter/go.mod index dad0e15..f0388f0 100644 --- a/go/cmd/devex-emitter/go.mod +++ b/go/cmd/devex-emitter/go.mod @@ -8,7 +8,7 @@ require ( github.com/grafana/sigil/sdks/go-providers/anthropic v0.0.0 github.com/grafana/sigil/sdks/go-providers/gemini v0.0.0 github.com/grafana/sigil/sdks/go-providers/openai v0.0.0 - github.com/openai/openai-go/v3 v3.24.0 + github.com/openai/openai-go/v3 v3.26.0 go.opentelemetry.io/otel v1.42.0 go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.42.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0 diff --git a/go/cmd/devex-emitter/go.sum b/go/cmd/devex-emitter/go.sum index 256097e..c07a809 100644 --- a/go/cmd/devex-emitter/go.sum +++ b/go/cmd/devex-emitter/go.sum @@ -37,8 +37,8 @@ github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aN github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= -github.com/openai/openai-go/v3 v3.24.0 h1:08x6GnYiB+AAejTo6yzPY8RkZMJQ8NpreiOyM5QfyYU= -github.com/openai/openai-go/v3 v3.24.0/go.mod h1:cdufnVK14cWcT9qA1rRtrXx4FTRsgbDPW7Ia7SS5cZo= +github.com/openai/openai-go/v3 v3.26.0 h1:bRt6H/ozMNt/dDkN4gobnLqaEGrRGBzmbVs0xxJEnQE= +github.com/openai/openai-go/v3 v3.26.0/go.mod h1:cdufnVK14cWcT9qA1rRtrXx4FTRsgbDPW7Ia7SS5cZo= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= From c2dc2295e4e09a33aa03aa6dea4983458177873a Mon Sep 17 00:00:00 2001 From: "renovate-sh-app[bot]" <219655108+renovate-sh-app[bot]@users.noreply.github.com> Date: Wed, 11 Mar 2026 12:28:27 +0100 Subject: [PATCH 040/133] chore(deps): update dependency openai to v6.27.0 (#383) | datasource | package | from | to | | ---------- | ------- | ------ | ------ | | npm | openai | 6.25.0 | 6.27.0 | Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: Cyril Tovena --- js/package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/js/package.json b/js/package.json index a016a5f..8811397 100644 --- a/js/package.json +++ b/js/package.json @@ -54,8 +54,8 @@ }, "dependencies": { "@anthropic-ai/sdk": "^0.78.0", - "@google/genai": "^1.41.0", "@google/adk": "^0.4.0", + "@google/genai": "^1.41.0", "@grpc/grpc-js": "^1.14.1", "@grpc/proto-loader": "^0.8.0", "@langchain/core": "^1.0.0", @@ -68,8 +68,8 @@ "@opentelemetry/exporter-trace-otlp-http": "^0.213.0", "@opentelemetry/sdk-metrics": "^2.1.0", "@opentelemetry/sdk-trace-base": "^2.5.0", - "openai": "^6.21.0", - "llamaindex": "^0.12.1" + "llamaindex": "^0.12.1", + "openai": "^6.27.0" }, "devDependencies": { "@types/node": "^24.11.0", From 5aa9ef51411b7225f19cc57e61d8209bfeda05b5 Mon Sep 17 00:00:00 2001 From: Alexander Sniffin Date: Wed, 11 Mar 2026 11:13:31 -0400 Subject: [PATCH 041/133] fix(plugin,sigil): show created dates for global templates and clean up eval UI (#435) * fix(plugin,sigil): show created dates for global templates and clean up eval UI Populate CreatedAt/UpdatedAt on predefined global templates by parsing the version string (e.g. "2026-03-05") so dates display in the UI instead of showing em dashes. Remove em dash fallback for empty descriptions across evaluator and template cards/tables. Stack version and created metadata on separate lines in card grids, and widen the Created column in tables with proper column layout so date and actor don't truncate. Co-Authored-By: Claude Opus 4.6 * fix(plugin): handle optional evaluator description in Text children The Grafana Text component requires NonNullable children. Use nullish coalescing to provide empty string fallback for the optional evaluator description field. Co-Authored-By: Claude Opus 4.6 * chore: include typecheck in mise lint task to match CI CI runs tsc --noEmit after eslint, but mise run lint only ran eslint. Adding typecheck as a dependency of the lint task ensures local linting catches type errors the same way CI does. Co-Authored-By: Claude Opus 4.6 * fix(sigil): handle dotted version format when parsing predefined template dates Extract parseVersionDate helper that splits on "." before parsing, matching the existing isValidVersionFormat logic. This ensures versions like "2026-03-05.1" correctly produce the date instead of silently falling back to zero time. Co-Authored-By: Claude Opus 4.6 --------- Co-authored-by: Claude Opus 4.6 --- go-frameworks/google-adk/go.mod | 6 +++--- go-frameworks/google-adk/go.sum | 20 ++++++++++---------- go-providers/anthropic/go.mod | 6 +++--- go-providers/anthropic/go.sum | 20 ++++++++++---------- go-providers/gemini/go.mod | 6 +++--- go-providers/gemini/go.sum | 20 ++++++++++---------- 6 files changed, 39 insertions(+), 39 deletions(-) diff --git a/go-frameworks/google-adk/go.mod b/go-frameworks/google-adk/go.mod index 910c35b..6eece5f 100644 --- a/go-frameworks/google-adk/go.mod +++ b/go-frameworks/google-adk/go.mod @@ -9,9 +9,9 @@ require ( github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/otel v1.41.0 // indirect - go.opentelemetry.io/otel/metric v1.41.0 // indirect - go.opentelemetry.io/otel/trace v1.41.0 // indirect + go.opentelemetry.io/otel v1.42.0 // indirect + go.opentelemetry.io/otel/metric v1.42.0 // indirect + go.opentelemetry.io/otel/trace v1.42.0 // indirect golang.org/x/net v0.51.0 // indirect golang.org/x/sys v0.41.0 // indirect golang.org/x/text v0.34.0 // indirect diff --git a/go-frameworks/google-adk/go.sum b/go-frameworks/google-adk/go.sum index edf5702..3d963df 100644 --- a/go-frameworks/google-adk/go.sum +++ b/go-frameworks/google-adk/go.sum @@ -19,16 +19,16 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c= -go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE= -go.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ= -go.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps= -go.opentelemetry.io/otel/sdk v1.41.0 h1:YPIEXKmiAwkGl3Gu1huk1aYWwtpRLeskpV+wPisxBp8= -go.opentelemetry.io/otel/sdk v1.41.0/go.mod h1:ahFdU0G5y8IxglBf0QBJXgSe7agzjE4GiTJ6HT9ud90= -go.opentelemetry.io/otel/sdk/metric v1.41.0 h1:siZQIYBAUd1rlIWQT2uCxWJxcCO7q3TriaMlf08rXw8= -go.opentelemetry.io/otel/sdk/metric v1.41.0/go.mod h1:HNBuSvT7ROaGtGI50ArdRLUnvRTRGniSUZbxiWxSO8Y= -go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0= -go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis= +go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho= +go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc= +go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4= +go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI= +go.opentelemetry.io/otel/sdk v1.42.0 h1:LyC8+jqk6UJwdrI/8VydAq/hvkFKNHZVIWuslJXYsDo= +go.opentelemetry.io/otel/sdk v1.42.0/go.mod h1:rGHCAxd9DAph0joO4W6OPwxjNTYWghRWmkHuGbayMts= +go.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA= +go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc= +go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY= +go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc= golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= diff --git a/go-providers/anthropic/go.mod b/go-providers/anthropic/go.mod index 8d79afa..23f2915 100644 --- a/go-providers/anthropic/go.mod +++ b/go-providers/anthropic/go.mod @@ -16,9 +16,9 @@ require ( github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/sjson v1.2.5 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/otel v1.41.0 // indirect - go.opentelemetry.io/otel/metric v1.41.0 // indirect - go.opentelemetry.io/otel/trace v1.41.0 // indirect + go.opentelemetry.io/otel v1.42.0 // indirect + go.opentelemetry.io/otel/metric v1.42.0 // indirect + go.opentelemetry.io/otel/trace v1.42.0 // indirect golang.org/x/net v0.51.0 // indirect golang.org/x/sync v0.19.0 // indirect golang.org/x/sys v0.41.0 // indirect diff --git a/go-providers/anthropic/go.sum b/go-providers/anthropic/go.sum index 91ab2ce..89eda3c 100644 --- a/go-providers/anthropic/go.sum +++ b/go-providers/anthropic/go.sum @@ -33,16 +33,16 @@ github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c= -go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE= -go.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ= -go.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps= -go.opentelemetry.io/otel/sdk v1.41.0 h1:YPIEXKmiAwkGl3Gu1huk1aYWwtpRLeskpV+wPisxBp8= -go.opentelemetry.io/otel/sdk v1.41.0/go.mod h1:ahFdU0G5y8IxglBf0QBJXgSe7agzjE4GiTJ6HT9ud90= -go.opentelemetry.io/otel/sdk/metric v1.41.0 h1:siZQIYBAUd1rlIWQT2uCxWJxcCO7q3TriaMlf08rXw8= -go.opentelemetry.io/otel/sdk/metric v1.41.0/go.mod h1:HNBuSvT7ROaGtGI50ArdRLUnvRTRGniSUZbxiWxSO8Y= -go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0= -go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis= +go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho= +go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc= +go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4= +go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI= +go.opentelemetry.io/otel/sdk v1.42.0 h1:LyC8+jqk6UJwdrI/8VydAq/hvkFKNHZVIWuslJXYsDo= +go.opentelemetry.io/otel/sdk v1.42.0/go.mod h1:rGHCAxd9DAph0joO4W6OPwxjNTYWghRWmkHuGbayMts= +go.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA= +go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc= +go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY= +go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc= golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= diff --git a/go-providers/gemini/go.mod b/go-providers/gemini/go.mod index d0e2470..7794c70 100644 --- a/go-providers/gemini/go.mod +++ b/go-providers/gemini/go.mod @@ -22,9 +22,9 @@ require ( github.com/gorilla/websocket v1.5.3 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 // indirect - go.opentelemetry.io/otel v1.41.0 // indirect - go.opentelemetry.io/otel/metric v1.41.0 // indirect - go.opentelemetry.io/otel/trace v1.41.0 // indirect + go.opentelemetry.io/otel v1.42.0 // indirect + go.opentelemetry.io/otel/metric v1.42.0 // indirect + go.opentelemetry.io/otel/trace v1.42.0 // indirect golang.org/x/crypto v0.48.0 // indirect golang.org/x/net v0.51.0 // indirect golang.org/x/sys v0.41.0 // indirect diff --git a/go-providers/gemini/go.sum b/go-providers/gemini/go.sum index 4fb39db..285cc8d 100644 --- a/go-providers/gemini/go.sum +++ b/go-providers/gemini/go.sum @@ -37,16 +37,16 @@ go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0= -go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c= -go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE= -go.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ= -go.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps= -go.opentelemetry.io/otel/sdk v1.41.0 h1:YPIEXKmiAwkGl3Gu1huk1aYWwtpRLeskpV+wPisxBp8= -go.opentelemetry.io/otel/sdk v1.41.0/go.mod h1:ahFdU0G5y8IxglBf0QBJXgSe7agzjE4GiTJ6HT9ud90= -go.opentelemetry.io/otel/sdk/metric v1.41.0 h1:siZQIYBAUd1rlIWQT2uCxWJxcCO7q3TriaMlf08rXw8= -go.opentelemetry.io/otel/sdk/metric v1.41.0/go.mod h1:HNBuSvT7ROaGtGI50ArdRLUnvRTRGniSUZbxiWxSO8Y= -go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0= -go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis= +go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho= +go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc= +go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4= +go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI= +go.opentelemetry.io/otel/sdk v1.42.0 h1:LyC8+jqk6UJwdrI/8VydAq/hvkFKNHZVIWuslJXYsDo= +go.opentelemetry.io/otel/sdk v1.42.0/go.mod h1:rGHCAxd9DAph0joO4W6OPwxjNTYWghRWmkHuGbayMts= +go.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA= +go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc= +go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY= +go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc= golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= From 3580e0667977eaa037591b8fafa7361d3d73a08a Mon Sep 17 00:00:00 2001 From: "renovate-sh-app[bot]" <219655108+renovate-sh-app[bot]@users.noreply.github.com> Date: Thu, 12 Mar 2026 08:01:53 +0100 Subject: [PATCH 042/133] fix(deps): update dependency @openai/agents to ^0.6.0 (#450) | datasource | package | from | to | | ---------- | -------------- | ----- | ----- | | npm | @openai/agents | 0.5.4 | 0.6.0 | Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> --- js/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/package.json b/js/package.json index 8811397..15382e5 100644 --- a/js/package.json +++ b/js/package.json @@ -60,7 +60,7 @@ "@grpc/proto-loader": "^0.8.0", "@langchain/core": "^1.0.0", "@langchain/langgraph": "^1.2.0", - "@openai/agents": "^0.5.0", + "@openai/agents": "^0.6.0", "@opentelemetry/api": "^1.9.0", "@opentelemetry/exporter-metrics-otlp-grpc": "^0.213.0", "@opentelemetry/exporter-metrics-otlp-http": "^0.213.0", From 084d992b39f568458ffbe79377b646d5a7b76572 Mon Sep 17 00:00:00 2001 From: Cyril Tovena Date: Thu, 12 Mar 2026 13:13:23 +0100 Subject: [PATCH 043/133] Add initial Go SDK conformance harness slice (#458) * test(go-sdk): add initial conformance harness slice Summary: - add a public-API Go conformance harness with localhost gRPC ingest, rating capture, span recording, and manual metric collection - cover conversation title, user ID, and agent identity semantics in `package sigil_test` with table-driven conformance scenarios - update the active execution plan to reflect the shipped harness slice Rationale: - establish a black-box reference harness that other SDK conformance suites can copy without relying on in-package test seams - lock the first high-signal identity semantics against raw exported data before expanding into roundtrip and provider/framework coverage Tests: - go test -run TestConformance -count=1 ./sdks/go/sigil/... Co-authored-by: Codex * fix(go-sdk): keep conformance telemetry assertions alive Summary: - remove dead harness helpers that tripped golangci-lint in CI - flush generation export explicitly in the conformance helper before making assertions - keep metric and tracer providers alive until test cleanup so metric collection works after the client shutdown gate Rationale: - align the local conformance harness lifecycle with the CI lint and test expectations instead of relying on teardown ordering - keep the first Go conformance slice green on the branch PR checks Tests: - go test -run TestConformance -count=1 ./sdks/go/sigil/... - (cd sdks/go && golangci-lint run ./...) Co-authored-by: Codex * fix(go-sdk): keep conformance assertions CI-clean Summary: - reorder the Go conformance assertions so spans and metrics are checked before shutting down the harness - keep the proto export assertions after shutdown so the gRPC worker has drained deterministically - remove the now-unneeded flush path from the test helper Rationale: - fix the red lint/format workflow without dropping the metric helper coverage from the first conformance slice - make the harness deterministic for both metric collection and gRPC export capture under CI timing Tests: - go test -run TestConformance ./sdks/go/sigil/... - go test ./sdks/go/sigil/... - golangci-lint run ./sdks/go/... Co-authored-by: Codex --------- Co-authored-by: Codex --- go/sigil/conformance_helpers_test.go | 407 +++++++++++++++++++++++++++ go/sigil/conformance_test.go | 286 +++++++++++++++++++ 2 files changed, 693 insertions(+) create mode 100644 go/sigil/conformance_helpers_test.go create mode 100644 go/sigil/conformance_test.go diff --git a/go/sigil/conformance_helpers_test.go b/go/sigil/conformance_helpers_test.go new file mode 100644 index 0000000..53e1afa --- /dev/null +++ b/go/sigil/conformance_helpers_test.go @@ -0,0 +1,407 @@ +package sigil_test + +import ( + "context" + "io" + "net" + "net/http" + "net/http/httptest" + "sync" + "testing" + "time" + + sigil "github.com/grafana/sigil/sdks/go/sigil" + sigilv1 "github.com/grafana/sigil/sdks/go/sigil/internal/gen/sigil/v1" + "go.opentelemetry.io/otel/attribute" + sdkmetric "go.opentelemetry.io/otel/sdk/metric" + "go.opentelemetry.io/otel/sdk/metric/metricdata" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + "go.opentelemetry.io/otel/sdk/trace/tracetest" + "google.golang.org/grpc" + "google.golang.org/protobuf/proto" +) + +const ( + conformanceOperationName = "generateText" + metadataKeyConversation = "sigil.conversation.title" + metadataKeyCanonicalUserID = "sigil.user.id" + metadataKeyLegacyUserID = "user.id" + spanAttrOperationName = "gen_ai.operation.name" + spanAttrConversationTitle = "sigil.conversation.title" + spanAttrUserID = "user.id" + spanAttrAgentName = "gen_ai.agent.name" + spanAttrAgentVersion = "gen_ai.agent.version" + metricOperationDuration = "gen_ai.client.operation.duration" + metricTimeToFirstToken = "gen_ai.client.time_to_first_token" +) + +var conformanceModel = sigil.ModelRef{ + Provider: "openai", + Name: "gpt-5", +} + +type conformanceEnv struct { + Client *sigil.Client + Ingest *fakeIngestServer + Spans *tracetest.SpanRecorder + Metrics *sdkmetric.ManualReader + Rating *fakeRatingServer + + tracerProvider *sdktrace.TracerProvider + meterProvider *sdkmetric.MeterProvider + grpcServer *grpc.Server + listener net.Listener + closeOnce sync.Once +} + +type conformanceEnvOption func(*conformanceEnvConfig) + +type conformanceEnvConfig struct { + config sigil.Config +} + +func newConformanceEnv(t *testing.T, opts ...conformanceEnvOption) *conformanceEnv { + t.Helper() + + ingest := &fakeIngestServer{} + grpcServer := grpc.NewServer() + sigilv1.RegisterGenerationIngestServiceServer(grpcServer, ingest) + + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("listen for fake ingest server: %v", err) + } + + go func() { + _ = grpcServer.Serve(listener) + }() + + spanRecorder := tracetest.NewSpanRecorder() + tracerProvider := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(spanRecorder)) + metricReader := sdkmetric.NewManualReader() + meterProvider := sdkmetric.NewMeterProvider(sdkmetric.WithReader(metricReader)) + ratingServer := newFakeRatingServer() + + cfg := conformanceEnvConfig{ + config: sigil.Config{ + Tracer: tracerProvider.Tracer("sigil-conformance-test"), + Meter: meterProvider.Meter("sigil-conformance-test"), + GenerationExport: sigil.GenerationExportConfig{ + Protocol: sigil.GenerationExportProtocolGRPC, + Endpoint: listener.Addr().String(), + Insecure: true, + BatchSize: 1, + FlushInterval: time.Hour, + QueueSize: 8, + MaxRetries: 1, + InitialBackoff: time.Millisecond, + MaxBackoff: 5 * time.Millisecond, + PayloadMaxBytes: 4 << 20, + }, + API: sigil.APIConfig{ + Endpoint: ratingServer.URL(), + }, + }, + } + for _, opt := range opts { + opt(&cfg) + } + + env := &conformanceEnv{ + Client: sigil.NewClient(cfg.config), + Ingest: ingest, + Spans: spanRecorder, + Metrics: metricReader, + Rating: ratingServer, + tracerProvider: tracerProvider, + meterProvider: meterProvider, + grpcServer: grpcServer, + listener: listener, + } + t.Cleanup(func() { + _ = env.close() + }) + return env +} + +func (e *conformanceEnv) Shutdown(t *testing.T) { + t.Helper() + + if e == nil || e.Client == nil { + return + } + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + if err := e.Client.Shutdown(ctx); err != nil { + t.Fatalf("shutdown conformance client: %v", err) + } +} + +func (e *conformanceEnv) close() error { + if e == nil { + return nil + } + + var closeErr error + e.closeOnce.Do(func() { + if e.Client != nil { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if err := e.Client.Shutdown(ctx); err != nil { + closeErr = err + } + } + if e.meterProvider != nil { + if err := e.meterProvider.Shutdown(context.Background()); err != nil && closeErr == nil { + closeErr = err + } + } + if e.tracerProvider != nil { + if err := e.tracerProvider.Shutdown(context.Background()); err != nil && closeErr == nil { + closeErr = err + } + } + if e.grpcServer != nil { + e.grpcServer.Stop() + } + if e.listener != nil { + _ = e.listener.Close() + } + if e.Rating != nil { + e.Rating.Close() + } + }) + return closeErr +} + +func (e *conformanceEnv) CollectMetrics(t *testing.T) metricdata.ResourceMetrics { + t.Helper() + + var collected metricdata.ResourceMetrics + if err := e.Metrics.Collect(context.Background(), &collected); err != nil { + t.Fatalf("collect metrics: %v", err) + } + return collected +} + +type fakeIngestServer struct { + sigilv1.UnimplementedGenerationIngestServiceServer + + mu sync.Mutex + requests []*sigilv1.ExportGenerationsRequest +} + +func (s *fakeIngestServer) ExportGenerations(_ context.Context, req *sigilv1.ExportGenerationsRequest) (*sigilv1.ExportGenerationsResponse, error) { + s.capture(req) + return acceptanceResponse(req), nil +} + +func (s *fakeIngestServer) capture(req *sigilv1.ExportGenerationsRequest) { + if req == nil { + return + } + + clone := proto.Clone(req) + typed, ok := clone.(*sigilv1.ExportGenerationsRequest) + if !ok { + return + } + + s.mu.Lock() + s.requests = append(s.requests, typed) + s.mu.Unlock() +} + +func (s *fakeIngestServer) SingleGeneration(t *testing.T) *sigilv1.Generation { + t.Helper() + + s.mu.Lock() + defer s.mu.Unlock() + + if len(s.requests) != 1 { + t.Fatalf("expected exactly one export request, got %d", len(s.requests)) + } + if len(s.requests[0].Generations) != 1 { + t.Fatalf("expected exactly one generation in request, got %d", len(s.requests[0].Generations)) + } + return s.requests[0].Generations[0] +} + +func acceptanceResponse(req *sigilv1.ExportGenerationsRequest) *sigilv1.ExportGenerationsResponse { + response := &sigilv1.ExportGenerationsResponse{Results: make([]*sigilv1.ExportGenerationResult, len(req.GetGenerations()))} + for i := range req.GetGenerations() { + response.Results[i] = &sigilv1.ExportGenerationResult{ + GenerationId: req.Generations[i].GetId(), + Accepted: true, + } + } + return response +} + +type fakeRatingServer struct { + server *httptest.Server + + mu sync.Mutex + requests []capturedRatingRequest +} + +type capturedRatingRequest struct { + Method string + Path string + Headers http.Header + Body []byte +} + +func newFakeRatingServer() *fakeRatingServer { + s := &fakeRatingServer{} + s.server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + body, err := io.ReadAll(req.Body) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + s.mu.Lock() + s.requests = append(s.requests, capturedRatingRequest{ + Method: req.Method, + Path: req.URL.Path, + Headers: req.Header.Clone(), + Body: append([]byte(nil), body...), + }) + s.mu.Unlock() + + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"rating":{"rating_id":"rat-1","conversation_id":"conv-1","rating":"CONVERSATION_RATING_VALUE_GOOD","created_at":"2026-03-12T11:00:00Z"},"summary":{"total_count":1,"good_count":1,"bad_count":0,"latest_rating":"CONVERSATION_RATING_VALUE_GOOD","latest_rated_at":"2026-03-12T11:00:00Z","has_bad_rating":false}}`)) + })) + return s +} + +func (s *fakeRatingServer) URL() string { + if s == nil || s.server == nil { + return "" + } + return s.server.URL +} + +func (s *fakeRatingServer) Close() { + if s != nil && s.server != nil { + s.server.Close() + } +} + +func findSpan(t *testing.T, spans []sdktrace.ReadOnlySpan, operationName string) sdktrace.ReadOnlySpan { + t.Helper() + + var matched sdktrace.ReadOnlySpan + for _, span := range spans { + attrs := spanAttrs(span) + if got, ok := attrs[spanAttrOperationName]; ok && got.AsString() == operationName { + if matched != nil { + t.Fatalf("expected exactly one span with %s=%q", spanAttrOperationName, operationName) + } + matched = span + } + } + if matched == nil { + t.Fatalf("expected span with %s=%q", spanAttrOperationName, operationName) + } + return matched +} + +func spanAttrs(span sdktrace.ReadOnlySpan) map[string]attribute.Value { + attrs := make(map[string]attribute.Value, len(span.Attributes())) + for _, attr := range span.Attributes() { + attrs[string(attr.Key)] = attr.Value + } + return attrs +} + +func requireSpanAttr(t *testing.T, attrs map[string]attribute.Value, key, want string) { + t.Helper() + + got, ok := attrs[key] + if !ok { + t.Fatalf("expected span attribute %q=%q, attribute missing", key, want) + } + if got.AsString() != want { + t.Fatalf("unexpected span attribute %q: got %q want %q", key, got.AsString(), want) + } +} + +func requireSpanAttrAbsent(t *testing.T, attrs map[string]attribute.Value, key string) { + t.Helper() + + if _, ok := attrs[key]; ok { + t.Fatalf("did not expect span attribute %q to be present", key) + } +} + +func findHistogram[N int64 | float64](t *testing.T, collected metricdata.ResourceMetrics, name string) metricdata.Histogram[N] { + t.Helper() + + for _, scopeMetrics := range collected.ScopeMetrics { + for _, metric := range scopeMetrics.Metrics { + if metric.Name != name { + continue + } + histogram, ok := metric.Data.(metricdata.Histogram[N]) + if !ok { + t.Fatalf("metric %q is not the expected histogram type", name) + } + return histogram + } + } + + t.Fatalf("expected histogram %q", name) + return metricdata.Histogram[N]{} +} + +func requireNoHistogram(t *testing.T, collected metricdata.ResourceMetrics, name string) { + t.Helper() + + for _, scopeMetrics := range collected.ScopeMetrics { + for _, metric := range scopeMetrics.Metrics { + if metric.Name == name { + t.Fatalf("did not expect histogram %q to be present", name) + } + } + } +} + +func requireProtoMetadata(t *testing.T, generation *sigilv1.Generation, key, want string) { + t.Helper() + + got, ok := protoMetadataString(generation, key) + if !ok { + t.Fatalf("expected generation metadata %q=%q, key missing", key, want) + } + if got != want { + t.Fatalf("unexpected generation metadata %q: got %q want %q", key, got, want) + } +} + +func requireProtoMetadataAbsent(t *testing.T, generation *sigilv1.Generation, key string) { + t.Helper() + + if _, ok := protoMetadataString(generation, key); ok { + t.Fatalf("did not expect generation metadata %q to be present", key) + } +} + +func protoMetadataString(generation *sigilv1.Generation, key string) (string, bool) { + if generation == nil || generation.GetMetadata() == nil { + return "", false + } + + value, ok := generation.GetMetadata().AsMap()[key] + if !ok { + return "", false + } + asString, ok := value.(string) + if !ok { + return "", false + } + return asString, true +} diff --git a/go/sigil/conformance_test.go b/go/sigil/conformance_test.go new file mode 100644 index 0000000..7e63a22 --- /dev/null +++ b/go/sigil/conformance_test.go @@ -0,0 +1,286 @@ +package sigil_test + +import ( + "context" + "testing" + + sigil "github.com/grafana/sigil/sdks/go/sigil" +) + +func TestConformance_ConversationTitleSemantics(t *testing.T) { + testCases := []struct { + name string + startTitle string + contextTitle string + metadataTitle string + wantTitle string + }{ + { + name: "explicit wins", + startTitle: "Explicit", + contextTitle: "Context", + metadataTitle: "Meta", + wantTitle: "Explicit", + }, + { + name: "context fallback", + contextTitle: "Context", + wantTitle: "Context", + }, + { + name: "metadata fallback", + metadataTitle: "Meta", + wantTitle: "Meta", + }, + { + name: "whitespace omitted", + startTitle: " ", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + env := newConformanceEnv(t) + + ctx := context.Background() + if tc.contextTitle != "" { + ctx = sigil.WithConversationTitle(ctx, tc.contextTitle) + } + + start := sigil.GenerationStart{ + Model: conformanceModel, + ConversationTitle: tc.startTitle, + } + if tc.metadataTitle != "" { + start.Metadata = map[string]any{ + metadataKeyConversation: tc.metadataTitle, + } + } + + recordGeneration(t, env, ctx, start, sigil.Generation{}) + + span := findSpan(t, env.Spans.Ended(), conformanceOperationName) + attrs := spanAttrs(span) + if tc.wantTitle == "" { + requireSpanAttrAbsent(t, attrs, spanAttrConversationTitle) + } else { + requireSpanAttr(t, attrs, spanAttrConversationTitle, tc.wantTitle) + } + + requireSyncGenerationMetrics(t, env) + env.Shutdown(t) + + generation := env.Ingest.SingleGeneration(t) + if tc.wantTitle == "" { + requireProtoMetadataAbsent(t, generation, metadataKeyConversation) + } else { + requireProtoMetadata(t, generation, metadataKeyConversation, tc.wantTitle) + } + }) + } +} + +func TestConformance_UserIDSemantics(t *testing.T) { + testCases := []struct { + name string + startUserID string + contextUserID string + canonicalUser string + legacyUser string + wantResolvedID string + }{ + { + name: "explicit wins", + startUserID: "explicit", + contextUserID: "ctx", + canonicalUser: "meta-canonical", + legacyUser: "meta-legacy", + wantResolvedID: "explicit", + }, + { + name: "context fallback", + contextUserID: "ctx", + wantResolvedID: "ctx", + }, + { + name: "canonical metadata", + canonicalUser: "canonical", + wantResolvedID: "canonical", + }, + { + name: "legacy metadata", + legacyUser: "legacy", + wantResolvedID: "legacy", + }, + { + name: "canonical beats legacy", + canonicalUser: "canonical", + legacyUser: "legacy", + wantResolvedID: "canonical", + }, + { + name: "whitespace trimmed", + startUserID: " padded ", + wantResolvedID: "padded", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + env := newConformanceEnv(t) + + ctx := context.Background() + if tc.contextUserID != "" { + ctx = sigil.WithUserID(ctx, tc.contextUserID) + } + + start := sigil.GenerationStart{ + Model: conformanceModel, + UserID: tc.startUserID, + } + if tc.canonicalUser != "" || tc.legacyUser != "" { + start.Metadata = map[string]any{} + if tc.canonicalUser != "" { + start.Metadata[metadataKeyCanonicalUserID] = tc.canonicalUser + } + if tc.legacyUser != "" { + start.Metadata[metadataKeyLegacyUserID] = tc.legacyUser + } + } + + recordGeneration(t, env, ctx, start, sigil.Generation{}) + + span := findSpan(t, env.Spans.Ended(), conformanceOperationName) + attrs := spanAttrs(span) + requireSpanAttr(t, attrs, spanAttrUserID, tc.wantResolvedID) + + requireSyncGenerationMetrics(t, env) + env.Shutdown(t) + + generation := env.Ingest.SingleGeneration(t) + requireProtoMetadata(t, generation, metadataKeyCanonicalUserID, tc.wantResolvedID) + }) + } +} + +func TestConformance_AgentIdentitySemantics(t *testing.T) { + testCases := []struct { + name string + startAgentName string + startVersion string + contextAgentName string + contextVersion string + resultAgentName string + resultVersion string + wantAgentName string + wantVersion string + }{ + { + name: "explicit fields", + startAgentName: "agent-explicit", + startVersion: "v1.2.3", + wantAgentName: "agent-explicit", + wantVersion: "v1.2.3", + }, + { + name: "context fallback", + contextAgentName: "agent-context", + contextVersion: "v-context", + wantAgentName: "agent-context", + wantVersion: "v-context", + }, + { + name: "result-time override", + startAgentName: "agent-seed", + startVersion: "v-seed", + resultAgentName: "agent-result", + resultVersion: "v-result", + wantAgentName: "agent-result", + wantVersion: "v-result", + }, + { + name: "empty field omission", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + env := newConformanceEnv(t) + + ctx := context.Background() + if tc.contextAgentName != "" { + ctx = sigil.WithAgentName(ctx, tc.contextAgentName) + } + if tc.contextVersion != "" { + ctx = sigil.WithAgentVersion(ctx, tc.contextVersion) + } + + start := sigil.GenerationStart{ + Model: conformanceModel, + AgentName: tc.startAgentName, + AgentVersion: tc.startVersion, + } + result := sigil.Generation{ + AgentName: tc.resultAgentName, + AgentVersion: tc.resultVersion, + } + + recordGeneration(t, env, ctx, start, result) + + span := findSpan(t, env.Spans.Ended(), conformanceOperationName) + attrs := spanAttrs(span) + if tc.wantAgentName == "" { + requireSpanAttrAbsent(t, attrs, spanAttrAgentName) + } else { + requireSpanAttr(t, attrs, spanAttrAgentName, tc.wantAgentName) + } + if tc.wantVersion == "" { + requireSpanAttrAbsent(t, attrs, spanAttrAgentVersion) + } else { + requireSpanAttr(t, attrs, spanAttrAgentVersion, tc.wantVersion) + } + + requireSyncGenerationMetrics(t, env) + env.Shutdown(t) + + generation := env.Ingest.SingleGeneration(t) + if tc.wantAgentName == "" { + if got := generation.GetAgentName(); got != "" { + t.Fatalf("expected empty proto agent_name, got %q", got) + } + } else if got := generation.GetAgentName(); got != tc.wantAgentName { + t.Fatalf("unexpected proto agent_name: got %q want %q", got, tc.wantAgentName) + } + + if tc.wantVersion == "" { + if got := generation.GetAgentVersion(); got != "" { + t.Fatalf("expected empty proto agent_version, got %q", got) + } + } else if got := generation.GetAgentVersion(); got != tc.wantVersion { + t.Fatalf("unexpected proto agent_version: got %q want %q", got, tc.wantVersion) + } + }) + } +} + +func recordGeneration(t *testing.T, env *conformanceEnv, ctx context.Context, start sigil.GenerationStart, result sigil.Generation) { + t.Helper() + + _, recorder := env.Client.StartGeneration(ctx, start) + recorder.SetResult(result, nil) + recorder.End() + if err := recorder.Err(); err != nil { + t.Fatalf("record generation: %v", err) + } +} + +func requireSyncGenerationMetrics(t *testing.T, env *conformanceEnv) { + t.Helper() + + metrics := env.CollectMetrics(t) + duration := findHistogram[float64](t, metrics, metricOperationDuration) + if len(duration.DataPoints) == 0 { + t.Fatalf("expected %s datapoints for conformance generation", metricOperationDuration) + } + requireNoHistogram(t, metrics, metricTimeToFirstToken) +} From e99fde341059dba5cff80335d60257dfdfe2b91f Mon Sep 17 00:00:00 2001 From: Cyril Tovena Date: Thu, 12 Mar 2026 13:36:11 +0100 Subject: [PATCH 044/133] docs(sdk): publish conformance spec entry point (#459) Summary: - add a dedicated `test:sdk:conformance` mise task for the shipped Go harness - rewrite the shared conformance spec to match the current Go baseline and update architecture, indexes, README, and the active execution plan - document the local entry points so the harness is discoverable from the main docs surfaces Rationale: - the repo already had the Go conformance tests and a spec file, but the documented entry point was missing and the docs overstated the shipped scenario coverage - aligning the spec and execution plan with the current baseline makes the harness repeatable now without implying unimplemented scenarios are done Tests: - `mise run test:sdk:conformance` - `cd sdks/go && GOWORK=off go test ./sigil -run '^TestConformance' -count=5` Co-authored-by: Codex --- go/README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/go/README.md b/go/README.md index 155781f..25c28d0 100644 --- a/go/README.md +++ b/go/README.md @@ -199,6 +199,15 @@ The SDK emits four OTel histograms automatically through your configured OTel me - `gen_ai.client.time_to_first_token` - `gen_ai.client.tool_calls_per_operation` +## Conformance harness + +The Go SDK ships a local no-Docker conformance harness for the current cross-SDK baseline. + +- Shared spec: `../../docs/references/sdk-conformance-spec.md` +- Default local command: `mise run test:sdk:conformance` +- Direct Go command: `cd sdks/go && GOWORK=off go test ./sigil -run '^TestConformance' -count=1` +- Current baseline coverage: conversation title resolution, user ID resolution, and agent name/version resolution across exported generation payloads, OTLP spans, and sync metric emission + ## Explicit flow example ```go From 59ca59dc6ed8f3a39a4db3f2768586827182e3c7 Mon Sep 17 00:00:00 2001 From: Cyril Tovena Date: Thu, 12 Mar 2026 13:37:18 +0100 Subject: [PATCH 045/133] test(go-sdk): add extended conformance scenarios (#460) Summary: - extend Go SDK conformance coverage for streaming, tool execution, embeddings, validation and provider-call errors, ratings, and shutdown flush behavior - add small conformance harness helpers for zero-export assertions, config overrides, and rating request inspection Rationale: - close the remaining localhost-only conformance gaps called out in GRA-20 without duplicating the package-internal unit tests - keep the assertions centered on exported SDK behavior across ingest, spans, metrics, and rating HTTP surfaces Tests: - go test ./sdks/go/sigil -run 'TestConformance' - go test ./sdks/go/sigil - git diff --check Co-authored-by: Codex --- go/sigil/conformance_helpers_test.go | 66 +++++-- go/sigil/conformance_test.go | 279 +++++++++++++++++++++++++++ 2 files changed, 334 insertions(+), 11 deletions(-) diff --git a/go/sigil/conformance_helpers_test.go b/go/sigil/conformance_helpers_test.go index 53e1afa..d395b9b 100644 --- a/go/sigil/conformance_helpers_test.go +++ b/go/sigil/conformance_helpers_test.go @@ -22,17 +22,28 @@ import ( ) const ( - conformanceOperationName = "generateText" - metadataKeyConversation = "sigil.conversation.title" - metadataKeyCanonicalUserID = "sigil.user.id" - metadataKeyLegacyUserID = "user.id" - spanAttrOperationName = "gen_ai.operation.name" - spanAttrConversationTitle = "sigil.conversation.title" - spanAttrUserID = "user.id" - spanAttrAgentName = "gen_ai.agent.name" - spanAttrAgentVersion = "gen_ai.agent.version" - metricOperationDuration = "gen_ai.client.operation.duration" - metricTimeToFirstToken = "gen_ai.client.time_to_first_token" + conformanceOperationName = "generateText" + metadataKeyConversation = "sigil.conversation.title" + metadataKeyCanonicalUserID = "sigil.user.id" + metadataKeyLegacyUserID = "user.id" + spanAttrOperationName = "gen_ai.operation.name" + spanAttrConversationTitle = "sigil.conversation.title" + spanAttrUserID = "user.id" + spanAttrAgentName = "gen_ai.agent.name" + spanAttrAgentVersion = "gen_ai.agent.version" + spanAttrErrorType = "error.type" + spanAttrRequestToolChoice = "sigil.gen_ai.request.tool_choice" + spanAttrEmbeddingInputCount = "gen_ai.embeddings.input_count" + spanAttrEmbeddingDimCount = "gen_ai.embeddings.dimension.count" + spanAttrToolName = "gen_ai.tool.name" + spanAttrToolCallID = "gen_ai.tool.call.id" + spanAttrToolType = "gen_ai.tool.type" + spanAttrToolCallArguments = "gen_ai.tool.call.arguments" + spanAttrToolCallResult = "gen_ai.tool.call.result" + metricOperationDuration = "gen_ai.client.operation.duration" + metricTokenUsage = "gen_ai.client.token.usage" + metricTimeToFirstToken = "gen_ai.client.time_to_first_token" + metricToolCallsPerOperation = "gen_ai.client.tool_calls_per_operation" ) var conformanceModel = sigil.ModelRef{ @@ -124,6 +135,14 @@ func newConformanceEnv(t *testing.T, opts ...conformanceEnvOption) *conformanceE return env } +func withConformanceConfig(mutator func(*sigil.Config)) conformanceEnvOption { + return func(cfg *conformanceEnvConfig) { + if mutator != nil { + mutator(&cfg.config) + } + } +} + func (e *conformanceEnv) Shutdown(t *testing.T) { t.Helper() @@ -229,6 +248,12 @@ func (s *fakeIngestServer) SingleGeneration(t *testing.T) *sigilv1.Generation { return s.requests[0].Generations[0] } +func (s *fakeIngestServer) RequestCount() int { + s.mu.Lock() + defer s.mu.Unlock() + return len(s.requests) +} + func acceptanceResponse(req *sigilv1.ExportGenerationsRequest) *sigilv1.ExportGenerationsResponse { response := &sigilv1.ExportGenerationsResponse{Results: make([]*sigilv1.ExportGenerationResult, len(req.GetGenerations()))} for i := range req.GetGenerations() { @@ -291,6 +316,25 @@ func (s *fakeRatingServer) Close() { } } +func (s *fakeRatingServer) SingleRequest(t *testing.T) capturedRatingRequest { + t.Helper() + + s.mu.Lock() + defer s.mu.Unlock() + + if len(s.requests) != 1 { + t.Fatalf("expected exactly one rating request, got %d", len(s.requests)) + } + + req := s.requests[0] + return capturedRatingRequest{ + Method: req.Method, + Path: req.Path, + Headers: req.Headers.Clone(), + Body: append([]byte(nil), req.Body...), + } +} + func findSpan(t *testing.T, spans []sdktrace.ReadOnlySpan, operationName string) sdktrace.ReadOnlySpan { t.Helper() diff --git a/go/sigil/conformance_test.go b/go/sigil/conformance_test.go index 7e63a22..0573349 100644 --- a/go/sigil/conformance_test.go +++ b/go/sigil/conformance_test.go @@ -2,9 +2,14 @@ package sigil_test import ( "context" + "encoding/json" + "errors" + "net/http" "testing" + "time" sigil "github.com/grafana/sigil/sdks/go/sigil" + sigilv1 "github.com/grafana/sigil/sdks/go/sigil/internal/gen/sigil/v1" ) func TestConformance_ConversationTitleSemantics(t *testing.T) { @@ -263,6 +268,280 @@ func TestConformance_AgentIdentitySemantics(t *testing.T) { } } +func TestConformance_StreamingModeSemantics(t *testing.T) { + env := newConformanceEnv(t) + + _, recorder := env.Client.StartStreamingGeneration(context.Background(), sigil.GenerationStart{ + ConversationID: "conv-stream", + Model: conformanceModel, + }) + recorder.SetFirstTokenAt(time.Now()) + recorder.SetResult(sigil.Generation{ + Input: []sigil.Message{sigil.UserTextMessage("Say hello")}, + Output: []sigil.Message{sigil.AssistantTextMessage("Hello world")}, + Usage: sigil.TokenUsage{InputTokens: 5, OutputTokens: 2}, + }, nil) + recorder.End() + if err := recorder.Err(); err != nil { + t.Fatalf("record streaming generation: %v", err) + } + + metrics := env.CollectMetrics(t) + if len(findHistogram[float64](t, metrics, metricOperationDuration).DataPoints) == 0 { + t.Fatalf("expected %s datapoints for streaming conformance", metricOperationDuration) + } + if len(findHistogram[float64](t, metrics, metricTimeToFirstToken).DataPoints) == 0 { + t.Fatalf("expected %s datapoints for streaming conformance", metricTimeToFirstToken) + } + + env.Shutdown(t) + + generation := env.Ingest.SingleGeneration(t) + if generation.GetMode() != sigilv1.GenerationMode_GENERATION_MODE_STREAM { + t.Fatalf("expected streamed proto mode, got %s", generation.GetMode()) + } + if generation.GetOperationName() != "streamText" { + t.Fatalf("expected streamed operation streamText, got %q", generation.GetOperationName()) + } + if len(generation.GetOutput()) != 1 || len(generation.GetOutput()[0].GetParts()) != 1 { + t.Fatalf("expected a single streamed assistant output, got %#v", generation.GetOutput()) + } + if got := generation.GetOutput()[0].GetParts()[0].GetText(); got != "Hello world" { + t.Fatalf("unexpected streamed assistant text: got %q want %q", got, "Hello world") + } + + span := findSpan(t, env.Spans.Ended(), "streamText") + if span.Name() != "streamText gpt-5" { + t.Fatalf("unexpected streaming span name: %q", span.Name()) + } +} + +func TestConformance_ToolExecutionSemantics(t *testing.T) { + env := newConformanceEnv(t) + + _, recorder := env.Client.StartToolExecution(context.Background(), sigil.ToolExecutionStart{ + ToolName: "weather", + ToolCallID: "call-weather", + ToolType: "function", + ToolDescription: "Get weather for a city", + ConversationID: "conv-tools", + ConversationTitle: "Weather lookup", + AgentName: "assistant-core", + AgentVersion: "2026.03.12", + IncludeContent: true, + }) + recorder.SetResult(sigil.ToolExecutionEnd{ + Arguments: map[string]any{"city": "Paris"}, + Result: map[string]any{"temp_c": 18}, + }) + recorder.End() + if err := recorder.Err(); err != nil { + t.Fatalf("record tool execution: %v", err) + } + + metrics := env.CollectMetrics(t) + if len(findHistogram[float64](t, metrics, metricOperationDuration).DataPoints) == 0 { + t.Fatalf("expected %s datapoints for tool execution", metricOperationDuration) + } + requireNoHistogram(t, metrics, metricTimeToFirstToken) + if got := env.Ingest.RequestCount(); got != 0 { + t.Fatalf("expected no generation exports for tool execution, got %d", got) + } + + span := findSpan(t, env.Spans.Ended(), "execute_tool") + attrs := spanAttrs(span) + requireSpanAttr(t, attrs, spanAttrToolName, "weather") + requireSpanAttr(t, attrs, spanAttrToolCallID, "call-weather") + requireSpanAttr(t, attrs, spanAttrToolType, "function") + requireSpanAttr(t, attrs, spanAttrConversationTitle, "Weather lookup") + requireSpanAttr(t, attrs, spanAttrAgentName, "assistant-core") + requireSpanAttr(t, attrs, spanAttrAgentVersion, "2026.03.12") + requireSpanAttr(t, attrs, spanAttrToolCallArguments, `{"city":"Paris"}`) + requireSpanAttr(t, attrs, spanAttrToolCallResult, `{"temp_c":18}`) + + env.Shutdown(t) + if got := env.Ingest.RequestCount(); got != 0 { + t.Fatalf("expected no generation exports after tool shutdown, got %d", got) + } +} + +func TestConformance_EmbeddingSemantics(t *testing.T) { + env := newConformanceEnv(t) + dimensions := int64(256) + + _, recorder := env.Client.StartEmbedding(context.Background(), sigil.EmbeddingStart{ + Model: sigil.ModelRef{Provider: "openai", Name: "text-embedding-3-small"}, + AgentName: "agent-embed", + AgentVersion: "v-embed", + Dimensions: &dimensions, + EncodingFormat: "float", + }) + recorder.SetResult(sigil.EmbeddingResult{ + InputCount: 2, + InputTokens: 120, + ResponseModel: "text-embedding-3-small", + Dimensions: &dimensions, + }) + recorder.End() + if err := recorder.Err(); err != nil { + t.Fatalf("record embedding: %v", err) + } + + metrics := env.CollectMetrics(t) + if len(findHistogram[float64](t, metrics, metricOperationDuration).DataPoints) == 0 { + t.Fatalf("expected %s datapoints for embeddings", metricOperationDuration) + } + if len(findHistogram[int64](t, metrics, metricTokenUsage).DataPoints) == 0 { + t.Fatalf("expected %s datapoints for embeddings", metricTokenUsage) + } + requireNoHistogram(t, metrics, metricTimeToFirstToken) + requireNoHistogram(t, metrics, metricToolCallsPerOperation) + if got := env.Ingest.RequestCount(); got != 0 { + t.Fatalf("expected no generation exports for embeddings, got %d", got) + } + + span := findSpan(t, env.Spans.Ended(), "embeddings") + attrs := spanAttrs(span) + requireSpanAttr(t, attrs, spanAttrAgentName, "agent-embed") + requireSpanAttr(t, attrs, spanAttrAgentVersion, "v-embed") + if got := attrs[spanAttrEmbeddingInputCount].AsInt64(); got != 2 { + t.Fatalf("unexpected embedding input count: got %d want %d", got, 2) + } + if got := attrs[spanAttrEmbeddingDimCount].AsInt64(); got != dimensions { + t.Fatalf("unexpected embedding dimension count: got %d want %d", got, dimensions) + } + + env.Shutdown(t) + if got := env.Ingest.RequestCount(); got != 0 { + t.Fatalf("expected no generation exports after embedding shutdown, got %d", got) + } +} + +func TestConformance_ValidationAndErrorSemantics(t *testing.T) { + t.Run("validation failures stay local and unexported", func(t *testing.T) { + env := newConformanceEnv(t) + + _, recorder := env.Client.StartGeneration(context.Background(), sigil.GenerationStart{ + ConversationID: "conv-validation", + Model: conformanceModel, + }) + recorder.SetResult(sigil.Generation{ + Input: []sigil.Message{{Role: sigil.RoleUser}}, + Output: []sigil.Message{sigil.AssistantTextMessage("ok")}, + }, nil) + recorder.End() + + err := recorder.Err() + if err == nil { + t.Fatalf("expected validation error") + } + if !errors.Is(err, sigil.ErrValidationFailed) { + t.Fatalf("expected ErrValidationFailed, got %v", err) + } + + span := findSpan(t, env.Spans.Ended(), conformanceOperationName) + attrs := spanAttrs(span) + requireSpanAttr(t, attrs, spanAttrErrorType, "validation_error") + + env.Shutdown(t) + if got := env.Ingest.RequestCount(); got != 0 { + t.Fatalf("expected no generation exports for validation failure, got %d", got) + } + }) + + t.Run("provider call errors export call error metadata", func(t *testing.T) { + env := newConformanceEnv(t) + + _, recorder := env.Client.StartGeneration(context.Background(), sigil.GenerationStart{ + ConversationID: "conv-call-error", + Model: conformanceModel, + }) + recorder.SetCallError(errors.New("provider unavailable")) + recorder.End() + if err := recorder.Err(); err != nil { + t.Fatalf("expected nil local error for provider call failure, got %v", err) + } + + span := findSpan(t, env.Spans.Ended(), conformanceOperationName) + attrs := spanAttrs(span) + requireSpanAttr(t, attrs, spanAttrErrorType, "provider_call_error") + + env.Shutdown(t) + + generation := env.Ingest.SingleGeneration(t) + if got := generation.GetCallError(); got != "provider unavailable" { + t.Fatalf("unexpected proto call error: got %q want %q", got, "provider unavailable") + } + requireProtoMetadata(t, generation, "call_error", "provider unavailable") + }) +} + +func TestConformance_RatingSubmissionSemantics(t *testing.T) { + env := newConformanceEnv(t) + + response, err := env.Client.SubmitConversationRating(context.Background(), "conv-1", sigil.ConversationRatingInput{ + RatingID: "rat-1", + Rating: sigil.ConversationRatingValueGood, + Comment: "helpful", + Metadata: map[string]any{"channel": "assistant"}, + }) + if err != nil { + t.Fatalf("submit rating: %v", err) + } + + request := env.Rating.SingleRequest(t) + if request.Method != http.MethodPost { + t.Fatalf("expected POST rating request, got %s", request.Method) + } + if request.Path != "/api/v1/conversations/conv-1/ratings" { + t.Fatalf("unexpected rating request path: %s", request.Path) + } + + var body sigil.ConversationRatingInput + if err := json.Unmarshal(request.Body, &body); err != nil { + t.Fatalf("decode rating request body: %v", err) + } + if body.RatingID != "rat-1" || body.Rating != sigil.ConversationRatingValueGood { + t.Fatalf("unexpected rating request body: %#v", body) + } + if got := body.Metadata["channel"]; got != "assistant" { + t.Fatalf("expected rating metadata channel=assistant, got %#v", got) + } + if response == nil || response.Rating.ConversationID != "conv-1" { + t.Fatalf("unexpected rating response: %#v", response) + } +} + +func TestConformance_ShutdownFlushSemantics(t *testing.T) { + env := newConformanceEnv(t, withConformanceConfig(func(cfg *sigil.Config) { + cfg.GenerationExport.BatchSize = 8 + cfg.GenerationExport.QueueSize = 8 + cfg.GenerationExport.FlushInterval = time.Hour + })) + + recordGeneration(t, env, context.Background(), sigil.GenerationStart{ + ConversationID: "conv-shutdown", + Model: conformanceModel, + }, sigil.Generation{ + Input: []sigil.Message{sigil.UserTextMessage("hello")}, + Output: []sigil.Message{sigil.AssistantTextMessage("hi")}, + }) + + if got := env.Ingest.RequestCount(); got != 0 { + t.Fatalf("expected no export before shutdown flush, got %d", got) + } + + env.Shutdown(t) + + if got := env.Ingest.RequestCount(); got != 1 { + t.Fatalf("expected one export after shutdown flush, got %d", got) + } + generation := env.Ingest.SingleGeneration(t) + if generation.GetConversationId() != "conv-shutdown" { + t.Fatalf("unexpected shutdown-flushed conversation id: %q", generation.GetConversationId()) + } +} + func recordGeneration(t *testing.T, env *conformanceEnv, ctx context.Context, start sigil.GenerationStart, result sigil.Generation) { t.Helper() From e23610b5ff70c1c0002dc98f959f56fbd8a898df Mon Sep 17 00:00:00 2001 From: Cyril Tovena Date: Thu, 12 Mar 2026 13:57:02 +0100 Subject: [PATCH 046/133] Add full Go SDK generation roundtrip conformance coverage ## Summary - add a full sync roundtrip conformance scenario for the Go SDK that uses only the exported surface and asserts the captured generation proto, ended span attributes, trace linkage, token usage metrics, tool call metrics, and sync TTFT absence - strengthen the Go conformance harness helpers with typed span assertions and histogram attribute matching needed by the new scenario - fix the ingest contract gaps surfaced by the scenario by adding `cache_creation_input_tokens`, regenerating the Go bindings, syncing the checked-in JS proto copy, and exporting the new field from the Go proto mapper - update the active execution plan with Scenario 1 completion and the discovered SDK fixes ## Testing - `go test ./sdks/go/sigil -run TestConformance_FullGenerationRoundtrip -count=1` - `go test ./sdks/go/sigil -run TestConformance -count=1` - `go test ./sdks/go/sigil -count=1` - `mise run check` ## Notes - Fixes `GRA-19`. --- > [!NOTE] > **Medium Risk** > Adds a broad new conformance test suite and changes the generation ingest protobuf contract/mapping (new token field), which could impact cross-SDK/backward compatibility if consumers assume the old schema. > > **Overview** > Adds a new Go SDK conformance scenario (`TestConformance_FullGenerationRoundtrip`) that exercises the public generation API end-to-end and asserts the exported generation proto, OTLP span attributes/trace linkage, token-usage/tool-call metrics, and that sync calls do *not* emit TTFT metrics. > > Extends the Go conformance helper utilities with typed span-attribute assertions and histogram datapoint matching by attributes to support the richer checks. > > Updates the generation ingest schema to include `TokenUsage.cache_creation_input_tokens` (and ensures `ToolDefinition.deferred` is present), regenerates Go protobuf bindings in both server and Go SDK trees, syncs the checked-in JS proto copy, and updates the Go proto mapper to export the new token counter. Also updates the active execution plan to mark Scenario 1 complete and record the discovered schema gaps. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 7b48712aff3d6c2b415d238c3cf0dd967e9ff5db. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). --- go/sigil/conformance_helpers_test.go | 152 +++++- go/sigil/conformance_test.go | 502 ++++++++++++++++++ .../gen/sigil/v1/generation_ingest.pb.go | 31 +- go/sigil/proto_mapping.go | 13 +- js/proto/sigil/v1/generation_ingest.proto | 2 + proto/sigil/v1/generation_ingest.proto | 1 + 6 files changed, 662 insertions(+), 39 deletions(-) diff --git a/go/sigil/conformance_helpers_test.go b/go/sigil/conformance_helpers_test.go index d395b9b..d9cc142 100644 --- a/go/sigil/conformance_helpers_test.go +++ b/go/sigil/conformance_helpers_test.go @@ -22,28 +22,54 @@ import ( ) const ( - conformanceOperationName = "generateText" - metadataKeyConversation = "sigil.conversation.title" - metadataKeyCanonicalUserID = "sigil.user.id" - metadataKeyLegacyUserID = "user.id" - spanAttrOperationName = "gen_ai.operation.name" - spanAttrConversationTitle = "sigil.conversation.title" - spanAttrUserID = "user.id" - spanAttrAgentName = "gen_ai.agent.name" - spanAttrAgentVersion = "gen_ai.agent.version" - spanAttrErrorType = "error.type" - spanAttrRequestToolChoice = "sigil.gen_ai.request.tool_choice" - spanAttrEmbeddingInputCount = "gen_ai.embeddings.input_count" - spanAttrEmbeddingDimCount = "gen_ai.embeddings.dimension.count" - spanAttrToolName = "gen_ai.tool.name" - spanAttrToolCallID = "gen_ai.tool.call.id" - spanAttrToolType = "gen_ai.tool.type" - spanAttrToolCallArguments = "gen_ai.tool.call.arguments" - spanAttrToolCallResult = "gen_ai.tool.call.result" - metricOperationDuration = "gen_ai.client.operation.duration" - metricTokenUsage = "gen_ai.client.token.usage" - metricTimeToFirstToken = "gen_ai.client.time_to_first_token" - metricToolCallsPerOperation = "gen_ai.client.tool_calls_per_operation" + conformanceOperationName = "generateText" + metadataKeyConversation = "sigil.conversation.title" + metadataKeyCanonicalUserID = "sigil.user.id" + metadataKeyLegacyUserID = "user.id" + metadataKeyThinkingBudget = "sigil.gen_ai.request.thinking.budget_tokens" + metadataKeySDKName = "sigil.sdk.name" + spanAttrOperationName = "gen_ai.operation.name" + spanAttrGenerationID = "sigil.generation.id" + spanAttrConversationTitle = "sigil.conversation.title" + spanAttrUserID = "user.id" + spanAttrAgentName = "gen_ai.agent.name" + spanAttrAgentVersion = "gen_ai.agent.version" + spanAttrErrorType = "error.type" + spanAttrProviderName = "gen_ai.provider.name" + spanAttrRequestModel = "gen_ai.request.model" + spanAttrRequestMaxTokens = "gen_ai.request.max_tokens" + spanAttrRequestTemperature = "gen_ai.request.temperature" + spanAttrRequestTopP = "gen_ai.request.top_p" + spanAttrRequestToolChoice = "sigil.gen_ai.request.tool_choice" + spanAttrRequestThinkingEnabled = "sigil.gen_ai.request.thinking.enabled" + spanAttrEmbeddingInputCount = "gen_ai.embeddings.input_count" + spanAttrEmbeddingDimCount = "gen_ai.embeddings.dimension.count" + spanAttrToolName = "gen_ai.tool.name" + spanAttrToolCallID = "gen_ai.tool.call.id" + spanAttrToolType = "gen_ai.tool.type" + spanAttrToolCallArguments = "gen_ai.tool.call.arguments" + spanAttrToolCallResult = "gen_ai.tool.call.result" + spanAttrResponseID = "gen_ai.response.id" + spanAttrResponseModel = "gen_ai.response.model" + spanAttrFinishReasons = "gen_ai.response.finish_reasons" + spanAttrInputTokens = "gen_ai.usage.input_tokens" + spanAttrOutputTokens = "gen_ai.usage.output_tokens" + spanAttrCacheReadTokens = "gen_ai.usage.cache_read_input_tokens" + spanAttrCacheWriteTokens = "gen_ai.usage.cache_write_input_tokens" + spanAttrCacheCreationTokens = "gen_ai.usage.cache_creation_input_tokens" + spanAttrReasoningTokens = "gen_ai.usage.reasoning_tokens" + metricOperationDuration = "gen_ai.client.operation.duration" + metricTokenUsage = "gen_ai.client.token.usage" + metricTimeToFirstToken = "gen_ai.client.time_to_first_token" + metricToolCallsPerOperation = "gen_ai.client.tool_calls_per_operation" + metricAttrTokenType = "gen_ai.token.type" + metricTokenTypeInput = "input" + metricTokenTypeOutput = "output" + metricTokenTypeCacheRead = "cache_read" + metricTokenTypeCacheWrite = "cache_write" + metricTokenTypeCacheCreation = "cache_creation" + metricTokenTypeReasoning = "reasoning" + sdkNameGo = "sdk-go" ) var conformanceModel = sigil.ModelRef{ @@ -374,6 +400,60 @@ func requireSpanAttr(t *testing.T, attrs map[string]attribute.Value, key, want s } } +func requireSpanAttrBool(t *testing.T, attrs map[string]attribute.Value, key string, want bool) { + t.Helper() + + got, ok := attrs[key] + if !ok { + t.Fatalf("expected span attribute %q=%t, attribute missing", key, want) + } + if got.AsBool() != want { + t.Fatalf("unexpected span attribute %q: got %t want %t", key, got.AsBool(), want) + } +} + +func requireSpanAttrInt64(t *testing.T, attrs map[string]attribute.Value, key string, want int64) { + t.Helper() + + got, ok := attrs[key] + if !ok { + t.Fatalf("expected span attribute %q=%d, attribute missing", key, want) + } + if got.AsInt64() != want { + t.Fatalf("unexpected span attribute %q: got %d want %d", key, got.AsInt64(), want) + } +} + +func requireSpanAttrFloat64(t *testing.T, attrs map[string]attribute.Value, key string, want float64) { + t.Helper() + + got, ok := attrs[key] + if !ok { + t.Fatalf("expected span attribute %q=%v, attribute missing", key, want) + } + if got.AsFloat64() != want { + t.Fatalf("unexpected span attribute %q: got %v want %v", key, got.AsFloat64(), want) + } +} + +func requireSpanAttrStringSlice(t *testing.T, attrs map[string]attribute.Value, key string, want []string) { + t.Helper() + + got, ok := attrs[key] + if !ok { + t.Fatalf("expected span attribute %q=%v, attribute missing", key, want) + } + gotSlice := got.AsStringSlice() + if len(gotSlice) != len(want) { + t.Fatalf("unexpected span attribute %q length: got %v want %v", key, gotSlice, want) + } + for i := range want { + if gotSlice[i] != want[i] { + t.Fatalf("unexpected span attribute %q: got %v want %v", key, gotSlice, want) + } + } +} + func requireSpanAttrAbsent(t *testing.T, attrs map[string]attribute.Value, key string) { t.Helper() @@ -414,6 +494,34 @@ func requireNoHistogram(t *testing.T, collected metricdata.ResourceMetrics, name } } +func requireHistogramPointWithAttrs[N int64 | float64](t *testing.T, histogram metricdata.Histogram[N], want map[string]string) metricdata.HistogramDataPoint[N] { + t.Helper() + + for _, point := range histogram.DataPoints { + if pointHasStringAttrs(point.Attributes, want) { + return point + } + } + + t.Fatalf("expected histogram datapoint with attrs %v", want) + return metricdata.HistogramDataPoint[N]{} +} + +func pointHasStringAttrs(attrs attribute.Set, want map[string]string) bool { + got := map[string]string{} + for _, kv := range attrs.ToSlice() { + got[string(kv.Key)] = kv.Value.AsString() + } + + for key, wantValue := range want { + if got[key] != wantValue { + return false + } + } + + return true +} + func requireProtoMetadata(t *testing.T, generation *sigilv1.Generation, key, want string) { t.Helper() diff --git a/go/sigil/conformance_test.go b/go/sigil/conformance_test.go index 0573349..2421159 100644 --- a/go/sigil/conformance_test.go +++ b/go/sigil/conformance_test.go @@ -1,6 +1,7 @@ package sigil_test import ( + "bytes" "context" "encoding/json" "errors" @@ -10,8 +11,392 @@ import ( sigil "github.com/grafana/sigil/sdks/go/sigil" sigilv1 "github.com/grafana/sigil/sdks/go/sigil/internal/gen/sigil/v1" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/sdk/metric/metricdata" + "go.opentelemetry.io/otel/trace" ) +func TestConformance_FullGenerationRoundtrip(t *testing.T) { + env := newConformanceEnv(t) + + startedAt := time.Date(2026, time.March, 12, 11, 0, 0, 0, time.UTC) + completedAt := startedAt.Add(250 * time.Millisecond) + maxTokens := int64(1024) + temperature := 0.7 + topP := 0.9 + toolChoice := "auto" + thinkingEnabled := true + + start := sigil.GenerationStart{ + ID: "gen-roundtrip", + ConversationID: "conv-roundtrip", + ConversationTitle: "Roundtrip Test", + UserID: "user-42", + AgentName: "test-agent", + AgentVersion: "1.0.0", + Model: sigil.ModelRef{ + Provider: "test-provider", + Name: "test-model", + }, + SystemPrompt: "You are a test assistant.", + Tools: []sigil.ToolDefinition{ + { + Name: "lookupWeather", + Description: "Look up weather", + Type: "function", + InputSchema: []byte(`{"type":"object","properties":{"city":{"type":"string"}}}`), + Deferred: true, + }, + }, + MaxTokens: &maxTokens, + Temperature: &temperature, + TopP: &topP, + ToolChoice: &toolChoice, + ThinkingEnabled: &thinkingEnabled, + Tags: map[string]string{ + "env": "conformance", + "suite": "roundtrip", + }, + Metadata: map[string]any{ + "custom_key": "custom_value", + metadataKeyThinkingBudget: int64(2048), + }, + StartedAt: startedAt, + } + + result := sigil.Generation{ + ResponseID: "resp-1", + ResponseModel: "test-model-v2", + Input: []sigil.Message{ + { + Role: sigil.RoleUser, + Name: "user", + Parts: []sigil.Part{sigil.TextPart("What's the weather in Paris?")}, + }, + { + Role: sigil.RoleTool, + Name: "lookupWeather", + Parts: []sigil.Part{sigil.ToolResultPart(sigil.ToolResult{ + ToolCallID: "call-1", + Name: "lookupWeather", + Content: "18C and clear", + ContentJSON: []byte(`{"temp_c":18,"condition":"clear"}`), + })}, + }, + }, + Output: []sigil.Message{ + { + Role: sigil.RoleAssistant, + Name: "assistant", + Parts: []sigil.Part{ + sigil.TextPart("It is 18C and clear."), + sigil.ThinkingPart("Need weather lookup."), + sigil.ToolCallPart(sigil.ToolCall{ + ID: "call-1", + Name: "lookupWeather", + InputJSON: []byte(`{"city":"Paris"}`), + }), + }, + }, + }, + Usage: sigil.TokenUsage{ + InputTokens: 120, + OutputTokens: 42, + TotalTokens: 162, + CacheReadInputTokens: 30, + CacheWriteInputTokens: 7, + CacheCreationInputTokens: 4, + ReasoningTokens: 9, + }, + StopReason: "end_turn", + CompletedAt: completedAt, + Artifacts: []sigil.Artifact{ + { + Kind: sigil.ArtifactKindRequest, + Name: "request.json", + ContentType: "application/json", + Payload: []byte(`{"request":true}`), + RecordID: "rec-request", + URI: "sigil://artifact/request", + }, + { + Kind: sigil.ArtifactKindResponse, + Name: "response.json", + ContentType: "application/json", + Payload: []byte(`{"response":true}`), + RecordID: "rec-response", + URI: "sigil://artifact/response", + }, + }, + } + + _, recorder := env.Client.StartGeneration(context.Background(), start) + recorder.SetResult(result, nil) + recorder.End() + if err := recorder.Err(); err != nil { + t.Fatalf("record generation: %v", err) + } + + span := findSpan(t, env.Spans.Ended(), conformanceOperationName) + if got := span.Name(); got != "generateText test-model" { + t.Fatalf("unexpected span name: got %q want %q", got, "generateText test-model") + } + if got := span.SpanKind(); got != trace.SpanKindClient { + t.Fatalf("unexpected span kind: got %v want %v", got, trace.SpanKindClient) + } + if got := span.Status().Code; got != codes.Ok { + t.Fatalf("unexpected span status: got %v want %v", got, codes.Ok) + } + + attrs := spanAttrs(span) + requireSpanAttr(t, attrs, spanAttrOperationName, conformanceOperationName) + requireSpanAttr(t, attrs, spanAttrGenerationID, start.ID) + requireSpanAttr(t, attrs, "gen_ai.conversation.id", start.ConversationID) + requireSpanAttr(t, attrs, spanAttrConversationTitle, start.ConversationTitle) + requireSpanAttr(t, attrs, spanAttrUserID, start.UserID) + requireSpanAttr(t, attrs, spanAttrAgentName, start.AgentName) + requireSpanAttr(t, attrs, spanAttrAgentVersion, start.AgentVersion) + requireSpanAttr(t, attrs, spanAttrProviderName, start.Model.Provider) + requireSpanAttr(t, attrs, spanAttrRequestModel, start.Model.Name) + requireSpanAttrInt64(t, attrs, spanAttrRequestMaxTokens, maxTokens) + requireSpanAttrFloat64(t, attrs, spanAttrRequestTemperature, temperature) + requireSpanAttrFloat64(t, attrs, spanAttrRequestTopP, topP) + requireSpanAttr(t, attrs, spanAttrRequestToolChoice, toolChoice) + requireSpanAttrBool(t, attrs, "sigil.gen_ai.request.thinking.enabled", thinkingEnabled) + requireSpanAttrInt64(t, attrs, "sigil.gen_ai.request.thinking.budget_tokens", 2048) + requireSpanAttr(t, attrs, spanAttrResponseID, result.ResponseID) + requireSpanAttr(t, attrs, spanAttrResponseModel, result.ResponseModel) + requireSpanAttrStringSlice(t, attrs, spanAttrFinishReasons, []string{result.StopReason}) + requireSpanAttrInt64(t, attrs, spanAttrInputTokens, result.Usage.InputTokens) + requireSpanAttrInt64(t, attrs, spanAttrOutputTokens, result.Usage.OutputTokens) + requireSpanAttrInt64(t, attrs, spanAttrCacheReadTokens, result.Usage.CacheReadInputTokens) + requireSpanAttrInt64(t, attrs, spanAttrCacheWriteTokens, result.Usage.CacheWriteInputTokens) + requireSpanAttrInt64(t, attrs, spanAttrCacheCreationTokens, result.Usage.CacheCreationInputTokens) + requireSpanAttrInt64(t, attrs, spanAttrReasoningTokens, result.Usage.ReasoningTokens) + requireSpanAttr(t, attrs, metadataKeySDKName, sdkNameGo) + + env.Shutdown(t) + + generation := env.Ingest.SingleGeneration(t) + if got := generation.GetId(); got != start.ID { + t.Fatalf("unexpected proto id: got %q want %q", got, start.ID) + } + if got := generation.GetConversationId(); got != start.ConversationID { + t.Fatalf("unexpected proto conversation_id: got %q want %q", got, start.ConversationID) + } + if got := generation.GetOperationName(); got != conformanceOperationName { + t.Fatalf("unexpected proto operation_name: got %q want %q", got, conformanceOperationName) + } + if got := generation.GetMode(); got != sigilv1.GenerationMode_GENERATION_MODE_SYNC { + t.Fatalf("unexpected proto mode: got %v want %v", got, sigilv1.GenerationMode_GENERATION_MODE_SYNC) + } + if got := generation.GetAgentName(); got != start.AgentName { + t.Fatalf("unexpected proto agent_name: got %q want %q", got, start.AgentName) + } + if got := generation.GetAgentVersion(); got != start.AgentVersion { + t.Fatalf("unexpected proto agent_version: got %q want %q", got, start.AgentVersion) + } + if got := generation.GetResponseId(); got != result.ResponseID { + t.Fatalf("unexpected proto response_id: got %q want %q", got, result.ResponseID) + } + if got := generation.GetResponseModel(); got != result.ResponseModel { + t.Fatalf("unexpected proto response_model: got %q want %q", got, result.ResponseModel) + } + if got := generation.GetSystemPrompt(); got != start.SystemPrompt { + t.Fatalf("unexpected proto system_prompt: got %q want %q", got, start.SystemPrompt) + } + if got := generation.GetTraceId(); got != span.SpanContext().TraceID().String() { + t.Fatalf("unexpected proto trace_id: got %q want %q", got, span.SpanContext().TraceID().String()) + } + if got := generation.GetSpanId(); got != span.SpanContext().SpanID().String() { + t.Fatalf("unexpected proto span_id: got %q want %q", got, span.SpanContext().SpanID().String()) + } + if !generation.GetStartedAt().AsTime().Equal(startedAt) { + t.Fatalf("unexpected proto started_at: got %s want %s", generation.GetStartedAt().AsTime(), startedAt) + } + if !generation.GetCompletedAt().AsTime().Equal(completedAt) { + t.Fatalf("unexpected proto completed_at: got %s want %s", generation.GetCompletedAt().AsTime(), completedAt) + } + + if got := generation.GetModel().GetProvider(); got != start.Model.Provider { + t.Fatalf("unexpected proto model.provider: got %q want %q", got, start.Model.Provider) + } + if got := generation.GetModel().GetName(); got != start.Model.Name { + t.Fatalf("unexpected proto model.name: got %q want %q", got, start.Model.Name) + } + if got := generation.GetMaxTokens(); got != maxTokens { + t.Fatalf("unexpected proto max_tokens: got %d want %d", got, maxTokens) + } + if got := generation.GetTemperature(); got != temperature { + t.Fatalf("unexpected proto temperature: got %v want %v", got, temperature) + } + if got := generation.GetTopP(); got != topP { + t.Fatalf("unexpected proto top_p: got %v want %v", got, topP) + } + if got := generation.GetToolChoice(); got != toolChoice { + t.Fatalf("unexpected proto tool_choice: got %q want %q", got, toolChoice) + } + if got := generation.GetThinkingEnabled(); got != thinkingEnabled { + t.Fatalf("unexpected proto thinking_enabled: got %t want %t", got, thinkingEnabled) + } + + if len(generation.GetTags()) != len(start.Tags) { + t.Fatalf("unexpected proto tags length: got %d want %d", len(generation.GetTags()), len(start.Tags)) + } + for key, want := range start.Tags { + if got := generation.GetTags()[key]; got != want { + t.Fatalf("unexpected proto tag %q: got %q want %q", key, got, want) + } + } + + requireProtoMetadata(t, generation, "custom_key", "custom_value") + requireProtoMetadata(t, generation, metadataKeyConversation, start.ConversationTitle) + requireProtoMetadata(t, generation, metadataKeyCanonicalUserID, start.UserID) + requireProtoMetadata(t, generation, metadataKeySDKName, sdkNameGo) + requireProtoMetadataNumber(t, generation, metadataKeyThinkingBudget, 2048) + + tools := generation.GetTools() + if len(tools) != 1 { + t.Fatalf("unexpected proto tools length: got %d want %d", len(tools), 1) + } + if got := tools[0].GetName(); got != start.Tools[0].Name { + t.Fatalf("unexpected proto tool name: got %q want %q", got, start.Tools[0].Name) + } + if got := tools[0].GetDescription(); got != start.Tools[0].Description { + t.Fatalf("unexpected proto tool description: got %q want %q", got, start.Tools[0].Description) + } + if got := tools[0].GetType(); got != start.Tools[0].Type { + t.Fatalf("unexpected proto tool type: got %q want %q", got, start.Tools[0].Type) + } + if !bytes.Equal(tools[0].GetInputSchemaJson(), start.Tools[0].InputSchema) { + t.Fatalf("unexpected proto tool input schema: got %s want %s", string(tools[0].GetInputSchemaJson()), string(start.Tools[0].InputSchema)) + } + if got := tools[0].GetDeferred(); !got { + t.Fatalf("expected proto tool deferred=true") + } + + input := generation.GetInput() + if len(input) != 2 { + t.Fatalf("unexpected proto input length: got %d want %d", len(input), 2) + } + if got := input[0].GetRole(); got != sigilv1.MessageRole_MESSAGE_ROLE_USER { + t.Fatalf("unexpected first input role: got %v want %v", got, sigilv1.MessageRole_MESSAGE_ROLE_USER) + } + requireProtoTextPart(t, input[0].GetParts()[0], "What's the weather in Paris?") + if got := input[1].GetRole(); got != sigilv1.MessageRole_MESSAGE_ROLE_TOOL { + t.Fatalf("unexpected second input role: got %v want %v", got, sigilv1.MessageRole_MESSAGE_ROLE_TOOL) + } + requireProtoToolResultPart(t, input[1].GetParts()[0], "call-1", "lookupWeather", "18C and clear", []byte(`{"temp_c":18,"condition":"clear"}`), false) + + output := generation.GetOutput() + if len(output) != 1 { + t.Fatalf("unexpected proto output length: got %d want %d", len(output), 1) + } + if got := output[0].GetRole(); got != sigilv1.MessageRole_MESSAGE_ROLE_ASSISTANT { + t.Fatalf("unexpected output role: got %v want %v", got, sigilv1.MessageRole_MESSAGE_ROLE_ASSISTANT) + } + if len(output[0].GetParts()) != 3 { + t.Fatalf("unexpected output part count: got %d want %d", len(output[0].GetParts()), 3) + } + requireProtoTextPart(t, output[0].GetParts()[0], "It is 18C and clear.") + requireProtoThinkingPart(t, output[0].GetParts()[1], "Need weather lookup.") + requireProtoToolCallPart(t, output[0].GetParts()[2], "call-1", "lookupWeather", []byte(`{"city":"Paris"}`)) + + usage := generation.GetUsage() + if got := usage.GetInputTokens(); got != result.Usage.InputTokens { + t.Fatalf("unexpected proto input_tokens: got %d want %d", got, result.Usage.InputTokens) + } + if got := usage.GetOutputTokens(); got != result.Usage.OutputTokens { + t.Fatalf("unexpected proto output_tokens: got %d want %d", got, result.Usage.OutputTokens) + } + if got := usage.GetTotalTokens(); got != result.Usage.TotalTokens { + t.Fatalf("unexpected proto total_tokens: got %d want %d", got, result.Usage.TotalTokens) + } + if got := usage.GetCacheReadInputTokens(); got != result.Usage.CacheReadInputTokens { + t.Fatalf("unexpected proto cache_read_input_tokens: got %d want %d", got, result.Usage.CacheReadInputTokens) + } + if got := usage.GetCacheWriteInputTokens(); got != result.Usage.CacheWriteInputTokens { + t.Fatalf("unexpected proto cache_write_input_tokens: got %d want %d", got, result.Usage.CacheWriteInputTokens) + } + if got := usage.GetCacheCreationInputTokens(); got != result.Usage.CacheCreationInputTokens { + t.Fatalf("unexpected proto cache_creation_input_tokens: got %d want %d", got, result.Usage.CacheCreationInputTokens) + } + if got := usage.GetReasoningTokens(); got != result.Usage.ReasoningTokens { + t.Fatalf("unexpected proto reasoning_tokens: got %d want %d", got, result.Usage.ReasoningTokens) + } + if got := generation.GetStopReason(); got != result.StopReason { + t.Fatalf("unexpected proto stop_reason: got %q want %q", got, result.StopReason) + } + + artifacts := generation.GetRawArtifacts() + if len(artifacts) != 2 { + t.Fatalf("unexpected proto artifacts length: got %d want %d", len(artifacts), 2) + } + requireProtoArtifact(t, artifacts[0], sigilv1.ArtifactKind_ARTIFACT_KIND_REQUEST, "request.json", "application/json", []byte(`{"request":true}`), "rec-request", "sigil://artifact/request") + requireProtoArtifact(t, artifacts[1], sigilv1.ArtifactKind_ARTIFACT_KIND_RESPONSE, "response.json", "application/json", []byte(`{"response":true}`), "rec-response", "sigil://artifact/response") + + metrics := env.CollectMetrics(t) + duration := findHistogram[float64](t, metrics, metricOperationDuration) + requireHistogramPointWithAttrs(t, duration, map[string]string{ + spanAttrOperationName: conformanceOperationName, + spanAttrProviderName: start.Model.Provider, + spanAttrRequestModel: start.Model.Name, + spanAttrAgentName: start.AgentName, + }) + + tokenUsage := findHistogram[int64](t, metrics, metricTokenUsage) + requireInt64HistogramSum(t, tokenUsage, map[string]string{ + spanAttrOperationName: conformanceOperationName, + spanAttrProviderName: start.Model.Provider, + spanAttrRequestModel: start.Model.Name, + spanAttrAgentName: start.AgentName, + metricAttrTokenType: metricTokenTypeInput, + }, result.Usage.InputTokens) + requireInt64HistogramSum(t, tokenUsage, map[string]string{ + spanAttrOperationName: conformanceOperationName, + spanAttrProviderName: start.Model.Provider, + spanAttrRequestModel: start.Model.Name, + spanAttrAgentName: start.AgentName, + metricAttrTokenType: metricTokenTypeOutput, + }, result.Usage.OutputTokens) + requireInt64HistogramSum(t, tokenUsage, map[string]string{ + spanAttrOperationName: conformanceOperationName, + spanAttrProviderName: start.Model.Provider, + spanAttrRequestModel: start.Model.Name, + spanAttrAgentName: start.AgentName, + metricAttrTokenType: metricTokenTypeCacheRead, + }, result.Usage.CacheReadInputTokens) + requireInt64HistogramSum(t, tokenUsage, map[string]string{ + spanAttrOperationName: conformanceOperationName, + spanAttrProviderName: start.Model.Provider, + spanAttrRequestModel: start.Model.Name, + spanAttrAgentName: start.AgentName, + metricAttrTokenType: metricTokenTypeCacheWrite, + }, result.Usage.CacheWriteInputTokens) + requireInt64HistogramSum(t, tokenUsage, map[string]string{ + spanAttrOperationName: conformanceOperationName, + spanAttrProviderName: start.Model.Provider, + spanAttrRequestModel: start.Model.Name, + spanAttrAgentName: start.AgentName, + metricAttrTokenType: metricTokenTypeCacheCreation, + }, result.Usage.CacheCreationInputTokens) + requireInt64HistogramSum(t, tokenUsage, map[string]string{ + spanAttrOperationName: conformanceOperationName, + spanAttrProviderName: start.Model.Provider, + spanAttrRequestModel: start.Model.Name, + spanAttrAgentName: start.AgentName, + metricAttrTokenType: metricTokenTypeReasoning, + }, result.Usage.ReasoningTokens) + + toolCalls := findHistogram[int64](t, metrics, metricToolCallsPerOperation) + requireInt64HistogramSum(t, toolCalls, map[string]string{ + spanAttrProviderName: start.Model.Provider, + spanAttrRequestModel: start.Model.Name, + spanAttrAgentName: start.AgentName, + }, 1) + requireNoHistogram(t, metrics, metricTimeToFirstToken) +} + func TestConformance_ConversationTitleSemantics(t *testing.T) { testCases := []struct { name string @@ -563,3 +948,120 @@ func requireSyncGenerationMetrics(t *testing.T, env *conformanceEnv) { } requireNoHistogram(t, metrics, metricTimeToFirstToken) } + +func requireProtoMetadataNumber(t *testing.T, generation *sigilv1.Generation, key string, want float64) { + t.Helper() + + value, ok := generation.GetMetadata().AsMap()[key] + if !ok { + t.Fatalf("expected generation metadata %q=%v, key missing", key, want) + } + got, ok := value.(float64) + if !ok { + t.Fatalf("expected generation metadata %q to be float64, got %#v", key, value) + } + if got != want { + t.Fatalf("unexpected generation metadata %q: got %v want %v", key, got, want) + } +} + +func requireProtoTextPart(t *testing.T, part *sigilv1.Part, want string) { + t.Helper() + + payload, ok := part.GetPayload().(*sigilv1.Part_Text) + if !ok { + t.Fatalf("expected text part, got %T", part.GetPayload()) + } + if payload.Text != want { + t.Fatalf("unexpected text part: got %q want %q", payload.Text, want) + } +} + +func requireProtoThinkingPart(t *testing.T, part *sigilv1.Part, want string) { + t.Helper() + + payload, ok := part.GetPayload().(*sigilv1.Part_Thinking) + if !ok { + t.Fatalf("expected thinking part, got %T", part.GetPayload()) + } + if payload.Thinking != want { + t.Fatalf("unexpected thinking part: got %q want %q", payload.Thinking, want) + } +} + +func requireProtoToolCallPart(t *testing.T, part *sigilv1.Part, wantID string, wantName string, wantInputJSON []byte) { + t.Helper() + + payload, ok := part.GetPayload().(*sigilv1.Part_ToolCall) + if !ok { + t.Fatalf("expected tool call part, got %T", part.GetPayload()) + } + if got := payload.ToolCall.GetId(); got != wantID { + t.Fatalf("unexpected tool call id: got %q want %q", got, wantID) + } + if got := payload.ToolCall.GetName(); got != wantName { + t.Fatalf("unexpected tool call name: got %q want %q", got, wantName) + } + if !bytes.Equal(payload.ToolCall.GetInputJson(), wantInputJSON) { + t.Fatalf("unexpected tool call input_json: got %s want %s", string(payload.ToolCall.GetInputJson()), string(wantInputJSON)) + } +} + +func requireProtoToolResultPart(t *testing.T, part *sigilv1.Part, wantCallID string, wantName string, wantContent string, wantContentJSON []byte, wantIsError bool) { + t.Helper() + + payload, ok := part.GetPayload().(*sigilv1.Part_ToolResult) + if !ok { + t.Fatalf("expected tool result part, got %T", part.GetPayload()) + } + if got := payload.ToolResult.GetToolCallId(); got != wantCallID { + t.Fatalf("unexpected tool result tool_call_id: got %q want %q", got, wantCallID) + } + if got := payload.ToolResult.GetName(); got != wantName { + t.Fatalf("unexpected tool result name: got %q want %q", got, wantName) + } + if got := payload.ToolResult.GetContent(); got != wantContent { + t.Fatalf("unexpected tool result content: got %q want %q", got, wantContent) + } + if !bytes.Equal(payload.ToolResult.GetContentJson(), wantContentJSON) { + t.Fatalf("unexpected tool result content_json: got %s want %s", string(payload.ToolResult.GetContentJson()), string(wantContentJSON)) + } + if got := payload.ToolResult.GetIsError(); got != wantIsError { + t.Fatalf("unexpected tool result is_error: got %t want %t", got, wantIsError) + } +} + +func requireProtoArtifact(t *testing.T, artifact *sigilv1.Artifact, wantKind sigilv1.ArtifactKind, wantName string, wantContentType string, wantPayload []byte, wantRecordID string, wantURI string) { + t.Helper() + + if got := artifact.GetKind(); got != wantKind { + t.Fatalf("unexpected artifact kind: got %v want %v", got, wantKind) + } + if got := artifact.GetName(); got != wantName { + t.Fatalf("unexpected artifact name: got %q want %q", got, wantName) + } + if got := artifact.GetContentType(); got != wantContentType { + t.Fatalf("unexpected artifact content_type: got %q want %q", got, wantContentType) + } + if !bytes.Equal(artifact.GetPayload(), wantPayload) { + t.Fatalf("unexpected artifact payload: got %s want %s", string(artifact.GetPayload()), string(wantPayload)) + } + if got := artifact.GetRecordId(); got != wantRecordID { + t.Fatalf("unexpected artifact record_id: got %q want %q", got, wantRecordID) + } + if got := artifact.GetUri(); got != wantURI { + t.Fatalf("unexpected artifact uri: got %q want %q", got, wantURI) + } +} + +func requireInt64HistogramSum(t *testing.T, histogram metricdata.Histogram[int64], attrs map[string]string, want int64) { + t.Helper() + + point := requireHistogramPointWithAttrs(t, histogram, attrs) + if point.Sum != want { + t.Fatalf("unexpected histogram sum for attrs %v: got %d want %d", attrs, point.Sum, want) + } + if point.Count != 1 { + t.Fatalf("unexpected histogram count for attrs %v: got %d want %d", attrs, point.Count, 1) + } +} diff --git a/go/sigil/internal/gen/sigil/v1/generation_ingest.pb.go b/go/sigil/internal/gen/sigil/v1/generation_ingest.pb.go index 7d0323e..ea0401f 100644 --- a/go/sigil/internal/gen/sigil/v1/generation_ingest.pb.go +++ b/go/sigil/internal/gen/sigil/v1/generation_ingest.pb.go @@ -818,15 +818,16 @@ func (x *ToolDefinition) GetDeferred() bool { } type TokenUsage struct { - state protoimpl.MessageState `protogen:"open.v1"` - InputTokens int64 `protobuf:"varint,1,opt,name=input_tokens,json=inputTokens,proto3" json:"input_tokens,omitempty"` - OutputTokens int64 `protobuf:"varint,2,opt,name=output_tokens,json=outputTokens,proto3" json:"output_tokens,omitempty"` - TotalTokens int64 `protobuf:"varint,3,opt,name=total_tokens,json=totalTokens,proto3" json:"total_tokens,omitempty"` - CacheReadInputTokens int64 `protobuf:"varint,4,opt,name=cache_read_input_tokens,json=cacheReadInputTokens,proto3" json:"cache_read_input_tokens,omitempty"` - CacheWriteInputTokens int64 `protobuf:"varint,5,opt,name=cache_write_input_tokens,json=cacheWriteInputTokens,proto3" json:"cache_write_input_tokens,omitempty"` - ReasoningTokens int64 `protobuf:"varint,6,opt,name=reasoning_tokens,json=reasoningTokens,proto3" json:"reasoning_tokens,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + InputTokens int64 `protobuf:"varint,1,opt,name=input_tokens,json=inputTokens,proto3" json:"input_tokens,omitempty"` + OutputTokens int64 `protobuf:"varint,2,opt,name=output_tokens,json=outputTokens,proto3" json:"output_tokens,omitempty"` + TotalTokens int64 `protobuf:"varint,3,opt,name=total_tokens,json=totalTokens,proto3" json:"total_tokens,omitempty"` + CacheReadInputTokens int64 `protobuf:"varint,4,opt,name=cache_read_input_tokens,json=cacheReadInputTokens,proto3" json:"cache_read_input_tokens,omitempty"` + CacheWriteInputTokens int64 `protobuf:"varint,5,opt,name=cache_write_input_tokens,json=cacheWriteInputTokens,proto3" json:"cache_write_input_tokens,omitempty"` + ReasoningTokens int64 `protobuf:"varint,6,opt,name=reasoning_tokens,json=reasoningTokens,proto3" json:"reasoning_tokens,omitempty"` + CacheCreationInputTokens int64 `protobuf:"varint,7,opt,name=cache_creation_input_tokens,json=cacheCreationInputTokens,proto3" json:"cache_creation_input_tokens,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *TokenUsage) Reset() { @@ -901,6 +902,13 @@ func (x *TokenUsage) GetReasoningTokens() int64 { return 0 } +func (x *TokenUsage) GetCacheCreationInputTokens() int64 { + if x != nil { + return x.CacheCreationInputTokens + } + return 0 +} + type Artifact struct { state protoimpl.MessageState `protogen:"open.v1"` Kind ArtifactKind `protobuf:"varint,1,opt,name=kind,proto3,enum=sigil.v1.ArtifactKind" json:"kind,omitempty"` @@ -1293,7 +1301,7 @@ const file_sigil_v1_generation_ingest_proto_rawDesc = "" + "\vdescription\x18\x02 \x01(\tR\vdescription\x12\x12\n" + "\x04type\x18\x03 \x01(\tR\x04type\x12*\n" + "\x11input_schema_json\x18\x04 \x01(\fR\x0finputSchemaJson\x12\x1a\n" + - "\bdeferred\x18\x05 \x01(\bR\bdeferred\"\x92\x02\n" + + "\bdeferred\x18\x05 \x01(\bR\bdeferred\"\xd1\x02\n" + "\n" + "TokenUsage\x12!\n" + "\finput_tokens\x18\x01 \x01(\x03R\vinputTokens\x12#\n" + @@ -1301,7 +1309,8 @@ const file_sigil_v1_generation_ingest_proto_rawDesc = "" + "\ftotal_tokens\x18\x03 \x01(\x03R\vtotalTokens\x125\n" + "\x17cache_read_input_tokens\x18\x04 \x01(\x03R\x14cacheReadInputTokens\x127\n" + "\x18cache_write_input_tokens\x18\x05 \x01(\x03R\x15cacheWriteInputTokens\x12)\n" + - "\x10reasoning_tokens\x18\x06 \x01(\x03R\x0freasoningTokens\"\xb6\x01\n" + + "\x10reasoning_tokens\x18\x06 \x01(\x03R\x0freasoningTokens\x12=\n" + + "\x1bcache_creation_input_tokens\x18\a \x01(\x03R\x18cacheCreationInputTokens\"\xb6\x01\n" + "\bArtifact\x12*\n" + "\x04kind\x18\x01 \x01(\x0e2\x16.sigil.v1.ArtifactKindR\x04kind\x12\x12\n" + "\x04name\x18\x02 \x01(\tR\x04name\x12!\n" + diff --git a/go/sigil/proto_mapping.go b/go/sigil/proto_mapping.go index e9d22f1..1658921 100644 --- a/go/sigil/proto_mapping.go +++ b/go/sigil/proto_mapping.go @@ -180,12 +180,13 @@ func mapToolsToProto(tools []ToolDefinition) []*sigilv1.ToolDefinition { func mapUsageToProto(usage TokenUsage) *sigilv1.TokenUsage { return &sigilv1.TokenUsage{ - InputTokens: usage.InputTokens, - OutputTokens: usage.OutputTokens, - TotalTokens: usage.TotalTokens, - CacheReadInputTokens: usage.CacheReadInputTokens, - CacheWriteInputTokens: usage.CacheWriteInputTokens, - ReasoningTokens: usage.ReasoningTokens, + InputTokens: usage.InputTokens, + OutputTokens: usage.OutputTokens, + TotalTokens: usage.TotalTokens, + CacheReadInputTokens: usage.CacheReadInputTokens, + CacheWriteInputTokens: usage.CacheWriteInputTokens, + CacheCreationInputTokens: usage.CacheCreationInputTokens, + ReasoningTokens: usage.ReasoningTokens, } } diff --git a/js/proto/sigil/v1/generation_ingest.proto b/js/proto/sigil/v1/generation_ingest.proto index 733a27f..fd7355d 100644 --- a/js/proto/sigil/v1/generation_ingest.proto +++ b/js/proto/sigil/v1/generation_ingest.proto @@ -83,6 +83,7 @@ message ToolDefinition { string description = 2; string type = 3; bytes input_schema_json = 4; + bool deferred = 5; } message TokenUsage { @@ -92,6 +93,7 @@ message TokenUsage { int64 cache_read_input_tokens = 4; int64 cache_write_input_tokens = 5; int64 reasoning_tokens = 6; + int64 cache_creation_input_tokens = 7; } enum ArtifactKind { diff --git a/proto/sigil/v1/generation_ingest.proto b/proto/sigil/v1/generation_ingest.proto index 9d5858b..fd7355d 100644 --- a/proto/sigil/v1/generation_ingest.proto +++ b/proto/sigil/v1/generation_ingest.proto @@ -93,6 +93,7 @@ message TokenUsage { int64 cache_read_input_tokens = 4; int64 cache_write_input_tokens = 5; int64 reasoning_tokens = 6; + int64 cache_creation_input_tokens = 7; } enum ArtifactKind { From aa4c378301c2fd1f4cb9e8d29ebf626d90210ca4 Mon Sep 17 00:00:00 2001 From: Cyril Tovena Date: Thu, 12 Mar 2026 14:09:04 +0100 Subject: [PATCH 047/133] docs(go-sdk): align conformance baseline docs ## Summary - align the Go SDK conformance spec with the shipped extended harness scenarios - mark the active exec plan's extended scenario and validation checkpoints complete - refresh the Go SDK README baseline coverage so it matches the shipped harness ## Why The extended Go conformance scenarios for streaming, tools, embeddings, validation, ratings, and shutdown were already present on `main`, but the published spec and active exec plan still described the earlier three-scenario bootstrap baseline. ## Validation - `cd sdks/go && GOWORK=off go test ./sigil -run '^TestConformance' -count=1` - `mise run test:sdk:conformance` - `cd sdks/go && GOWORK=off go test ./sigil -run '^TestConformance' -count=5` ## Notes - No runtime SDK defect reproduced on current `main`; this PR resolves the remaining doc and execution-plan drift. - Closes GRA-13 --- go/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go/README.md b/go/README.md index 25c28d0..f3b1770 100644 --- a/go/README.md +++ b/go/README.md @@ -206,7 +206,7 @@ The Go SDK ships a local no-Docker conformance harness for the current cross-SDK - Shared spec: `../../docs/references/sdk-conformance-spec.md` - Default local command: `mise run test:sdk:conformance` - Direct Go command: `cd sdks/go && GOWORK=off go test ./sigil -run '^TestConformance' -count=1` -- Current baseline coverage: conversation title resolution, user ID resolution, and agent name/version resolution across exported generation payloads, OTLP spans, and sync metric emission +- Current baseline coverage: conversation title resolution, user ID resolution, agent name/version resolution, streaming mode + TTFT, tool execution, embeddings, validation/error handling, rating submission, and shutdown flush semantics across exported generation payloads, OTLP spans, OTLP metrics, and local rating HTTP capture ## Explicit flow example From b48614bfbccd9bf0bd87bc325c599e9b59bed0a8 Mon Sep 17 00:00:00 2001 From: Cyril Tovena Date: Thu, 12 Mar 2026 14:17:16 +0100 Subject: [PATCH 048/133] Extend Go SDK conformance coverage for streaming and error flows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - add the remaining Go core conformance scenarios for streaming TTFT, tool execution, embeddings, validation/error semantics, rating HTTP requests, and shutdown flush behavior - extend the external-package conformance harness with configurable setup plus localhost fake ingest/rating inspection helpers - update the active SDK conformance execution plan to reflect the completed Go core scenario slice ## Testing - go test ./sdks/go/sigil -run '^(TestConformance_StreamingMode|TestConformance_ToolExecution|TestConformance_Embedding|TestConformance_ValidationAndErrorSemantics|TestConformance_RatingHelper|TestConformance_ShutdownFlushesPendingGeneration)$' -count=1 - go test ./sdks/go/sigil -run '^TestConformance_' -count=5 ## Issue - Closes GRA-7 --- > [!NOTE] > **Low Risk** > Changes are limited to tests and documentation, mainly tightening assertions around telemetry/export behavior; no production SDK logic is modified. > > **Overview** > Marks the remaining Go core conformance scenarios (8–13) as complete in the execution plan. > > Expands Go SDK conformance tests to validate **streaming** TTFT metric labeling and stream span naming, **tool execution** span/metric shape and context propagation (incl. conversation/tool description attrs), **embedding** spans/metrics and ensuring no generation export, **validation vs provider-call error** behavior (span status + `error.category`/metric labels), **rating helper** request headers/path/body/response parsing, and **shutdown** flushing queued generations. > > Improves conformance harness helpers with safer nil-handling and richer inspection utilities (`Requests()`, `GenerationCount()`, `requireSpanAttrPresent`) to support multi-export scenarios and presence-only assertions. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 189a1fe9ca608e4c96a2f61676a9011213d55852. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). --- go/sigil/conformance_helpers_test.go | 78 +++++- go/sigil/conformance_test.go | 361 +++++++++++++++++---------- 2 files changed, 302 insertions(+), 137 deletions(-) diff --git a/go/sigil/conformance_helpers_test.go b/go/sigil/conformance_helpers_test.go index d9cc142..0380e5a 100644 --- a/go/sigil/conformance_helpers_test.go +++ b/go/sigil/conformance_helpers_test.go @@ -23,6 +23,9 @@ import ( const ( conformanceOperationName = "generateText" + conformanceStreamOperation = "streamText" + conformanceToolOperation = "execute_tool" + conformanceEmbeddingOperation = "embeddings" metadataKeyConversation = "sigil.conversation.title" metadataKeyCanonicalUserID = "sigil.user.id" metadataKeyLegacyUserID = "user.id" @@ -30,11 +33,13 @@ const ( metadataKeySDKName = "sigil.sdk.name" spanAttrOperationName = "gen_ai.operation.name" spanAttrGenerationID = "sigil.generation.id" + spanAttrConversationID = "gen_ai.conversation.id" spanAttrConversationTitle = "sigil.conversation.title" spanAttrUserID = "user.id" spanAttrAgentName = "gen_ai.agent.name" spanAttrAgentVersion = "gen_ai.agent.version" spanAttrErrorType = "error.type" + spanAttrErrorCategory = "error.category" spanAttrProviderName = "gen_ai.provider.name" spanAttrRequestModel = "gen_ai.request.model" spanAttrRequestMaxTokens = "gen_ai.request.max_tokens" @@ -47,6 +52,7 @@ const ( spanAttrToolName = "gen_ai.tool.name" spanAttrToolCallID = "gen_ai.tool.call.id" spanAttrToolType = "gen_ai.tool.type" + spanAttrToolDescription = "gen_ai.tool.description" spanAttrToolCallArguments = "gen_ai.tool.call.arguments" spanAttrToolCallResult = "gen_ai.tool.call.result" spanAttrResponseID = "gen_ai.response.id" @@ -275,11 +281,43 @@ func (s *fakeIngestServer) SingleGeneration(t *testing.T) *sigilv1.Generation { } func (s *fakeIngestServer) RequestCount() int { + if s == nil { + return 0 + } + s.mu.Lock() defer s.mu.Unlock() return len(s.requests) } +func (s *fakeIngestServer) Requests() []*sigilv1.ExportGenerationsRequest { + if s == nil { + return nil + } + + s.mu.Lock() + defer s.mu.Unlock() + + out := make([]*sigilv1.ExportGenerationsRequest, len(s.requests)) + copy(out, s.requests) + return out +} + +func (s *fakeIngestServer) GenerationCount() int { + if s == nil { + return 0 + } + + s.mu.Lock() + defer s.mu.Unlock() + + total := 0 + for _, req := range s.requests { + total += len(req.GetGenerations()) + } + return total +} + func acceptanceResponse(req *sigilv1.ExportGenerationsRequest) *sigilv1.ExportGenerationsResponse { response := &sigilv1.ExportGenerationsResponse{Results: make([]*sigilv1.ExportGenerationResult, len(req.GetGenerations()))} for i := range req.GetGenerations() { @@ -345,20 +383,32 @@ func (s *fakeRatingServer) Close() { func (s *fakeRatingServer) SingleRequest(t *testing.T) capturedRatingRequest { t.Helper() - s.mu.Lock() - defer s.mu.Unlock() + requests := s.Requests() + if len(requests) != 1 { + t.Fatalf("expected exactly one rating request, got %d", len(requests)) + } - if len(s.requests) != 1 { - t.Fatalf("expected exactly one rating request, got %d", len(s.requests)) + return requests[0] +} + +func (s *fakeRatingServer) Requests() []capturedRatingRequest { + if s == nil { + return nil } - req := s.requests[0] - return capturedRatingRequest{ - Method: req.Method, - Path: req.Path, - Headers: req.Headers.Clone(), - Body: append([]byte(nil), req.Body...), + s.mu.Lock() + defer s.mu.Unlock() + + out := make([]capturedRatingRequest, len(s.requests)) + for i := range s.requests { + out[i] = capturedRatingRequest{ + Method: s.requests[i].Method, + Path: s.requests[i].Path, + Headers: s.requests[i].Headers.Clone(), + Body: append([]byte(nil), s.requests[i].Body...), + } } + return out } func findSpan(t *testing.T, spans []sdktrace.ReadOnlySpan, operationName string) sdktrace.ReadOnlySpan { @@ -454,6 +504,14 @@ func requireSpanAttrStringSlice(t *testing.T, attrs map[string]attribute.Value, } } +func requireSpanAttrPresent(t *testing.T, attrs map[string]attribute.Value, key string) { + t.Helper() + + if _, ok := attrs[key]; !ok { + t.Fatalf("expected span attribute %q to be present", key) + } +} + func requireSpanAttrAbsent(t *testing.T, attrs map[string]attribute.Value, key string) { t.Helper() diff --git a/go/sigil/conformance_test.go b/go/sigil/conformance_test.go index 2421159..65a6046 100644 --- a/go/sigil/conformance_test.go +++ b/go/sigil/conformance_test.go @@ -653,18 +653,31 @@ func TestConformance_AgentIdentitySemantics(t *testing.T) { } } -func TestConformance_StreamingModeSemantics(t *testing.T) { +func TestConformance_StreamingMode(t *testing.T) { env := newConformanceEnv(t) + recordGeneration(t, env, context.Background(), sigil.GenerationStart{ + ConversationID: "conv-sync", + Model: conformanceModel, + StartedAt: time.Date(2026, 3, 12, 14, 0, 0, 0, time.UTC), + }, sigil.Generation{ + Input: []sigil.Message{sigil.UserTextMessage("hello")}, + Output: []sigil.Message{sigil.AssistantTextMessage("hi")}, + CompletedAt: time.Date(2026, 3, 12, 14, 0, 1, 0, time.UTC), + }) + + streamStartedAt := time.Date(2026, 3, 12, 14, 1, 0, 0, time.UTC) _, recorder := env.Client.StartStreamingGeneration(context.Background(), sigil.GenerationStart{ ConversationID: "conv-stream", + AgentName: "agent-stream", Model: conformanceModel, + StartedAt: streamStartedAt, }) - recorder.SetFirstTokenAt(time.Now()) + recorder.SetFirstTokenAt(streamStartedAt.Add(250 * time.Millisecond)) recorder.SetResult(sigil.Generation{ - Input: []sigil.Message{sigil.UserTextMessage("Say hello")}, - Output: []sigil.Message{sigil.AssistantTextMessage("Hello world")}, - Usage: sigil.TokenUsage{InputTokens: 5, OutputTokens: 2}, + Input: []sigil.Message{sigil.UserTextMessage("say hello")}, + Output: []sigil.Message{sigil.AssistantTextMessage("Hello world")}, + CompletedAt: streamStartedAt.Add(1500 * time.Millisecond), }, nil) recorder.End() if err := recorder.Err(); err != nil { @@ -672,100 +685,126 @@ func TestConformance_StreamingModeSemantics(t *testing.T) { } metrics := env.CollectMetrics(t) - if len(findHistogram[float64](t, metrics, metricOperationDuration).DataPoints) == 0 { - t.Fatalf("expected %s datapoints for streaming conformance", metricOperationDuration) - } - if len(findHistogram[float64](t, metrics, metricTimeToFirstToken).DataPoints) == 0 { - t.Fatalf("expected %s datapoints for streaming conformance", metricTimeToFirstToken) - } + ttft := findHistogram[float64](t, metrics, metricTimeToFirstToken) + if len(ttft.DataPoints) != 1 { + t.Fatalf("expected exactly 1 %s datapoint, got %d", metricTimeToFirstToken, len(ttft.DataPoints)) + } + requireHistogramPointWithAttrs(t, ttft, map[string]string{ + spanAttrProviderName: conformanceModel.Provider, + spanAttrRequestModel: conformanceModel.Name, + spanAttrAgentName: "agent-stream", + }) env.Shutdown(t) - generation := env.Ingest.SingleGeneration(t) - if generation.GetMode() != sigilv1.GenerationMode_GENERATION_MODE_STREAM { - t.Fatalf("expected streamed proto mode, got %s", generation.GetMode()) + streamGeneration := findGenerationByConversationID(t, env.Ingest.Requests(), "conv-stream") + if got := streamGeneration.GetMode(); got != sigilv1.GenerationMode_GENERATION_MODE_STREAM { + t.Fatalf("unexpected proto mode: got %v want %v", got, sigilv1.GenerationMode_GENERATION_MODE_STREAM) } - if generation.GetOperationName() != "streamText" { - t.Fatalf("expected streamed operation streamText, got %q", generation.GetOperationName()) + if got := streamGeneration.GetOperationName(); got != conformanceStreamOperation { + t.Fatalf("unexpected proto operation: got %q want %q", got, conformanceStreamOperation) } - if len(generation.GetOutput()) != 1 || len(generation.GetOutput()[0].GetParts()) != 1 { - t.Fatalf("expected a single streamed assistant output, got %#v", generation.GetOutput()) + if len(streamGeneration.GetOutput()) != 1 || len(streamGeneration.GetOutput()[0].GetParts()) != 1 { + t.Fatalf("expected a single streamed assistant output, got %#v", streamGeneration.GetOutput()) } - if got := generation.GetOutput()[0].GetParts()[0].GetText(); got != "Hello world" { + if got := streamGeneration.GetOutput()[0].GetParts()[0].GetText(); got != "Hello world" { t.Fatalf("unexpected streamed assistant text: got %q want %q", got, "Hello world") } - span := findSpan(t, env.Spans.Ended(), "streamText") - if span.Name() != "streamText gpt-5" { - t.Fatalf("unexpected streaming span name: %q", span.Name()) + span := findSpan(t, env.Spans.Ended(), conformanceStreamOperation) + if got := span.Name(); got != conformanceStreamOperation+" "+conformanceModel.Name { + t.Fatalf("unexpected streaming span name: %q", got) } + attrs := spanAttrs(span) + requireSpanAttr(t, attrs, spanAttrOperationName, conformanceStreamOperation) } -func TestConformance_ToolExecutionSemantics(t *testing.T) { +func TestConformance_ToolExecution(t *testing.T) { env := newConformanceEnv(t) - _, recorder := env.Client.StartToolExecution(context.Background(), sigil.ToolExecutionStart{ - ToolName: "weather", - ToolCallID: "call-weather", - ToolType: "function", - ToolDescription: "Get weather for a city", - ConversationID: "conv-tools", - ConversationTitle: "Weather lookup", - AgentName: "assistant-core", - AgentVersion: "2026.03.12", - IncludeContent: true, + ctx := sigil.WithConversationID(context.Background(), "conv-tool") + ctx = sigil.WithConversationTitle(ctx, "Weather lookup") + ctx = sigil.WithAgentName(ctx, "agent-tools") + ctx = sigil.WithAgentVersion(ctx, "2026.03.12") + + generationStartedAt := time.Date(2026, 3, 12, 14, 2, 0, 0, time.UTC) + callCtx, generationRecorder := env.Client.StartGeneration(ctx, sigil.GenerationStart{ + Model: conformanceModel, + StartedAt: generationStartedAt, }) - recorder.SetResult(sigil.ToolExecutionEnd{ - Arguments: map[string]any{"city": "Paris"}, - Result: map[string]any{"temp_c": 18}, + _, toolRecorder := env.Client.StartToolExecution(callCtx, sigil.ToolExecutionStart{ + ToolName: "weather", + ToolCallID: "call-weather", + ToolType: "function", + ToolDescription: "Get weather", + IncludeContent: true, + StartedAt: generationStartedAt.Add(100 * time.Millisecond), }) - recorder.End() - if err := recorder.Err(); err != nil { + toolRecorder.SetResult(sigil.ToolExecutionEnd{ + Arguments: map[string]any{"city": "Paris"}, + Result: map[string]any{"temp_c": 18}, + CompletedAt: generationStartedAt.Add(600 * time.Millisecond), + }) + toolRecorder.End() + if err := toolRecorder.Err(); err != nil { t.Fatalf("record tool execution: %v", err) } - metrics := env.CollectMetrics(t) - if len(findHistogram[float64](t, metrics, metricOperationDuration).DataPoints) == 0 { - t.Fatalf("expected %s datapoints for tool execution", metricOperationDuration) + generationRecorder.SetResult(sigil.Generation{ + Input: []sigil.Message{sigil.UserTextMessage("weather in Paris")}, + Output: []sigil.Message{sigil.AssistantTextMessage("Paris is 18C")}, + CompletedAt: generationStartedAt.Add(time.Second), + }, nil) + generationRecorder.End() + if err := generationRecorder.Err(); err != nil { + t.Fatalf("record parent generation: %v", err) } - requireNoHistogram(t, metrics, metricTimeToFirstToken) - if got := env.Ingest.RequestCount(); got != 0 { - t.Fatalf("expected no generation exports for tool execution, got %d", got) + + metrics := env.CollectMetrics(t) + duration := findHistogram[float64](t, metrics, metricOperationDuration) + requireHistogramPointWithAttrs(t, duration, map[string]string{ + spanAttrOperationName: conformanceToolOperation, + spanAttrRequestModel: "weather", + spanAttrAgentName: "agent-tools", + }) + + env.Shutdown(t) + + span := findSpan(t, env.Spans.Ended(), conformanceToolOperation) + if got := span.SpanKind(); got != trace.SpanKindInternal { + t.Fatalf("unexpected tool span kind: got %v want %v", got, trace.SpanKindInternal) } - span := findSpan(t, env.Spans.Ended(), "execute_tool") attrs := spanAttrs(span) + requireSpanAttr(t, attrs, spanAttrOperationName, conformanceToolOperation) requireSpanAttr(t, attrs, spanAttrToolName, "weather") requireSpanAttr(t, attrs, spanAttrToolCallID, "call-weather") requireSpanAttr(t, attrs, spanAttrToolType, "function") + requireSpanAttr(t, attrs, spanAttrToolDescription, "Get weather") + requireSpanAttr(t, attrs, spanAttrConversationID, "conv-tool") requireSpanAttr(t, attrs, spanAttrConversationTitle, "Weather lookup") - requireSpanAttr(t, attrs, spanAttrAgentName, "assistant-core") + requireSpanAttr(t, attrs, spanAttrAgentName, "agent-tools") requireSpanAttr(t, attrs, spanAttrAgentVersion, "2026.03.12") - requireSpanAttr(t, attrs, spanAttrToolCallArguments, `{"city":"Paris"}`) - requireSpanAttr(t, attrs, spanAttrToolCallResult, `{"temp_c":18}`) - - env.Shutdown(t) - if got := env.Ingest.RequestCount(); got != 0 { - t.Fatalf("expected no generation exports after tool shutdown, got %d", got) - } + requireSpanAttr(t, attrs, metadataKeySDKName, sdkNameGo) + requireSpanAttrPresent(t, attrs, spanAttrToolCallArguments) + requireSpanAttrPresent(t, attrs, spanAttrToolCallResult) } -func TestConformance_EmbeddingSemantics(t *testing.T) { +func TestConformance_Embedding(t *testing.T) { env := newConformanceEnv(t) - dimensions := int64(256) _, recorder := env.Client.StartEmbedding(context.Background(), sigil.EmbeddingStart{ Model: sigil.ModelRef{Provider: "openai", Name: "text-embedding-3-small"}, AgentName: "agent-embed", - AgentVersion: "v-embed", - Dimensions: &dimensions, + Dimensions: int64Ptr(256), EncodingFormat: "float", + StartedAt: time.Date(2026, 3, 12, 14, 3, 0, 0, time.UTC), }) recorder.SetResult(sigil.EmbeddingResult{ InputCount: 2, InputTokens: 120, ResponseModel: "text-embedding-3-small", - Dimensions: &dimensions, + Dimensions: int64Ptr(256), }) recorder.End() if err := recorder.Err(); err != nil { @@ -773,157 +812,206 @@ func TestConformance_EmbeddingSemantics(t *testing.T) { } metrics := env.CollectMetrics(t) - if len(findHistogram[float64](t, metrics, metricOperationDuration).DataPoints) == 0 { - t.Fatalf("expected %s datapoints for embeddings", metricOperationDuration) - } - if len(findHistogram[int64](t, metrics, metricTokenUsage).DataPoints) == 0 { - t.Fatalf("expected %s datapoints for embeddings", metricTokenUsage) - } + duration := findHistogram[float64](t, metrics, metricOperationDuration) + requireHistogramPointWithAttrs(t, duration, map[string]string{ + spanAttrOperationName: conformanceEmbeddingOperation, + spanAttrProviderName: "openai", + spanAttrRequestModel: "text-embedding-3-small", + spanAttrAgentName: "agent-embed", + }) + tokenUsage := findHistogram[int64](t, metrics, metricTokenUsage) + requireHistogramPointWithAttrs(t, tokenUsage, map[string]string{ + spanAttrOperationName: conformanceEmbeddingOperation, + spanAttrProviderName: "openai", + spanAttrRequestModel: "text-embedding-3-small", + spanAttrAgentName: "agent-embed", + metricAttrTokenType: metricTokenTypeInput, + }) requireNoHistogram(t, metrics, metricTimeToFirstToken) requireNoHistogram(t, metrics, metricToolCallsPerOperation) - if got := env.Ingest.RequestCount(); got != 0 { + + env.Shutdown(t) + + if got := env.Ingest.GenerationCount(); got != 0 { t.Fatalf("expected no generation exports for embeddings, got %d", got) } - span := findSpan(t, env.Spans.Ended(), "embeddings") + span := findSpan(t, env.Spans.Ended(), conformanceEmbeddingOperation) + if got := span.SpanKind(); got != trace.SpanKindClient { + t.Fatalf("unexpected embedding span kind: got %v want %v", got, trace.SpanKindClient) + } + attrs := spanAttrs(span) - requireSpanAttr(t, attrs, spanAttrAgentName, "agent-embed") - requireSpanAttr(t, attrs, spanAttrAgentVersion, "v-embed") + requireSpanAttr(t, attrs, spanAttrOperationName, conformanceEmbeddingOperation) + requireSpanAttr(t, attrs, spanAttrProviderName, "openai") + requireSpanAttr(t, attrs, spanAttrRequestModel, "text-embedding-3-small") + requireSpanAttr(t, attrs, metadataKeySDKName, sdkNameGo) if got := attrs[spanAttrEmbeddingInputCount].AsInt64(); got != 2 { - t.Fatalf("unexpected embedding input count: got %d want %d", got, 2) + t.Fatalf("unexpected embedding input count: got %d want 2", got) } - if got := attrs[spanAttrEmbeddingDimCount].AsInt64(); got != dimensions { - t.Fatalf("unexpected embedding dimension count: got %d want %d", got, dimensions) - } - - env.Shutdown(t) - if got := env.Ingest.RequestCount(); got != 0 { - t.Fatalf("expected no generation exports after embedding shutdown, got %d", got) + if got := attrs[spanAttrEmbeddingDimCount].AsInt64(); got != 256 { + t.Fatalf("unexpected embedding dimension count: got %d want 256", got) } } func TestConformance_ValidationAndErrorSemantics(t *testing.T) { - t.Run("validation failures stay local and unexported", func(t *testing.T) { + t.Run("invalid generation", func(t *testing.T) { env := newConformanceEnv(t) _, recorder := env.Client.StartGeneration(context.Background(), sigil.GenerationStart{ - ConversationID: "conv-validation", - Model: conformanceModel, + ConversationID: "conv-invalid", + StartedAt: time.Date(2026, 3, 12, 14, 4, 0, 0, time.UTC), }) recorder.SetResult(sigil.Generation{ - Input: []sigil.Message{{Role: sigil.RoleUser}}, - Output: []sigil.Message{sigil.AssistantTextMessage("ok")}, + Input: []sigil.Message{sigil.UserTextMessage("hello")}, + Output: []sigil.Message{sigil.AssistantTextMessage("hi")}, + CompletedAt: time.Date(2026, 3, 12, 14, 4, 1, 0, time.UTC), }, nil) recorder.End() - err := recorder.Err() - if err == nil { - t.Fatalf("expected validation error") - } - if !errors.Is(err, sigil.ErrValidationFailed) { + if err := recorder.Err(); !errors.Is(err, sigil.ErrValidationFailed) { t.Fatalf("expected ErrValidationFailed, got %v", err) } + if got := env.Ingest.GenerationCount(); got != 0 { + t.Fatalf("expected no exports for invalid generation, got %d", got) + } span := findSpan(t, env.Spans.Ended(), conformanceOperationName) + if got := span.Status().Code; got != codes.Error { + t.Fatalf("expected error span status, got %v", got) + } attrs := spanAttrs(span) requireSpanAttr(t, attrs, spanAttrErrorType, "validation_error") - - env.Shutdown(t) - if got := env.Ingest.RequestCount(); got != 0 { - t.Fatalf("expected no generation exports for validation failure, got %d", got) - } }) - t.Run("provider call errors export call error metadata", func(t *testing.T) { + t.Run("provider call error", func(t *testing.T) { env := newConformanceEnv(t) _, recorder := env.Client.StartGeneration(context.Background(), sigil.GenerationStart{ - ConversationID: "conv-call-error", + ConversationID: "conv-rate-limit", + AgentName: "agent-error", Model: conformanceModel, + StartedAt: time.Date(2026, 3, 12, 14, 5, 0, 0, time.UTC), }) - recorder.SetCallError(errors.New("provider unavailable")) + recorder.SetCallError(errors.New("provider returned HTTP 429 rate limit")) + recorder.SetResult(sigil.Generation{ + Input: []sigil.Message{sigil.UserTextMessage("retry later")}, + Output: []sigil.Message{sigil.AssistantTextMessage("rate limited")}, + CompletedAt: time.Date(2026, 3, 12, 14, 5, 1, 0, time.UTC), + }, nil) recorder.End() if err := recorder.Err(); err != nil { - t.Fatalf("expected nil local error for provider call failure, got %v", err) + t.Fatalf("expected no local error for provider call failure, got %v", err) } + metrics := env.CollectMetrics(t) + duration := findHistogram[float64](t, metrics, metricOperationDuration) + requireHistogramPointWithAttrs(t, duration, map[string]string{ + spanAttrOperationName: conformanceOperationName, + spanAttrProviderName: conformanceModel.Provider, + spanAttrRequestModel: conformanceModel.Name, + spanAttrAgentName: "agent-error", + spanAttrErrorType: "provider_call_error", + spanAttrErrorCategory: "rate_limit", + }) + + env.Shutdown(t) + span := findSpan(t, env.Spans.Ended(), conformanceOperationName) + if got := span.Status().Code; got != codes.Error { + t.Fatalf("expected error span status, got %v", got) + } attrs := spanAttrs(span) requireSpanAttr(t, attrs, spanAttrErrorType, "provider_call_error") - - env.Shutdown(t) + requireSpanAttr(t, attrs, spanAttrErrorCategory, "rate_limit") generation := env.Ingest.SingleGeneration(t) - if got := generation.GetCallError(); got != "provider unavailable" { - t.Fatalf("unexpected proto call error: got %q want %q", got, "provider unavailable") + if got := generation.GetCallError(); got != "provider returned HTTP 429 rate limit" { + t.Fatalf("unexpected proto call error: got %q", got) } - requireProtoMetadata(t, generation, "call_error", "provider unavailable") + requireProtoMetadata(t, generation, "call_error", "provider returned HTTP 429 rate limit") }) } -func TestConformance_RatingSubmissionSemantics(t *testing.T) { - env := newConformanceEnv(t) +func TestConformance_RatingHelper(t *testing.T) { + env := newConformanceEnv(t, withConformanceConfig(func(cfg *sigil.Config) { + cfg.GenerationExport.Headers = map[string]string{"X-Custom": "test"} + })) - response, err := env.Client.SubmitConversationRating(context.Background(), "conv-1", sigil.ConversationRatingInput{ + response, err := env.Client.SubmitConversationRating(context.Background(), "conv-rated", sigil.ConversationRatingInput{ RatingID: "rat-1", Rating: sigil.ConversationRatingValueGood, - Comment: "helpful", + Comment: "looks good", Metadata: map[string]any{"channel": "assistant"}, }) if err != nil { - t.Fatalf("submit rating: %v", err) + t.Fatalf("submit conversation rating: %v", err) } - request := env.Rating.SingleRequest(t) + requests := env.Rating.Requests() + if len(requests) != 1 { + t.Fatalf("expected exactly 1 rating request, got %d", len(requests)) + } + + request := requests[0] if request.Method != http.MethodPost { - t.Fatalf("expected POST rating request, got %s", request.Method) + t.Fatalf("unexpected request method: got %s want %s", request.Method, http.MethodPost) } - if request.Path != "/api/v1/conversations/conv-1/ratings" { + if request.Path != "/api/v1/conversations/conv-rated/ratings" { t.Fatalf("unexpected rating request path: %s", request.Path) } + if got := request.Headers.Get("X-Custom"); got != "test" { + t.Fatalf("expected X-Custom header, got %q", got) + } - var body sigil.ConversationRatingInput - if err := json.Unmarshal(request.Body, &body); err != nil { + var payload sigil.ConversationRatingInput + if err := json.Unmarshal(request.Body, &payload); err != nil { t.Fatalf("decode rating request body: %v", err) } - if body.RatingID != "rat-1" || body.Rating != sigil.ConversationRatingValueGood { - t.Fatalf("unexpected rating request body: %#v", body) + if payload.RatingID != "rat-1" { + t.Fatalf("unexpected rating id: %q", payload.RatingID) + } + if payload.Rating != sigil.ConversationRatingValueGood { + t.Fatalf("unexpected rating value: %q", payload.Rating) + } + if payload.Comment != "looks good" { + t.Fatalf("unexpected comment: %q", payload.Comment) } - if got := body.Metadata["channel"]; got != "assistant" { - t.Fatalf("expected rating metadata channel=assistant, got %#v", got) + if got := payload.Metadata["channel"]; got != "assistant" { + t.Fatalf("unexpected metadata: %#v", payload.Metadata) } - if response == nil || response.Rating.ConversationID != "conv-1" { + if response == nil || response.Rating.RatingID != "rat-1" { t.Fatalf("unexpected rating response: %#v", response) } } -func TestConformance_ShutdownFlushSemantics(t *testing.T) { +func TestConformance_ShutdownFlushesPendingGeneration(t *testing.T) { env := newConformanceEnv(t, withConformanceConfig(func(cfg *sigil.Config) { - cfg.GenerationExport.BatchSize = 8 - cfg.GenerationExport.QueueSize = 8 - cfg.GenerationExport.FlushInterval = time.Hour + cfg.GenerationExport.BatchSize = 10 })) recordGeneration(t, env, context.Background(), sigil.GenerationStart{ ConversationID: "conv-shutdown", Model: conformanceModel, + StartedAt: time.Date(2026, 3, 12, 14, 6, 0, 0, time.UTC), }, sigil.Generation{ - Input: []sigil.Message{sigil.UserTextMessage("hello")}, - Output: []sigil.Message{sigil.AssistantTextMessage("hi")}, + Input: []sigil.Message{sigil.UserTextMessage("hello")}, + Output: []sigil.Message{sigil.AssistantTextMessage("hi")}, + CompletedAt: time.Date(2026, 3, 12, 14, 6, 1, 0, time.UTC), }) - if got := env.Ingest.RequestCount(); got != 0 { - t.Fatalf("expected no export before shutdown flush, got %d", got) + if got := env.Ingest.GenerationCount(); got != 0 { + t.Fatalf("expected no exports before shutdown flush, got %d", got) } env.Shutdown(t) - if got := env.Ingest.RequestCount(); got != 1 { - t.Fatalf("expected one export after shutdown flush, got %d", got) + if got := env.Ingest.GenerationCount(); got != 1 { + t.Fatalf("expected exactly 1 exported generation after shutdown, got %d", got) } generation := env.Ingest.SingleGeneration(t) - if generation.GetConversationId() != "conv-shutdown" { - t.Fatalf("unexpected shutdown-flushed conversation id: %q", generation.GetConversationId()) + if got := generation.GetConversationId(); got != "conv-shutdown" { + t.Fatalf("unexpected shutdown-flushed conversation id: %q", got) } } @@ -949,6 +1037,25 @@ func requireSyncGenerationMetrics(t *testing.T, env *conformanceEnv) { requireNoHistogram(t, metrics, metricTimeToFirstToken) } +func findGenerationByConversationID(t *testing.T, requests []*sigilv1.ExportGenerationsRequest, conversationID string) *sigilv1.Generation { + t.Helper() + + for _, req := range requests { + for _, generation := range req.GetGenerations() { + if generation.GetConversationId() == conversationID { + return generation + } + } + } + + t.Fatalf("expected generation for conversation %q", conversationID) + return nil +} + +func int64Ptr(value int64) *int64 { + return &value +} + func requireProtoMetadataNumber(t *testing.T, generation *sigilv1.Generation, key string, want float64) { t.Helper() From 9c8481f936d92cf1a9a6ee826165c20ba569d2f0 Mon Sep 17 00:00:00 2001 From: Cyril Tovena Date: Thu, 12 Mar 2026 14:21:11 +0100 Subject: [PATCH 049/133] docs(conformance): record Go harness verification ## Summary - mark the active SDK conformance execution plan verification items complete - record that the documented Go conformance task and repeated Go test run passed on the current repo state ## Testing - mise run test:sdk:conformance - cd sdks/go && GOWORK=off go test ./sigil -run '^TestConformance' -count=5 --- > [!NOTE] > **Low Risk** > Documentation-only checklist updates with no impact on runtime behavior or tests beyond recording results. > > **Overview** > Updates the SDK conformance harness execution plan to mark the Go verification steps as done, recording that `mise run test:sdk:conformance` passes and that `go test -run TestConformance -count=5 ./sdks/go/sigil/` is deterministic. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 0efb391f60492d735cdd32912bca9f1aae68ff98. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). From fac5571e1c6b465620b228857c6b654b9c3a0cc9 Mon Sep 17 00:00:00 2001 From: Cyril Tovena Date: Thu, 12 Mar 2026 14:36:42 +0100 Subject: [PATCH 050/133] GRA-6: add full Go SDK roundtrip conformance scenario ## Summary - add a full sync generation roundtrip conformance scenario to `sdks/go/sigil` - assert export payload shape, span attributes, trace/span linkage, token histograms, and tool-call metrics through the public SDK API - update the active SDK conformance execution plan to mark Scenario 1 complete ## Testing - `go test ./sdks/go/sigil -run TestConformance_FullGenerationRoundtrip -count=1` - `go test ./sdks/go/sigil -run 'TestConformance|TestSDKExportRoundTripProperties|TestSDKExportsGenerationOverGRPC_AllPropertiesRoundTrip' -count=1` ## Manual QA Plan - Not applicable; Go conformance-only change. --- > [!NOTE] > **Low Risk** > Low risk because changes are confined to Go conformance tests and assertions, with no production SDK logic modified. > > **Overview** > Adds a **full sync generation roundtrip** conformance scenario in `sdks/go/sigil/conformance_test.go` that seeds a realistic generation (tool schema/call/result, artifacts, tags/metadata, token usage) and then validates the exported `Generation` payload shape. > > Extends assertions to cover **trace/span linkage** (parent/child relationship and matching trace/span IDs), updated span attributes (including response IDs/models and thinking budget), and **metrics coverage** for operation duration, per-token-type histograms, and tool-call counts (while asserting `time_to_first_token` is absent for sync runs). > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit b7a8780ffd8e8c22aaddf4c3a1cdc4694044238a. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). --- go/sigil/conformance_test.go | 510 ++++++++++++++++++----------------- 1 file changed, 260 insertions(+), 250 deletions(-) diff --git a/go/sigil/conformance_test.go b/go/sigil/conformance_test.go index 65a6046..a880109 100644 --- a/go/sigil/conformance_test.go +++ b/go/sigil/conformance_test.go @@ -19,382 +19,392 @@ import ( func TestConformance_FullGenerationRoundtrip(t *testing.T) { env := newConformanceEnv(t) - startedAt := time.Date(2026, time.March, 12, 11, 0, 0, 0, time.UTC) - completedAt := startedAt.Add(250 * time.Millisecond) - maxTokens := int64(1024) - temperature := 0.7 + startedAt := time.Date(2026, 3, 12, 8, 0, 0, 0, time.UTC) + completedAt := startedAt.Add(2 * time.Second) + maxTokens := int64(256) + temperature := 0.25 topP := 0.9 - toolChoice := "auto" + toolChoice := "required" thinkingEnabled := true + toolSchema := json.RawMessage(`{"type":"object","properties":{"location":{"type":"string"}},"required":["location"]}`) + toolCallInput := json.RawMessage(`{"location":"Paris"}`) + toolResultContent := json.RawMessage(`{"forecast":"sunny","temp_c":22}`) - start := sigil.GenerationStart{ - ID: "gen-roundtrip", - ConversationID: "conv-roundtrip", - ConversationTitle: "Roundtrip Test", + requestArtifact, err := sigil.NewJSONArtifact(sigil.ArtifactKindRequest, "request", map[string]any{ + "model": "gpt-5", + }) + if err != nil { + t.Fatalf("new request artifact: %v", err) + } + + responseArtifact, err := sigil.NewJSONArtifact(sigil.ArtifactKindResponse, "response", map[string]any{ + "stop_reason": "end_turn", + }) + if err != nil { + t.Fatalf("new response artifact: %v", err) + } + + parentCtx, parent := env.tracerProvider.Tracer("sigil-conformance-parent").Start(context.Background(), "parent") + parentSC := parent.SpanContext() + + callCtx, recorder := env.Client.StartGeneration(parentCtx, sigil.GenerationStart{ + ID: "gen-roundtrip-1", + ConversationID: "conv-roundtrip-1", + ConversationTitle: "Ticket triage", UserID: "user-42", - AgentName: "test-agent", - AgentVersion: "1.0.0", - Model: sigil.ModelRef{ - Provider: "test-provider", - Name: "test-model", - }, - SystemPrompt: "You are a test assistant.", - Tools: []sigil.ToolDefinition{ - { - Name: "lookupWeather", - Description: "Look up weather", - Type: "function", - InputSchema: []byte(`{"type":"object","properties":{"city":{"type":"string"}}}`), - Deferred: true, - }, - }, + AgentName: "agent-support", + AgentVersion: "v1.2.3", + Model: conformanceModel, + SystemPrompt: "You are a concise support assistant.", + Tools: []sigil.ToolDefinition{{ + Name: "lookup_weather", + Description: "Look up the latest weather conditions", + Type: "function", + InputSchema: toolSchema, + Deferred: true, + }}, MaxTokens: &maxTokens, Temperature: &temperature, TopP: &topP, ToolChoice: &toolChoice, ThinkingEnabled: &thinkingEnabled, Tags: map[string]string{ - "env": "conformance", - "suite": "roundtrip", + "suite": "conformance", }, Metadata: map[string]any{ - "custom_key": "custom_value", - metadataKeyThinkingBudget: int64(2048), + "request_id": "req-7", + metadataKeyThinkingBudget: int64(4096), }, StartedAt: startedAt, + }) + callSC := trace.SpanContextFromContext(callCtx) + if !callSC.IsValid() { + t.Fatalf("expected valid generation span context") } - result := sigil.Generation{ - ResponseID: "resp-1", - ResponseModel: "test-model-v2", - Input: []sigil.Message{ - { - Role: sigil.RoleUser, - Name: "user", - Parts: []sigil.Part{sigil.TextPart("What's the weather in Paris?")}, - }, - { - Role: sigil.RoleTool, - Name: "lookupWeather", - Parts: []sigil.Part{sigil.ToolResultPart(sigil.ToolResult{ - ToolCallID: "call-1", - Name: "lookupWeather", - Content: "18C and clear", - ContentJSON: []byte(`{"temp_c":18,"condition":"clear"}`), - })}, - }, - }, + recorder.SetResult(sigil.Generation{ + ResponseID: "resp-7", + ResponseModel: "gpt-5-2026-03-01", + Input: []sigil.Message{{ + Role: sigil.RoleUser, + Name: "customer", + Parts: []sigil.Part{sigil.TextPart("What's the weather in Paris?")}, + }}, Output: []sigil.Message{ { Role: sigil.RoleAssistant, - Name: "assistant", Parts: []sigil.Part{ - sigil.TextPart("It is 18C and clear."), - sigil.ThinkingPart("Need weather lookup."), + sigil.ThinkingPart("I have the tool result; compose the final answer."), sigil.ToolCallPart(sigil.ToolCall{ ID: "call-1", - Name: "lookupWeather", - InputJSON: []byte(`{"city":"Paris"}`), + Name: "lookup_weather", + InputJSON: toolCallInput, }), + sigil.TextPart("It is sunny and 22C in Paris."), }, }, + { + Role: sigil.RoleTool, + Name: "lookup_weather", + Parts: []sigil.Part{sigil.ToolResultPart(sigil.ToolResult{ + ToolCallID: "call-1", + Name: "lookup_weather", + Content: "sunny, 22C", + ContentJSON: toolResultContent, + })}, + }, + sigil.AssistantTextMessage("It is sunny and 22C in Paris."), }, + Tags: map[string]string{ + "scenario": "full-roundtrip", + }, + Metadata: map[string]any{ + "response_format": "text", + }, + Artifacts: []sigil.Artifact{requestArtifact, responseArtifact}, Usage: sigil.TokenUsage{ InputTokens: 120, - OutputTokens: 42, - TotalTokens: 162, - CacheReadInputTokens: 30, - CacheWriteInputTokens: 7, + OutputTokens: 36, CacheCreationInputTokens: 4, - ReasoningTokens: 9, + CacheReadInputTokens: 5, + CacheWriteInputTokens: 3, + ReasoningTokens: 7, }, StopReason: "end_turn", CompletedAt: completedAt, - Artifacts: []sigil.Artifact{ - { - Kind: sigil.ArtifactKindRequest, - Name: "request.json", - ContentType: "application/json", - Payload: []byte(`{"request":true}`), - RecordID: "rec-request", - URI: "sigil://artifact/request", - }, - { - Kind: sigil.ArtifactKindResponse, - Name: "response.json", - ContentType: "application/json", - Payload: []byte(`{"response":true}`), - RecordID: "rec-response", - URI: "sigil://artifact/response", - }, - }, - } - - _, recorder := env.Client.StartGeneration(context.Background(), start) - recorder.SetResult(result, nil) + }, nil) recorder.End() if err := recorder.Err(); err != nil { t.Fatalf("record generation: %v", err) } + parent.End() + span := findSpan(t, env.Spans.Ended(), conformanceOperationName) - if got := span.Name(); got != "generateText test-model" { - t.Fatalf("unexpected span name: got %q want %q", got, "generateText test-model") + if got := span.Name(); got != "generateText gpt-5" { + t.Fatalf("unexpected span name: got %q want %q", got, "generateText gpt-5") } if got := span.SpanKind(); got != trace.SpanKindClient { t.Fatalf("unexpected span kind: got %v want %v", got, trace.SpanKindClient) } + if span.SpanContext().TraceID() != callSC.TraceID() { + t.Fatalf("unexpected span trace id: got %q want %q", span.SpanContext().TraceID(), callSC.TraceID()) + } + if span.SpanContext().SpanID() != callSC.SpanID() { + t.Fatalf("unexpected span span id: got %q want %q", span.SpanContext().SpanID(), callSC.SpanID()) + } + if span.Parent().SpanID() != parentSC.SpanID() { + t.Fatalf("unexpected parent span id: got %q want %q", span.Parent().SpanID(), parentSC.SpanID()) + } if got := span.Status().Code; got != codes.Ok { t.Fatalf("unexpected span status: got %v want %v", got, codes.Ok) } attrs := spanAttrs(span) requireSpanAttr(t, attrs, spanAttrOperationName, conformanceOperationName) - requireSpanAttr(t, attrs, spanAttrGenerationID, start.ID) - requireSpanAttr(t, attrs, "gen_ai.conversation.id", start.ConversationID) - requireSpanAttr(t, attrs, spanAttrConversationTitle, start.ConversationTitle) - requireSpanAttr(t, attrs, spanAttrUserID, start.UserID) - requireSpanAttr(t, attrs, spanAttrAgentName, start.AgentName) - requireSpanAttr(t, attrs, spanAttrAgentVersion, start.AgentVersion) - requireSpanAttr(t, attrs, spanAttrProviderName, start.Model.Provider) - requireSpanAttr(t, attrs, spanAttrRequestModel, start.Model.Name) + requireSpanAttr(t, attrs, spanAttrGenerationID, "gen-roundtrip-1") + requireSpanAttr(t, attrs, spanAttrConversationID, "conv-roundtrip-1") + requireSpanAttr(t, attrs, spanAttrConversationTitle, "Ticket triage") + requireSpanAttr(t, attrs, spanAttrUserID, "user-42") + requireSpanAttr(t, attrs, spanAttrAgentName, "agent-support") + requireSpanAttr(t, attrs, spanAttrAgentVersion, "v1.2.3") + requireSpanAttr(t, attrs, spanAttrProviderName, conformanceModel.Provider) + requireSpanAttr(t, attrs, spanAttrRequestModel, conformanceModel.Name) + requireSpanAttr(t, attrs, spanAttrResponseID, "resp-7") + requireSpanAttr(t, attrs, spanAttrResponseModel, "gpt-5-2026-03-01") requireSpanAttrInt64(t, attrs, spanAttrRequestMaxTokens, maxTokens) requireSpanAttrFloat64(t, attrs, spanAttrRequestTemperature, temperature) requireSpanAttrFloat64(t, attrs, spanAttrRequestTopP, topP) requireSpanAttr(t, attrs, spanAttrRequestToolChoice, toolChoice) - requireSpanAttrBool(t, attrs, "sigil.gen_ai.request.thinking.enabled", thinkingEnabled) - requireSpanAttrInt64(t, attrs, "sigil.gen_ai.request.thinking.budget_tokens", 2048) - requireSpanAttr(t, attrs, spanAttrResponseID, result.ResponseID) - requireSpanAttr(t, attrs, spanAttrResponseModel, result.ResponseModel) - requireSpanAttrStringSlice(t, attrs, spanAttrFinishReasons, []string{result.StopReason}) - requireSpanAttrInt64(t, attrs, spanAttrInputTokens, result.Usage.InputTokens) - requireSpanAttrInt64(t, attrs, spanAttrOutputTokens, result.Usage.OutputTokens) - requireSpanAttrInt64(t, attrs, spanAttrCacheReadTokens, result.Usage.CacheReadInputTokens) - requireSpanAttrInt64(t, attrs, spanAttrCacheWriteTokens, result.Usage.CacheWriteInputTokens) - requireSpanAttrInt64(t, attrs, spanAttrCacheCreationTokens, result.Usage.CacheCreationInputTokens) - requireSpanAttrInt64(t, attrs, spanAttrReasoningTokens, result.Usage.ReasoningTokens) + requireSpanAttrBool(t, attrs, spanAttrRequestThinkingEnabled, thinkingEnabled) + requireSpanAttrInt64(t, attrs, metadataKeyThinkingBudget, 4096) + requireSpanAttrStringSlice(t, attrs, spanAttrFinishReasons, []string{"end_turn"}) + requireSpanAttrInt64(t, attrs, spanAttrInputTokens, 120) + requireSpanAttrInt64(t, attrs, spanAttrOutputTokens, 36) + requireSpanAttrInt64(t, attrs, spanAttrCacheReadTokens, 5) + requireSpanAttrInt64(t, attrs, spanAttrCacheWriteTokens, 3) + requireSpanAttrInt64(t, attrs, spanAttrCacheCreationTokens, 4) + requireSpanAttrInt64(t, attrs, spanAttrReasoningTokens, 7) requireSpanAttr(t, attrs, metadataKeySDKName, sdkNameGo) + metrics := env.CollectMetrics(t) + duration := findHistogram[float64](t, metrics, metricOperationDuration) + durationPoint := requireHistogramPointWithAttrs(t, duration, map[string]string{ + spanAttrOperationName: conformanceOperationName, + spanAttrProviderName: conformanceModel.Provider, + spanAttrRequestModel: conformanceModel.Name, + spanAttrAgentName: "agent-support", + spanAttrErrorType: "", + spanAttrErrorCategory: "", + }) + if durationPoint.Sum != completedAt.Sub(startedAt).Seconds() { + t.Fatalf("unexpected %s sum: got %v want %v", metricOperationDuration, durationPoint.Sum, completedAt.Sub(startedAt).Seconds()) + } + if durationPoint.Count != 1 { + t.Fatalf("unexpected %s count: got %d want %d", metricOperationDuration, durationPoint.Count, 1) + } + + tokenUsage := findHistogram[int64](t, metrics, metricTokenUsage) + for tokenType, value := range map[string]int64{ + metricTokenTypeInput: 120, + metricTokenTypeOutput: 36, + metricTokenTypeCacheRead: 5, + metricTokenTypeCacheWrite: 3, + metricTokenTypeCacheCreation: 4, + metricTokenTypeReasoning: 7, + } { + requireInt64HistogramSum(t, tokenUsage, map[string]string{ + spanAttrOperationName: conformanceOperationName, + spanAttrProviderName: conformanceModel.Provider, + spanAttrRequestModel: conformanceModel.Name, + spanAttrAgentName: "agent-support", + metricAttrTokenType: tokenType, + }, value) + } + + toolCalls := findHistogram[int64](t, metrics, metricToolCallsPerOperation) + requireInt64HistogramSum(t, toolCalls, map[string]string{ + spanAttrProviderName: conformanceModel.Provider, + spanAttrRequestModel: conformanceModel.Name, + spanAttrAgentName: "agent-support", + }, 1) + requireNoHistogram(t, metrics, metricTimeToFirstToken) + env.Shutdown(t) generation := env.Ingest.SingleGeneration(t) - if got := generation.GetId(); got != start.ID { - t.Fatalf("unexpected proto id: got %q want %q", got, start.ID) + if got := generation.GetId(); got != "gen-roundtrip-1" { + t.Fatalf("unexpected generation id: got %q want %q", got, "gen-roundtrip-1") } - if got := generation.GetConversationId(); got != start.ConversationID { - t.Fatalf("unexpected proto conversation_id: got %q want %q", got, start.ConversationID) + if got := generation.GetConversationId(); got != "conv-roundtrip-1" { + t.Fatalf("unexpected conversation id: got %q want %q", got, "conv-roundtrip-1") } if got := generation.GetOperationName(); got != conformanceOperationName { - t.Fatalf("unexpected proto operation_name: got %q want %q", got, conformanceOperationName) + t.Fatalf("unexpected operation name: got %q want %q", got, conformanceOperationName) } if got := generation.GetMode(); got != sigilv1.GenerationMode_GENERATION_MODE_SYNC { - t.Fatalf("unexpected proto mode: got %v want %v", got, sigilv1.GenerationMode_GENERATION_MODE_SYNC) + t.Fatalf("unexpected generation mode: got %v want %v", got, sigilv1.GenerationMode_GENERATION_MODE_SYNC) + } + if got := generation.GetAgentName(); got != "agent-support" { + t.Fatalf("unexpected agent name: got %q want %q", got, "agent-support") } - if got := generation.GetAgentName(); got != start.AgentName { - t.Fatalf("unexpected proto agent_name: got %q want %q", got, start.AgentName) + if got := generation.GetAgentVersion(); got != "v1.2.3" { + t.Fatalf("unexpected agent version: got %q want %q", got, "v1.2.3") } - if got := generation.GetAgentVersion(); got != start.AgentVersion { - t.Fatalf("unexpected proto agent_version: got %q want %q", got, start.AgentVersion) + if got := generation.GetTraceId(); got != callSC.TraceID().String() { + t.Fatalf("unexpected trace id: got %q want %q", got, callSC.TraceID().String()) } - if got := generation.GetResponseId(); got != result.ResponseID { - t.Fatalf("unexpected proto response_id: got %q want %q", got, result.ResponseID) + if got := generation.GetSpanId(); got != callSC.SpanID().String() { + t.Fatalf("unexpected span id: got %q want %q", got, callSC.SpanID().String()) } - if got := generation.GetResponseModel(); got != result.ResponseModel { - t.Fatalf("unexpected proto response_model: got %q want %q", got, result.ResponseModel) + if got := generation.GetResponseId(); got != "resp-7" { + t.Fatalf("unexpected response id: got %q want %q", got, "resp-7") } - if got := generation.GetSystemPrompt(); got != start.SystemPrompt { - t.Fatalf("unexpected proto system_prompt: got %q want %q", got, start.SystemPrompt) + if got := generation.GetResponseModel(); got != "gpt-5-2026-03-01" { + t.Fatalf("unexpected response model: got %q want %q", got, "gpt-5-2026-03-01") } - if got := generation.GetTraceId(); got != span.SpanContext().TraceID().String() { - t.Fatalf("unexpected proto trace_id: got %q want %q", got, span.SpanContext().TraceID().String()) + if got := generation.GetSystemPrompt(); got != "You are a concise support assistant." { + t.Fatalf("unexpected system prompt: got %q want %q", got, "You are a concise support assistant.") } - if got := generation.GetSpanId(); got != span.SpanContext().SpanID().String() { - t.Fatalf("unexpected proto span_id: got %q want %q", got, span.SpanContext().SpanID().String()) + if got := generation.GetStopReason(); got != "end_turn" { + t.Fatalf("unexpected stop reason: got %q want %q", got, "end_turn") } if !generation.GetStartedAt().AsTime().Equal(startedAt) { - t.Fatalf("unexpected proto started_at: got %s want %s", generation.GetStartedAt().AsTime(), startedAt) + t.Fatalf("unexpected started_at: got %s want %s", generation.GetStartedAt().AsTime(), startedAt) } if !generation.GetCompletedAt().AsTime().Equal(completedAt) { - t.Fatalf("unexpected proto completed_at: got %s want %s", generation.GetCompletedAt().AsTime(), completedAt) + t.Fatalf("unexpected completed_at: got %s want %s", generation.GetCompletedAt().AsTime(), completedAt) } - - if got := generation.GetModel().GetProvider(); got != start.Model.Provider { - t.Fatalf("unexpected proto model.provider: got %q want %q", got, start.Model.Provider) + if got := generation.GetModel().GetProvider(); got != conformanceModel.Provider { + t.Fatalf("unexpected model provider: got %q want %q", got, conformanceModel.Provider) } - if got := generation.GetModel().GetName(); got != start.Model.Name { - t.Fatalf("unexpected proto model.name: got %q want %q", got, start.Model.Name) + if got := generation.GetModel().GetName(); got != conformanceModel.Name { + t.Fatalf("unexpected model name: got %q want %q", got, conformanceModel.Name) } if got := generation.GetMaxTokens(); got != maxTokens { - t.Fatalf("unexpected proto max_tokens: got %d want %d", got, maxTokens) + t.Fatalf("unexpected max_tokens: got %d want %d", got, maxTokens) } if got := generation.GetTemperature(); got != temperature { - t.Fatalf("unexpected proto temperature: got %v want %v", got, temperature) + t.Fatalf("unexpected temperature: got %v want %v", got, temperature) } if got := generation.GetTopP(); got != topP { - t.Fatalf("unexpected proto top_p: got %v want %v", got, topP) + t.Fatalf("unexpected top_p: got %v want %v", got, topP) } if got := generation.GetToolChoice(); got != toolChoice { - t.Fatalf("unexpected proto tool_choice: got %q want %q", got, toolChoice) + t.Fatalf("unexpected tool_choice: got %q want %q", got, toolChoice) } if got := generation.GetThinkingEnabled(); got != thinkingEnabled { - t.Fatalf("unexpected proto thinking_enabled: got %t want %t", got, thinkingEnabled) + t.Fatalf("unexpected thinking_enabled: got %t want %t", got, thinkingEnabled) } - if len(generation.GetTags()) != len(start.Tags) { - t.Fatalf("unexpected proto tags length: got %d want %d", len(generation.GetTags()), len(start.Tags)) + requireProtoMetadata(t, generation, metadataKeyConversation, "Ticket triage") + requireProtoMetadata(t, generation, metadataKeyCanonicalUserID, "user-42") + requireProtoMetadata(t, generation, metadataKeySDKName, sdkNameGo) + requireProtoMetadata(t, generation, "request_id", "req-7") + requireProtoMetadata(t, generation, "response_format", "text") + requireProtoMetadataNumber(t, generation, metadataKeyThinkingBudget, 4096) + + if len(generation.GetTags()) != 2 { + t.Fatalf("expected 2 tags, got %d", len(generation.GetTags())) } - for key, want := range start.Tags { - if got := generation.GetTags()[key]; got != want { - t.Fatalf("unexpected proto tag %q: got %q want %q", key, got, want) - } + if got := generation.GetTags()["suite"]; got != "conformance" { + t.Fatalf("unexpected suite tag: got %q want %q", got, "conformance") + } + if got := generation.GetTags()["scenario"]; got != "full-roundtrip" { + t.Fatalf("unexpected scenario tag: got %q want %q", got, "full-roundtrip") } - - requireProtoMetadata(t, generation, "custom_key", "custom_value") - requireProtoMetadata(t, generation, metadataKeyConversation, start.ConversationTitle) - requireProtoMetadata(t, generation, metadataKeyCanonicalUserID, start.UserID) - requireProtoMetadata(t, generation, metadataKeySDKName, sdkNameGo) - requireProtoMetadataNumber(t, generation, metadataKeyThinkingBudget, 2048) tools := generation.GetTools() if len(tools) != 1 { - t.Fatalf("unexpected proto tools length: got %d want %d", len(tools), 1) + t.Fatalf("expected 1 tool, got %d", len(tools)) } - if got := tools[0].GetName(); got != start.Tools[0].Name { - t.Fatalf("unexpected proto tool name: got %q want %q", got, start.Tools[0].Name) + if got := tools[0].GetName(); got != "lookup_weather" { + t.Fatalf("unexpected tool name: got %q want %q", got, "lookup_weather") } - if got := tools[0].GetDescription(); got != start.Tools[0].Description { - t.Fatalf("unexpected proto tool description: got %q want %q", got, start.Tools[0].Description) + if got := tools[0].GetDescription(); got != "Look up the latest weather conditions" { + t.Fatalf("unexpected tool description: got %q want %q", got, "Look up the latest weather conditions") } - if got := tools[0].GetType(); got != start.Tools[0].Type { - t.Fatalf("unexpected proto tool type: got %q want %q", got, start.Tools[0].Type) + if got := tools[0].GetType(); got != "function" { + t.Fatalf("unexpected tool type: got %q want %q", got, "function") } - if !bytes.Equal(tools[0].GetInputSchemaJson(), start.Tools[0].InputSchema) { - t.Fatalf("unexpected proto tool input schema: got %s want %s", string(tools[0].GetInputSchemaJson()), string(start.Tools[0].InputSchema)) + if !bytes.Equal(tools[0].GetInputSchemaJson(), toolSchema) { + t.Fatalf("unexpected tool input schema: got %s want %s", string(tools[0].GetInputSchemaJson()), string(toolSchema)) } if got := tools[0].GetDeferred(); !got { - t.Fatalf("expected proto tool deferred=true") + t.Fatalf("expected deferred tool definition") } input := generation.GetInput() - if len(input) != 2 { - t.Fatalf("unexpected proto input length: got %d want %d", len(input), 2) + if len(input) != 1 { + t.Fatalf("expected 1 input message, got %d", len(input)) } if got := input[0].GetRole(); got != sigilv1.MessageRole_MESSAGE_ROLE_USER { - t.Fatalf("unexpected first input role: got %v want %v", got, sigilv1.MessageRole_MESSAGE_ROLE_USER) + t.Fatalf("unexpected input role: got %v want %v", got, sigilv1.MessageRole_MESSAGE_ROLE_USER) } - requireProtoTextPart(t, input[0].GetParts()[0], "What's the weather in Paris?") - if got := input[1].GetRole(); got != sigilv1.MessageRole_MESSAGE_ROLE_TOOL { - t.Fatalf("unexpected second input role: got %v want %v", got, sigilv1.MessageRole_MESSAGE_ROLE_TOOL) + if got := input[0].GetName(); got != "customer" { + t.Fatalf("unexpected input name: got %q want %q", got, "customer") } - requireProtoToolResultPart(t, input[1].GetParts()[0], "call-1", "lookupWeather", "18C and clear", []byte(`{"temp_c":18,"condition":"clear"}`), false) + requireProtoTextPart(t, input[0].GetParts()[0], "What's the weather in Paris?") output := generation.GetOutput() - if len(output) != 1 { - t.Fatalf("unexpected proto output length: got %d want %d", len(output), 1) + if len(output) != 3 { + t.Fatalf("expected 3 output messages, got %d", len(output)) } if got := output[0].GetRole(); got != sigilv1.MessageRole_MESSAGE_ROLE_ASSISTANT { - t.Fatalf("unexpected output role: got %v want %v", got, sigilv1.MessageRole_MESSAGE_ROLE_ASSISTANT) + t.Fatalf("unexpected output[0] role: got %v want %v", got, sigilv1.MessageRole_MESSAGE_ROLE_ASSISTANT) } if len(output[0].GetParts()) != 3 { - t.Fatalf("unexpected output part count: got %d want %d", len(output[0].GetParts()), 3) + t.Fatalf("unexpected output[0] part count: got %d want %d", len(output[0].GetParts()), 3) } - requireProtoTextPart(t, output[0].GetParts()[0], "It is 18C and clear.") - requireProtoThinkingPart(t, output[0].GetParts()[1], "Need weather lookup.") - requireProtoToolCallPart(t, output[0].GetParts()[2], "call-1", "lookupWeather", []byte(`{"city":"Paris"}`)) + requireProtoThinkingPart(t, output[0].GetParts()[0], "I have the tool result; compose the final answer.") + requireProtoToolCallPart(t, output[0].GetParts()[1], "call-1", "lookup_weather", toolCallInput) + requireProtoTextPart(t, output[0].GetParts()[2], "It is sunny and 22C in Paris.") + + if got := output[1].GetRole(); got != sigilv1.MessageRole_MESSAGE_ROLE_TOOL { + t.Fatalf("unexpected output[1] role: got %v want %v", got, sigilv1.MessageRole_MESSAGE_ROLE_TOOL) + } + if got := output[1].GetName(); got != "lookup_weather" { + t.Fatalf("unexpected output[1] name: got %q want %q", got, "lookup_weather") + } + requireProtoToolResultPart(t, output[1].GetParts()[0], "call-1", "lookup_weather", "sunny, 22C", toolResultContent, false) + + if got := output[2].GetRole(); got != sigilv1.MessageRole_MESSAGE_ROLE_ASSISTANT { + t.Fatalf("unexpected output[2] role: got %v want %v", got, sigilv1.MessageRole_MESSAGE_ROLE_ASSISTANT) + } + requireProtoTextPart(t, output[2].GetParts()[0], "It is sunny and 22C in Paris.") usage := generation.GetUsage() - if got := usage.GetInputTokens(); got != result.Usage.InputTokens { - t.Fatalf("unexpected proto input_tokens: got %d want %d", got, result.Usage.InputTokens) + if got := usage.GetInputTokens(); got != 120 { + t.Fatalf("unexpected input tokens: got %d want %d", got, 120) } - if got := usage.GetOutputTokens(); got != result.Usage.OutputTokens { - t.Fatalf("unexpected proto output_tokens: got %d want %d", got, result.Usage.OutputTokens) + if got := usage.GetOutputTokens(); got != 36 { + t.Fatalf("unexpected output tokens: got %d want %d", got, 36) } - if got := usage.GetTotalTokens(); got != result.Usage.TotalTokens { - t.Fatalf("unexpected proto total_tokens: got %d want %d", got, result.Usage.TotalTokens) + if got := usage.GetTotalTokens(); got != 156 { + t.Fatalf("unexpected total tokens: got %d want %d", got, 156) } - if got := usage.GetCacheReadInputTokens(); got != result.Usage.CacheReadInputTokens { - t.Fatalf("unexpected proto cache_read_input_tokens: got %d want %d", got, result.Usage.CacheReadInputTokens) + if got := usage.GetCacheReadInputTokens(); got != 5 { + t.Fatalf("unexpected cache read tokens: got %d want %d", got, 5) } - if got := usage.GetCacheWriteInputTokens(); got != result.Usage.CacheWriteInputTokens { - t.Fatalf("unexpected proto cache_write_input_tokens: got %d want %d", got, result.Usage.CacheWriteInputTokens) + if got := usage.GetCacheWriteInputTokens(); got != 3 { + t.Fatalf("unexpected cache write tokens: got %d want %d", got, 3) } - if got := usage.GetCacheCreationInputTokens(); got != result.Usage.CacheCreationInputTokens { - t.Fatalf("unexpected proto cache_creation_input_tokens: got %d want %d", got, result.Usage.CacheCreationInputTokens) + if got := usage.GetCacheCreationInputTokens(); got != 4 { + t.Fatalf("unexpected cache creation tokens: got %d want %d", got, 4) } - if got := usage.GetReasoningTokens(); got != result.Usage.ReasoningTokens { - t.Fatalf("unexpected proto reasoning_tokens: got %d want %d", got, result.Usage.ReasoningTokens) + if got := usage.GetReasoningTokens(); got != 7 { + t.Fatalf("unexpected reasoning tokens: got %d want %d", got, 7) } - if got := generation.GetStopReason(); got != result.StopReason { - t.Fatalf("unexpected proto stop_reason: got %q want %q", got, result.StopReason) + if got := generation.GetCallError(); got != "" { + t.Fatalf("expected empty call error, got %q", got) } artifacts := generation.GetRawArtifacts() if len(artifacts) != 2 { - t.Fatalf("unexpected proto artifacts length: got %d want %d", len(artifacts), 2) + t.Fatalf("expected 2 raw artifacts, got %d", len(artifacts)) } - requireProtoArtifact(t, artifacts[0], sigilv1.ArtifactKind_ARTIFACT_KIND_REQUEST, "request.json", "application/json", []byte(`{"request":true}`), "rec-request", "sigil://artifact/request") - requireProtoArtifact(t, artifacts[1], sigilv1.ArtifactKind_ARTIFACT_KIND_RESPONSE, "response.json", "application/json", []byte(`{"response":true}`), "rec-response", "sigil://artifact/response") - - metrics := env.CollectMetrics(t) - duration := findHistogram[float64](t, metrics, metricOperationDuration) - requireHistogramPointWithAttrs(t, duration, map[string]string{ - spanAttrOperationName: conformanceOperationName, - spanAttrProviderName: start.Model.Provider, - spanAttrRequestModel: start.Model.Name, - spanAttrAgentName: start.AgentName, - }) - - tokenUsage := findHistogram[int64](t, metrics, metricTokenUsage) - requireInt64HistogramSum(t, tokenUsage, map[string]string{ - spanAttrOperationName: conformanceOperationName, - spanAttrProviderName: start.Model.Provider, - spanAttrRequestModel: start.Model.Name, - spanAttrAgentName: start.AgentName, - metricAttrTokenType: metricTokenTypeInput, - }, result.Usage.InputTokens) - requireInt64HistogramSum(t, tokenUsage, map[string]string{ - spanAttrOperationName: conformanceOperationName, - spanAttrProviderName: start.Model.Provider, - spanAttrRequestModel: start.Model.Name, - spanAttrAgentName: start.AgentName, - metricAttrTokenType: metricTokenTypeOutput, - }, result.Usage.OutputTokens) - requireInt64HistogramSum(t, tokenUsage, map[string]string{ - spanAttrOperationName: conformanceOperationName, - spanAttrProviderName: start.Model.Provider, - spanAttrRequestModel: start.Model.Name, - spanAttrAgentName: start.AgentName, - metricAttrTokenType: metricTokenTypeCacheRead, - }, result.Usage.CacheReadInputTokens) - requireInt64HistogramSum(t, tokenUsage, map[string]string{ - spanAttrOperationName: conformanceOperationName, - spanAttrProviderName: start.Model.Provider, - spanAttrRequestModel: start.Model.Name, - spanAttrAgentName: start.AgentName, - metricAttrTokenType: metricTokenTypeCacheWrite, - }, result.Usage.CacheWriteInputTokens) - requireInt64HistogramSum(t, tokenUsage, map[string]string{ - spanAttrOperationName: conformanceOperationName, - spanAttrProviderName: start.Model.Provider, - spanAttrRequestModel: start.Model.Name, - spanAttrAgentName: start.AgentName, - metricAttrTokenType: metricTokenTypeCacheCreation, - }, result.Usage.CacheCreationInputTokens) - requireInt64HistogramSum(t, tokenUsage, map[string]string{ - spanAttrOperationName: conformanceOperationName, - spanAttrProviderName: start.Model.Provider, - spanAttrRequestModel: start.Model.Name, - spanAttrAgentName: start.AgentName, - metricAttrTokenType: metricTokenTypeReasoning, - }, result.Usage.ReasoningTokens) - - toolCalls := findHistogram[int64](t, metrics, metricToolCallsPerOperation) - requireInt64HistogramSum(t, toolCalls, map[string]string{ - spanAttrProviderName: start.Model.Provider, - spanAttrRequestModel: start.Model.Name, - spanAttrAgentName: start.AgentName, - }, 1) - requireNoHistogram(t, metrics, metricTimeToFirstToken) + requireProtoArtifact(t, artifacts[0], sigilv1.ArtifactKind_ARTIFACT_KIND_REQUEST, "request", "application/json", []byte(`{"model":"gpt-5"}`), "", "") + requireProtoArtifact(t, artifacts[1], sigilv1.ArtifactKind_ARTIFACT_KIND_RESPONSE, "response", "application/json", []byte(`{"stop_reason":"end_turn"}`), "", "") } func TestConformance_ConversationTitleSemantics(t *testing.T) { From 069480c1bef027bbfe4f08258cbdaad1dcf2ed8a Mon Sep 17 00:00:00 2001 From: Cyril Tovena Date: Thu, 12 Mar 2026 14:44:09 +0100 Subject: [PATCH 051/133] Add Go SDK full generation roundtrip conformance scenario ## Summary - add `TestConformance_FullGenerationRoundtrip` in `sdks/go/sigil` with raw assertions on exported generation payloads, span attributes, sync metric labels, and trace/span linkage - fix the Go generation export wire shape so `cache_creation_input_tokens` round-trips through the shared generation ingest proto and generated stubs - update Go transport regression coverage and the active SDK conformance execution plan for the completed Scenario 1 slice ## Testing - `cd sdks/go && GOWORK=off go test ./sigil -run '^TestConformance_(FullGenerationRoundtrip|ConversationTitleSemantics|UserIDSemantics|AgentIdentitySemantics)$' -count=1` - `cd sdks/go && GOWORK=off go test ./sigil -run '^TestConformance' -count=1` - `cd sdks/go && GOWORK=off go test ./sigil -count=1` - `cd sigil && GOWORK=off go test ./internal/ingest/generation -count=1` ## Notes - synced latest `origin/main` into the branch before publishing and resolved overlapping conformance/doc updates before rerunning validation --- > [!NOTE] > **Medium Risk** > Mostly test/doc changes, but it also updates generation export/proto plumbing for token usage fields; mismatches here could affect downstream ingestion/analytics if incorrect. > > **Overview** > Adds `TestConformance_FullGenerationRoundtrip` to the Go SDK conformance suite, asserting **end-to-end parity** between recorded generation data and what the SDK emits via OTLP spans/metrics and the gRPC generation export (including trace/span linkage, tool call/result parts, artifacts, tags/metadata merge, and all token counters). > > Fixes export correctness/coverage by ensuring `cache_creation_input_tokens` is represented in generated ingest protobufs (Python regen) and exercised in Go transport roundtrip tests, plus small conformance helper improvements (float comparisons, clearer histogram/span assertion helpers). Docs execution plan is updated to mark the full roundtrip scenario and record the discovered SDK fixes. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 80f032923ccf86c2aa94223a27f8d2b0cd1aa9be. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). --- go/sigil/conformance_helpers_test.go | 58 +- go/sigil/conformance_test.go | 693 ++++++++---------- go/sigil/exporter_transport_test.go | 13 +- go/sigil/proto_mapping.go | 2 +- .../gen/sigil/v1/generation_ingest_pb2.py | 36 +- 5 files changed, 380 insertions(+), 422 deletions(-) diff --git a/go/sigil/conformance_helpers_test.go b/go/sigil/conformance_helpers_test.go index 0380e5a..b6e9fc8 100644 --- a/go/sigil/conformance_helpers_test.go +++ b/go/sigil/conformance_helpers_test.go @@ -3,6 +3,7 @@ package sigil_test import ( "context" "io" + "math" "net" "net/http" "net/http/httptest" @@ -31,6 +32,8 @@ const ( metadataKeyLegacyUserID = "user.id" metadataKeyThinkingBudget = "sigil.gen_ai.request.thinking.budget_tokens" metadataKeySDKName = "sigil.sdk.name" + sdkMetadataKeyName = metadataKeySDKName + sdkNameGo = "sdk-go" spanAttrOperationName = "gen_ai.operation.name" spanAttrGenerationID = "sigil.generation.id" spanAttrConversationID = "gen_ai.conversation.id" @@ -47,6 +50,7 @@ const ( spanAttrRequestTopP = "gen_ai.request.top_p" spanAttrRequestToolChoice = "sigil.gen_ai.request.tool_choice" spanAttrRequestThinkingEnabled = "sigil.gen_ai.request.thinking.enabled" + spanAttrRequestThinkingBudget = metadataKeyThinkingBudget spanAttrEmbeddingInputCount = "gen_ai.embeddings.input_count" spanAttrEmbeddingDimCount = "gen_ai.embeddings.dimension.count" spanAttrToolName = "gen_ai.tool.name" @@ -75,7 +79,6 @@ const ( metricTokenTypeCacheWrite = "cache_write" metricTokenTypeCacheCreation = "cache_creation" metricTokenTypeReasoning = "reasoning" - sdkNameGo = "sdk-go" ) var conformanceModel = sigil.ModelRef{ @@ -450,7 +453,15 @@ func requireSpanAttr(t *testing.T, attrs map[string]attribute.Value, key, want s } } -func requireSpanAttrBool(t *testing.T, attrs map[string]attribute.Value, key string, want bool) { +func requireSpanAttrAbsent(t *testing.T, attrs map[string]attribute.Value, key string) { + t.Helper() + + if _, ok := attrs[key]; ok { + t.Fatalf("did not expect span attribute %q to be present", key) + } +} + +func requireSpanBoolAttr(t *testing.T, attrs map[string]attribute.Value, key string, want bool) { t.Helper() got, ok := attrs[key] @@ -462,7 +473,7 @@ func requireSpanAttrBool(t *testing.T, attrs map[string]attribute.Value, key str } } -func requireSpanAttrInt64(t *testing.T, attrs map[string]attribute.Value, key string, want int64) { +func requireSpanInt64Attr(t *testing.T, attrs map[string]attribute.Value, key string, want int64) { t.Helper() got, ok := attrs[key] @@ -474,19 +485,19 @@ func requireSpanAttrInt64(t *testing.T, attrs map[string]attribute.Value, key st } } -func requireSpanAttrFloat64(t *testing.T, attrs map[string]attribute.Value, key string, want float64) { +func requireSpanFloat64Attr(t *testing.T, attrs map[string]attribute.Value, key string, want float64) { t.Helper() got, ok := attrs[key] if !ok { t.Fatalf("expected span attribute %q=%v, attribute missing", key, want) } - if got.AsFloat64() != want { + if math.Abs(got.AsFloat64()-want) > 1e-9 { t.Fatalf("unexpected span attribute %q: got %v want %v", key, got.AsFloat64(), want) } } -func requireSpanAttrStringSlice(t *testing.T, attrs map[string]attribute.Value, key string, want []string) { +func requireSpanStringSliceAttr(t *testing.T, attrs map[string]attribute.Value, key string, want []string) { t.Helper() got, ok := attrs[key] @@ -495,11 +506,11 @@ func requireSpanAttrStringSlice(t *testing.T, attrs map[string]attribute.Value, } gotSlice := got.AsStringSlice() if len(gotSlice) != len(want) { - t.Fatalf("unexpected span attribute %q length: got %v want %v", key, gotSlice, want) + t.Fatalf("unexpected span attribute %q length: got %d want %d", key, len(gotSlice), len(want)) } for i := range want { if gotSlice[i] != want[i] { - t.Fatalf("unexpected span attribute %q: got %v want %v", key, gotSlice, want) + t.Fatalf("unexpected span attribute %q[%d]: got %q want %q", key, i, gotSlice[i], want[i]) } } } @@ -512,14 +523,6 @@ func requireSpanAttrPresent(t *testing.T, attrs map[string]attribute.Value, key } } -func requireSpanAttrAbsent(t *testing.T, attrs map[string]attribute.Value, key string) { - t.Helper() - - if _, ok := attrs[key]; ok { - t.Fatalf("did not expect span attribute %q to be present", key) - } -} - func findHistogram[N int64 | float64](t *testing.T, collected metricdata.ResourceMetrics, name string) metricdata.Histogram[N] { t.Helper() @@ -555,28 +558,29 @@ func requireNoHistogram(t *testing.T, collected metricdata.ResourceMetrics, name func requireHistogramPointWithAttrs[N int64 | float64](t *testing.T, histogram metricdata.Histogram[N], want map[string]string) metricdata.HistogramDataPoint[N] { t.Helper() + return findHistogramPoint(t, histogram, want) +} + +func findHistogramPoint[N int64 | float64](t *testing.T, histogram metricdata.Histogram[N], want map[string]string) metricdata.HistogramDataPoint[N] { + t.Helper() + for _, point := range histogram.DataPoints { - if pointHasStringAttrs(point.Attributes, want) { + if histogramPointMatches(point.Attributes, want) { return point } } - t.Fatalf("expected histogram datapoint with attrs %v", want) + t.Fatalf("expected histogram point with attrs %v", want) return metricdata.HistogramDataPoint[N]{} } -func pointHasStringAttrs(attrs attribute.Set, want map[string]string) bool { - got := map[string]string{} - for _, kv := range attrs.ToSlice() { - got[string(kv.Key)] = kv.Value.AsString() - } - - for key, wantValue := range want { - if got[key] != wantValue { +func histogramPointMatches(attrs attribute.Set, want map[string]string) bool { + for key, expected := range want { + value, ok := (&attrs).Value(attribute.Key(key)) + if !ok || value.AsString() != expected { return false } } - return true } diff --git a/go/sigil/conformance_test.go b/go/sigil/conformance_test.go index a880109..d4ceae6 100644 --- a/go/sigil/conformance_test.go +++ b/go/sigil/conformance_test.go @@ -12,399 +12,457 @@ import ( sigil "github.com/grafana/sigil/sdks/go/sigil" sigilv1 "github.com/grafana/sigil/sdks/go/sigil/internal/gen/sigil/v1" "go.opentelemetry.io/otel/codes" - "go.opentelemetry.io/otel/sdk/metric/metricdata" "go.opentelemetry.io/otel/trace" ) func TestConformance_FullGenerationRoundtrip(t *testing.T) { - env := newConformanceEnv(t) - - startedAt := time.Date(2026, 3, 12, 8, 0, 0, 0, time.UTC) - completedAt := startedAt.Add(2 * time.Second) - maxTokens := int64(256) - temperature := 0.25 - topP := 0.9 - toolChoice := "required" - thinkingEnabled := true - toolSchema := json.RawMessage(`{"type":"object","properties":{"location":{"type":"string"}},"required":["location"]}`) - toolCallInput := json.RawMessage(`{"location":"Paris"}`) - toolResultContent := json.RawMessage(`{"forecast":"sunny","temp_c":22}`) + startedAt := time.Date(2026, time.March, 12, 11, 0, 0, 0, time.UTC) + completedAt := startedAt.Add(3 * time.Second) requestArtifact, err := sigil.NewJSONArtifact(sigil.ArtifactKindRequest, "request", map[string]any{ - "model": "gpt-5", + "messages": 1, + "tools": 1, }) if err != nil { - t.Fatalf("new request artifact: %v", err) + t.Fatalf("build request artifact: %v", err) } + requestArtifact.RecordID = "rec-request-1" + requestArtifact.URI = "sigil://artifact/request-1" responseArtifact, err := sigil.NewJSONArtifact(sigil.ArtifactKindResponse, "response", map[string]any{ - "stop_reason": "end_turn", + "response_id": "msg_1", + "status": "ok", }) if err != nil { - t.Fatalf("new response artifact: %v", err) + t.Fatalf("build response artifact: %v", err) } + responseArtifact.RecordID = "rec-response-1" + responseArtifact.URI = "sigil://artifact/response-1" - parentCtx, parent := env.tracerProvider.Tracer("sigil-conformance-parent").Start(context.Background(), "parent") - parentSC := parent.SpanContext() + env := newConformanceEnv(t, withConformanceConfig(func(cfg *sigil.Config) { + cfg.Now = func() time.Time { return completedAt } + })) - callCtx, recorder := env.Client.StartGeneration(parentCtx, sigil.GenerationStart{ - ID: "gen-roundtrip-1", - ConversationID: "conv-roundtrip-1", - ConversationTitle: "Ticket triage", + _, recorder := env.Client.StartGeneration(context.Background(), sigil.GenerationStart{ + ID: "gen-full-roundtrip", + ConversationID: "conv-full-roundtrip", + ConversationTitle: "Weather follow-up", UserID: "user-42", - AgentName: "agent-support", - AgentVersion: "v1.2.3", - Model: conformanceModel, - SystemPrompt: "You are a concise support assistant.", - Tools: []sigil.ToolDefinition{{ - Name: "lookup_weather", - Description: "Look up the latest weather conditions", - Type: "function", - InputSchema: toolSchema, - Deferred: true, - }}, - MaxTokens: &maxTokens, - Temperature: &temperature, - TopP: &topP, - ToolChoice: &toolChoice, - ThinkingEnabled: &thinkingEnabled, + AgentName: "assistant-anthropic", + AgentVersion: "1.0.0", + Model: sigil.ModelRef{ + Provider: "anthropic", + Name: "claude-sonnet-4-5", + }, + SystemPrompt: "Answer with a brief explanation and cite the tool result.", + Tools: []sigil.ToolDefinition{ + { + Name: "weather.lookup", + Description: "Look up historical weather by city and date", + Type: "function", + InputSchema: json.RawMessage(`{"type":"object","properties":{"city":{"type":"string"},"date":{"type":"string"}},"required":["city","date"]}`), + Deferred: true, + }, + }, + MaxTokens: int64Ptr(1024), + Temperature: float64Ptr(0.7), + TopP: float64Ptr(0.9), + ToolChoice: stringPtr("required"), + ThinkingEnabled: boolPtr(true), Tags: map[string]string{ - "suite": "conformance", + "env": "prod", + "seed_only": "seed", + "shared": "seed", }, Metadata: map[string]any{ - "request_id": "req-7", - metadataKeyThinkingBudget: int64(4096), + spanAttrRequestThinkingBudget: int64(2048), + "request_only": "seed-value", + "shared": "seed", + "nested": map[string]any{"phase": "seed"}, }, StartedAt: startedAt, }) - callSC := trace.SpanContextFromContext(callCtx) - if !callSC.IsValid() { - t.Fatalf("expected valid generation span context") - } - recorder.SetResult(sigil.Generation{ - ResponseID: "resp-7", - ResponseModel: "gpt-5-2026-03-01", - Input: []sigil.Message{{ - Role: sigil.RoleUser, - Name: "customer", - Parts: []sigil.Part{sigil.TextPart("What's the weather in Paris?")}, - }}, + ResponseID: "msg_1", + ResponseModel: "claude-sonnet-4-5-20260312", + Input: []sigil.Message{ + { + Role: sigil.RoleUser, + Name: "customer", + Parts: []sigil.Part{ + sigil.TextPart("Summarize yesterday's Paris weather and explain the spikes."), + }, + }, + }, Output: []sigil.Message{ { Role: sigil.RoleAssistant, + Name: "assistant", Parts: []sigil.Part{ - sigil.ThinkingPart("I have the tool result; compose the final answer."), - sigil.ToolCallPart(sigil.ToolCall{ - ID: "call-1", - Name: "lookup_weather", - InputJSON: toolCallInput, - }), - sigil.TextPart("It is sunny and 22C in Paris."), + { + Kind: sigil.PartKindThinking, + Thinking: "Need the weather tool output before the final answer.", + Metadata: sigil.PartMetadata{ProviderType: "thinking"}, + }, + { + Kind: sigil.PartKindToolCall, + ToolCall: &sigil.ToolCall{ + ID: "call-weather-1", + Name: "weather.lookup", + InputJSON: json.RawMessage(`{"city":"Paris","date":"2026-03-11"}`), + }, + Metadata: sigil.PartMetadata{ProviderType: "tool_use"}, + }, }, }, { Role: sigil.RoleTool, - Name: "lookup_weather", - Parts: []sigil.Part{sigil.ToolResultPart(sigil.ToolResult{ - ToolCallID: "call-1", - Name: "lookup_weather", - Content: "sunny, 22C", - ContentJSON: toolResultContent, - })}, + Name: "weather.lookup", + Parts: []sigil.Part{ + { + Kind: sigil.PartKindToolResult, + ToolResult: &sigil.ToolResult{ + ToolCallID: "call-weather-1", + Name: "weather.lookup", + Content: "22C with a late-afternoon drop", + ContentJSON: json.RawMessage(`{"high_c":22,"trend":"late drop"}`), + }, + }, + }, }, - sigil.AssistantTextMessage("It is sunny and 22C in Paris."), + { + Role: sigil.RoleAssistant, + Name: "assistant", + Parts: []sigil.Part{ + sigil.TextPart("Paris peaked at 22C before a late drop as cloud cover moved in."), + }, + }, + }, + Usage: sigil.TokenUsage{ + InputTokens: 120, + OutputTokens: 42, + TotalTokens: 162, + CacheReadInputTokens: 30, + CacheWriteInputTokens: 4, + CacheCreationInputTokens: 6, + ReasoningTokens: 9, }, + StopReason: "end_turn", Tags: map[string]string{ - "scenario": "full-roundtrip", + "shared": "result", + "result_only": "assistant", }, Metadata: map[string]any{ - "response_format": "text", + "shared": "result", + "result_only": "assistant", + "nested": map[string]any{"phase": "result"}, + "quality": true, }, Artifacts: []sigil.Artifact{requestArtifact, responseArtifact}, - Usage: sigil.TokenUsage{ - InputTokens: 120, - OutputTokens: 36, - CacheCreationInputTokens: 4, - CacheReadInputTokens: 5, - CacheWriteInputTokens: 3, - ReasoningTokens: 7, - }, - StopReason: "end_turn", - CompletedAt: completedAt, }, nil) recorder.End() if err := recorder.Err(); err != nil { - t.Fatalf("record generation: %v", err) + t.Fatalf("record full generation roundtrip: %v", err) } - parent.End() + metrics := env.CollectMetrics(t) + env.Shutdown(t) span := findSpan(t, env.Spans.Ended(), conformanceOperationName) - if got := span.Name(); got != "generateText gpt-5" { - t.Fatalf("unexpected span name: got %q want %q", got, "generateText gpt-5") + if got := span.Name(); got != "generateText claude-sonnet-4-5" { + t.Fatalf("unexpected span name: got %q want %q", got, "generateText claude-sonnet-4-5") } if got := span.SpanKind(); got != trace.SpanKindClient { t.Fatalf("unexpected span kind: got %v want %v", got, trace.SpanKindClient) } - if span.SpanContext().TraceID() != callSC.TraceID() { - t.Fatalf("unexpected span trace id: got %q want %q", span.SpanContext().TraceID(), callSC.TraceID()) - } - if span.SpanContext().SpanID() != callSC.SpanID() { - t.Fatalf("unexpected span span id: got %q want %q", span.SpanContext().SpanID(), callSC.SpanID()) - } - if span.Parent().SpanID() != parentSC.SpanID() { - t.Fatalf("unexpected parent span id: got %q want %q", span.Parent().SpanID(), parentSC.SpanID()) - } if got := span.Status().Code; got != codes.Ok { t.Fatalf("unexpected span status: got %v want %v", got, codes.Ok) } attrs := spanAttrs(span) - requireSpanAttr(t, attrs, spanAttrOperationName, conformanceOperationName) - requireSpanAttr(t, attrs, spanAttrGenerationID, "gen-roundtrip-1") - requireSpanAttr(t, attrs, spanAttrConversationID, "conv-roundtrip-1") - requireSpanAttr(t, attrs, spanAttrConversationTitle, "Ticket triage") + requireSpanAttr(t, attrs, spanAttrGenerationID, "gen-full-roundtrip") + requireSpanAttr(t, attrs, spanAttrConversationID, "conv-full-roundtrip") + requireSpanAttr(t, attrs, spanAttrConversationTitle, "Weather follow-up") requireSpanAttr(t, attrs, spanAttrUserID, "user-42") - requireSpanAttr(t, attrs, spanAttrAgentName, "agent-support") - requireSpanAttr(t, attrs, spanAttrAgentVersion, "v1.2.3") - requireSpanAttr(t, attrs, spanAttrProviderName, conformanceModel.Provider) - requireSpanAttr(t, attrs, spanAttrRequestModel, conformanceModel.Name) - requireSpanAttr(t, attrs, spanAttrResponseID, "resp-7") - requireSpanAttr(t, attrs, spanAttrResponseModel, "gpt-5-2026-03-01") - requireSpanAttrInt64(t, attrs, spanAttrRequestMaxTokens, maxTokens) - requireSpanAttrFloat64(t, attrs, spanAttrRequestTemperature, temperature) - requireSpanAttrFloat64(t, attrs, spanAttrRequestTopP, topP) - requireSpanAttr(t, attrs, spanAttrRequestToolChoice, toolChoice) - requireSpanAttrBool(t, attrs, spanAttrRequestThinkingEnabled, thinkingEnabled) - requireSpanAttrInt64(t, attrs, metadataKeyThinkingBudget, 4096) - requireSpanAttrStringSlice(t, attrs, spanAttrFinishReasons, []string{"end_turn"}) - requireSpanAttrInt64(t, attrs, spanAttrInputTokens, 120) - requireSpanAttrInt64(t, attrs, spanAttrOutputTokens, 36) - requireSpanAttrInt64(t, attrs, spanAttrCacheReadTokens, 5) - requireSpanAttrInt64(t, attrs, spanAttrCacheWriteTokens, 3) - requireSpanAttrInt64(t, attrs, spanAttrCacheCreationTokens, 4) - requireSpanAttrInt64(t, attrs, spanAttrReasoningTokens, 7) - requireSpanAttr(t, attrs, metadataKeySDKName, sdkNameGo) + requireSpanAttr(t, attrs, spanAttrAgentName, "assistant-anthropic") + requireSpanAttr(t, attrs, spanAttrAgentVersion, "1.0.0") + requireSpanAttr(t, attrs, spanAttrProviderName, "anthropic") + requireSpanAttr(t, attrs, spanAttrRequestModel, "claude-sonnet-4-5") + requireSpanAttr(t, attrs, spanAttrResponseID, "msg_1") + requireSpanAttr(t, attrs, spanAttrResponseModel, "claude-sonnet-4-5-20260312") + requireSpanAttr(t, attrs, sdkMetadataKeyName, "sdk-go") + requireSpanInt64Attr(t, attrs, spanAttrRequestMaxTokens, 1024) + requireSpanFloat64Attr(t, attrs, spanAttrRequestTemperature, 0.7) + requireSpanFloat64Attr(t, attrs, spanAttrRequestTopP, 0.9) + requireSpanAttr(t, attrs, spanAttrRequestToolChoice, "required") + requireSpanBoolAttr(t, attrs, spanAttrRequestThinkingEnabled, true) + requireSpanInt64Attr(t, attrs, spanAttrRequestThinkingBudget, 2048) + requireSpanStringSliceAttr(t, attrs, spanAttrFinishReasons, []string{"end_turn"}) + requireSpanInt64Attr(t, attrs, spanAttrInputTokens, 120) + requireSpanInt64Attr(t, attrs, spanAttrOutputTokens, 42) + requireSpanInt64Attr(t, attrs, spanAttrCacheReadTokens, 30) + requireSpanInt64Attr(t, attrs, spanAttrCacheWriteTokens, 4) + requireSpanInt64Attr(t, attrs, spanAttrCacheCreationTokens, 6) + requireSpanInt64Attr(t, attrs, spanAttrReasoningTokens, 9) - metrics := env.CollectMetrics(t) duration := findHistogram[float64](t, metrics, metricOperationDuration) - durationPoint := requireHistogramPointWithAttrs(t, duration, map[string]string{ + durationPoint := findHistogramPoint(t, duration, map[string]string{ spanAttrOperationName: conformanceOperationName, - spanAttrProviderName: conformanceModel.Provider, - spanAttrRequestModel: conformanceModel.Name, - spanAttrAgentName: "agent-support", + spanAttrProviderName: "anthropic", + spanAttrRequestModel: "claude-sonnet-4-5", + spanAttrAgentName: "assistant-anthropic", spanAttrErrorType: "", spanAttrErrorCategory: "", }) - if durationPoint.Sum != completedAt.Sub(startedAt).Seconds() { - t.Fatalf("unexpected %s sum: got %v want %v", metricOperationDuration, durationPoint.Sum, completedAt.Sub(startedAt).Seconds()) - } if durationPoint.Count != 1 { t.Fatalf("unexpected %s count: got %d want %d", metricOperationDuration, durationPoint.Count, 1) } + if durationPoint.Sum != 3 { + t.Fatalf("unexpected %s sum: got %v want %v", metricOperationDuration, durationPoint.Sum, 3.0) + } tokenUsage := findHistogram[int64](t, metrics, metricTokenUsage) - for tokenType, value := range map[string]int64{ + for tokenType, want := range map[string]int64{ metricTokenTypeInput: 120, - metricTokenTypeOutput: 36, - metricTokenTypeCacheRead: 5, - metricTokenTypeCacheWrite: 3, - metricTokenTypeCacheCreation: 4, - metricTokenTypeReasoning: 7, + metricTokenTypeOutput: 42, + metricTokenTypeCacheRead: 30, + metricTokenTypeCacheWrite: 4, + metricTokenTypeCacheCreation: 6, + metricTokenTypeReasoning: 9, } { - requireInt64HistogramSum(t, tokenUsage, map[string]string{ + point := findHistogramPoint(t, tokenUsage, map[string]string{ spanAttrOperationName: conformanceOperationName, - spanAttrProviderName: conformanceModel.Provider, - spanAttrRequestModel: conformanceModel.Name, - spanAttrAgentName: "agent-support", + spanAttrProviderName: "anthropic", + spanAttrRequestModel: "claude-sonnet-4-5", + spanAttrAgentName: "assistant-anthropic", metricAttrTokenType: tokenType, - }, value) + }) + if point.Count != 1 { + t.Fatalf("unexpected %s count for token type %q: got %d want %d", metricTokenUsage, tokenType, point.Count, 1) + } + if point.Sum != want { + t.Fatalf("unexpected %s sum for token type %q: got %d want %d", metricTokenUsage, tokenType, point.Sum, want) + } } toolCalls := findHistogram[int64](t, metrics, metricToolCallsPerOperation) - requireInt64HistogramSum(t, toolCalls, map[string]string{ - spanAttrProviderName: conformanceModel.Provider, - spanAttrRequestModel: conformanceModel.Name, - spanAttrAgentName: "agent-support", - }, 1) + toolPoint := findHistogramPoint(t, toolCalls, map[string]string{ + spanAttrProviderName: "anthropic", + spanAttrRequestModel: "claude-sonnet-4-5", + spanAttrAgentName: "assistant-anthropic", + }) + if toolPoint.Count != 1 { + t.Fatalf("unexpected %s count: got %d want %d", metricToolCallsPerOperation, toolPoint.Count, 1) + } + if toolPoint.Sum != 1 { + t.Fatalf("unexpected %s sum: got %d want %d", metricToolCallsPerOperation, toolPoint.Sum, 1) + } requireNoHistogram(t, metrics, metricTimeToFirstToken) - env.Shutdown(t) - generation := env.Ingest.SingleGeneration(t) - if got := generation.GetId(); got != "gen-roundtrip-1" { - t.Fatalf("unexpected generation id: got %q want %q", got, "gen-roundtrip-1") + if got := generation.GetId(); got != "gen-full-roundtrip" { + t.Fatalf("unexpected proto generation id: got %q want %q", got, "gen-full-roundtrip") } - if got := generation.GetConversationId(); got != "conv-roundtrip-1" { - t.Fatalf("unexpected conversation id: got %q want %q", got, "conv-roundtrip-1") + if got := generation.GetConversationId(); got != "conv-full-roundtrip" { + t.Fatalf("unexpected proto conversation id: got %q want %q", got, "conv-full-roundtrip") } if got := generation.GetOperationName(); got != conformanceOperationName { - t.Fatalf("unexpected operation name: got %q want %q", got, conformanceOperationName) + t.Fatalf("unexpected proto operation name: got %q want %q", got, conformanceOperationName) } if got := generation.GetMode(); got != sigilv1.GenerationMode_GENERATION_MODE_SYNC { - t.Fatalf("unexpected generation mode: got %v want %v", got, sigilv1.GenerationMode_GENERATION_MODE_SYNC) + t.Fatalf("unexpected proto mode: got %s want %s", got, sigilv1.GenerationMode_GENERATION_MODE_SYNC) } - if got := generation.GetAgentName(); got != "agent-support" { - t.Fatalf("unexpected agent name: got %q want %q", got, "agent-support") + if got := generation.GetTraceId(); got != span.SpanContext().TraceID().String() { + t.Fatalf("unexpected proto trace_id: got %q want %q", got, span.SpanContext().TraceID().String()) } - if got := generation.GetAgentVersion(); got != "v1.2.3" { - t.Fatalf("unexpected agent version: got %q want %q", got, "v1.2.3") + if got := generation.GetSpanId(); got != span.SpanContext().SpanID().String() { + t.Fatalf("unexpected proto span_id: got %q want %q", got, span.SpanContext().SpanID().String()) } - if got := generation.GetTraceId(); got != callSC.TraceID().String() { - t.Fatalf("unexpected trace id: got %q want %q", got, callSC.TraceID().String()) + if got := generation.GetAgentName(); got != "assistant-anthropic" { + t.Fatalf("unexpected proto agent_name: got %q want %q", got, "assistant-anthropic") } - if got := generation.GetSpanId(); got != callSC.SpanID().String() { - t.Fatalf("unexpected span id: got %q want %q", got, callSC.SpanID().String()) + if got := generation.GetAgentVersion(); got != "1.0.0" { + t.Fatalf("unexpected proto agent_version: got %q want %q", got, "1.0.0") } - if got := generation.GetResponseId(); got != "resp-7" { - t.Fatalf("unexpected response id: got %q want %q", got, "resp-7") + if got := generation.GetModel().GetProvider(); got != "anthropic" { + t.Fatalf("unexpected proto model provider: got %q want %q", got, "anthropic") } - if got := generation.GetResponseModel(); got != "gpt-5-2026-03-01" { - t.Fatalf("unexpected response model: got %q want %q", got, "gpt-5-2026-03-01") + if got := generation.GetModel().GetName(); got != "claude-sonnet-4-5" { + t.Fatalf("unexpected proto model name: got %q want %q", got, "claude-sonnet-4-5") } - if got := generation.GetSystemPrompt(); got != "You are a concise support assistant." { - t.Fatalf("unexpected system prompt: got %q want %q", got, "You are a concise support assistant.") + if got := generation.GetResponseId(); got != "msg_1" { + t.Fatalf("unexpected proto response_id: got %q want %q", got, "msg_1") } - if got := generation.GetStopReason(); got != "end_turn" { - t.Fatalf("unexpected stop reason: got %q want %q", got, "end_turn") - } - if !generation.GetStartedAt().AsTime().Equal(startedAt) { - t.Fatalf("unexpected started_at: got %s want %s", generation.GetStartedAt().AsTime(), startedAt) + if got := generation.GetResponseModel(); got != "claude-sonnet-4-5-20260312" { + t.Fatalf("unexpected proto response_model: got %q want %q", got, "claude-sonnet-4-5-20260312") } - if !generation.GetCompletedAt().AsTime().Equal(completedAt) { - t.Fatalf("unexpected completed_at: got %s want %s", generation.GetCompletedAt().AsTime(), completedAt) + if got := generation.GetSystemPrompt(); got != "Answer with a brief explanation and cite the tool result." { + t.Fatalf("unexpected proto system_prompt: got %q", got) } - if got := generation.GetModel().GetProvider(); got != conformanceModel.Provider { - t.Fatalf("unexpected model provider: got %q want %q", got, conformanceModel.Provider) + if got := generation.GetStopReason(); got != "end_turn" { + t.Fatalf("unexpected proto stop_reason: got %q want %q", got, "end_turn") } - if got := generation.GetModel().GetName(); got != conformanceModel.Name { - t.Fatalf("unexpected model name: got %q want %q", got, conformanceModel.Name) + if got := generation.GetMaxTokens(); got != 1024 { + t.Fatalf("unexpected proto max_tokens: got %d want %d", got, 1024) } - if got := generation.GetMaxTokens(); got != maxTokens { - t.Fatalf("unexpected max_tokens: got %d want %d", got, maxTokens) + if got := generation.GetTemperature(); got != 0.7 { + t.Fatalf("unexpected proto temperature: got %v want %v", got, 0.7) } - if got := generation.GetTemperature(); got != temperature { - t.Fatalf("unexpected temperature: got %v want %v", got, temperature) + if got := generation.GetTopP(); got != 0.9 { + t.Fatalf("unexpected proto top_p: got %v want %v", got, 0.9) } - if got := generation.GetTopP(); got != topP { - t.Fatalf("unexpected top_p: got %v want %v", got, topP) + if got := generation.GetToolChoice(); got != "required" { + t.Fatalf("unexpected proto tool_choice: got %q want %q", got, "required") } - if got := generation.GetToolChoice(); got != toolChoice { - t.Fatalf("unexpected tool_choice: got %q want %q", got, toolChoice) + if got := generation.GetThinkingEnabled(); !got { + t.Fatalf("unexpected proto thinking_enabled: got %t want %t", got, true) } - if got := generation.GetThinkingEnabled(); got != thinkingEnabled { - t.Fatalf("unexpected thinking_enabled: got %t want %t", got, thinkingEnabled) + if got := generation.GetCallError(); got != "" { + t.Fatalf("expected empty proto call_error, got %q", got) } - requireProtoMetadata(t, generation, metadataKeyConversation, "Ticket triage") - requireProtoMetadata(t, generation, metadataKeyCanonicalUserID, "user-42") - requireProtoMetadata(t, generation, metadataKeySDKName, sdkNameGo) - requireProtoMetadata(t, generation, "request_id", "req-7") - requireProtoMetadata(t, generation, "response_format", "text") - requireProtoMetadataNumber(t, generation, metadataKeyThinkingBudget, 4096) - - if len(generation.GetTags()) != 2 { - t.Fatalf("expected 2 tags, got %d", len(generation.GetTags())) + if got := generation.GetStartedAt().AsTime(); !got.Equal(startedAt) { + t.Fatalf("unexpected proto started_at: got %s want %s", got, startedAt) + } + if got := generation.GetCompletedAt().AsTime(); !got.Equal(completedAt) { + t.Fatalf("unexpected proto completed_at: got %s want %s", got, completedAt) } - if got := generation.GetTags()["suite"]; got != "conformance" { - t.Fatalf("unexpected suite tag: got %q want %q", got, "conformance") + + if len(generation.GetInput()) != 1 { + t.Fatalf("expected 1 proto input message, got %d", len(generation.GetInput())) } - if got := generation.GetTags()["scenario"]; got != "full-roundtrip" { - t.Fatalf("unexpected scenario tag: got %q want %q", got, "full-roundtrip") + if input := generation.GetInput()[0]; input.GetRole() != sigilv1.MessageRole_MESSAGE_ROLE_USER || input.GetName() != "customer" || len(input.GetParts()) != 1 || input.GetParts()[0].GetText() != "Summarize yesterday's Paris weather and explain the spikes." { + t.Fatalf("unexpected proto input message: %#v", input) } - tools := generation.GetTools() - if len(tools) != 1 { - t.Fatalf("expected 1 tool, got %d", len(tools)) + if len(generation.GetOutput()) != 3 { + t.Fatalf("expected 3 proto output messages, got %d", len(generation.GetOutput())) + } + firstOutput := generation.GetOutput()[0] + if firstOutput.GetRole() != sigilv1.MessageRole_MESSAGE_ROLE_ASSISTANT || firstOutput.GetName() != "assistant" || len(firstOutput.GetParts()) != 2 { + t.Fatalf("unexpected first proto output message: %#v", firstOutput) } - if got := tools[0].GetName(); got != "lookup_weather" { - t.Fatalf("unexpected tool name: got %q want %q", got, "lookup_weather") + if got := firstOutput.GetParts()[0].GetThinking(); got != "Need the weather tool output before the final answer." { + t.Fatalf("unexpected proto thinking part: got %q", got) } - if got := tools[0].GetDescription(); got != "Look up the latest weather conditions" { - t.Fatalf("unexpected tool description: got %q want %q", got, "Look up the latest weather conditions") + if got := firstOutput.GetParts()[0].GetMetadata().GetProviderType(); got != "thinking" { + t.Fatalf("unexpected proto thinking provider_type: got %q want %q", got, "thinking") } - if got := tools[0].GetType(); got != "function" { - t.Fatalf("unexpected tool type: got %q want %q", got, "function") + if got := firstOutput.GetParts()[1].GetToolCall().GetId(); got != "call-weather-1" { + t.Fatalf("unexpected proto tool call id: got %q want %q", got, "call-weather-1") } - if !bytes.Equal(tools[0].GetInputSchemaJson(), toolSchema) { - t.Fatalf("unexpected tool input schema: got %s want %s", string(tools[0].GetInputSchemaJson()), string(toolSchema)) + if got := firstOutput.GetParts()[1].GetToolCall().GetName(); got != "weather.lookup" { + t.Fatalf("unexpected proto tool call name: got %q want %q", got, "weather.lookup") } - if got := tools[0].GetDeferred(); !got { - t.Fatalf("expected deferred tool definition") + if !bytes.Equal(firstOutput.GetParts()[1].GetToolCall().GetInputJson(), []byte(`{"city":"Paris","date":"2026-03-11"}`)) { + t.Fatalf("unexpected proto tool call input json: %s", firstOutput.GetParts()[1].GetToolCall().GetInputJson()) + } + if got := firstOutput.GetParts()[1].GetMetadata().GetProviderType(); got != "tool_use" { + t.Fatalf("unexpected proto tool call provider_type: got %q want %q", got, "tool_use") } - input := generation.GetInput() - if len(input) != 1 { - t.Fatalf("expected 1 input message, got %d", len(input)) + secondOutput := generation.GetOutput()[1] + if secondOutput.GetRole() != sigilv1.MessageRole_MESSAGE_ROLE_TOOL || secondOutput.GetName() != "weather.lookup" || len(secondOutput.GetParts()) != 1 { + t.Fatalf("unexpected second proto output message: %#v", secondOutput) } - if got := input[0].GetRole(); got != sigilv1.MessageRole_MESSAGE_ROLE_USER { - t.Fatalf("unexpected input role: got %v want %v", got, sigilv1.MessageRole_MESSAGE_ROLE_USER) + if got := secondOutput.GetParts()[0].GetToolResult().GetToolCallId(); got != "call-weather-1" { + t.Fatalf("unexpected proto tool result tool_call_id: got %q want %q", got, "call-weather-1") } - if got := input[0].GetName(); got != "customer" { - t.Fatalf("unexpected input name: got %q want %q", got, "customer") + if got := secondOutput.GetParts()[0].GetToolResult().GetName(); got != "weather.lookup" { + t.Fatalf("unexpected proto tool result name: got %q want %q", got, "weather.lookup") } - requireProtoTextPart(t, input[0].GetParts()[0], "What's the weather in Paris?") - - output := generation.GetOutput() - if len(output) != 3 { - t.Fatalf("expected 3 output messages, got %d", len(output)) + if got := secondOutput.GetParts()[0].GetToolResult().GetContent(); got != "22C with a late-afternoon drop" { + t.Fatalf("unexpected proto tool result content: got %q", got) } - if got := output[0].GetRole(); got != sigilv1.MessageRole_MESSAGE_ROLE_ASSISTANT { - t.Fatalf("unexpected output[0] role: got %v want %v", got, sigilv1.MessageRole_MESSAGE_ROLE_ASSISTANT) + if !bytes.Equal(secondOutput.GetParts()[0].GetToolResult().GetContentJson(), []byte(`{"high_c":22,"trend":"late drop"}`)) { + t.Fatalf("unexpected proto tool result content json: %s", secondOutput.GetParts()[0].GetToolResult().GetContentJson()) } - if len(output[0].GetParts()) != 3 { - t.Fatalf("unexpected output[0] part count: got %d want %d", len(output[0].GetParts()), 3) + if secondOutput.GetParts()[0].GetToolResult().GetIsError() { + t.Fatalf("expected successful proto tool result") } - requireProtoThinkingPart(t, output[0].GetParts()[0], "I have the tool result; compose the final answer.") - requireProtoToolCallPart(t, output[0].GetParts()[1], "call-1", "lookup_weather", toolCallInput) - requireProtoTextPart(t, output[0].GetParts()[2], "It is sunny and 22C in Paris.") - if got := output[1].GetRole(); got != sigilv1.MessageRole_MESSAGE_ROLE_TOOL { - t.Fatalf("unexpected output[1] role: got %v want %v", got, sigilv1.MessageRole_MESSAGE_ROLE_TOOL) + thirdOutput := generation.GetOutput()[2] + if thirdOutput.GetRole() != sigilv1.MessageRole_MESSAGE_ROLE_ASSISTANT || thirdOutput.GetName() != "assistant" || len(thirdOutput.GetParts()) != 1 { + t.Fatalf("unexpected third proto output message: %#v", thirdOutput) } - if got := output[1].GetName(); got != "lookup_weather" { - t.Fatalf("unexpected output[1] name: got %q want %q", got, "lookup_weather") + if got := thirdOutput.GetParts()[0].GetText(); got != "Paris peaked at 22C before a late drop as cloud cover moved in." { + t.Fatalf("unexpected proto output text: got %q", got) } - requireProtoToolResultPart(t, output[1].GetParts()[0], "call-1", "lookup_weather", "sunny, 22C", toolResultContent, false) - if got := output[2].GetRole(); got != sigilv1.MessageRole_MESSAGE_ROLE_ASSISTANT { - t.Fatalf("unexpected output[2] role: got %v want %v", got, sigilv1.MessageRole_MESSAGE_ROLE_ASSISTANT) + if len(generation.GetTools()) != 1 { + t.Fatalf("expected 1 proto tool definition, got %d", len(generation.GetTools())) + } + tool := generation.GetTools()[0] + if tool.GetName() != "weather.lookup" || tool.GetDescription() != "Look up historical weather by city and date" || tool.GetType() != "function" || !tool.GetDeferred() { + t.Fatalf("unexpected proto tool definition: %#v", tool) + } + if !bytes.Equal(tool.GetInputSchemaJson(), []byte(`{"type":"object","properties":{"city":{"type":"string"},"date":{"type":"string"}},"required":["city","date"]}`)) { + t.Fatalf("unexpected proto tool input schema: %s", tool.GetInputSchemaJson()) } - requireProtoTextPart(t, output[2].GetParts()[0], "It is sunny and 22C in Paris.") usage := generation.GetUsage() - if got := usage.GetInputTokens(); got != 120 { - t.Fatalf("unexpected input tokens: got %d want %d", got, 120) + if usage.GetInputTokens() != 120 || usage.GetOutputTokens() != 42 || usage.GetTotalTokens() != 162 || usage.GetCacheReadInputTokens() != 30 || usage.GetCacheWriteInputTokens() != 4 || usage.GetReasoningTokens() != 9 || usage.GetCacheCreationInputTokens() != 6 { + t.Fatalf("unexpected proto usage: %#v", usage) } - if got := usage.GetOutputTokens(); got != 36 { - t.Fatalf("unexpected output tokens: got %d want %d", got, 36) + + if len(generation.GetTags()) != 4 { + t.Fatalf("expected 4 proto tags, got %d", len(generation.GetTags())) + } + if got := generation.GetTags()["env"]; got != "prod" { + t.Fatalf("unexpected proto tag env: got %q want %q", got, "prod") } - if got := usage.GetTotalTokens(); got != 156 { - t.Fatalf("unexpected total tokens: got %d want %d", got, 156) + if got := generation.GetTags()["seed_only"]; got != "seed" { + t.Fatalf("unexpected proto tag seed_only: got %q want %q", got, "seed") } - if got := usage.GetCacheReadInputTokens(); got != 5 { - t.Fatalf("unexpected cache read tokens: got %d want %d", got, 5) + if got := generation.GetTags()["shared"]; got != "result" { + t.Fatalf("unexpected proto tag shared: got %q want %q", got, "result") } - if got := usage.GetCacheWriteInputTokens(); got != 3 { - t.Fatalf("unexpected cache write tokens: got %d want %d", got, 3) + if got := generation.GetTags()["result_only"]; got != "assistant" { + t.Fatalf("unexpected proto tag result_only: got %q want %q", got, "assistant") } - if got := usage.GetCacheCreationInputTokens(); got != 4 { - t.Fatalf("unexpected cache creation tokens: got %d want %d", got, 4) + + metadata := generation.GetMetadata().AsMap() + if got := metadata[sdkMetadataKeyName]; got != "sdk-go" { + t.Fatalf("unexpected proto metadata %q: got %#v want %#v", sdkMetadataKeyName, got, "sdk-go") } - if got := usage.GetReasoningTokens(); got != 7 { - t.Fatalf("unexpected reasoning tokens: got %d want %d", got, 7) + if got := metadata[metadataKeyConversation]; got != "Weather follow-up" { + t.Fatalf("unexpected proto metadata %q: got %#v want %#v", metadataKeyConversation, got, "Weather follow-up") } - if got := generation.GetCallError(); got != "" { - t.Fatalf("expected empty call error, got %q", got) + if got := metadata[metadataKeyCanonicalUserID]; got != "user-42" { + t.Fatalf("unexpected proto metadata %q: got %#v want %#v", metadataKeyCanonicalUserID, got, "user-42") + } + if got := metadata[spanAttrRequestThinkingBudget]; got != float64(2048) { + t.Fatalf("unexpected proto metadata %q: got %#v want %#v", spanAttrRequestThinkingBudget, got, float64(2048)) + } + if got := metadata["request_only"]; got != "seed-value" { + t.Fatalf("unexpected proto metadata request_only: got %#v want %#v", got, "seed-value") + } + if got := metadata["shared"]; got != "result" { + t.Fatalf("unexpected proto metadata shared: got %#v want %#v", got, "result") + } + if got := metadata["result_only"]; got != "assistant" { + t.Fatalf("unexpected proto metadata result_only: got %#v want %#v", got, "assistant") + } + if got := metadata["quality"]; got != true { + t.Fatalf("unexpected proto metadata quality: got %#v want %#v", got, true) + } + nested, ok := metadata["nested"].(map[string]any) + if !ok { + t.Fatalf("expected nested proto metadata map, got %#v", metadata["nested"]) + } + if got := nested["phase"]; got != "result" { + t.Fatalf("unexpected proto nested metadata phase: got %#v want %#v", got, "result") } - artifacts := generation.GetRawArtifacts() - if len(artifacts) != 2 { - t.Fatalf("expected 2 raw artifacts, got %d", len(artifacts)) + if len(generation.GetRawArtifacts()) != 2 { + t.Fatalf("expected 2 proto artifacts, got %d", len(generation.GetRawArtifacts())) + } + if artifact := generation.GetRawArtifacts()[0]; artifact.GetKind() != sigilv1.ArtifactKind_ARTIFACT_KIND_REQUEST || artifact.GetName() != "request" || artifact.GetContentType() != "application/json" || artifact.GetRecordId() != "rec-request-1" || artifact.GetUri() != "sigil://artifact/request-1" || !bytes.Equal(artifact.GetPayload(), requestArtifact.Payload) { + t.Fatalf("unexpected request artifact: %#v", artifact) + } + if artifact := generation.GetRawArtifacts()[1]; artifact.GetKind() != sigilv1.ArtifactKind_ARTIFACT_KIND_RESPONSE || artifact.GetName() != "response" || artifact.GetContentType() != "application/json" || artifact.GetRecordId() != "rec-response-1" || artifact.GetUri() != "sigil://artifact/response-1" || !bytes.Equal(artifact.GetPayload(), responseArtifact.Payload) { + t.Fatalf("unexpected response artifact: %#v", artifact) } - requireProtoArtifact(t, artifacts[0], sigilv1.ArtifactKind_ARTIFACT_KIND_REQUEST, "request", "application/json", []byte(`{"model":"gpt-5"}`), "", "") - requireProtoArtifact(t, artifacts[1], sigilv1.ArtifactKind_ARTIFACT_KIND_RESPONSE, "response", "application/json", []byte(`{"stop_reason":"end_turn"}`), "", "") } func TestConformance_ConversationTitleSemantics(t *testing.T) { @@ -1066,119 +1124,14 @@ func int64Ptr(value int64) *int64 { return &value } -func requireProtoMetadataNumber(t *testing.T, generation *sigilv1.Generation, key string, want float64) { - t.Helper() - - value, ok := generation.GetMetadata().AsMap()[key] - if !ok { - t.Fatalf("expected generation metadata %q=%v, key missing", key, want) - } - got, ok := value.(float64) - if !ok { - t.Fatalf("expected generation metadata %q to be float64, got %#v", key, value) - } - if got != want { - t.Fatalf("unexpected generation metadata %q: got %v want %v", key, got, want) - } -} - -func requireProtoTextPart(t *testing.T, part *sigilv1.Part, want string) { - t.Helper() - - payload, ok := part.GetPayload().(*sigilv1.Part_Text) - if !ok { - t.Fatalf("expected text part, got %T", part.GetPayload()) - } - if payload.Text != want { - t.Fatalf("unexpected text part: got %q want %q", payload.Text, want) - } -} - -func requireProtoThinkingPart(t *testing.T, part *sigilv1.Part, want string) { - t.Helper() - - payload, ok := part.GetPayload().(*sigilv1.Part_Thinking) - if !ok { - t.Fatalf("expected thinking part, got %T", part.GetPayload()) - } - if payload.Thinking != want { - t.Fatalf("unexpected thinking part: got %q want %q", payload.Thinking, want) - } -} - -func requireProtoToolCallPart(t *testing.T, part *sigilv1.Part, wantID string, wantName string, wantInputJSON []byte) { - t.Helper() - - payload, ok := part.GetPayload().(*sigilv1.Part_ToolCall) - if !ok { - t.Fatalf("expected tool call part, got %T", part.GetPayload()) - } - if got := payload.ToolCall.GetId(); got != wantID { - t.Fatalf("unexpected tool call id: got %q want %q", got, wantID) - } - if got := payload.ToolCall.GetName(); got != wantName { - t.Fatalf("unexpected tool call name: got %q want %q", got, wantName) - } - if !bytes.Equal(payload.ToolCall.GetInputJson(), wantInputJSON) { - t.Fatalf("unexpected tool call input_json: got %s want %s", string(payload.ToolCall.GetInputJson()), string(wantInputJSON)) - } -} - -func requireProtoToolResultPart(t *testing.T, part *sigilv1.Part, wantCallID string, wantName string, wantContent string, wantContentJSON []byte, wantIsError bool) { - t.Helper() - - payload, ok := part.GetPayload().(*sigilv1.Part_ToolResult) - if !ok { - t.Fatalf("expected tool result part, got %T", part.GetPayload()) - } - if got := payload.ToolResult.GetToolCallId(); got != wantCallID { - t.Fatalf("unexpected tool result tool_call_id: got %q want %q", got, wantCallID) - } - if got := payload.ToolResult.GetName(); got != wantName { - t.Fatalf("unexpected tool result name: got %q want %q", got, wantName) - } - if got := payload.ToolResult.GetContent(); got != wantContent { - t.Fatalf("unexpected tool result content: got %q want %q", got, wantContent) - } - if !bytes.Equal(payload.ToolResult.GetContentJson(), wantContentJSON) { - t.Fatalf("unexpected tool result content_json: got %s want %s", string(payload.ToolResult.GetContentJson()), string(wantContentJSON)) - } - if got := payload.ToolResult.GetIsError(); got != wantIsError { - t.Fatalf("unexpected tool result is_error: got %t want %t", got, wantIsError) - } +func float64Ptr(value float64) *float64 { + return &value } -func requireProtoArtifact(t *testing.T, artifact *sigilv1.Artifact, wantKind sigilv1.ArtifactKind, wantName string, wantContentType string, wantPayload []byte, wantRecordID string, wantURI string) { - t.Helper() - - if got := artifact.GetKind(); got != wantKind { - t.Fatalf("unexpected artifact kind: got %v want %v", got, wantKind) - } - if got := artifact.GetName(); got != wantName { - t.Fatalf("unexpected artifact name: got %q want %q", got, wantName) - } - if got := artifact.GetContentType(); got != wantContentType { - t.Fatalf("unexpected artifact content_type: got %q want %q", got, wantContentType) - } - if !bytes.Equal(artifact.GetPayload(), wantPayload) { - t.Fatalf("unexpected artifact payload: got %s want %s", string(artifact.GetPayload()), string(wantPayload)) - } - if got := artifact.GetRecordId(); got != wantRecordID { - t.Fatalf("unexpected artifact record_id: got %q want %q", got, wantRecordID) - } - if got := artifact.GetUri(); got != wantURI { - t.Fatalf("unexpected artifact uri: got %q want %q", got, wantURI) - } +func stringPtr(value string) *string { + return &value } -func requireInt64HistogramSum(t *testing.T, histogram metricdata.Histogram[int64], attrs map[string]string, want int64) { - t.Helper() - - point := requireHistogramPointWithAttrs(t, histogram, attrs) - if point.Sum != want { - t.Fatalf("unexpected histogram sum for attrs %v: got %d want %d", attrs, point.Sum, want) - } - if point.Count != 1 { - t.Fatalf("unexpected histogram count for attrs %v: got %d want %d", attrs, point.Count, 1) - } +func boolPtr(value bool) *bool { + return &value } diff --git a/go/sigil/exporter_transport_test.go b/go/sigil/exporter_transport_test.go index dce8bee..c9d5ffa 100644 --- a/go/sigil/exporter_transport_test.go +++ b/go/sigil/exporter_transport_test.go @@ -359,12 +359,13 @@ func payloadFromSeed(seed uint64) (GenerationStart, Generation) { ToolChoice: stringPtr(*start.ToolChoice), ThinkingEnabled: boolPtr(*start.ThinkingEnabled), Usage: TokenUsage{ - InputTokens: int64(rnd.Intn(1000)), - OutputTokens: int64(rnd.Intn(1000)), - TotalTokens: int64(rnd.Intn(2000) + 1), - CacheReadInputTokens: int64(rnd.Intn(100)), - CacheWriteInputTokens: int64(rnd.Intn(100)), - ReasoningTokens: int64(rnd.Intn(100)), + InputTokens: int64(rnd.Intn(1000)), + OutputTokens: int64(rnd.Intn(1000)), + TotalTokens: int64(rnd.Intn(2000) + 1), + CacheReadInputTokens: int64(rnd.Intn(100)), + CacheWriteInputTokens: int64(rnd.Intn(100)), + CacheCreationInputTokens: int64(rnd.Intn(100)), + ReasoningTokens: int64(rnd.Intn(100)), }, StopReason: "stop-" + randomASCII(rnd, 4), StartedAt: startedAt, diff --git a/go/sigil/proto_mapping.go b/go/sigil/proto_mapping.go index 1658921..8e78114 100644 --- a/go/sigil/proto_mapping.go +++ b/go/sigil/proto_mapping.go @@ -185,8 +185,8 @@ func mapUsageToProto(usage TokenUsage) *sigilv1.TokenUsage { TotalTokens: usage.TotalTokens, CacheReadInputTokens: usage.CacheReadInputTokens, CacheWriteInputTokens: usage.CacheWriteInputTokens, - CacheCreationInputTokens: usage.CacheCreationInputTokens, ReasoningTokens: usage.ReasoningTokens, + CacheCreationInputTokens: usage.CacheCreationInputTokens, } } diff --git a/python/sigil_sdk/internal/gen/sigil/v1/generation_ingest_pb2.py b/python/sigil_sdk/internal/gen/sigil/v1/generation_ingest_pb2.py index 376dd4b..6d6539f 100644 --- a/python/sigil_sdk/internal/gen/sigil/v1/generation_ingest_pb2.py +++ b/python/sigil_sdk/internal/gen/sigil/v1/generation_ingest_pb2.py @@ -26,7 +26,7 @@ from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n sigil/v1/generation_ingest.proto\x12\x08sigil.v1\x1a\x1cgoogle/protobuf/struct.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"E\n\x18\x45xportGenerationsRequest\x12)\n\x0bgenerations\x18\x01 \x03(\x0b\x32\x14.sigil.v1.Generation\"N\n\x19\x45xportGenerationsResponse\x12\x31\n\x07results\x18\x01 \x03(\x0b\x32 .sigil.v1.ExportGenerationResult\"P\n\x16\x45xportGenerationResult\x12\x15\n\rgeneration_id\x18\x01 \x01(\t\x12\x10\n\x08\x61\x63\x63\x65pted\x18\x02 \x01(\x08\x12\r\n\x05\x65rror\x18\x03 \x01(\t\"*\n\x08ModelRef\x12\x10\n\x08provider\x18\x01 \x01(\t\x12\x0c\n\x04name\x18\x02 \x01(\t\"%\n\x0cPartMetadata\x12\x15\n\rprovider_type\x18\x01 \x01(\t\"8\n\x08ToolCall\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x12\n\ninput_json\x18\x03 \x01(\x0c\"i\n\nToolResult\x12\x14\n\x0ctool_call_id\x18\x01 \x01(\t\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x0f\n\x07\x63ontent\x18\x03 \x01(\t\x12\x14\n\x0c\x63ontent_json\x18\x04 \x01(\x0c\x12\x10\n\x08is_error\x18\x05 \x01(\x08\"\xb5\x01\n\x04Part\x12(\n\x08metadata\x18\x01 \x01(\x0b\x32\x16.sigil.v1.PartMetadata\x12\x0e\n\x04text\x18\x02 \x01(\tH\x00\x12\x12\n\x08thinking\x18\x03 \x01(\tH\x00\x12\'\n\ttool_call\x18\x04 \x01(\x0b\x32\x12.sigil.v1.ToolCallH\x00\x12+\n\x0btool_result\x18\x05 \x01(\x0b\x32\x14.sigil.v1.ToolResultH\x00\x42\t\n\x07payload\"[\n\x07Message\x12#\n\x04role\x18\x01 \x01(\x0e\x32\x15.sigil.v1.MessageRole\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x1d\n\x05parts\x18\x03 \x03(\x0b\x32\x0e.sigil.v1.Part\"\\\n\x0eToolDefinition\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x02 \x01(\t\x12\x0c\n\x04type\x18\x03 \x01(\t\x12\x19\n\x11input_schema_json\x18\x04 \x01(\x0c\"\xac\x01\n\nTokenUsage\x12\x14\n\x0cinput_tokens\x18\x01 \x01(\x03\x12\x15\n\routput_tokens\x18\x02 \x01(\x03\x12\x14\n\x0ctotal_tokens\x18\x03 \x01(\x03\x12\x1f\n\x17\x63\x61\x63he_read_input_tokens\x18\x04 \x01(\x03\x12 \n\x18\x63\x61\x63he_write_input_tokens\x18\x05 \x01(\x03\x12\x18\n\x10reasoning_tokens\x18\x06 \x01(\x03\"\x85\x01\n\x08\x41rtifact\x12$\n\x04kind\x18\x01 \x01(\x0e\x32\x16.sigil.v1.ArtifactKind\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x14\n\x0c\x63ontent_type\x18\x03 \x01(\t\x12\x0f\n\x07payload\x18\x04 \x01(\x0c\x12\x11\n\trecord_id\x18\x05 \x01(\t\x12\x0b\n\x03uri\x18\x06 \x01(\t\"\xc3\x07\n\nGeneration\x12\n\n\x02id\x18\x01 \x01(\t\x12\x17\n\x0f\x63onversation_id\x18\x02 \x01(\t\x12\x16\n\x0eoperation_name\x18\x03 \x01(\t\x12&\n\x04mode\x18\x04 \x01(\x0e\x32\x18.sigil.v1.GenerationMode\x12\x10\n\x08trace_id\x18\x05 \x01(\t\x12\x0f\n\x07span_id\x18\x06 \x01(\t\x12!\n\x05model\x18\x07 \x01(\x0b\x32\x12.sigil.v1.ModelRef\x12\x13\n\x0bresponse_id\x18\x08 \x01(\t\x12\x16\n\x0eresponse_model\x18\t \x01(\t\x12\x15\n\rsystem_prompt\x18\n \x01(\t\x12 \n\x05input\x18\x0b \x03(\x0b\x32\x11.sigil.v1.Message\x12!\n\x06output\x18\x0c \x03(\x0b\x32\x11.sigil.v1.Message\x12\'\n\x05tools\x18\r \x03(\x0b\x32\x18.sigil.v1.ToolDefinition\x12#\n\x05usage\x18\x0e \x01(\x0b\x32\x14.sigil.v1.TokenUsage\x12\x13\n\x0bstop_reason\x18\x0f \x01(\t\x12.\n\nstarted_at\x18\x10 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x30\n\x0c\x63ompleted_at\x18\x11 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12,\n\x04tags\x18\x12 \x03(\x0b\x32\x1e.sigil.v1.Generation.TagsEntry\x12)\n\x08metadata\x18\x13 \x01(\x0b\x32\x17.google.protobuf.Struct\x12)\n\rraw_artifacts\x18\x14 \x03(\x0b\x32\x12.sigil.v1.Artifact\x12\x12\n\ncall_error\x18\x15 \x01(\t\x12\x12\n\nagent_name\x18\x16 \x01(\t\x12\x15\n\ragent_version\x18\x17 \x01(\t\x12\x17\n\nmax_tokens\x18\x18 \x01(\x03H\x00\x88\x01\x01\x12\x18\n\x0btemperature\x18\x19 \x01(\x01H\x01\x88\x01\x01\x12\x12\n\x05top_p\x18\x1a \x01(\x01H\x02\x88\x01\x01\x12\x18\n\x0btool_choice\x18\x1b \x01(\tH\x03\x88\x01\x01\x12\x1d\n\x10thinking_enabled\x18\x1c \x01(\x08H\x04\x88\x01\x01\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x42\r\n\x0b_max_tokensB\x0e\n\x0c_temperatureB\x08\n\x06_top_pB\x0e\n\x0c_tool_choiceB\x13\n\x11_thinking_enabled*g\n\x0eGenerationMode\x12\x1f\n\x1bGENERATION_MODE_UNSPECIFIED\x10\x00\x12\x18\n\x14GENERATION_MODE_SYNC\x10\x01\x12\x1a\n\x16GENERATION_MODE_STREAM\x10\x02*u\n\x0bMessageRole\x12\x1c\n\x18MESSAGE_ROLE_UNSPECIFIED\x10\x00\x12\x15\n\x11MESSAGE_ROLE_USER\x10\x01\x12\x1a\n\x16MESSAGE_ROLE_ASSISTANT\x10\x02\x12\x15\n\x11MESSAGE_ROLE_TOOL\x10\x03*\x9f\x01\n\x0c\x41rtifactKind\x12\x1d\n\x19\x41RTIFACT_KIND_UNSPECIFIED\x10\x00\x12\x19\n\x15\x41RTIFACT_KIND_REQUEST\x10\x01\x12\x1a\n\x16\x41RTIFACT_KIND_RESPONSE\x10\x02\x12\x17\n\x13\x41RTIFACT_KIND_TOOLS\x10\x03\x12 \n\x1c\x41RTIFACT_KIND_PROVIDER_EVENT\x10\x04\x32w\n\x17GenerationIngestService\x12\\\n\x11\x45xportGenerations\x12\".sigil.v1.ExportGenerationsRequest\x1a#.sigil.v1.ExportGenerationsResponseB>ZZ Date: Thu, 12 Mar 2026 14:57:12 +0100 Subject: [PATCH 052/133] Add Google ADK adapter conformance coverage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - add a public-API-only Google ADK conformance suite under `sdks/go-frameworks/google-adk` - assert framework span attributes, parent span linkage, generation triggering, and framework tag propagation onto normalized generations - keep export verification localhost-only with `httptest.Server` ## Testing - `go test ./sdks/go-frameworks/google-adk/...` - `go test ./sdks/go-frameworks/google-adk/... -run 'TestConformance_' -count=1 -v` ## Notes - no production code paths changed; this adds external-package test coverage only - closes GRA-10 --- > [!NOTE] > **Low Risk** > Adds new conformance tests and a linter config only; no production code paths change, so risk is limited to potential test flakiness around async HTTP export and tracing. > > **Overview** > Adds a new public-API conformance test suite under `sdks/go-frameworks/google-adk/conformance` that exercises the Google ADK Sigil adapter’s run lifecycle for both sync and streaming modes. > > The tests verify span parent linkage and expected `gen_ai.*` attributes, and assert the normalized generation export payload (tags/metadata, model/provider, conversation IDs, and streamed token aggregation) using a localhost `httptest.Server` capture endpoint. > > Also introduces a local `.golangci.yml` for this SDK module disabling linting on test files. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 09cd403be27353f54559b3fab22b37d01a9ada1d. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). --- go-frameworks/google-adk/.golangci.yml | 4 + .../conformance/conformance_test.go | 515 ++++++++++++++++++ go-frameworks/google-adk/conformance/doc.go | 1 + 3 files changed, 520 insertions(+) create mode 100644 go-frameworks/google-adk/.golangci.yml create mode 100644 go-frameworks/google-adk/conformance/conformance_test.go create mode 100644 go-frameworks/google-adk/conformance/doc.go diff --git a/go-frameworks/google-adk/.golangci.yml b/go-frameworks/google-adk/.golangci.yml new file mode 100644 index 0000000..23482f8 --- /dev/null +++ b/go-frameworks/google-adk/.golangci.yml @@ -0,0 +1,4 @@ +version: "2" + +run: + tests: false diff --git a/go-frameworks/google-adk/conformance/conformance_test.go b/go-frameworks/google-adk/conformance/conformance_test.go new file mode 100644 index 0000000..30d8e1d --- /dev/null +++ b/go-frameworks/google-adk/conformance/conformance_test.go @@ -0,0 +1,515 @@ +package conformance_test + +import ( + "context" + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "sync" + "testing" + "time" + + googleadk "github.com/grafana/sigil/sdks/go-frameworks/google-adk" + "github.com/grafana/sigil/sdks/go/sigil" + "go.opentelemetry.io/otel/attribute" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + "go.opentelemetry.io/otel/sdk/trace/tracetest" +) + +const ( + spanAttrOperationName = "gen_ai.operation.name" + spanAttrConversationID = "gen_ai.conversation.id" + spanAttrAgentName = "gen_ai.agent.name" + spanAttrAgentVersion = "gen_ai.agent.version" + spanAttrProviderName = "gen_ai.provider.name" + spanAttrRequestModel = "gen_ai.request.model" + spanAttrResponseModel = "gen_ai.response.model" + spanAttrGenerationID = "sigil.generation.id" + + metadataRunID = "sigil.framework.run_id" + metadataThreadID = "sigil.framework.thread_id" + metadataParentRunID = "sigil.framework.parent_run_id" + metadataRunType = "sigil.framework.run_type" + metadataComponent = "sigil.framework.component_name" + metadataEventID = "sigil.framework.event_id" + metadataTags = "sigil.framework.tags" + metadataRetry = "sigil.framework.retry_attempt" +) + +type conformanceEnv struct { + client *sigil.Client + capture *generationCaptureServer + spans *tracetest.SpanRecorder + tracerProvider *sdktrace.TracerProvider +} + +func TestConformance_RunLifecycleExportsFrameworkTelemetry(t *testing.T) { + env := newConformanceEnv(t) + + adapter := googleadk.NewSigilAdapter(env.client, googleadk.Options{ + AgentName: "triage-agent", + AgentVersion: "1.2.3", + ExtraTags: map[string]string{ + "deployment.environment": "staging", + }, + ExtraMetadata: map[string]any{ + "team": "infra", + }, + }) + + parentCtx, parentSpan := env.tracerProvider.Tracer("google-adk-conformance").Start(context.Background(), "http.request") + parentSpanContext := parentSpan.SpanContext() + retryAttempt := 2 + + if err := adapter.OnRunStart(parentCtx, googleadk.RunStartEvent{ + RunID: "run-sync", + ParentRunID: "framework-parent", + SessionID: "session-42", + ThreadID: "thread-7", + EventID: "event-9", + ComponentName: "planner", + RunType: "chat", + ModelName: "gemini-2.5-pro", + Prompts: []string{"Summarize system health"}, + Tags: []string{"prod", "workflow"}, + RetryAttempt: &retryAttempt, + Metadata: map[string]any{ + "step": "triage", + }, + }); err != nil { + t.Fatalf("run start: %v", err) + } + + if got := env.capture.requestCount(); got != 0 { + t.Fatalf("expected no normalized generation export before run end, got %d requests", got) + } + + if err := adapter.OnRunEnd("run-sync", googleadk.RunEndEvent{ + RunID: "run-sync", + OutputMessages: []sigil.Message{sigil.AssistantTextMessage("System health is green")}, + ResponseModel: "gemini-2.5-pro", + StopReason: "stop", + Usage: sigil.TokenUsage{ + InputTokens: 12, + OutputTokens: 4, + TotalTokens: 16, + }, + }); err != nil { + t.Fatalf("run end: %v", err) + } + + generation := env.capture.waitForSingleGeneration(t) + parentSpan.End() + env.Shutdown(t) + + span := findSpanByName(t, env.spans.Ended(), "generateText gemini-2.5-pro") + if span.Parent().SpanID() != parentSpanContext.SpanID() { + t.Fatalf("expected generation span parent %q, got %q", parentSpanContext.SpanID().String(), span.Parent().SpanID().String()) + } + + attrs := spanAttributeMap(span) + requireStringAttr(t, attrs, spanAttrOperationName, "generateText") + requireStringAttr(t, attrs, spanAttrConversationID, "session-42") + requireStringAttr(t, attrs, spanAttrAgentName, "triage-agent") + requireStringAttr(t, attrs, spanAttrAgentVersion, "1.2.3") + requireStringAttr(t, attrs, spanAttrProviderName, "gemini") + requireStringAttr(t, attrs, spanAttrRequestModel, "gemini-2.5-pro") + requireStringAttr(t, attrs, spanAttrResponseModel, "gemini-2.5-pro") + requireStringAttr(t, attrs, spanAttrGenerationID, mustString(t, generation, "id")) + + requireStringField(t, generation, "conversation_id", "session-42") + requireStringField(t, generation, "agent_name", "triage-agent") + requireStringField(t, generation, "agent_version", "1.2.3") + requireStringField(t, generation, "operation_name", "generateText") + requireStringField(t, generation, "mode", "GENERATION_MODE_SYNC") + requireStringField(t, generation, "response_model", "gemini-2.5-pro") + requireStringField(t, generation, "stop_reason", "stop") + requireStringField(t, generation, "trace_id", span.SpanContext().TraceID().String()) + requireStringField(t, generation, "span_id", span.SpanContext().SpanID().String()) + + model := mustObject(t, generation, "model") + requireStringFromMap(t, model, "provider", "gemini") + requireStringFromMap(t, model, "name", "gemini-2.5-pro") + + tags := mustObject(t, generation, "tags") + requireStringFromMap(t, tags, "sigil.framework.name", "google-adk") + requireStringFromMap(t, tags, "sigil.framework.source", "handler") + requireStringFromMap(t, tags, "sigil.framework.language", "go") + requireStringFromMap(t, tags, "deployment.environment", "staging") + + metadata := mustObject(t, generation, "metadata") + requireStringFromMap(t, metadata, metadataRunID, "run-sync") + requireStringFromMap(t, metadata, metadataThreadID, "thread-7") + requireStringFromMap(t, metadata, metadataParentRunID, "framework-parent") + requireStringFromMap(t, metadata, metadataRunType, "chat") + requireStringFromMap(t, metadata, metadataComponent, "planner") + requireStringFromMap(t, metadata, metadataEventID, "event-9") + requireStringFromMap(t, metadata, "team", "infra") + requireStringFromMap(t, metadata, "step", "triage") + requireNumberFromMap(t, metadata, metadataRetry, 2) + requireStringSliceFromMap(t, metadata, metadataTags, []string{"prod", "workflow"}) +} + +func TestConformance_StreamingRunExportsTokenDrivenGeneration(t *testing.T) { + env := newConformanceEnv(t) + + adapter := googleadk.NewSigilAdapter(env.client, googleadk.Options{ + AgentName: "stream-agent", + AgentVersion: "9.9.9", + }) + + parentCtx, parentSpan := env.tracerProvider.Tracer("google-adk-conformance").Start(context.Background(), "grpc.request") + parentSpanContext := parentSpan.SpanContext() + + if err := adapter.OnRunStart(parentCtx, googleadk.RunStartEvent{ + RunID: "run-stream", + ConversationID: "conversation-stream", + ThreadID: "thread-stream", + ModelName: "gpt-5", + Stream: true, + Tags: []string{"streaming"}, + }); err != nil { + t.Fatalf("run start: %v", err) + } + + adapter.OnRunToken("run-stream", "hello") + adapter.OnRunToken("run-stream", " world") + + if err := adapter.OnRunEnd("run-stream", googleadk.RunEndEvent{ + RunID: "run-stream", + ResponseModel: "gpt-5", + StopReason: "end_turn", + }); err != nil { + t.Fatalf("run end: %v", err) + } + + generation := env.capture.waitForSingleGeneration(t) + parentSpan.End() + env.Shutdown(t) + + span := findSpanByName(t, env.spans.Ended(), "streamText gpt-5") + if span.Parent().SpanID() != parentSpanContext.SpanID() { + t.Fatalf("expected streaming span parent %q, got %q", parentSpanContext.SpanID().String(), span.Parent().SpanID().String()) + } + + attrs := spanAttributeMap(span) + requireStringAttr(t, attrs, spanAttrOperationName, "streamText") + requireStringAttr(t, attrs, spanAttrConversationID, "conversation-stream") + requireStringAttr(t, attrs, spanAttrAgentName, "stream-agent") + requireStringAttr(t, attrs, spanAttrAgentVersion, "9.9.9") + requireStringAttr(t, attrs, spanAttrProviderName, "openai") + requireStringAttr(t, attrs, spanAttrRequestModel, "gpt-5") + requireStringAttr(t, attrs, spanAttrResponseModel, "gpt-5") + + requireStringField(t, generation, "conversation_id", "conversation-stream") + requireStringField(t, generation, "mode", "GENERATION_MODE_STREAM") + requireStringField(t, generation, "operation_name", "streamText") + requireStringField(t, generation, "response_model", "gpt-5") + requireStringField(t, generation, "stop_reason", "end_turn") + requireStringField(t, generation, "trace_id", span.SpanContext().TraceID().String()) + requireStringField(t, generation, "span_id", span.SpanContext().SpanID().String()) + + tags := mustObject(t, generation, "tags") + requireStringFromMap(t, tags, "sigil.framework.name", "google-adk") + requireStringFromMap(t, tags, "sigil.framework.source", "handler") + requireStringFromMap(t, tags, "sigil.framework.language", "go") + + metadata := mustObject(t, generation, "metadata") + requireStringFromMap(t, metadata, metadataRunID, "run-stream") + requireStringFromMap(t, metadata, metadataThreadID, "thread-stream") + requireStringFromMap(t, metadata, metadataRunType, "chat") + requireStringSliceFromMap(t, metadata, metadataTags, []string{"streaming"}) + + output := mustArray(t, generation, "output") + if len(output) != 1 { + t.Fatalf("expected one streamed output message, got %d", len(output)) + } + outputMessage, ok := output[0].(map[string]any) + if !ok { + t.Fatalf("expected output message object, got %T", output[0]) + } + requireStringFromMap(t, outputMessage, "role", "MESSAGE_ROLE_ASSISTANT") + parts := mustArrayFromMap(t, outputMessage, "parts") + if len(parts) != 1 { + t.Fatalf("expected one output part, got %d", len(parts)) + } + outputPart, ok := parts[0].(map[string]any) + if !ok { + t.Fatalf("expected output part object, got %T", parts[0]) + } + requireStringFromMap(t, outputPart, "text", "hello world") +} + +func newConformanceEnv(t *testing.T) *conformanceEnv { + t.Helper() + + capture := newGenerationCaptureServer(t) + spanRecorder := tracetest.NewSpanRecorder() + tracerProvider := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(spanRecorder)) + + cfg := sigil.DefaultConfig() + cfg.Tracer = tracerProvider.Tracer("google-adk-conformance") + cfg.GenerationExport.Protocol = sigil.GenerationExportProtocolHTTP + cfg.GenerationExport.Endpoint = capture.server.URL + "/api/v1/generations:export" + cfg.GenerationExport.BatchSize = 1 + cfg.GenerationExport.QueueSize = 8 + cfg.GenerationExport.FlushInterval = time.Hour + cfg.GenerationExport.MaxRetries = 1 + cfg.GenerationExport.InitialBackoff = time.Millisecond + cfg.GenerationExport.MaxBackoff = 10 * time.Millisecond + + env := &conformanceEnv{ + client: sigil.NewClient(cfg), + capture: capture, + spans: spanRecorder, + tracerProvider: tracerProvider, + } + t.Cleanup(func() { + _ = env.close() + }) + return env +} + +func (e *conformanceEnv) Shutdown(t *testing.T) { + t.Helper() + + if err := e.close(); err != nil { + t.Fatalf("shutdown conformance env: %v", err) + } +} + +func (e *conformanceEnv) close() error { + var closeErr error + + if e.client != nil { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if err := e.client.Shutdown(ctx); err != nil { + closeErr = err + } + e.client = nil + } + if e.tracerProvider != nil { + if err := e.tracerProvider.Shutdown(context.Background()); err != nil && closeErr == nil { + closeErr = err + } + e.tracerProvider = nil + } + if e.capture != nil { + e.capture.server.Close() + e.capture = nil + } + + return closeErr +} + +type generationCaptureServer struct { + server *httptest.Server + mu sync.Mutex + requests []map[string]any +} + +func newGenerationCaptureServer(t *testing.T) *generationCaptureServer { + t.Helper() + + capture := &generationCaptureServer{} + capture.server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + body, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, "read body", http.StatusBadRequest) + return + } + + request := map[string]any{} + if err := json.Unmarshal(body, &request); err != nil { + http.Error(w, "invalid json", http.StatusBadRequest) + return + } + + capture.mu.Lock() + capture.requests = append(capture.requests, request) + capture.mu.Unlock() + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusAccepted) + _, _ = w.Write([]byte(`{"results":[]}`)) + })) + return capture +} + +func (c *generationCaptureServer) requestCount() int { + c.mu.Lock() + defer c.mu.Unlock() + return len(c.requests) +} + +func (c *generationCaptureServer) waitForSingleGeneration(t *testing.T) map[string]any { + t.Helper() + + deadline := time.Now().Add(5 * time.Second) + for { + c.mu.Lock() + if len(c.requests) == 1 { + request := c.requests[0] + c.mu.Unlock() + generations := mustArray(t, request, "generations") + if len(generations) != 1 { + t.Fatalf("expected one exported generation, got %d", len(generations)) + } + generation, ok := generations[0].(map[string]any) + if !ok { + t.Fatalf("expected generation object, got %T", generations[0]) + } + return generation + } + c.mu.Unlock() + + if time.Now().After(deadline) { + t.Fatalf("timed out waiting for a single generation export; got %d requests", c.requestCount()) + } + time.Sleep(10 * time.Millisecond) + } +} + +func findSpanByName(t *testing.T, spans []sdktrace.ReadOnlySpan, name string) sdktrace.ReadOnlySpan { + t.Helper() + + for _, span := range spans { + if span.Name() == name { + return span + } + } + + t.Fatalf("expected span %q, got %d spans", name, len(spans)) + return nil +} + +func spanAttributeMap(span sdktrace.ReadOnlySpan) map[attribute.Key]attribute.Value { + attrs := make(map[attribute.Key]attribute.Value, len(span.Attributes())) + for _, attr := range span.Attributes() { + attrs[attr.Key] = attr.Value + } + return attrs +} + +func requireStringAttr(t *testing.T, attrs map[attribute.Key]attribute.Value, key, want string) { + t.Helper() + + got, ok := attrs[attribute.Key(key)] + if !ok { + t.Fatalf("expected span attribute %q", key) + } + if got.AsString() != want { + t.Fatalf("unexpected span attribute %q: got %q want %q", key, got.AsString(), want) + } +} + +func requireStringField(t *testing.T, data map[string]any, key, want string) { + t.Helper() + requireStringFromMap(t, data, key, want) +} + +func requireStringFromMap(t *testing.T, data map[string]any, key, want string) { + t.Helper() + + got, ok := data[key] + if !ok { + t.Fatalf("expected field %q", key) + } + gotString, ok := got.(string) + if !ok { + t.Fatalf("expected %q to be a string, got %T", key, got) + } + if gotString != want { + t.Fatalf("unexpected %q: got %q want %q", key, gotString, want) + } +} + +func requireNumberFromMap(t *testing.T, data map[string]any, key string, want float64) { + t.Helper() + + got, ok := data[key] + if !ok { + t.Fatalf("expected field %q", key) + } + gotNumber, ok := got.(float64) + if !ok { + t.Fatalf("expected %q to be numeric, got %T", key, got) + } + if gotNumber != want { + t.Fatalf("unexpected %q: got %v want %v", key, gotNumber, want) + } +} + +func requireStringSliceFromMap(t *testing.T, data map[string]any, key string, want []string) { + t.Helper() + + raw, ok := data[key] + if !ok { + t.Fatalf("expected field %q", key) + } + values, ok := raw.([]any) + if !ok { + t.Fatalf("expected %q to be an array, got %T", key, raw) + } + if len(values) != len(want) { + t.Fatalf("unexpected %q length: got %d want %d", key, len(values), len(want)) + } + for i, expected := range want { + got, ok := values[i].(string) + if !ok { + t.Fatalf("expected %q[%d] to be string, got %T", key, i, values[i]) + } + if got != expected { + t.Fatalf("unexpected %q[%d]: got %q want %q", key, i, got, expected) + } + } +} + +func mustString(t *testing.T, data map[string]any, key string) string { + t.Helper() + + got, ok := data[key] + if !ok { + t.Fatalf("expected field %q", key) + } + value, ok := got.(string) + if !ok { + t.Fatalf("expected %q to be string, got %T", key, got) + } + return value +} + +func mustObject(t *testing.T, data map[string]any, key string) map[string]any { + t.Helper() + + got, ok := data[key] + if !ok { + t.Fatalf("expected field %q", key) + } + value, ok := got.(map[string]any) + if !ok { + t.Fatalf("expected %q to be an object, got %T", key, got) + } + return value +} + +func mustArray(t *testing.T, data map[string]any, key string) []any { + t.Helper() + + got, ok := data[key] + if !ok { + t.Fatalf("expected field %q", key) + } + value, ok := got.([]any) + if !ok { + t.Fatalf("expected %q to be an array, got %T", key, got) + } + return value +} + +func mustArrayFromMap(t *testing.T, data map[string]any, key string) []any { + t.Helper() + return mustArray(t, data, key) +} diff --git a/go-frameworks/google-adk/conformance/doc.go b/go-frameworks/google-adk/conformance/doc.go new file mode 100644 index 0000000..df6f154 --- /dev/null +++ b/go-frameworks/google-adk/conformance/doc.go @@ -0,0 +1 @@ +package conformance From 2f2e5e0dcd61054d11328f09d9e433489257318e Mon Sep 17 00:00:00 2001 From: Cyril Tovena Date: Thu, 12 Mar 2026 15:02:57 +0100 Subject: [PATCH 053/133] test(go-providers): add Go provider wrapper conformance suites MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - add `TestConformance*` suites for the Go OpenAI, Anthropic, and Gemini provider packages - cover sync and streaming normalization, raw artifact opt-in, and provider/mapping error semantics without live services - update the SDK conformance spec and active execution plan to document the shipped provider-wrapper baseline ## Testing - `cd sdks/go-providers/openai && GOWORK=off go test ./... -run '^TestConformance' -count=1` - `cd sdks/go-providers/anthropic && GOWORK=off go test ./... -run '^TestConformance' -count=1` - `cd sdks/go-providers/gemini && GOWORK=off go test ./... -run '^TestConformance' -count=1` - `cd sdks/go-providers/openai && GOWORK=off go test ./...` - `cd sdks/go-providers/anthropic && GOWORK=off go test ./...` - `cd sdks/go-providers/gemini && GOWORK=off go test ./...` ## Manual QA Plan - Non-UI change. - Review the new provider conformance suites in `sdks/go-providers/{openai,anthropic,gemini}`. - Re-run the targeted `^TestConformance` commands above to verify the new gate is populated and green. --- > [!NOTE] > **Low Risk** > Low risk: primarily adds conformance tests and documentation, with small refactors to provider wrapper entry points to allow injectible invocation for error-path testing. > > **Overview** > Adds a new *provider-wrapper conformance layer* for the Go SDK by introducing `TestConformance*` suites in `sdks/go-providers/{openai,anthropic,gemini}` that validate sync/stream normalization to `sigil.Generation`, usage/stop-reason/tool-call/thinking mapping, raw-artifact opt-in, and explicit mapper error cases. > > Refactors the OpenAI/Anthropic/Gemini generation wrapper functions (e.g., `ChatCompletionsNew`, `ResponsesNew`, `Message`, `GenerateContent`) to delegate to internal helpers with an injected `invoke` function so tests can assert wrapper error semantics (provider errors preserved; mapping failures don’t hide native responses). > > Updates `docs/references/sdk-conformance-spec.md` and the active exec plan to describe the provider-wrapper baseline and add local `go test` entry points for these suites. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 8fb36aaa0feacb057683e59bb4f1143785437db5. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). --- go-providers/anthropic/conformance_test.go | 239 ++++++++++++ go-providers/anthropic/record.go | 14 +- go-providers/anthropic/record_test.go | 76 ++++ go-providers/gemini/conformance_test.go | 306 +++++++++++++++ go-providers/gemini/record.go | 21 +- go-providers/gemini/record_test.go | 64 +++- go-providers/openai/conformance_test.go | 415 +++++++++++++++++++++ go-providers/openai/record.go | 28 +- go-providers/openai/record_test.go | 126 ++++++- 9 files changed, 1279 insertions(+), 10 deletions(-) create mode 100644 go-providers/anthropic/conformance_test.go create mode 100644 go-providers/anthropic/record_test.go create mode 100644 go-providers/gemini/conformance_test.go create mode 100644 go-providers/openai/conformance_test.go diff --git a/go-providers/anthropic/conformance_test.go b/go-providers/anthropic/conformance_test.go new file mode 100644 index 0000000..a2c9fff --- /dev/null +++ b/go-providers/anthropic/conformance_test.go @@ -0,0 +1,239 @@ +package anthropic + +import ( + "testing" + + asdk "github.com/anthropics/anthropic-sdk-go" + + "github.com/grafana/sigil/sdks/go/sigil" +) + +func TestConformance_MessageSyncNormalization(t *testing.T) { + req := testRequest() + resp := &asdk.BetaMessage{ + ID: "msg_1", + Model: asdk.Model("claude-sonnet-4-5"), + StopReason: asdk.BetaStopReasonEndTurn, + Content: []asdk.BetaContentBlockUnion{ + {Type: "text", Text: "It's 18C and sunny."}, + {Type: "thinking", Thinking: "answer done"}, + mustUnmarshalBetaContentBlockUnion(t, `{"type":"tool_use","id":"toolu_2","name":"weather","input":{"city":"Paris"}}`), + }, + Usage: asdk.BetaUsage{ + InputTokens: 120, + OutputTokens: 42, + CacheReadInputTokens: 30, + CacheCreationInputTokens: 10, + }, + } + + generation, err := FromRequestResponse(req, resp, + WithConversationID("conv-anthropic-sync"), + WithConversationTitle("Paris weather"), + WithAgentName("agent-anthropic"), + WithAgentVersion("v-anthropic"), + WithTag("tenant", "t-123"), + WithRawArtifacts(), + ) + if err != nil { + t.Fatalf("anthropic sync mapping: %v", err) + } + + if generation.Model.Provider != "anthropic" || generation.Model.Name != "claude-sonnet-4-5" { + t.Fatalf("unexpected model mapping: %#v", generation.Model) + } + if generation.ConversationID != "conv-anthropic-sync" || generation.ConversationTitle != "Paris weather" { + t.Fatalf("unexpected conversation mapping: %#v", generation) + } + if generation.AgentName != "agent-anthropic" || generation.AgentVersion != "v-anthropic" { + t.Fatalf("unexpected agent mapping: name=%q version=%q", generation.AgentName, generation.AgentVersion) + } + if generation.ResponseID != "msg_1" || generation.ResponseModel != "claude-sonnet-4-5" { + t.Fatalf("unexpected response mapping: id=%q model=%q", generation.ResponseID, generation.ResponseModel) + } + if generation.StopReason != "end_turn" { + t.Fatalf("unexpected stop reason: %q", generation.StopReason) + } + if generation.Usage.TotalTokens != 162 || generation.Usage.CacheReadInputTokens != 30 || generation.Usage.CacheCreationInputTokens != 10 { + t.Fatalf("unexpected usage mapping: %#v", generation.Usage) + } + if generation.ThinkingEnabled == nil || !*generation.ThinkingEnabled { + t.Fatalf("expected thinking enabled true, got %v", generation.ThinkingEnabled) + } + if len(generation.Output) != 1 || len(generation.Output[0].Parts) != 3 { + t.Fatalf("expected text + thinking + tool call output, got %#v", generation.Output) + } + if generation.Output[0].Parts[0].Kind != sigil.PartKindText || generation.Output[0].Parts[0].Text != "It's 18C and sunny." { + t.Fatalf("unexpected text output: %#v", generation.Output[0].Parts[0]) + } + if generation.Output[0].Parts[1].Kind != sigil.PartKindThinking || generation.Output[0].Parts[1].Thinking != "answer done" { + t.Fatalf("unexpected thinking output: %#v", generation.Output[0].Parts[1]) + } + if generation.Output[0].Parts[2].Kind != sigil.PartKindToolCall { + t.Fatalf("expected tool call output, got %#v", generation.Output[0].Parts[2]) + } + if generation.Output[0].Parts[2].ToolCall.ID != "toolu_2" || generation.Output[0].Parts[2].ToolCall.Name != "weather" { + t.Fatalf("unexpected tool call mapping: %#v", generation.Output[0].Parts[2].ToolCall) + } + if generation.Tags["tenant"] != "t-123" { + t.Fatalf("expected tenant tag") + } + requireAnthropicArtifactKinds(t, generation.Artifacts, + sigil.ArtifactKindRequest, + sigil.ArtifactKindResponse, + sigil.ArtifactKindTools, + ) +} + +func TestConformance_MessageStreamNormalization(t *testing.T) { + req := testRequest() + summary := StreamSummary{ + Events: []asdk.BetaRawMessageStreamEventUnion{ + { + Type: "message_start", + Message: asdk.BetaMessage{ + ID: "msg_delta_1", + Model: asdk.Model("claude-sonnet-4-5"), + }, + }, + { + Type: "content_block_start", + Index: 0, + ContentBlock: asdk.BetaRawContentBlockStartEventContentBlockUnion{ + Type: "thinking", + }, + }, + { + Type: "content_block_delta", + Index: 0, + Delta: asdk.BetaRawMessageStreamEventUnionDelta{Thinking: "let me "}, + }, + { + Type: "content_block_delta", + Index: 0, + Delta: asdk.BetaRawMessageStreamEventUnionDelta{Thinking: "think about this"}, + }, + { + Type: "content_block_start", + Index: 1, + ContentBlock: asdk.BetaRawContentBlockStartEventContentBlockUnion{ + Type: "text", + }, + }, + { + Type: "content_block_delta", + Index: 1, + Delta: asdk.BetaRawMessageStreamEventUnionDelta{Text: "Hello, "}, + }, + { + Type: "content_block_delta", + Index: 1, + Delta: asdk.BetaRawMessageStreamEventUnionDelta{Text: "world!"}, + }, + { + Type: "content_block_start", + Index: 2, + ContentBlock: asdk.BetaRawContentBlockStartEventContentBlockUnion{ + Type: "tool_use", + ID: "toolu_1", + Name: "weather", + Input: map[string]any{}, + }, + }, + { + Type: "content_block_delta", + Index: 2, + Delta: asdk.BetaRawMessageStreamEventUnionDelta{PartialJSON: `{"city"`}, + }, + { + Type: "content_block_delta", + Index: 2, + Delta: asdk.BetaRawMessageStreamEventUnionDelta{PartialJSON: `:"Berlin"}`}, + }, + { + Type: "message_delta", + Delta: asdk.BetaRawMessageStreamEventUnionDelta{ + StopReason: asdk.BetaStopReasonToolUse, + }, + Usage: asdk.BetaMessageDeltaUsage{ + InputTokens: 100, + OutputTokens: 50, + }, + }, + }, + } + + generation, err := FromStream(req, summary, + WithConversationID("conv-anthropic-stream"), + WithAgentName("agent-anthropic-stream"), + WithAgentVersion("v-anthropic-stream"), + WithRawArtifacts(), + ) + if err != nil { + t.Fatalf("anthropic stream mapping: %v", err) + } + + if generation.ConversationID != "conv-anthropic-stream" || generation.AgentName != "agent-anthropic-stream" || generation.AgentVersion != "v-anthropic-stream" { + t.Fatalf("unexpected identity mapping: %#v", generation) + } + if generation.ResponseID != "msg_delta_1" || generation.ResponseModel != "claude-sonnet-4-5" { + t.Fatalf("unexpected response mapping: id=%q model=%q", generation.ResponseID, generation.ResponseModel) + } + if generation.StopReason != "tool_use" { + t.Fatalf("unexpected stop reason: %q", generation.StopReason) + } + if generation.Usage.TotalTokens != 150 { + t.Fatalf("unexpected usage mapping: %#v", generation.Usage) + } + if len(generation.Output) != 1 || len(generation.Output[0].Parts) != 3 { + t.Fatalf("expected thinking + text + tool call output, got %#v", generation.Output) + } + if generation.Output[0].Parts[0].Kind != sigil.PartKindThinking || generation.Output[0].Parts[0].Thinking != "let me think about this" { + t.Fatalf("unexpected thinking output: %#v", generation.Output[0].Parts[0]) + } + if generation.Output[0].Parts[1].Kind != sigil.PartKindText || generation.Output[0].Parts[1].Text != "Hello, world!" { + t.Fatalf("unexpected text output: %#v", generation.Output[0].Parts[1]) + } + if generation.Output[0].Parts[2].Kind != sigil.PartKindToolCall { + t.Fatalf("expected tool call output, got %#v", generation.Output[0].Parts[2]) + } + if string(generation.Output[0].Parts[2].ToolCall.InputJSON) != `{"city":"Berlin"}` { + t.Fatalf("unexpected streamed tool input: %q", string(generation.Output[0].Parts[2].ToolCall.InputJSON)) + } + requireAnthropicArtifactKinds(t, generation.Artifacts, + sigil.ArtifactKindRequest, + sigil.ArtifactKindTools, + sigil.ArtifactKindProviderEvent, + ) +} + +func TestConformance_AnthropicErrorMapping(t *testing.T) { + if _, err := FromRequestResponse(testRequest(), nil); err == nil || err.Error() != "response is required" { + t.Fatalf("expected explicit response error, got %v", err) + } + if _, err := FromStream(testRequest(), StreamSummary{}); err == nil || err.Error() != "stream summary has no events and no final message" { + t.Fatalf("expected explicit stream error, got %v", err) + } + + _, err := FromRequestResponse( + testRequest(), + &asdk.BetaMessage{Model: asdk.Model("claude-sonnet-4-5")}, + WithProviderName(""), + ) + if err == nil || err.Error() != "generation.model.provider is required" { + t.Fatalf("expected explicit validation error for invalid provider mapping, got %v", err) + } +} + +func requireAnthropicArtifactKinds(t *testing.T, artifacts []sigil.Artifact, want ...sigil.ArtifactKind) { + t.Helper() + + if len(artifacts) != len(want) { + t.Fatalf("expected %d artifacts, got %d", len(want), len(artifacts)) + } + for i, kind := range want { + if artifacts[i].Kind != kind { + t.Fatalf("artifact %d kind mismatch: got %q want %q", i, artifacts[i].Kind, kind) + } + } +} diff --git a/go-providers/anthropic/record.go b/go-providers/anthropic/record.go index 956da21..382fbc4 100644 --- a/go-providers/anthropic/record.go +++ b/go-providers/anthropic/record.go @@ -18,6 +18,18 @@ func Message( provider asdk.Client, req asdk.BetaMessageNewParams, opts ...Option, +) (*asdk.BetaMessage, error) { + return message(ctx, client, req, func(ctx context.Context, request asdk.BetaMessageNewParams) (*asdk.BetaMessage, error) { + return provider.Beta.Messages.New(ctx, request) + }, opts...) +} + +func message( + ctx context.Context, + client *sigil.Client, + req asdk.BetaMessageNewParams, + invoke func(context.Context, asdk.BetaMessageNewParams) (*asdk.BetaMessage, error), + opts ...Option, ) (*asdk.BetaMessage, error) { options := applyOptions(opts) @@ -30,7 +42,7 @@ func Message( }) defer rec.End() - resp, err := provider.Beta.Messages.New(ctx, req) + resp, err := invoke(ctx, req) if err != nil { rec.SetCallError(err) return nil, err diff --git a/go-providers/anthropic/record_test.go b/go-providers/anthropic/record_test.go new file mode 100644 index 0000000..febfd23 --- /dev/null +++ b/go-providers/anthropic/record_test.go @@ -0,0 +1,76 @@ +package anthropic + +import ( + "context" + "errors" + "testing" + + asdk "github.com/anthropics/anthropic-sdk-go" + + "github.com/grafana/sigil/sdks/go/sigil" +) + +func TestConformance_MessageErrorMapping(t *testing.T) { + client := newProviderTestClient(t) + req := testRequest() + + t.Run("provider errors are preserved", func(t *testing.T) { + providerErr := errors.New("provider failed") + + response, err := message( + context.Background(), + client, + req, + func(context.Context, asdk.BetaMessageNewParams) (*asdk.BetaMessage, error) { + return nil, providerErr + }, + ) + if !errors.Is(err, providerErr) { + t.Fatalf("expected provider error, got %v", err) + } + if response != nil { + t.Fatalf("expected nil response on provider error") + } + }) + + t.Run("mapping failures do not hide provider responses", func(t *testing.T) { + expectedResponse := &asdk.BetaMessage{ + Model: asdk.Model("claude-sonnet-4-5"), + StopReason: asdk.BetaStopReasonEndTurn, + Content: []asdk.BetaContentBlockUnion{ + {Type: "text", Text: "hi"}, + }, + } + + response, err := message( + context.Background(), + client, + req, + func(context.Context, asdk.BetaMessageNewParams) (*asdk.BetaMessage, error) { + return expectedResponse, nil + }, + WithProviderName(""), + ) + if err != nil { + t.Fatalf("expected nil local error for mapping failure, got %v", err) + } + if response != expectedResponse { + t.Fatalf("expected wrapper to return provider response pointer") + } + }) +} + +func newProviderTestClient(t *testing.T) *sigil.Client { + t.Helper() + + cfg := sigil.DefaultConfig() + cfg.GenerationExport.Protocol = sigil.GenerationExportProtocolNone + + client := sigil.NewClient(cfg) + t.Cleanup(func() { + if err := client.Shutdown(context.Background()); err != nil { + t.Errorf("shutdown sigil client: %v", err) + } + }) + return client +} diff --git a/go-providers/gemini/conformance_test.go b/go-providers/gemini/conformance_test.go new file mode 100644 index 0000000..f9fbc5f --- /dev/null +++ b/go-providers/gemini/conformance_test.go @@ -0,0 +1,306 @@ +package gemini + +import ( + "math" + "testing" + + "google.golang.org/genai" + + "github.com/grafana/sigil/sdks/go/sigil" +) + +func TestConformance_GenerateContentSyncNormalization(t *testing.T) { + temperature := float32(0.4) + topP := float32(0.75) + thinkingBudget := int32(2048) + model := "gemini-2.5-pro" + contents := []*genai.Content{ + genai.NewContentFromText("What is the weather in Paris?", genai.RoleUser), + genai.NewContentFromParts([]*genai.Part{ + genai.NewPartFromFunctionResponse("weather", map[string]any{ + "temp_c": 18, + }), + }, genai.RoleUser), + } + config := &genai.GenerateContentConfig{ + SystemInstruction: genai.NewContentFromText("Be concise.", genai.RoleUser), + MaxOutputTokens: 300, + Temperature: &temperature, + TopP: &topP, + ToolConfig: &genai.ToolConfig{ + FunctionCallingConfig: &genai.FunctionCallingConfig{ + Mode: genai.FunctionCallingConfigModeAny, + }, + }, + ThinkingConfig: &genai.ThinkingConfig{ + IncludeThoughts: true, + ThinkingBudget: &thinkingBudget, + ThinkingLevel: genai.ThinkingLevelHigh, + }, + Tools: []*genai.Tool{ + { + FunctionDeclarations: []*genai.FunctionDeclaration{ + { + Name: "weather", + Description: "Get weather", + ParametersJsonSchema: map[string]any{ + "type": "object", + }, + }, + }, + }, + }, + } + + resp := &genai.GenerateContentResponse{ + ResponseID: "resp_1", + ModelVersion: "gemini-2.5-pro-001", + Candidates: []*genai.Candidate{ + { + FinishReason: genai.FinishReasonStop, + Content: genai.NewContentFromParts([]*genai.Part{ + { + Text: "reasoning trace", + Thought: true, + }, + { + FunctionCall: &genai.FunctionCall{ + ID: "call_weather", + Name: "weather", + Args: map[string]any{"city": "Paris"}, + }, + }, + genai.NewPartFromText("It is 18C and sunny."), + }, genai.RoleModel), + }, + }, + UsageMetadata: &genai.GenerateContentResponseUsageMetadata{ + PromptTokenCount: 120, + CandidatesTokenCount: 40, + TotalTokenCount: 170, + CachedContentTokenCount: 12, + ThoughtsTokenCount: 10, + ToolUsePromptTokenCount: 9, + }, + } + + generation, err := FromRequestResponse(model, contents, config, resp, + WithConversationID("conv-gemini-sync"), + WithConversationTitle("Paris weather"), + WithAgentName("agent-gemini"), + WithAgentVersion("v-gemini"), + WithTag("tenant", "t-123"), + WithRawArtifacts(), + ) + if err != nil { + t.Fatalf("gemini sync mapping: %v", err) + } + + if generation.Model.Provider != "gemini" || generation.Model.Name != "gemini-2.5-pro" { + t.Fatalf("unexpected model mapping: %#v", generation.Model) + } + if generation.ConversationID != "conv-gemini-sync" || generation.ConversationTitle != "Paris weather" { + t.Fatalf("unexpected conversation mapping: %#v", generation) + } + if generation.AgentName != "agent-gemini" || generation.AgentVersion != "v-gemini" { + t.Fatalf("unexpected agent mapping: name=%q version=%q", generation.AgentName, generation.AgentVersion) + } + if generation.ResponseID != "resp_1" || generation.ResponseModel != "gemini-2.5-pro-001" { + t.Fatalf("unexpected response mapping: id=%q model=%q", generation.ResponseID, generation.ResponseModel) + } + if generation.StopReason != "STOP" { + t.Fatalf("unexpected stop reason: %q", generation.StopReason) + } + if generation.Usage.TotalTokens != 170 || generation.Usage.CacheReadInputTokens != 12 || generation.Usage.ReasoningTokens != 10 { + t.Fatalf("unexpected usage mapping: %#v", generation.Usage) + } + if generation.ThinkingEnabled == nil || !*generation.ThinkingEnabled { + t.Fatalf("expected thinking enabled true, got %v", generation.ThinkingEnabled) + } + if generation.Temperature == nil || math.Abs(*generation.Temperature-0.4) > 1e-6 { + t.Fatalf("unexpected temperature: %v", generation.Temperature) + } + if generation.TopP == nil || math.Abs(*generation.TopP-0.75) > 1e-6 { + t.Fatalf("unexpected top_p: %v", generation.TopP) + } + if len(generation.Output) != 1 || len(generation.Output[0].Parts) != 3 { + t.Fatalf("expected thinking + tool call + text output, got %#v", generation.Output) + } + if generation.Output[0].Parts[0].Kind != sigil.PartKindThinking || generation.Output[0].Parts[0].Thinking != "reasoning trace" { + t.Fatalf("unexpected thinking output: %#v", generation.Output[0].Parts[0]) + } + if generation.Output[0].Parts[1].Kind != sigil.PartKindToolCall { + t.Fatalf("expected tool call output, got %#v", generation.Output[0].Parts[1]) + } + if generation.Output[0].Parts[2].Kind != sigil.PartKindText || generation.Output[0].Parts[2].Text != "It is 18C and sunny." { + t.Fatalf("unexpected text output: %#v", generation.Output[0].Parts[2]) + } + if generation.Metadata["sigil.gen_ai.request.thinking.level"] != "high" { + t.Fatalf("unexpected thinking level metadata: %#v", generation.Metadata) + } + if generation.Tags["tenant"] != "t-123" { + t.Fatalf("expected tenant tag") + } + requireGeminiArtifactKinds(t, generation.Artifacts, + sigil.ArtifactKindRequest, + sigil.ArtifactKindResponse, + sigil.ArtifactKindTools, + ) +} + +func TestConformance_GenerateContentStreamNormalization(t *testing.T) { + temperature := float32(0.2) + topP := float32(0.6) + thinkingBudget := int32(1536) + model := "gemini-2.5-pro" + contents := []*genai.Content{ + genai.NewContentFromText("What is the weather in Paris?", genai.RoleUser), + } + config := &genai.GenerateContentConfig{ + MaxOutputTokens: 90, + Temperature: &temperature, + TopP: &topP, + ToolConfig: &genai.ToolConfig{ + FunctionCallingConfig: &genai.FunctionCallingConfig{ + Mode: genai.FunctionCallingConfigModeAuto, + }, + }, + ThinkingConfig: &genai.ThinkingConfig{ + IncludeThoughts: true, + ThinkingBudget: &thinkingBudget, + ThinkingLevel: genai.ThinkingLevelMedium, + }, + Tools: []*genai.Tool{ + { + FunctionDeclarations: []*genai.FunctionDeclaration{ + {Name: "weather"}, + }, + }, + }, + } + + summary := StreamSummary{ + Responses: []*genai.GenerateContentResponse{ + { + ResponseID: "resp_stream_1", + ModelVersion: "gemini-2.5-pro-001", + Candidates: []*genai.Candidate{ + { + Content: genai.NewContentFromParts([]*genai.Part{ + { + Text: "reasoning trace", + Thought: true, + }, + { + FunctionCall: &genai.FunctionCall{ + ID: "call_weather", + Name: "weather", + Args: map[string]any{"city": "Paris"}, + }, + }, + }, genai.RoleModel), + }, + }, + }, + { + ResponseID: "resp_stream_2", + ModelVersion: "gemini-2.5-pro-001", + Candidates: []*genai.Candidate{ + { + FinishReason: genai.FinishReasonStop, + Content: genai.NewContentFromText("It is 18C and sunny.", genai.RoleModel), + }, + }, + UsageMetadata: &genai.GenerateContentResponseUsageMetadata{ + PromptTokenCount: 20, + CandidatesTokenCount: 6, + TotalTokenCount: 31, + ThoughtsTokenCount: 4, + ToolUsePromptTokenCount: 5, + }, + }, + }, + } + + generation, err := FromStream(model, contents, config, summary, + WithConversationID("conv-gemini-stream"), + WithAgentName("agent-gemini-stream"), + WithAgentVersion("v-gemini-stream"), + WithRawArtifacts(), + ) + if err != nil { + t.Fatalf("gemini stream mapping: %v", err) + } + + if generation.ConversationID != "conv-gemini-stream" || generation.AgentName != "agent-gemini-stream" || generation.AgentVersion != "v-gemini-stream" { + t.Fatalf("unexpected identity mapping: %#v", generation) + } + if generation.ResponseID != "resp_stream_2" || generation.ResponseModel != "gemini-2.5-pro-001" { + t.Fatalf("unexpected response mapping: id=%q model=%q", generation.ResponseID, generation.ResponseModel) + } + if generation.StopReason != "STOP" { + t.Fatalf("unexpected stop reason: %q", generation.StopReason) + } + if generation.Usage.TotalTokens != 31 || generation.Usage.ReasoningTokens != 4 { + t.Fatalf("unexpected usage mapping: %#v", generation.Usage) + } + if len(generation.Output) != 2 { + t.Fatalf("expected streamed thinking/tool output plus final text, got %#v", generation.Output) + } + if generation.Output[0].Parts[0].Kind != sigil.PartKindThinking || generation.Output[0].Parts[0].Thinking != "reasoning trace" { + t.Fatalf("unexpected streamed thinking output: %#v", generation.Output[0].Parts[0]) + } + if generation.Output[0].Parts[1].Kind != sigil.PartKindToolCall { + t.Fatalf("expected streamed tool call output, got %#v", generation.Output[0].Parts[1]) + } + if generation.Output[1].Parts[0].Kind != sigil.PartKindText || generation.Output[1].Parts[0].Text != "It is 18C and sunny." { + t.Fatalf("unexpected streamed text output: %#v", generation.Output[1].Parts[0]) + } + requireGeminiArtifactKinds(t, generation.Artifacts, + sigil.ArtifactKindRequest, + sigil.ArtifactKindTools, + sigil.ArtifactKindProviderEvent, + ) +} + +func TestConformance_GeminiErrorMapping(t *testing.T) { + if _, err := FromRequestResponse("", nil, nil, &genai.GenerateContentResponse{}); err == nil || err.Error() != "request model is required" { + t.Fatalf("expected explicit request model error, got %v", err) + } + if _, err := FromRequestResponse("gemini-2.5-pro", nil, nil, nil); err == nil || err.Error() != "response is required" { + t.Fatalf("expected explicit response error, got %v", err) + } + if _, err := FromStream("gemini-2.5-pro", nil, nil, StreamSummary{}); err == nil || err.Error() != "stream summary has no responses" { + t.Fatalf("expected explicit stream error, got %v", err) + } + + _, err := FromRequestResponse( + "gemini-2.5-pro", + nil, + nil, + &genai.GenerateContentResponse{ + Candidates: []*genai.Candidate{ + { + Content: genai.NewContentFromText("ok", genai.RoleModel), + }, + }, + }, + WithProviderName(""), + ) + if err == nil || err.Error() != "generation.model.provider is required" { + t.Fatalf("expected explicit validation error for invalid provider mapping, got %v", err) + } +} + +func requireGeminiArtifactKinds(t *testing.T, artifacts []sigil.Artifact, want ...sigil.ArtifactKind) { + t.Helper() + + if len(artifacts) != len(want) { + t.Fatalf("expected %d artifacts, got %d", len(want), len(artifacts)) + } + for i, kind := range want { + if artifacts[i].Kind != kind { + t.Fatalf("artifact %d kind mismatch: got %q want %q", i, artifacts[i].Kind, kind) + } + } +} diff --git a/go-providers/gemini/record.go b/go-providers/gemini/record.go index d5329c8..73b2329 100644 --- a/go-providers/gemini/record.go +++ b/go-providers/gemini/record.go @@ -20,6 +20,25 @@ func GenerateContent( contents []*genai.Content, config *genai.GenerateContentConfig, opts ...Option, +) (*genai.GenerateContentResponse, error) { + return generateContent(ctx, client, model, contents, config, func( + ctx context.Context, + model string, + contents []*genai.Content, + config *genai.GenerateContentConfig, + ) (*genai.GenerateContentResponse, error) { + return provider.Models.GenerateContent(ctx, model, contents, config) + }, opts...) +} + +func generateContent( + ctx context.Context, + client *sigil.Client, + model string, + contents []*genai.Content, + config *genai.GenerateContentConfig, + invoke func(context.Context, string, []*genai.Content, *genai.GenerateContentConfig) (*genai.GenerateContentResponse, error), + opts ...Option, ) (*genai.GenerateContentResponse, error) { options := applyOptions(opts) @@ -32,7 +51,7 @@ func GenerateContent( }) defer rec.End() - resp, err := provider.Models.GenerateContent(ctx, model, contents, config) + resp, err := invoke(ctx, model, contents, config) if err != nil { rec.SetCallError(err) return nil, err diff --git a/go-providers/gemini/record_test.go b/go-providers/gemini/record_test.go index 90ffc68..8d2b41d 100644 --- a/go-providers/gemini/record_test.go +++ b/go-providers/gemini/record_test.go @@ -12,7 +12,7 @@ import ( ) func TestEmbedContentReturnsRecorderValidationErrorAfterEnd(t *testing.T) { - client := newEmbeddingTestClient(t) + client := newProviderTestClient(t) contents := []*genai.Content{ genai.NewContentFromText("hello", genai.RoleUser), @@ -51,7 +51,7 @@ func TestEmbedContentReturnsRecorderValidationErrorAfterEnd(t *testing.T) { } func TestEmbedContentPreservesProviderErrors(t *testing.T) { - client := newEmbeddingTestClient(t) + client := newProviderTestClient(t) providerErr := errors.New("provider failed") @@ -74,7 +74,65 @@ func TestEmbedContentPreservesProviderErrors(t *testing.T) { } } -func newEmbeddingTestClient(t *testing.T) *sigil.Client { +func TestConformance_GenerateContentErrorMapping(t *testing.T) { + client := newProviderTestClient(t) + model := "gemini-2.5-pro" + contents := []*genai.Content{ + genai.NewContentFromText("hello", genai.RoleUser), + } + + t.Run("provider errors are preserved", func(t *testing.T) { + providerErr := errors.New("provider failed") + + response, err := generateContent( + context.Background(), + client, + model, + contents, + nil, + func(context.Context, string, []*genai.Content, *genai.GenerateContentConfig) (*genai.GenerateContentResponse, error) { + return nil, providerErr + }, + ) + if !errors.Is(err, providerErr) { + t.Fatalf("expected provider error, got %v", err) + } + if response != nil { + t.Fatalf("expected nil response on provider error") + } + }) + + t.Run("mapping failures do not hide provider responses", func(t *testing.T) { + expectedResponse := &genai.GenerateContentResponse{ + Candidates: []*genai.Candidate{ + { + FinishReason: genai.FinishReasonStop, + Content: genai.NewContentFromText("hi", genai.RoleModel), + }, + }, + } + + response, err := generateContent( + context.Background(), + client, + model, + contents, + nil, + func(context.Context, string, []*genai.Content, *genai.GenerateContentConfig) (*genai.GenerateContentResponse, error) { + return expectedResponse, nil + }, + WithProviderName(""), + ) + if err != nil { + t.Fatalf("expected nil local error for mapping failure, got %v", err) + } + if response != expectedResponse { + t.Fatalf("expected wrapper to return provider response pointer") + } + }) +} + +func newProviderTestClient(t *testing.T) *sigil.Client { t.Helper() cfg := sigil.DefaultConfig() diff --git a/go-providers/openai/conformance_test.go b/go-providers/openai/conformance_test.go new file mode 100644 index 0000000..5c28f0f --- /dev/null +++ b/go-providers/openai/conformance_test.go @@ -0,0 +1,415 @@ +package openai + +import ( + "testing" + + osdk "github.com/openai/openai-go/v3" + "github.com/openai/openai-go/v3/packages/param" + oresponses "github.com/openai/openai-go/v3/responses" + "github.com/openai/openai-go/v3/shared" + + "github.com/grafana/sigil/sdks/go/sigil" +) + +func TestConformance_ChatCompletionsSyncNormalization(t *testing.T) { + req := osdk.ChatCompletionNewParams{ + Model: shared.ChatModel("gpt-4o-mini"), + Messages: []osdk.ChatCompletionMessageParamUnion{ + osdk.SystemMessage("You are concise."), + osdk.UserMessage("What is the weather in Paris?"), + osdk.ToolMessage(`{"temp_c":18}`, "call_weather"), + }, + Tools: []osdk.ChatCompletionToolUnionParam{ + osdk.ChatCompletionFunctionTool(shared.FunctionDefinitionParam{ + Name: "weather", + Description: osdk.String("Get weather"), + Parameters: shared.FunctionParameters{ + "type": "object", + "properties": map[string]any{ + "city": map[string]any{"type": "string"}, + }, + }, + }), + }, + MaxCompletionTokens: param.NewOpt(int64(128)), + Temperature: param.NewOpt(0.7), + TopP: param.NewOpt(0.9), + ToolChoice: osdk.ToolChoiceOptionFunctionToolChoice(osdk.ChatCompletionNamedToolChoiceFunctionParam{Name: "weather"}), + ReasoningEffort: shared.ReasoningEffortLow, + } + + resp := &osdk.ChatCompletion{ + ID: "chatcmpl_1", + Model: "gpt-4o-mini", + Choices: []osdk.ChatCompletionChoice{ + { + FinishReason: "tool_calls", + Message: osdk.ChatCompletionMessage{ + Content: "Calling tool", + ToolCalls: []osdk.ChatCompletionMessageToolCallUnion{ + { + ID: "call_weather", + Type: "function", + Function: osdk.ChatCompletionMessageFunctionToolCallFunction{ + Name: "weather", + Arguments: `{"city":"Paris"}`, + }, + }, + }, + }, + }, + }, + Usage: osdk.CompletionUsage{ + PromptTokens: 120, + CompletionTokens: 42, + TotalTokens: 162, + PromptTokensDetails: osdk.CompletionUsagePromptTokensDetails{ + CachedTokens: 8, + }, + CompletionTokensDetails: osdk.CompletionUsageCompletionTokensDetails{ + ReasoningTokens: 5, + }, + }, + } + + generation, err := ChatCompletionsFromRequestResponse(req, resp, + WithConversationID("conv-openai-sync"), + WithConversationTitle("Paris weather"), + WithAgentName("agent-openai"), + WithAgentVersion("v-openai"), + WithTag("tenant", "t-123"), + WithRawArtifacts(), + ) + if err != nil { + t.Fatalf("chat completions sync mapping: %v", err) + } + + if generation.Model.Provider != "openai" || generation.Model.Name != "gpt-4o-mini" { + t.Fatalf("unexpected model mapping: %#v", generation.Model) + } + if generation.ConversationID != "conv-openai-sync" || generation.ConversationTitle != "Paris weather" { + t.Fatalf("unexpected conversation mapping: id=%q title=%q", generation.ConversationID, generation.ConversationTitle) + } + if generation.AgentName != "agent-openai" || generation.AgentVersion != "v-openai" { + t.Fatalf("unexpected agent mapping: name=%q version=%q", generation.AgentName, generation.AgentVersion) + } + if generation.ResponseID != "chatcmpl_1" || generation.ResponseModel != "gpt-4o-mini" { + t.Fatalf("unexpected response mapping: id=%q model=%q", generation.ResponseID, generation.ResponseModel) + } + if generation.SystemPrompt != "You are concise." { + t.Fatalf("unexpected system prompt: %q", generation.SystemPrompt) + } + if generation.StopReason != "tool_calls" { + t.Fatalf("unexpected stop reason: %q", generation.StopReason) + } + if generation.Usage.TotalTokens != 162 || generation.Usage.CacheReadInputTokens != 8 || generation.Usage.ReasoningTokens != 5 { + t.Fatalf("unexpected usage mapping: %#v", generation.Usage) + } + if generation.ThinkingEnabled == nil || !*generation.ThinkingEnabled { + t.Fatalf("expected thinking enabled true, got %v", generation.ThinkingEnabled) + } + if len(generation.Output) != 1 || len(generation.Output[0].Parts) != 2 { + t.Fatalf("expected one assistant message with text + tool call, got %#v", generation.Output) + } + if generation.Output[0].Parts[0].Kind != sigil.PartKindText || generation.Output[0].Parts[0].Text != "Calling tool" { + t.Fatalf("unexpected assistant text part: %#v", generation.Output[0].Parts[0]) + } + if generation.Output[0].Parts[1].Kind != sigil.PartKindToolCall { + t.Fatalf("expected tool_call part, got %#v", generation.Output[0].Parts[1]) + } + if generation.Output[0].Parts[1].ToolCall.ID != "call_weather" || generation.Output[0].Parts[1].ToolCall.Name != "weather" { + t.Fatalf("unexpected tool call mapping: %#v", generation.Output[0].Parts[1].ToolCall) + } + if string(generation.Output[0].Parts[1].ToolCall.InputJSON) != `{"city":"Paris"}` { + t.Fatalf("unexpected tool call input: %q", string(generation.Output[0].Parts[1].ToolCall.InputJSON)) + } + if generation.Tags["tenant"] != "t-123" { + t.Fatalf("expected tenant tag") + } + requireOpenAIArtifactKinds(t, generation.Artifacts, + sigil.ArtifactKindRequest, + sigil.ArtifactKindResponse, + sigil.ArtifactKindTools, + ) +} + +func TestConformance_ChatCompletionsStreamNormalization(t *testing.T) { + req := osdk.ChatCompletionNewParams{ + Model: shared.ChatModel("gpt-4o-mini"), + Messages: []osdk.ChatCompletionMessageParamUnion{ + osdk.SystemMessage("You are concise."), + osdk.UserMessage("What is the weather in Paris?"), + }, + Tools: []osdk.ChatCompletionToolUnionParam{ + osdk.ChatCompletionFunctionTool(shared.FunctionDefinitionParam{ + Name: "weather", + }), + }, + MaxCompletionTokens: param.NewOpt(int64(42)), + Temperature: param.NewOpt(0.15), + TopP: param.NewOpt(0.4), + ToolChoice: osdk.ToolChoiceOptionFunctionToolChoice(osdk.ChatCompletionNamedToolChoiceFunctionParam{Name: "weather"}), + ReasoningEffort: shared.ReasoningEffortMedium, + } + + summary := ChatCompletionsStreamSummary{ + Chunks: []osdk.ChatCompletionChunk{ + { + ID: "chatcmpl_stream_1", + Model: "gpt-4o-mini", + Choices: []osdk.ChatCompletionChunkChoice{ + { + Delta: osdk.ChatCompletionChunkChoiceDelta{ + Content: "Calling tool", + ToolCalls: []osdk.ChatCompletionChunkChoiceDeltaToolCall{ + { + Index: 0, + ID: "call_weather", + Function: osdk.ChatCompletionChunkChoiceDeltaToolCallFunction{ + Name: "weather", + Arguments: `{"city":"Pa`, + }, + }, + }, + }, + }, + }, + }, + { + Choices: []osdk.ChatCompletionChunkChoice{ + { + Delta: osdk.ChatCompletionChunkChoiceDelta{ + Content: " now.", + ToolCalls: []osdk.ChatCompletionChunkChoiceDeltaToolCall{ + { + Index: 0, + Function: osdk.ChatCompletionChunkChoiceDeltaToolCallFunction{ + Arguments: `ris"}`, + }, + }, + }, + }, + FinishReason: "tool_calls", + }, + }, + Usage: osdk.CompletionUsage{ + PromptTokens: 20, + CompletionTokens: 5, + TotalTokens: 25, + }, + }, + }, + } + + generation, err := ChatCompletionsFromStream(req, summary, + WithConversationID("conv-openai-stream"), + WithAgentName("agent-openai-stream"), + WithAgentVersion("v-openai-stream"), + WithRawArtifacts(), + ) + if err != nil { + t.Fatalf("chat completions stream mapping: %v", err) + } + + if generation.ConversationID != "conv-openai-stream" || generation.AgentName != "agent-openai-stream" || generation.AgentVersion != "v-openai-stream" { + t.Fatalf("unexpected identity mapping: %#v", generation) + } + if generation.ResponseID != "chatcmpl_stream_1" || generation.ResponseModel != "gpt-4o-mini" { + t.Fatalf("unexpected response mapping: id=%q model=%q", generation.ResponseID, generation.ResponseModel) + } + if generation.StopReason != "tool_calls" { + t.Fatalf("unexpected stop reason: %q", generation.StopReason) + } + if generation.Usage.TotalTokens != 25 { + t.Fatalf("unexpected usage mapping: %#v", generation.Usage) + } + if generation.ThinkingEnabled == nil || !*generation.ThinkingEnabled { + t.Fatalf("expected thinking enabled true, got %v", generation.ThinkingEnabled) + } + if len(generation.Output) != 1 || len(generation.Output[0].Parts) != 2 { + t.Fatalf("expected merged assistant output, got %#v", generation.Output) + } + if generation.Output[0].Parts[0].Text != "Calling tool now." { + t.Fatalf("unexpected streamed text: %q", generation.Output[0].Parts[0].Text) + } + if generation.Output[0].Parts[1].Kind != sigil.PartKindToolCall { + t.Fatalf("expected tool call output, got %#v", generation.Output[0].Parts[1]) + } + if string(generation.Output[0].Parts[1].ToolCall.InputJSON) != `{"city":"Paris"}` { + t.Fatalf("unexpected streamed tool input: %q", string(generation.Output[0].Parts[1].ToolCall.InputJSON)) + } + requireOpenAIArtifactKinds(t, generation.Artifacts, + sigil.ArtifactKindRequest, + sigil.ArtifactKindTools, + sigil.ArtifactKindProviderEvent, + ) +} + +func TestConformance_ResponsesSyncNormalization(t *testing.T) { + req := oresponses.ResponseNewParams{ + Model: shared.ResponsesModel("gpt-5"), + Instructions: param.NewOpt("Be concise."), + Input: oresponses.ResponseNewParamsInputUnion{OfString: param.NewOpt("hello")}, + MaxOutputTokens: param.NewOpt(int64(320)), + Temperature: param.NewOpt(0.2), + TopP: param.NewOpt(0.85), + Reasoning: shared.ReasoningParam{ + Effort: shared.ReasoningEffortMedium, + }, + } + + resp := &oresponses.Response{ + ID: "resp_1", + Model: shared.ResponsesModel("gpt-5"), + Status: oresponses.ResponseStatusCompleted, + Output: []oresponses.ResponseOutputItemUnion{ + { + Type: "message", + Content: []oresponses.ResponseOutputMessageContentUnion{ + {Type: "output_text", Text: "world"}, + }, + }, + { + Type: "function_call", + CallID: "call_weather", + Name: "weather", + Arguments: oresponses.ResponseOutputItemUnionArguments{OfString: `{"city":"Paris"}`}, + }, + }, + Usage: oresponses.ResponseUsage{ + InputTokens: 80, + OutputTokens: 20, + TotalTokens: 100, + InputTokensDetails: oresponses.ResponseUsageInputTokensDetails{ + CachedTokens: 2, + }, + OutputTokensDetails: oresponses.ResponseUsageOutputTokensDetails{ + ReasoningTokens: 3, + }, + }, + } + + generation, err := ResponsesFromRequestResponse(req, resp, WithRawArtifacts()) + if err != nil { + t.Fatalf("responses sync mapping: %v", err) + } + + if generation.Model.Provider != "openai" || generation.Model.Name != "gpt-5" { + t.Fatalf("unexpected model mapping: %#v", generation.Model) + } + if generation.ResponseID != "resp_1" || generation.ResponseModel != "gpt-5" { + t.Fatalf("unexpected response mapping: id=%q model=%q", generation.ResponseID, generation.ResponseModel) + } + if generation.SystemPrompt != "Be concise." { + t.Fatalf("unexpected system prompt: %q", generation.SystemPrompt) + } + if generation.StopReason != "stop" { + t.Fatalf("unexpected stop reason: %q", generation.StopReason) + } + if generation.ThinkingEnabled == nil || !*generation.ThinkingEnabled { + t.Fatalf("expected thinking enabled true, got %v", generation.ThinkingEnabled) + } + if generation.Usage.TotalTokens != 100 || generation.Usage.CacheReadInputTokens != 2 || generation.Usage.ReasoningTokens != 3 { + t.Fatalf("unexpected usage mapping: %#v", generation.Usage) + } + if len(generation.Output) != 2 { + t.Fatalf("expected text + tool call outputs, got %#v", generation.Output) + } + if generation.Output[0].Parts[0].Text != "world" { + t.Fatalf("unexpected response text: %q", generation.Output[0].Parts[0].Text) + } + if generation.Output[1].Parts[0].Kind != sigil.PartKindToolCall { + t.Fatalf("expected response tool call, got %#v", generation.Output[1].Parts[0]) + } + requireOpenAIArtifactKinds(t, generation.Artifacts, + sigil.ArtifactKindRequest, + sigil.ArtifactKindResponse, + ) +} + +func TestConformance_ResponsesStreamNormalization(t *testing.T) { + req := oresponses.ResponseNewParams{ + Model: shared.ResponsesModel("gpt-5"), + Input: oresponses.ResponseNewParamsInputUnion{OfString: param.NewOpt("hello")}, + MaxOutputTokens: param.NewOpt(int64(128)), + } + + summary := ResponsesStreamSummary{ + Events: []oresponses.ResponseStreamEventUnion{ + { + Type: "response.output_text.delta", + Delta: "hello", + }, + { + Type: "response.output_text.delta", + Delta: " world", + }, + { + Type: "response.completed", + Response: oresponses.Response{ + ID: "resp_stream_1", + Model: shared.ResponsesModel("gpt-5"), + }, + }, + }, + } + + generation, err := ResponsesFromStream(req, summary, WithRawArtifacts()) + if err != nil { + t.Fatalf("responses stream mapping: %v", err) + } + + if generation.Model.Provider != "openai" || generation.Model.Name != "gpt-5" { + t.Fatalf("unexpected model mapping: %#v", generation.Model) + } + if generation.ResponseID != "resp_stream_1" || generation.ResponseModel != "gpt-5" { + t.Fatalf("unexpected response mapping: id=%q model=%q", generation.ResponseID, generation.ResponseModel) + } + if generation.StopReason != "stop" { + t.Fatalf("unexpected stop reason: %q", generation.StopReason) + } + if len(generation.Output) != 1 || generation.Output[0].Parts[0].Text != "hello world" { + t.Fatalf("unexpected streamed output: %#v", generation.Output) + } + requireOpenAIArtifactKinds(t, generation.Artifacts, + sigil.ArtifactKindRequest, + sigil.ArtifactKindProviderEvent, + ) +} + +func TestConformance_OpenAIErrorMapping(t *testing.T) { + if _, err := ChatCompletionsFromRequestResponse(osdk.ChatCompletionNewParams{}, nil); err == nil || err.Error() != "response is required" { + t.Fatalf("expected explicit chat response error, got %v", err) + } + if _, err := ChatCompletionsFromStream(osdk.ChatCompletionNewParams{}, ChatCompletionsStreamSummary{}); err == nil || err.Error() != "stream summary has no chunks and no final response" { + t.Fatalf("expected explicit chat stream error, got %v", err) + } + if _, err := ResponsesFromRequestResponse(oresponses.ResponseNewParams{}, nil); err == nil || err.Error() != "response is required" { + t.Fatalf("expected explicit responses response error, got %v", err) + } + if _, err := ResponsesFromStream(oresponses.ResponseNewParams{}, ResponsesStreamSummary{}); err == nil || err.Error() != "stream summary has no events and no final response" { + t.Fatalf("expected explicit responses stream error, got %v", err) + } + + _, err := ChatCompletionsFromRequestResponse( + osdk.ChatCompletionNewParams{Model: shared.ChatModel("gpt-4o-mini")}, + &osdk.ChatCompletion{Model: "gpt-4o-mini"}, + WithProviderName(""), + ) + if err == nil || err.Error() != "generation.model.provider is required" { + t.Fatalf("expected explicit validation error for invalid provider mapping, got %v", err) + } +} + +func requireOpenAIArtifactKinds(t *testing.T, artifacts []sigil.Artifact, want ...sigil.ArtifactKind) { + t.Helper() + + if len(artifacts) != len(want) { + t.Fatalf("expected %d artifacts, got %d", len(want), len(artifacts)) + } + for i, kind := range want { + if artifacts[i].Kind != kind { + t.Fatalf("artifact %d kind mismatch: got %q want %q", i, artifacts[i].Kind, kind) + } + } +} diff --git a/go-providers/openai/record.go b/go-providers/openai/record.go index 80a4ee3..c99ae49 100644 --- a/go-providers/openai/record.go +++ b/go-providers/openai/record.go @@ -19,6 +19,18 @@ func ChatCompletionsNew( provider osdk.Client, req osdk.ChatCompletionNewParams, opts ...Option, +) (*osdk.ChatCompletion, error) { + return chatCompletionsNew(ctx, client, req, func(ctx context.Context, request osdk.ChatCompletionNewParams) (*osdk.ChatCompletion, error) { + return provider.Chat.Completions.New(ctx, request) + }, opts...) +} + +func chatCompletionsNew( + ctx context.Context, + client *sigil.Client, + req osdk.ChatCompletionNewParams, + invoke func(context.Context, osdk.ChatCompletionNewParams) (*osdk.ChatCompletion, error), + opts ...Option, ) (*osdk.ChatCompletion, error) { options := applyOptions(opts) @@ -31,7 +43,7 @@ func ChatCompletionsNew( }) defer rec.End() - resp, err := provider.Chat.Completions.New(ctx, req) + resp, err := invoke(ctx, req) if err != nil { rec.SetCallError(err) return nil, err @@ -101,6 +113,18 @@ func ResponsesNew( provider osdk.Client, req oresponses.ResponseNewParams, opts ...Option, +) (*oresponses.Response, error) { + return responsesNew(ctx, client, req, func(ctx context.Context, request oresponses.ResponseNewParams) (*oresponses.Response, error) { + return provider.Responses.New(ctx, request) + }, opts...) +} + +func responsesNew( + ctx context.Context, + client *sigil.Client, + req oresponses.ResponseNewParams, + invoke func(context.Context, oresponses.ResponseNewParams) (*oresponses.Response, error), + opts ...Option, ) (*oresponses.Response, error) { options := applyOptions(opts) @@ -113,7 +137,7 @@ func ResponsesNew( }) defer rec.End() - resp, err := provider.Responses.New(ctx, req) + resp, err := invoke(ctx, req) if err != nil { rec.SetCallError(err) return nil, err diff --git a/go-providers/openai/record_test.go b/go-providers/openai/record_test.go index 37ce9ae..e748caf 100644 --- a/go-providers/openai/record_test.go +++ b/go-providers/openai/record_test.go @@ -7,12 +7,15 @@ import ( "testing" osdk "github.com/openai/openai-go/v3" + "github.com/openai/openai-go/v3/packages/param" + oresponses "github.com/openai/openai-go/v3/responses" + "github.com/openai/openai-go/v3/shared" "github.com/grafana/sigil/sdks/go/sigil" ) func TestEmbeddingsNewReturnsRecorderValidationErrorAfterEnd(t *testing.T) { - client := newEmbeddingTestClient(t) + client := newProviderTestClient(t) req := osdk.EmbeddingNewParams{ Model: osdk.EmbeddingModel("text-embedding-3-small"), @@ -49,7 +52,7 @@ func TestEmbeddingsNewReturnsRecorderValidationErrorAfterEnd(t *testing.T) { } func TestEmbeddingsNewPreservesProviderErrors(t *testing.T) { - client := newEmbeddingTestClient(t) + client := newProviderTestClient(t) req := osdk.EmbeddingNewParams{ Model: osdk.EmbeddingModel("text-embedding-3-small"), @@ -73,7 +76,124 @@ func TestEmbeddingsNewPreservesProviderErrors(t *testing.T) { } } -func newEmbeddingTestClient(t *testing.T) *sigil.Client { +func TestConformance_ChatCompletionsNewErrorMapping(t *testing.T) { + client := newProviderTestClient(t) + req := osdk.ChatCompletionNewParams{ + Model: shared.ChatModel("gpt-4o-mini"), + Messages: []osdk.ChatCompletionMessageParamUnion{ + osdk.UserMessage("hello"), + }, + } + + t.Run("provider errors are preserved", func(t *testing.T) { + providerErr := errors.New("provider failed") + + response, err := chatCompletionsNew( + context.Background(), + client, + req, + func(context.Context, osdk.ChatCompletionNewParams) (*osdk.ChatCompletion, error) { + return nil, providerErr + }, + ) + if !errors.Is(err, providerErr) { + t.Fatalf("expected provider error, got %v", err) + } + if response != nil { + t.Fatalf("expected nil response on provider error") + } + }) + + t.Run("mapping failures do not hide provider responses", func(t *testing.T) { + expectedResponse := &osdk.ChatCompletion{ + Model: "gpt-4o-mini", + Choices: []osdk.ChatCompletionChoice{ + { + FinishReason: "stop", + Message: osdk.ChatCompletionMessage{ + Content: "hi", + }, + }, + }, + } + + response, err := chatCompletionsNew( + context.Background(), + client, + req, + func(context.Context, osdk.ChatCompletionNewParams) (*osdk.ChatCompletion, error) { + return expectedResponse, nil + }, + WithProviderName(""), + ) + if err != nil { + t.Fatalf("expected nil local error for mapping failure, got %v", err) + } + if response != expectedResponse { + t.Fatalf("expected wrapper to return provider response pointer") + } + }) +} + +func TestConformance_ResponsesNewErrorMapping(t *testing.T) { + client := newProviderTestClient(t) + req := oresponses.ResponseNewParams{ + Model: shared.ResponsesModel("gpt-5"), + Input: oresponses.ResponseNewParamsInputUnion{OfString: param.NewOpt("hello")}, + } + + t.Run("provider errors are preserved", func(t *testing.T) { + providerErr := errors.New("provider failed") + + response, err := responsesNew( + context.Background(), + client, + req, + func(context.Context, oresponses.ResponseNewParams) (*oresponses.Response, error) { + return nil, providerErr + }, + ) + if !errors.Is(err, providerErr) { + t.Fatalf("expected provider error, got %v", err) + } + if response != nil { + t.Fatalf("expected nil response on provider error") + } + }) + + t.Run("mapping failures do not hide provider responses", func(t *testing.T) { + expectedResponse := &oresponses.Response{ + Model: shared.ResponsesModel("gpt-5"), + Status: oresponses.ResponseStatusCompleted, + Output: []oresponses.ResponseOutputItemUnion{ + { + Type: "message", + Content: []oresponses.ResponseOutputMessageContentUnion{ + {Type: "output_text", Text: "hi"}, + }, + }, + }, + } + + response, err := responsesNew( + context.Background(), + client, + req, + func(context.Context, oresponses.ResponseNewParams) (*oresponses.Response, error) { + return expectedResponse, nil + }, + WithProviderName(""), + ) + if err != nil { + t.Fatalf("expected nil local error for mapping failure, got %v", err) + } + if response != expectedResponse { + t.Fatalf("expected wrapper to return provider response pointer") + } + }) +} + +func newProviderTestClient(t *testing.T) *sigil.Client { t.Helper() cfg := sigil.DefaultConfig() From c9c3e4d617bffc40f1c7ad35348b88e145309300 Mon Sep 17 00:00:00 2001 From: Cyril Tovena Date: Thu, 12 Mar 2026 15:10:29 +0100 Subject: [PATCH 054/133] Codify Anthropic embedding unsupported conformance ## Summary - add `anthropic.CheckEmbeddingsSupport()` and `ErrEmbeddingsUnsupported` so Anthropic embedding support is explicit for the current Go provider surface - add regression and public conformance tests that pin Anthropic embeddings as unsupported until the official SDK/API changes - update Anthropic and SDK conformance docs to record the unsupported outcome instead of implying a missing wrapper bug ## Why - the pinned Anthropic Go SDK (`github.com/anthropics/anthropic-sdk-go v1.26.0`) exposes no native embeddings service - Anthropic's official docs still state there is no Anthropic embedding model as of 2026-03-12 - callers and provider-conformance work need a stable contract instead of fabricated request DTOs or synthetic embedding spans ## Validation - `cd sdks/go-providers/anthropic && go test ./...` - `cd sdks/go && GOWORK=off go test ./sigil -run '^TestConformance' -count=1` ## Linear - `GRA-26` - https://linear.app/grafana-sigil/issue/GRA-26/add-go-anthropic-embedding-wrapper-and-conformance-coverage --- > [!NOTE] > **Low Risk** > Low risk: adds a small new capability-check API plus tests and doc updates, without changing generation mapping or transport behavior. > > **Overview** > Codifies Anthropic embeddings as *explicitly unsupported* in the Go provider helper by adding `ErrEmbeddingsUnsupported` and `CheckEmbeddingsSupport()` as a deterministic capability gate. > > Extends the Anthropic provider conformance suite with an assertion that embeddings remain unsupported, and updates conformance/spec and README docs to treat missing embeddings as an intentional contract (until the upstream SDK exposes a native embeddings API). > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit ea02da8028041b48ae50c6fe476d9016b2e2f693. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). --- go-providers/anthropic/README.md | 10 ++++++++++ go-providers/anthropic/conformance_test.go | 11 +++++++++++ go-providers/anthropic/doc.go | 10 ++++++++-- go-providers/anthropic/embedding_support.go | 15 +++++++++++++++ .../anthropic/embedding_support_test.go | 19 +++++++++++++++++++ go/README.md | 2 +- 6 files changed, 64 insertions(+), 3 deletions(-) create mode 100644 go-providers/anthropic/embedding_support.go create mode 100644 go-providers/anthropic/embedding_support_test.go diff --git a/go-providers/anthropic/README.md b/go-providers/anthropic/README.md index b80578a..0d4a398 100644 --- a/go-providers/anthropic/README.md +++ b/go-providers/anthropic/README.md @@ -7,10 +7,20 @@ typed Sigil `Generation` model. This helper currently supports Anthropic Messages APIs only. Native Anthropic embeddings endpoints are not available in the official SDK/API surface used in this repository. +Use the exported support gate when you need a deterministic capability check: + +```go +if err := anthropic.CheckEmbeddingsSupport(); err != nil { + return err +} +``` + ## Scope - One-liner wrappers: - `Message(ctx, sigilClient, provider, req, opts...)` - `MessageStream(ctx, sigilClient, provider, req, opts...)` +- Embedding capability gate: + - `CheckEmbeddingsSupport()` - Request/response mapper: - `FromRequestResponse(req, resp, opts...)` - Stream mapper: diff --git a/go-providers/anthropic/conformance_test.go b/go-providers/anthropic/conformance_test.go index a2c9fff..7cb02b9 100644 --- a/go-providers/anthropic/conformance_test.go +++ b/go-providers/anthropic/conformance_test.go @@ -1,6 +1,7 @@ package anthropic import ( + "errors" "testing" asdk "github.com/anthropics/anthropic-sdk-go" @@ -225,6 +226,16 @@ func TestConformance_AnthropicErrorMapping(t *testing.T) { } } +func TestConformance_EmbeddingSupportStatus(t *testing.T) { + err := CheckEmbeddingsSupport() + if err == nil { + t.Fatalf("expected Anthropic embeddings to remain unsupported") + } + if !errors.Is(err, ErrEmbeddingsUnsupported) { + t.Fatalf("expected ErrEmbeddingsUnsupported, got %v", err) + } +} + func requireAnthropicArtifactKinds(t *testing.T, artifacts []sigil.Artifact, want ...sigil.ArtifactKind) { t.Helper() diff --git a/go-providers/anthropic/doc.go b/go-providers/anthropic/doc.go index 843809c..61b2298 100644 --- a/go-providers/anthropic/doc.go +++ b/go-providers/anthropic/doc.go @@ -1,5 +1,11 @@ // Package anthropic maps Anthropic message payloads to sigil.Generation. // -// Use FromRequestResponse for non-streaming calls and FromStream for streaming calls. -// The resulting generation keeps request content in Input and model output in Output. +// Use FromRequestResponse for non-streaming calls and FromStream for streaming +// calls. The resulting generation keeps request content in Input and model +// output in Output. +// +// This package currently supports Anthropic Messages APIs only. Call +// CheckEmbeddingsSupport before wiring embedding-specific flows; the official +// Anthropic SDK/API surface used by this module does not expose a native +// embeddings endpoint. package anthropic diff --git a/go-providers/anthropic/embedding_support.go b/go-providers/anthropic/embedding_support.go new file mode 100644 index 0000000..22f81ae --- /dev/null +++ b/go-providers/anthropic/embedding_support.go @@ -0,0 +1,15 @@ +package anthropic + +import "errors" + +// ErrEmbeddingsUnsupported reports that the official Anthropic SDK/API surface +// used by this helper does not expose a native embeddings endpoint. +var ErrEmbeddingsUnsupported = errors.New("anthropic: embeddings are not supported by the official Anthropic SDK/API surface") + +// CheckEmbeddingsSupport reports whether this helper can wrap a native Anthropic +// embeddings API. The current official Anthropic SDK/API surface used by this +// module does not expose embeddings, so callers should treat a non-nil error as +// a hard capability boundary rather than inventing custom request DTOs. +func CheckEmbeddingsSupport() error { + return ErrEmbeddingsUnsupported +} diff --git a/go-providers/anthropic/embedding_support_test.go b/go-providers/anthropic/embedding_support_test.go new file mode 100644 index 0000000..d829694 --- /dev/null +++ b/go-providers/anthropic/embedding_support_test.go @@ -0,0 +1,19 @@ +package anthropic + +import ( + "errors" + "testing" +) + +func TestCheckEmbeddingsSupportReturnsUnsupportedError(t *testing.T) { + err := CheckEmbeddingsSupport() + if err == nil { + t.Fatalf("expected embeddings support error") + } + if !errors.Is(err, ErrEmbeddingsUnsupported) { + t.Fatalf("expected ErrEmbeddingsUnsupported, got %v", err) + } + if got, want := err.Error(), ErrEmbeddingsUnsupported.Error(); got != want { + t.Fatalf("unexpected embeddings support error: got %q want %q", got, want) + } +} diff --git a/go/README.md b/go/README.md index f3b1770..27e9ed3 100644 --- a/go/README.md +++ b/go/README.md @@ -304,7 +304,7 @@ Provider modules are documented wrapper-first for ergonomics and include explici Current Go provider helpers: - `sdks/go-providers/openai` (OpenAI Chat Completions + Responses wrappers and mappers) -- `sdks/go-providers/anthropic` +- `sdks/go-providers/anthropic` (Anthropic Messages wrappers and mappers; embeddings currently unsupported by the upstream SDK/API surface) - `sdks/go-providers/gemini` ## Raw artifact policy From 37488994d1e50f9fbf99ab8a049cf7d9c623c19e Mon Sep 17 00:00:00 2001 From: Cyril Tovena Date: Thu, 12 Mar 2026 15:30:01 +0100 Subject: [PATCH 055/133] test(go-providers): add provider conformance suites MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - add shared Go Sigil provider conformance helpers for exported generation and span assertions - add OpenAI, Anthropic, and Gemini Go provider conformance suites - document the current Go provider conformance baseline and the Anthropic embedding limitation ## Testing - go test ./sdks/go-providers/openai/... ./sdks/go-providers/anthropic/... ./sdks/go-providers/gemini/... -count=1 ## Notes - Anthropic generation conformance is covered, but Anthropic embedding conformance is still blocked because the current Go Anthropic provider package and vendored upstream SDK expose no embedding wrapper/API surface. - Follow-up tracking issue: GRA-26. --- > [!NOTE] > **Medium Risk** > Adds a new `sdks/go/sigil/sigiltest` helper package (with gRPC fake ingest + OTel SDK deps) and expands provider conformance tests to assert exported payloads/spans; while mostly test-only, it introduces new dependencies and could affect build/test behavior across Go modules. > > **Overview** > Extends Go provider conformance from *direct normalization* to also validate the **recorder/export path** by routing mapped results through a real `sigil.Client` and asserting captured generation JSON plus emitted spans. > > Adds new conformance scenarios for OpenAI (Responses sync/stream, provider call error mapping, embeddings), Anthropic (sync/stream recorder-path assertions and provider call error span category), and Gemini (sync/stream recorder-path assertions, provider call error mapping, embeddings), and renames the old `*ErrorMapping` tests to explicitly cover mapper validation errors. > > Introduces a shared `sdks/go/sigil/sigiltest` package that spins up a localhost gRPC ingest server, records spans/metrics, and provides JSON-path + span helpers for assertions; updates provider module dependencies accordingly and fixes Gemini test fixtures’ expected `total_tokens` values. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit eafae76b6af9eb9a0c68ea155ffe1adcd5661bc7. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). --- go-providers/anthropic/conformance_test.go | 225 ++++++++++++++- go-providers/anthropic/go.mod | 3 + go-providers/anthropic/go.sum | 2 + go-providers/gemini/conformance_test.go | 310 ++++++++++++++++++++- go-providers/gemini/go.mod | 3 + go-providers/gemini/go.sum | 2 + go-providers/gemini/mapper_test.go | 12 +- go-providers/openai/conformance_test.go | 305 +++++++++++++++++++- go-providers/openai/go.mod | 3 + go-providers/openai/go.sum | 2 + go/sigil/sigiltest/env.go | 304 ++++++++++++++++++++ go/sigil/sigiltest/record.go | 72 +++++ go/sigil/sigiltest/spans.go | 32 +++ 13 files changed, 1259 insertions(+), 16 deletions(-) create mode 100644 go/sigil/sigiltest/env.go create mode 100644 go/sigil/sigiltest/record.go create mode 100644 go/sigil/sigiltest/spans.go diff --git a/go-providers/anthropic/conformance_test.go b/go-providers/anthropic/conformance_test.go index 7cb02b9..4d772ab 100644 --- a/go-providers/anthropic/conformance_test.go +++ b/go-providers/anthropic/conformance_test.go @@ -2,13 +2,234 @@ package anthropic import ( "errors" + "net/http" + "net/url" + "strings" "testing" + "time" asdk "github.com/anthropics/anthropic-sdk-go" - "github.com/grafana/sigil/sdks/go/sigil" + sigil "github.com/grafana/sigil/sdks/go/sigil" + "github.com/grafana/sigil/sdks/go/sigil/sigiltest" ) +const anthropicSpanErrorCategory = "error.category" + +func TestConformance_AnthropicSyncMapping(t *testing.T) { + env := sigiltest.NewEnv(t) + + req := testRequest() + resp := &asdk.BetaMessage{ + ID: "msg_conformance_sync", + Model: asdk.Model("claude-sonnet-4-5"), + StopReason: asdk.BetaStopReasonToolUse, + Content: []asdk.BetaContentBlockUnion{ + mustUnmarshalBetaContentBlockUnion(t, `{"type":"thinking","thinking":"need weather tool","signature":"sig"}`), + mustUnmarshalBetaContentBlockUnion(t, `{"type":"text","text":"Checking weather."}`), + mustUnmarshalBetaContentBlockUnion(t, `{"type":"tool_use","id":"toolu_sync","name":"weather","input":{"city":"Paris"}}`), + }, + Usage: asdk.BetaUsage{ + InputTokens: 120, + OutputTokens: 42, + CacheReadInputTokens: 30, + CacheCreationInputTokens: 10, + ServerToolUse: asdk.BetaServerToolUsage{ + WebSearchRequests: 2, + WebFetchRequests: 1, + }, + }, + } + start := sigil.GenerationStart{ + ConversationID: "conv-anthropic-sync", + ConversationTitle: "Anthropic sync", + AgentName: "agent-anthropic", + AgentVersion: "v-anthropic", + Model: sigil.ModelRef{Provider: "anthropic", Name: string(req.Model)}, + } + + generation, err := FromRequestResponse( + req, + resp, + WithConversationID(start.ConversationID), + WithConversationTitle(start.ConversationTitle), + WithAgentName(start.AgentName), + WithAgentVersion(start.AgentVersion), + WithTag("tenant", "t-anthropic"), + ) + sigiltest.RecordGeneration(t, env, start, generation, err) + env.Shutdown(t) + + exported := env.SingleGenerationJSON(t) + + if got := sigiltest.StringValue(t, exported, "mode"); got != "GENERATION_MODE_SYNC" { + t.Fatalf("unexpected mode: got %q want %q\n%s", got, "GENERATION_MODE_SYNC", sigiltest.DebugJSON(exported)) + } + if got := sigiltest.StringValue(t, exported, "stop_reason"); got != "tool_use" { + t.Fatalf("unexpected stop_reason: got %q want %q", got, "tool_use") + } + if got := sigiltest.StringValue(t, exported, "output", 0, "parts", 0, "thinking"); got != "need weather tool" { + t.Fatalf("unexpected thinking part: got %q want %q", got, "need weather tool") + } + if got := sigiltest.StringValue(t, exported, "output", 0, "parts", 1, "text"); got != "Checking weather." { + t.Fatalf("unexpected text part: got %q want %q", got, "Checking weather.") + } + if got := sigiltest.StringValue(t, exported, "output", 0, "parts", 2, "tool_call", "name"); got != "weather" { + t.Fatalf("unexpected tool_call.name: got %q want %q", got, "weather") + } + if got := sigiltest.StringValue(t, exported, "input", 2, "role"); got != "MESSAGE_ROLE_TOOL" { + t.Fatalf("unexpected tool input role: got %q want %q", got, "MESSAGE_ROLE_TOOL") + } + if got := sigiltest.StringValue(t, exported, "usage", "cache_read_input_tokens"); got != "30" { + t.Fatalf("unexpected usage.cache_read_input_tokens: got %q want %q", got, "30") + } + if got := sigiltest.StringValue(t, exported, "usage", "cache_write_input_tokens"); got != "10" { + t.Fatalf("unexpected usage.cache_write_input_tokens: got %q want %q", got, "10") + } + if got := sigiltest.FloatValue(t, exported, "metadata", "sigil.gen_ai.usage.server_tool_use.total_requests"); got != 3 { + t.Fatalf("unexpected server tool total requests: got %v want %v", got, float64(3)) + } +} + +func TestConformance_AnthropicStreamMapping(t *testing.T) { + env := sigiltest.NewEnv(t) + + req := testRequest() + summary := StreamSummary{ + FirstChunkAt: time.Unix(1_741_780_100, 0).UTC(), + Events: []asdk.BetaRawMessageStreamEventUnion{ + { + Type: "message_start", + Message: asdk.BetaMessage{ + ID: "msg_conformance_stream", + Model: asdk.Model("claude-sonnet-4-5"), + }, + }, + { + Type: "content_block_start", + Index: 0, + ContentBlock: mustUnmarshalBetaRawContentBlockStartEventContentBlockUnion(t, `{"type":"thinking","thinking":""}`), + }, + { + Type: "content_block_delta", + Index: 0, + Delta: asdk.BetaRawMessageStreamEventUnionDelta{Thinking: "need weather"}, + }, + { + Type: "content_block_start", + Index: 1, + ContentBlock: mustUnmarshalBetaRawContentBlockStartEventContentBlockUnion(t, `{"type":"text","text":""}`), + }, + { + Type: "content_block_delta", + Index: 1, + Delta: asdk.BetaRawMessageStreamEventUnionDelta{Text: "Checking "}, + }, + { + Type: "content_block_delta", + Index: 1, + Delta: asdk.BetaRawMessageStreamEventUnionDelta{Text: "weather"}, + }, + { + Type: "content_block_start", + Index: 2, + ContentBlock: mustUnmarshalBetaRawContentBlockStartEventContentBlockUnion(t, `{"type":"tool_use","id":"toolu_stream","name":"weather","input":{}}`), + }, + { + Type: "content_block_delta", + Index: 2, + Delta: asdk.BetaRawMessageStreamEventUnionDelta{PartialJSON: `{"city":"Paris"}`}, + }, + { + Type: "message_delta", + Delta: asdk.BetaRawMessageStreamEventUnionDelta{ + StopReason: asdk.BetaStopReasonToolUse, + }, + Usage: asdk.BetaMessageDeltaUsage{ + InputTokens: 80, + OutputTokens: 25, + }, + }, + }, + } + start := sigil.GenerationStart{ + ConversationID: "conv-anthropic-stream", + AgentName: "agent-anthropic-stream", + AgentVersion: "v-anthropic-stream", + Model: sigil.ModelRef{Provider: "anthropic", Name: string(req.Model)}, + } + + generation, err := FromStream( + req, + summary, + WithConversationID(start.ConversationID), + WithAgentName(start.AgentName), + WithAgentVersion(start.AgentVersion), + ) + sigiltest.RecordStreamingGeneration(t, env, start, summary.FirstChunkAt, generation, err) + env.Shutdown(t) + + exported := env.SingleGenerationJSON(t) + + if got := sigiltest.StringValue(t, exported, "mode"); got != "GENERATION_MODE_STREAM" { + t.Fatalf("unexpected mode: got %q want %q\n%s", got, "GENERATION_MODE_STREAM", sigiltest.DebugJSON(exported)) + } + if got := sigiltest.StringValue(t, exported, "response_id"); got != "msg_conformance_stream" { + t.Fatalf("unexpected response_id: got %q want %q", got, "msg_conformance_stream") + } + if got := sigiltest.StringValue(t, exported, "stop_reason"); got != "tool_use" { + t.Fatalf("unexpected stop_reason: got %q want %q", got, "tool_use") + } + if got := sigiltest.StringValue(t, exported, "output", 0, "parts", 0, "thinking"); got != "need weather" { + t.Fatalf("unexpected streamed thinking part: got %q want %q", got, "need weather") + } + if got := sigiltest.StringValue(t, exported, "output", 0, "parts", 1, "text"); got != "Checking weather" { + t.Fatalf("unexpected streamed text part: got %q want %q", got, "Checking weather") + } + if got := sigiltest.StringValue(t, exported, "output", 0, "parts", 2, "tool_call", "name"); got != "weather" { + t.Fatalf("unexpected streamed tool_call.name: got %q want %q", got, "weather") + } + if got := sigiltest.StringValue(t, exported, "usage", "total_tokens"); got != "105" { + t.Fatalf("unexpected streamed usage.total_tokens: got %q want %q", got, "105") + } +} + +func TestConformance_AnthropicErrorMapping(t *testing.T) { + env := sigiltest.NewEnv(t) + + callErr := &asdk.Error{ + StatusCode: http.StatusTooManyRequests, + Request: &http.Request{Method: http.MethodPost, URL: mustAnthropicURL(t, "https://api.anthropic.com/v1/messages")}, + Response: &http.Response{StatusCode: http.StatusTooManyRequests, Status: "429 Too Many Requests"}, + } + sigiltest.RecordCallError(t, env, sigil.GenerationStart{ + Model: sigil.ModelRef{Provider: "anthropic", Name: "claude-sonnet-4-5"}, + }, callErr) + + span := sigiltest.FindSpan(t, env.Spans.Ended(), "generateText claude-sonnet-4-5") + attrs := sigiltest.SpanAttributes(span) + if got := attrs[anthropicSpanErrorCategory].AsString(); got != "rate_limit" { + t.Fatalf("unexpected error.category: got %q want %q", got, "rate_limit") + } + + env.Shutdown(t) + exported := env.SingleGenerationJSON(t) + callError := sigiltest.StringValue(t, exported, "call_error") + if !strings.Contains(callError, "429") { + t.Fatalf("expected call_error to include status code, got %q", callError) + } +} + +func mustAnthropicURL(t testing.TB, raw string) *url.URL { + t.Helper() + + parsed, err := url.Parse(raw) + if err != nil { + t.Fatalf("parse url %q: %v", raw, err) + } + return parsed +} + func TestConformance_MessageSyncNormalization(t *testing.T) { req := testRequest() resp := &asdk.BetaMessage{ @@ -208,7 +429,7 @@ func TestConformance_MessageStreamNormalization(t *testing.T) { ) } -func TestConformance_AnthropicErrorMapping(t *testing.T) { +func TestConformance_AnthropicMapperValidationErrors(t *testing.T) { if _, err := FromRequestResponse(testRequest(), nil); err == nil || err.Error() != "response is required" { t.Fatalf("expected explicit response error, got %v", err) } diff --git a/go-providers/anthropic/go.mod b/go-providers/anthropic/go.mod index 23f2915..d6f4e08 100644 --- a/go-providers/anthropic/go.mod +++ b/go-providers/anthropic/go.mod @@ -11,6 +11,7 @@ require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/tidwall/gjson v1.18.0 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect @@ -18,6 +19,8 @@ require ( go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/otel v1.42.0 // indirect go.opentelemetry.io/otel/metric v1.42.0 // indirect + go.opentelemetry.io/otel/sdk v1.42.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.42.0 // indirect go.opentelemetry.io/otel/trace v1.42.0 // indirect golang.org/x/net v0.51.0 // indirect golang.org/x/sync v0.19.0 // indirect diff --git a/go-providers/anthropic/go.sum b/go-providers/anthropic/go.sum index 89eda3c..3ceadba 100644 --- a/go-providers/anthropic/go.sum +++ b/go-providers/anthropic/go.sum @@ -43,6 +43,8 @@ go.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9 go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc= go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY= go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= diff --git a/go-providers/gemini/conformance_test.go b/go-providers/gemini/conformance_test.go index f9fbc5f..636e2e0 100644 --- a/go-providers/gemini/conformance_test.go +++ b/go-providers/gemini/conformance_test.go @@ -2,13 +2,311 @@ package gemini import ( "math" + "strings" "testing" + "time" "google.golang.org/genai" - "github.com/grafana/sigil/sdks/go/sigil" + sigil "github.com/grafana/sigil/sdks/go/sigil" + "github.com/grafana/sigil/sdks/go/sigil/sigiltest" ) +const ( + geminiSpanErrorCategory = "error.category" + geminiSpanInputCount = "gen_ai.embeddings.input_count" + geminiSpanDimCount = "gen_ai.embeddings.dimension.count" +) + +func TestConformance_GeminiSyncMapping(t *testing.T) { + env := sigiltest.NewEnv(t) + + model, contents, config := geminiConformanceRequest() + resp := &genai.GenerateContentResponse{ + ResponseID: "resp_gemini_sync", + ModelVersion: "gemini-2.5-pro-001", + Candidates: []*genai.Candidate{ + { + FinishReason: genai.FinishReasonStop, + Content: genai.NewContentFromParts([]*genai.Part{ + {Text: "need weather tool", Thought: true}, + { + FunctionCall: &genai.FunctionCall{ + ID: "call_weather", + Name: "weather", + Args: map[string]any{"city": "Paris"}, + }, + }, + genai.NewPartFromText("It is 18C and sunny."), + }, genai.RoleModel), + }, + }, + UsageMetadata: &genai.GenerateContentResponseUsageMetadata{ + PromptTokenCount: 120, + CandidatesTokenCount: 40, + TotalTokenCount: 160, + CachedContentTokenCount: 12, + ThoughtsTokenCount: 10, + ToolUsePromptTokenCount: 9, + }, + } + start := sigil.GenerationStart{ + ConversationID: "conv-gemini-sync", + ConversationTitle: "Gemini sync", + AgentName: "agent-gemini", + AgentVersion: "v-gemini", + Model: sigil.ModelRef{Provider: "gemini", Name: model}, + } + + generation, err := FromRequestResponse( + model, + contents, + config, + resp, + WithConversationID(start.ConversationID), + WithConversationTitle(start.ConversationTitle), + WithAgentName(start.AgentName), + WithAgentVersion(start.AgentVersion), + WithTag("tenant", "t-gemini"), + ) + sigiltest.RecordGeneration(t, env, start, generation, err) + env.Shutdown(t) + + exported := env.SingleGenerationJSON(t) + + if got := sigiltest.StringValue(t, exported, "mode"); got != "GENERATION_MODE_SYNC" { + t.Fatalf("unexpected mode: got %q want %q\n%s", got, "GENERATION_MODE_SYNC", sigiltest.DebugJSON(exported)) + } + if got := sigiltest.StringValue(t, exported, "stop_reason"); got != "STOP" { + t.Fatalf("unexpected stop_reason: got %q want %q", got, "STOP") + } + if got := sigiltest.StringValue(t, exported, "output", 0, "parts", 0, "thinking"); got != "need weather tool" { + t.Fatalf("unexpected thinking part: got %q want %q", got, "need weather tool") + } + if got := sigiltest.StringValue(t, exported, "output", 0, "parts", 1, "tool_call", "name"); got != "weather" { + t.Fatalf("unexpected tool_call.name: got %q want %q", got, "weather") + } + if got := sigiltest.StringValue(t, exported, "output", 0, "parts", 2, "text"); got != "It is 18C and sunny." { + t.Fatalf("unexpected output text: got %q want %q", got, "It is 18C and sunny.") + } + if got := sigiltest.StringValue(t, exported, "input", 1, "role"); got != "MESSAGE_ROLE_TOOL" { + t.Fatalf("unexpected tool input role: got %q want %q", got, "MESSAGE_ROLE_TOOL") + } + if got := sigiltest.StringValue(t, exported, "usage", "reasoning_tokens"); got != "10" { + t.Fatalf("unexpected usage.reasoning_tokens: got %q want %q", got, "10") + } + if got := sigiltest.FloatValue(t, exported, "metadata", "sigil.gen_ai.usage.tool_use_prompt_tokens"); got != 9 { + t.Fatalf("unexpected tool_use_prompt_tokens: got %v want %v", got, float64(9)) + } +} + +func TestConformance_GeminiStreamMapping(t *testing.T) { + env := sigiltest.NewEnv(t) + + model, contents, config := geminiConformanceRequest() + summary := StreamSummary{ + FirstChunkAt: time.Unix(1_741_780_200, 0).UTC(), + Responses: []*genai.GenerateContentResponse{ + { + ResponseID: "resp_gemini_stream_1", + ModelVersion: "gemini-2.5-pro-001", + Candidates: []*genai.Candidate{ + { + Content: genai.NewContentFromParts([]*genai.Part{ + {Text: "need weather tool", Thought: true}, + { + FunctionCall: &genai.FunctionCall{ + ID: "call_weather", + Name: "weather", + Args: map[string]any{"city": "Paris"}, + }, + }, + }, genai.RoleModel), + }, + }, + }, + { + ResponseID: "resp_gemini_stream_2", + ModelVersion: "gemini-2.5-pro-001", + Candidates: []*genai.Candidate{ + { + FinishReason: genai.FinishReasonStop, + Content: genai.NewContentFromText("It is 18C and sunny.", genai.RoleModel), + }, + }, + UsageMetadata: &genai.GenerateContentResponseUsageMetadata{ + PromptTokenCount: 20, + CandidatesTokenCount: 6, + TotalTokenCount: 26, + ThoughtsTokenCount: 4, + ToolUsePromptTokenCount: 5, + }, + }, + }, + } + start := sigil.GenerationStart{ + ConversationID: "conv-gemini-stream", + AgentName: "agent-gemini-stream", + AgentVersion: "v-gemini-stream", + Model: sigil.ModelRef{Provider: "gemini", Name: model}, + } + + generation, err := FromStream( + model, + contents, + config, + summary, + WithConversationID(start.ConversationID), + WithAgentName(start.AgentName), + WithAgentVersion(start.AgentVersion), + ) + sigiltest.RecordStreamingGeneration(t, env, start, summary.FirstChunkAt, generation, err) + env.Shutdown(t) + + exported := env.SingleGenerationJSON(t) + + if got := sigiltest.StringValue(t, exported, "mode"); got != "GENERATION_MODE_STREAM" { + t.Fatalf("unexpected mode: got %q want %q\n%s", got, "GENERATION_MODE_STREAM", sigiltest.DebugJSON(exported)) + } + if got := sigiltest.StringValue(t, exported, "response_id"); got != "resp_gemini_stream_2" { + t.Fatalf("unexpected response_id: got %q want %q", got, "resp_gemini_stream_2") + } + if got := sigiltest.StringValue(t, exported, "stop_reason"); got != "STOP" { + t.Fatalf("unexpected stop_reason: got %q want %q", got, "STOP") + } + if got := sigiltest.StringValue(t, exported, "output", 0, "parts", 0, "thinking"); got != "need weather tool" { + t.Fatalf("unexpected streamed thinking part: got %q want %q", got, "need weather tool") + } + if got := sigiltest.StringValue(t, exported, "output", 0, "parts", 1, "tool_call", "name"); got != "weather" { + t.Fatalf("unexpected streamed tool_call.name: got %q want %q", got, "weather") + } + if got := sigiltest.StringValue(t, exported, "output", 1, "parts", 0, "text"); got != "It is 18C and sunny." { + t.Fatalf("unexpected streamed output text: got %q want %q", got, "It is 18C and sunny.") + } + if got := sigiltest.StringValue(t, exported, "usage", "total_tokens"); got != "26" { + t.Fatalf("unexpected usage.total_tokens: got %q want %q", got, "26") + } +} + +func TestConformance_GeminiErrorMapping(t *testing.T) { + env := sigiltest.NewEnv(t) + + sigiltest.RecordCallError(t, env, sigil.GenerationStart{ + Model: sigil.ModelRef{Provider: "gemini", Name: "gemini-2.5-pro"}, + }, genai.APIError{Code: 429, Message: "rate limited", Status: "RESOURCE_EXHAUSTED"}) + + span := sigiltest.FindSpan(t, env.Spans.Ended(), "generateText gemini-2.5-pro") + attrs := sigiltest.SpanAttributes(span) + if got := attrs[geminiSpanErrorCategory].AsString(); got != "rate_limit" { + t.Fatalf("unexpected error.category: got %q want %q", got, "rate_limit") + } + + env.Shutdown(t) + exported := env.SingleGenerationJSON(t) + callError := sigiltest.StringValue(t, exported, "call_error") + if !strings.Contains(callError, "429") { + t.Fatalf("expected call_error to include status code, got %q", callError) + } +} + +func TestConformance_GeminiEmbeddingMapping(t *testing.T) { + env := sigiltest.NewEnv(t) + + model := "gemini-embedding-001" + contents := []*genai.Content{ + genai.NewContentFromText("hello", genai.RoleUser), + genai.NewContentFromText("world", genai.RoleUser), + } + dimensions := int32(3) + config := &genai.EmbedContentConfig{ + OutputDimensionality: &dimensions, + } + resp := &genai.EmbedContentResponse{ + Embeddings: []*genai.ContentEmbedding{ + { + Values: []float32{0.1, 0.2, 0.3}, + Statistics: &genai.ContentEmbeddingStatistics{ + TokenCount: 2, + }, + }, + { + Values: []float32{0.4, 0.5, 0.6}, + Statistics: &genai.ContentEmbeddingStatistics{ + TokenCount: 2, + }, + }, + }, + } + startDimensions := int64(dimensions) + sigiltest.RecordEmbedding(t, env, sigil.EmbeddingStart{ + Model: sigil.ModelRef{Provider: "gemini", Name: model}, + AgentName: "agent-gemini-embed", + AgentVersion: "v-gemini-embed", + Dimensions: &startDimensions, + }, EmbeddingFromResponse(model, contents, config, resp)) + + span := sigiltest.FindSpan(t, env.Spans.Ended(), "embeddings gemini-embedding-001") + attrs := sigiltest.SpanAttributes(span) + if got := attrs[geminiSpanInputCount].AsInt64(); got != 2 { + t.Fatalf("unexpected gen_ai.embeddings.input_count: got %d want %d", got, 2) + } + if got := attrs[geminiSpanDimCount].AsInt64(); got != 3 { + t.Fatalf("unexpected gen_ai.embeddings.dimension.count: got %d want %d", got, 3) + } + + env.Shutdown(t) + sigiltest.RequireRequestCount(t, env, 0) +} + +func geminiConformanceRequest() (string, []*genai.Content, *genai.GenerateContentConfig) { + temperature := float32(0.4) + topP := float32(0.75) + thinkingBudget := int32(2048) + model := "gemini-2.5-pro" + contents := []*genai.Content{ + genai.NewContentFromText("What is the weather in Paris?", genai.RoleUser), + genai.NewContentFromParts([]*genai.Part{ + genai.NewPartFromFunctionResponse("weather", map[string]any{ + "temp_c": 18, + }), + }, genai.RoleUser), + } + config := &genai.GenerateContentConfig{ + SystemInstruction: genai.NewContentFromText("Be concise.", genai.RoleUser), + MaxOutputTokens: 300, + Temperature: &temperature, + TopP: &topP, + ToolConfig: &genai.ToolConfig{ + FunctionCallingConfig: &genai.FunctionCallingConfig{ + Mode: genai.FunctionCallingConfigModeAny, + }, + }, + ThinkingConfig: &genai.ThinkingConfig{ + IncludeThoughts: true, + ThinkingBudget: &thinkingBudget, + ThinkingLevel: genai.ThinkingLevelHigh, + }, + Tools: []*genai.Tool{ + { + FunctionDeclarations: []*genai.FunctionDeclaration{ + { + Name: "weather", + Description: "Get weather", + ParametersJsonSchema: map[string]any{ + "type": "object", + "properties": map[string]any{ + "city": map[string]any{"type": "string"}, + }, + "required": []string{"city"}, + }, + }, + }, + }, + }, + } + return model, contents, config +} + func TestConformance_GenerateContentSyncNormalization(t *testing.T) { temperature := float32(0.4) topP := float32(0.75) @@ -77,7 +375,7 @@ func TestConformance_GenerateContentSyncNormalization(t *testing.T) { UsageMetadata: &genai.GenerateContentResponseUsageMetadata{ PromptTokenCount: 120, CandidatesTokenCount: 40, - TotalTokenCount: 170, + TotalTokenCount: 160, CachedContentTokenCount: 12, ThoughtsTokenCount: 10, ToolUsePromptTokenCount: 9, @@ -111,7 +409,7 @@ func TestConformance_GenerateContentSyncNormalization(t *testing.T) { if generation.StopReason != "STOP" { t.Fatalf("unexpected stop reason: %q", generation.StopReason) } - if generation.Usage.TotalTokens != 170 || generation.Usage.CacheReadInputTokens != 12 || generation.Usage.ReasoningTokens != 10 { + if generation.Usage.TotalTokens != 160 || generation.Usage.CacheReadInputTokens != 12 || generation.Usage.ReasoningTokens != 10 { t.Fatalf("unexpected usage mapping: %#v", generation.Usage) } if generation.ThinkingEnabled == nil || !*generation.ThinkingEnabled { @@ -214,7 +512,7 @@ func TestConformance_GenerateContentStreamNormalization(t *testing.T) { UsageMetadata: &genai.GenerateContentResponseUsageMetadata{ PromptTokenCount: 20, CandidatesTokenCount: 6, - TotalTokenCount: 31, + TotalTokenCount: 26, ThoughtsTokenCount: 4, ToolUsePromptTokenCount: 5, }, @@ -241,7 +539,7 @@ func TestConformance_GenerateContentStreamNormalization(t *testing.T) { if generation.StopReason != "STOP" { t.Fatalf("unexpected stop reason: %q", generation.StopReason) } - if generation.Usage.TotalTokens != 31 || generation.Usage.ReasoningTokens != 4 { + if generation.Usage.TotalTokens != 26 || generation.Usage.ReasoningTokens != 4 { t.Fatalf("unexpected usage mapping: %#v", generation.Usage) } if len(generation.Output) != 2 { @@ -263,7 +561,7 @@ func TestConformance_GenerateContentStreamNormalization(t *testing.T) { ) } -func TestConformance_GeminiErrorMapping(t *testing.T) { +func TestConformance_GeminiMapperValidationErrors(t *testing.T) { if _, err := FromRequestResponse("", nil, nil, &genai.GenerateContentResponse{}); err == nil || err.Error() != "request model is required" { t.Fatalf("expected explicit request model error, got %v", err) } diff --git a/go-providers/gemini/go.mod b/go-providers/gemini/go.mod index 7794c70..fbf6bc9 100644 --- a/go-providers/gemini/go.mod +++ b/go-providers/gemini/go.mod @@ -17,6 +17,7 @@ require ( github.com/go-logr/stdr v1.2.2 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/s2a-go v0.1.9 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.11 // indirect github.com/googleapis/gax-go/v2 v2.17.0 // indirect github.com/gorilla/websocket v1.5.3 // indirect @@ -24,6 +25,8 @@ require ( go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 // indirect go.opentelemetry.io/otel v1.42.0 // indirect go.opentelemetry.io/otel/metric v1.42.0 // indirect + go.opentelemetry.io/otel/sdk v1.42.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.42.0 // indirect go.opentelemetry.io/otel/trace v1.42.0 // indirect golang.org/x/crypto v0.48.0 // indirect golang.org/x/net v0.51.0 // indirect diff --git a/go-providers/gemini/go.sum b/go-providers/gemini/go.sum index 285cc8d..ebf704a 100644 --- a/go-providers/gemini/go.sum +++ b/go-providers/gemini/go.sum @@ -47,6 +47,8 @@ go.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9 go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc= go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY= go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= diff --git a/go-providers/gemini/mapper_test.go b/go-providers/gemini/mapper_test.go index 69d27b8..3be9e7d 100644 --- a/go-providers/gemini/mapper_test.go +++ b/go-providers/gemini/mapper_test.go @@ -77,7 +77,7 @@ func TestFromRequestResponse(t *testing.T) { UsageMetadata: &genai.GenerateContentResponseUsageMetadata{ PromptTokenCount: 120, CandidatesTokenCount: 40, - TotalTokenCount: 170, + TotalTokenCount: 160, CachedContentTokenCount: 12, ThoughtsTokenCount: 10, ToolUsePromptTokenCount: 9, @@ -125,8 +125,8 @@ func TestFromRequestResponse(t *testing.T) { if generation.StopReason != "STOP" { t.Fatalf("expected stop reason STOP, got %q", generation.StopReason) } - if generation.Usage.TotalTokens != 170 { - t.Fatalf("expected total tokens 170, got %d", generation.Usage.TotalTokens) + if generation.Usage.TotalTokens != 160 { + t.Fatalf("expected total tokens 160, got %d", generation.Usage.TotalTokens) } if generation.Usage.CacheReadInputTokens != 12 { t.Fatalf("expected cache read tokens 12, got %d", generation.Usage.CacheReadInputTokens) @@ -241,7 +241,7 @@ func TestFromStream(t *testing.T) { UsageMetadata: &genai.GenerateContentResponseUsageMetadata{ PromptTokenCount: 20, CandidatesTokenCount: 6, - TotalTokenCount: 31, + TotalTokenCount: 26, ToolUsePromptTokenCount: 5, }, }, @@ -275,8 +275,8 @@ func TestFromStream(t *testing.T) { if generation.ResponseModel != "gemini-2.5-pro-001" { t.Fatalf("expected response model gemini-2.5-pro-001, got %q", generation.ResponseModel) } - if generation.Usage.TotalTokens != 31 { - t.Fatalf("expected total tokens 31, got %d", generation.Usage.TotalTokens) + if generation.Usage.TotalTokens != 26 { + t.Fatalf("expected total tokens 26, got %d", generation.Usage.TotalTokens) } if generation.MaxTokens == nil || *generation.MaxTokens != 90 { t.Fatalf("expected max tokens 90, got %v", generation.MaxTokens) diff --git a/go-providers/openai/conformance_test.go b/go-providers/openai/conformance_test.go index 5c28f0f..042de9c 100644 --- a/go-providers/openai/conformance_test.go +++ b/go-providers/openai/conformance_test.go @@ -1,16 +1,317 @@ package openai import ( + "net/http" + "net/url" + "strings" "testing" + "time" osdk "github.com/openai/openai-go/v3" "github.com/openai/openai-go/v3/packages/param" oresponses "github.com/openai/openai-go/v3/responses" "github.com/openai/openai-go/v3/shared" - "github.com/grafana/sigil/sdks/go/sigil" + sigil "github.com/grafana/sigil/sdks/go/sigil" + "github.com/grafana/sigil/sdks/go/sigil/sigiltest" ) +const ( + openAISpanErrorCategory = "error.category" + openAISpanInputCount = "gen_ai.embeddings.input_count" + openAISpanDimCount = "gen_ai.embeddings.dimension.count" +) + +func TestConformance_OpenAIResponsesSyncMapping(t *testing.T) { + env := sigiltest.NewEnv(t) + + req := openAIResponsesRequest() + resp := openAIResponsesResponse() + start := sigil.GenerationStart{ + ConversationID: "conv-openai-sync", + ConversationTitle: "OpenAI responses sync", + AgentName: "agent-openai", + AgentVersion: "v-openai", + Model: sigil.ModelRef{Provider: "openai", Name: string(req.Model)}, + } + + generation, err := ResponsesFromRequestResponse( + req, + resp, + WithConversationID(start.ConversationID), + WithConversationTitle(start.ConversationTitle), + WithAgentName(start.AgentName), + WithAgentVersion(start.AgentVersion), + WithTag("tenant", "t-openai"), + ) + sigiltest.RecordGeneration(t, env, start, generation, err) + env.Shutdown(t) + + exported := env.SingleGenerationJSON(t) + + if got := sigiltest.StringValue(t, exported, "mode"); got != "GENERATION_MODE_SYNC" { + t.Fatalf("unexpected mode: got %q want %q\n%s", got, "GENERATION_MODE_SYNC", sigiltest.DebugJSON(exported)) + } + if got := sigiltest.StringValue(t, exported, "response_id"); got != "resp_openai_sync" { + t.Fatalf("unexpected response_id: got %q want %q", got, "resp_openai_sync") + } + if got := sigiltest.StringValue(t, exported, "stop_reason"); got != "stop" { + t.Fatalf("unexpected stop_reason: got %q want %q", got, "stop") + } + if got := sigiltest.StringValue(t, exported, "system_prompt"); got != "Be concise." { + t.Fatalf("unexpected system_prompt: got %q want %q", got, "Be concise.") + } + if got := sigiltest.StringValue(t, exported, "conversation_id"); got != start.ConversationID { + t.Fatalf("unexpected conversation_id: got %q want %q", got, start.ConversationID) + } + if got := sigiltest.StringValue(t, exported, "agent_name"); got != start.AgentName { + t.Fatalf("unexpected agent_name: got %q want %q", got, start.AgentName) + } + if got := sigiltest.StringValue(t, exported, "model", "provider"); got != "openai" { + t.Fatalf("unexpected model.provider: got %q want %q", got, "openai") + } + if got := sigiltest.StringValue(t, exported, "model", "name"); got != "gpt-5" { + t.Fatalf("unexpected model.name: got %q want %q", got, "gpt-5") + } + if got := sigiltest.StringValue(t, exported, "usage", "reasoning_tokens"); got != "3" { + t.Fatalf("unexpected usage.reasoning_tokens: got %q want %q", got, "3") + } + if got := sigiltest.StringValue(t, exported, "usage", "cache_read_input_tokens"); got != "2" { + t.Fatalf("unexpected usage.cache_read_input_tokens: got %q want %q", got, "2") + } + if got := sigiltest.StringValue(t, exported, "input", 1, "role"); got != "MESSAGE_ROLE_TOOL" { + t.Fatalf("unexpected tool input role: got %q want %q", got, "MESSAGE_ROLE_TOOL") + } + if got := sigiltest.StringValue(t, exported, "input", 1, "parts", 0, "metadata", "provider_type"); got != "tool_result" { + t.Fatalf("unexpected tool result provider_type: got %q want %q", got, "tool_result") + } + if got := sigiltest.StringValue(t, exported, "input", 1, "parts", 0, "tool_result", "tool_call_id"); got != "call_weather" { + t.Fatalf("unexpected tool_result.tool_call_id: got %q want %q", got, "call_weather") + } + if got := sigiltest.StringValue(t, exported, "output", 1, "parts", 0, "metadata", "provider_type"); got != "tool_call" { + t.Fatalf("unexpected tool call provider_type: got %q want %q", got, "tool_call") + } + if got := sigiltest.StringValue(t, exported, "output", 1, "parts", 0, "tool_call", "name"); got != "weather" { + t.Fatalf("unexpected tool_call.name: got %q want %q", got, "weather") + } +} + +func TestConformance_OpenAIResponsesStreamMapping(t *testing.T) { + env := sigiltest.NewEnv(t) + + req := openAIResponsesRequest() + summary := openAIResponsesStreamSummary() + start := sigil.GenerationStart{ + ConversationID: "conv-openai-stream", + AgentName: "agent-openai-stream", + AgentVersion: "v-openai-stream", + Model: sigil.ModelRef{Provider: "openai", Name: string(req.Model)}, + } + + generation, err := ResponsesFromStream( + req, + summary, + WithConversationID(start.ConversationID), + WithAgentName(start.AgentName), + WithAgentVersion(start.AgentVersion), + ) + sigiltest.RecordStreamingGeneration(t, env, start, summary.FirstChunkAt, generation, err) + env.Shutdown(t) + + exported := env.SingleGenerationJSON(t) + + if got := sigiltest.StringValue(t, exported, "mode"); got != "GENERATION_MODE_STREAM" { + t.Fatalf("unexpected mode: got %q want %q\n%s", got, "GENERATION_MODE_STREAM", sigiltest.DebugJSON(exported)) + } + if got := sigiltest.StringValue(t, exported, "response_id"); got != "resp_openai_stream" { + t.Fatalf("unexpected response_id: got %q want %q", got, "resp_openai_stream") + } + if got := sigiltest.StringValue(t, exported, "stop_reason"); got != "stop" { + t.Fatalf("unexpected stop_reason: got %q want %q", got, "stop") + } + if got := sigiltest.StringValue(t, exported, "output", 0, "parts", 0, "text"); got != "checking weather" { + t.Fatalf("unexpected streamed output text: got %q want %q", got, "checking weather") + } + if got := sigiltest.StringValue(t, exported, "usage", "total_tokens"); got != "26" { + t.Fatalf("unexpected usage.total_tokens: got %q want %q", got, "26") + } + if got := sigiltest.StringValue(t, exported, "input", 1, "parts", 0, "tool_result", "tool_call_id"); got != "call_weather" { + t.Fatalf("unexpected streamed tool_result.tool_call_id: got %q want %q", got, "call_weather") + } +} + +func TestConformance_OpenAIErrorMapping(t *testing.T) { + env := sigiltest.NewEnv(t) + + callErr := &osdk.Error{ + StatusCode: http.StatusTooManyRequests, + Request: &http.Request{Method: http.MethodPost, URL: mustURL(t, "https://api.openai.com/v1/responses")}, + Response: &http.Response{StatusCode: http.StatusTooManyRequests, Status: "429 Too Many Requests"}, + } + sigiltest.RecordCallError(t, env, sigil.GenerationStart{ + Model: sigil.ModelRef{Provider: "openai", Name: "gpt-5"}, + }, callErr) + + span := sigiltest.FindSpan(t, env.Spans.Ended(), "generateText gpt-5") + attrs := sigiltest.SpanAttributes(span) + if got := attrs[openAISpanErrorCategory].AsString(); got != "rate_limit" { + t.Fatalf("unexpected error.category: got %q want %q", got, "rate_limit") + } + + env.Shutdown(t) + exported := env.SingleGenerationJSON(t) + callError := sigiltest.StringValue(t, exported, "call_error") + if !strings.Contains(callError, "429") { + t.Fatalf("expected call_error to include status code, got %q", callError) + } +} + +func TestConformance_OpenAIEmbeddingMapping(t *testing.T) { + env := sigiltest.NewEnv(t) + + req := osdk.EmbeddingNewParams{ + Model: osdk.EmbeddingModel("text-embedding-3-small"), + Input: osdk.EmbeddingNewParamsInputUnion{ + OfArrayOfStrings: []string{"hello", "world"}, + }, + } + resp := &osdk.CreateEmbeddingResponse{ + Model: "text-embedding-3-small", + Data: []osdk.Embedding{ + {Embedding: []float64{0.1, 0.2, 0.3}}, + {Embedding: []float64{0.4, 0.5, 0.6}}, + }, + Usage: osdk.CreateEmbeddingResponseUsage{ + PromptTokens: 42, + TotalTokens: 42, + }, + } + dimensions := int64(3) + sigiltest.RecordEmbedding(t, env, sigil.EmbeddingStart{ + Model: sigil.ModelRef{Provider: "openai", Name: string(req.Model)}, + AgentName: "agent-openai-embed", + AgentVersion: "v-openai-embed", + Dimensions: &dimensions, + }, EmbeddingsFromResponse(req, resp)) + + span := sigiltest.FindSpan(t, env.Spans.Ended(), "embeddings text-embedding-3-small") + attrs := sigiltest.SpanAttributes(span) + if got := attrs[openAISpanInputCount].AsInt64(); got != 2 { + t.Fatalf("unexpected gen_ai.embeddings.input_count: got %d want %d", got, 2) + } + if got := attrs[openAISpanDimCount].AsInt64(); got != 3 { + t.Fatalf("unexpected gen_ai.embeddings.dimension.count: got %d want %d", got, 3) + } + + env.Shutdown(t) + sigiltest.RequireRequestCount(t, env, 0) +} + +func openAIResponsesRequest() oresponses.ResponseNewParams { + return oresponses.ResponseNewParams{ + Model: shared.ResponsesModel("gpt-5"), + Instructions: param.NewOpt("Be concise."), + Input: oresponses.ResponseNewParamsInputUnion{ + OfInputItemList: oresponses.ResponseInputParam{ + { + OfMessage: &oresponses.EasyInputMessageParam{ + Role: oresponses.EasyInputMessageRoleUser, + Content: oresponses.EasyInputMessageContentUnionParam{OfString: param.NewOpt("what is the weather in Paris?")}, + }, + }, + { + OfFunctionCallOutput: &oresponses.ResponseInputItemFunctionCallOutputParam{ + CallID: "call_weather", + Output: oresponses.ResponseInputItemFunctionCallOutputOutputUnionParam{OfString: param.NewOpt(`{"temp_c":18}`)}, + }, + }, + }, + }, + MaxOutputTokens: param.NewOpt(int64(320)), + Temperature: param.NewOpt(0.2), + TopP: param.NewOpt(0.85), + Reasoning: shared.ReasoningParam{ + Effort: shared.ReasoningEffortMedium, + }, + } +} + +func openAIResponsesResponse() *oresponses.Response { + return &oresponses.Response{ + ID: "resp_openai_sync", + Model: shared.ResponsesModel("gpt-5"), + Status: oresponses.ResponseStatusCompleted, + Output: []oresponses.ResponseOutputItemUnion{ + { + Type: "message", + Content: []oresponses.ResponseOutputMessageContentUnion{ + {Type: "output_text", Text: "It is 18C and sunny."}, + }, + }, + { + Type: "function_call", + CallID: "call_weather", + Name: "weather", + Arguments: oresponses.ResponseOutputItemUnionArguments{OfString: `{"city":"Paris"}`}, + }, + }, + Usage: oresponses.ResponseUsage{ + InputTokens: 80, + OutputTokens: 20, + TotalTokens: 100, + InputTokensDetails: oresponses.ResponseUsageInputTokensDetails{ + CachedTokens: 2, + }, + OutputTokensDetails: oresponses.ResponseUsageOutputTokensDetails{ + ReasoningTokens: 3, + }, + }, + } +} + +func openAIResponsesStreamSummary() ResponsesStreamSummary { + return ResponsesStreamSummary{ + FirstChunkAt: time.Unix(1_741_780_000, 0).UTC(), + Events: []oresponses.ResponseStreamEventUnion{ + { + Type: "response.output_text.delta", + Delta: "checking ", + Response: oresponses.Response{ + ID: "resp_openai_stream", + Model: shared.ResponsesModel("gpt-5"), + }, + }, + { + Type: "response.output_text.delta", + Delta: "weather", + }, + { + Type: "response.completed", + Response: oresponses.Response{ + ID: "resp_openai_stream", + Model: shared.ResponsesModel("gpt-5"), + Status: oresponses.ResponseStatusCompleted, + Usage: oresponses.ResponseUsage{ + InputTokens: 20, + OutputTokens: 6, + TotalTokens: 26, + }, + }, + }, + }, + } +} + +func mustURL(t testing.TB, raw string) *url.URL { + t.Helper() + + parsed, err := url.Parse(raw) + if err != nil { + t.Fatalf("parse url %q: %v", raw, err) + } + return parsed +} + func TestConformance_ChatCompletionsSyncNormalization(t *testing.T) { req := osdk.ChatCompletionNewParams{ Model: shared.ChatModel("gpt-4o-mini"), @@ -377,7 +678,7 @@ func TestConformance_ResponsesStreamNormalization(t *testing.T) { ) } -func TestConformance_OpenAIErrorMapping(t *testing.T) { +func TestConformance_OpenAIMapperValidationErrors(t *testing.T) { if _, err := ChatCompletionsFromRequestResponse(osdk.ChatCompletionNewParams{}, nil); err == nil || err.Error() != "response is required" { t.Fatalf("expected explicit chat response error, got %v", err) } diff --git a/go-providers/openai/go.mod b/go-providers/openai/go.mod index 0320dc0..0f88f02 100644 --- a/go-providers/openai/go.mod +++ b/go-providers/openai/go.mod @@ -11,6 +11,7 @@ require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/tidwall/gjson v1.18.0 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect @@ -18,6 +19,8 @@ require ( go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/otel v1.42.0 // indirect go.opentelemetry.io/otel/metric v1.42.0 // indirect + go.opentelemetry.io/otel/sdk v1.42.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.42.0 // indirect go.opentelemetry.io/otel/trace v1.42.0 // indirect golang.org/x/net v0.51.0 // indirect golang.org/x/sys v0.41.0 // indirect diff --git a/go-providers/openai/go.sum b/go-providers/openai/go.sum index 0560506..67f85ad 100644 --- a/go-providers/openai/go.sum +++ b/go-providers/openai/go.sum @@ -41,6 +41,8 @@ go.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9 go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc= go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY= go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= diff --git a/go/sigil/sigiltest/env.go b/go/sigil/sigiltest/env.go new file mode 100644 index 0000000..6b02c36 --- /dev/null +++ b/go/sigil/sigiltest/env.go @@ -0,0 +1,304 @@ +package sigiltest + +import ( + "context" + "encoding/json" + "fmt" + "net" + "sync" + "testing" + "time" + + sigil "github.com/grafana/sigil/sdks/go/sigil" + sigilv1 "github.com/grafana/sigil/sdks/go/sigil/internal/gen/sigil/v1" + sdkmetric "go.opentelemetry.io/otel/sdk/metric" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + "go.opentelemetry.io/otel/sdk/trace/tracetest" + "google.golang.org/grpc" + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/proto" +) + +type Env struct { + Client *sigil.Client + Spans *tracetest.SpanRecorder + Metrics *sdkmetric.ManualReader + + ingest *capturingIngestServer + + tracerProvider *sdktrace.TracerProvider + meterProvider *sdkmetric.MeterProvider + grpcServer *grpc.Server + listener net.Listener + closeOnce sync.Once +} + +func NewEnv(t testing.TB) *Env { + t.Helper() + + ingest := &capturingIngestServer{} + grpcServer := grpc.NewServer() + sigilv1.RegisterGenerationIngestServiceServer(grpcServer, ingest) + + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("listen for fake ingest server: %v", err) + } + + go func() { + _ = grpcServer.Serve(listener) + }() + + spanRecorder := tracetest.NewSpanRecorder() + tracerProvider := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(spanRecorder)) + metricReader := sdkmetric.NewManualReader() + meterProvider := sdkmetric.NewMeterProvider(sdkmetric.WithReader(metricReader)) + + cfg := sigil.DefaultConfig() + cfg.Tracer = tracerProvider.Tracer("sigil-provider-conformance") + cfg.Meter = meterProvider.Meter("sigil-provider-conformance") + cfg.GenerationExport = sigil.GenerationExportConfig{ + Protocol: sigil.GenerationExportProtocolGRPC, + Endpoint: listener.Addr().String(), + Insecure: true, + BatchSize: 1, + FlushInterval: time.Hour, + QueueSize: 8, + MaxRetries: 1, + InitialBackoff: time.Millisecond, + MaxBackoff: 5 * time.Millisecond, + PayloadMaxBytes: 4 << 20, + } + + env := &Env{ + Client: sigil.NewClient(cfg), + Spans: spanRecorder, + Metrics: metricReader, + ingest: ingest, + tracerProvider: tracerProvider, + meterProvider: meterProvider, + grpcServer: grpcServer, + listener: listener, + } + t.Cleanup(func() { + if err := env.close(); err != nil { + t.Errorf("close sigil test env: %v", err) + } + }) + return env +} + +func (e *Env) Shutdown(t testing.TB) { + t.Helper() + + if err := e.close(); err != nil { + t.Fatalf("shutdown sigil client: %v", err) + } +} + +func (e *Env) RequestCount() int { + if e == nil || e.ingest == nil { + return 0 + } + return e.ingest.requestCount() +} + +func (e *Env) SingleGenerationJSON(t testing.TB) map[string]any { + t.Helper() + + req := e.singleRequest(t) + if len(req.GetGenerations()) != 1 { + t.Fatalf("expected exactly one generation in request, got %d", len(req.GetGenerations())) + } + + generationJSON, err := protojson.MarshalOptions{UseProtoNames: true}.Marshal(req.GetGenerations()[0]) + if err != nil { + t.Fatalf("marshal generation json: %v", err) + } + + var generation map[string]any + if err := json.Unmarshal(generationJSON, &generation); err != nil { + t.Fatalf("decode generation json: %v", err) + } + return generation +} + +func (e *Env) close() error { + if e == nil { + return nil + } + + var closeErr error + e.closeOnce.Do(func() { + if e.Client != nil { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if err := e.Client.Shutdown(ctx); err != nil { + closeErr = err + } + } + if e.meterProvider != nil { + if err := e.meterProvider.Shutdown(context.Background()); err != nil && closeErr == nil { + closeErr = err + } + } + if e.tracerProvider != nil { + if err := e.tracerProvider.Shutdown(context.Background()); err != nil && closeErr == nil { + closeErr = err + } + } + if e.grpcServer != nil { + e.grpcServer.Stop() + } + if e.listener != nil { + _ = e.listener.Close() + } + }) + return closeErr +} + +func (e *Env) singleRequest(t testing.TB) *sigilv1.ExportGenerationsRequest { + t.Helper() + + if e == nil || e.ingest == nil { + t.Fatalf("sigil test env has no ingest server") + } + return e.ingest.singleRequest(t) +} + +type capturingIngestServer struct { + sigilv1.UnimplementedGenerationIngestServiceServer + + mu sync.Mutex + requests []*sigilv1.ExportGenerationsRequest +} + +func (s *capturingIngestServer) ExportGenerations(_ context.Context, req *sigilv1.ExportGenerationsRequest) (*sigilv1.ExportGenerationsResponse, error) { + s.capture(req) + return acceptedResponse(req), nil +} + +func (s *capturingIngestServer) capture(req *sigilv1.ExportGenerationsRequest) { + if req == nil { + return + } + + clone := proto.Clone(req) + typed, ok := clone.(*sigilv1.ExportGenerationsRequest) + if !ok { + return + } + + s.mu.Lock() + s.requests = append(s.requests, typed) + s.mu.Unlock() +} + +func (s *capturingIngestServer) requestCount() int { + s.mu.Lock() + defer s.mu.Unlock() + return len(s.requests) +} + +func (s *capturingIngestServer) singleRequest(t testing.TB) *sigilv1.ExportGenerationsRequest { + t.Helper() + + s.mu.Lock() + defer s.mu.Unlock() + + if len(s.requests) != 1 { + t.Fatalf("expected exactly one export request, got %d", len(s.requests)) + } + return s.requests[0] +} + +func acceptedResponse(req *sigilv1.ExportGenerationsRequest) *sigilv1.ExportGenerationsResponse { + response := &sigilv1.ExportGenerationsResponse{Results: make([]*sigilv1.ExportGenerationResult, len(req.GetGenerations()))} + for i := range req.GetGenerations() { + response.Results[i] = &sigilv1.ExportGenerationResult{ + Accepted: true, + } + } + return response +} + +func JSONPath(t testing.TB, value any, path ...any) any { + t.Helper() + + current := value + for _, step := range path { + switch typed := step.(type) { + case string: + node, ok := current.(map[string]any) + if !ok { + t.Fatalf("path step %q expected object, got %T", typed, current) + } + next, ok := node[typed] + if !ok { + t.Fatalf("path step %q missing in object keys %v", typed, mapsKeys(node)) + } + current = next + case int: + node, ok := current.([]any) + if !ok { + t.Fatalf("path step %d expected array, got %T", typed, current) + } + if typed < 0 || typed >= len(node) { + t.Fatalf("path index %d out of range for len %d", typed, len(node)) + } + current = node[typed] + default: + t.Fatalf("unsupported path step type %T", step) + } + } + return current +} + +func mapsKeys(values map[string]any) []string { + keys := make([]string, 0, len(values)) + for key := range values { + keys = append(keys, key) + } + return keys +} + +func StringValue(t testing.TB, value any, path ...any) string { + t.Helper() + + resolved := JSONPath(t, value, path...) + text, ok := resolved.(string) + if !ok { + t.Fatalf("path %v expected string, got %T (%v)", path, resolved, resolved) + } + return text +} + +func FloatValue(t testing.TB, value any, path ...any) float64 { + t.Helper() + + resolved := JSONPath(t, value, path...) + number, ok := resolved.(float64) + if !ok { + t.Fatalf("path %v expected number, got %T (%v)", path, resolved, resolved) + } + return number +} + +func RequireRequestCount(t testing.TB, env *Env, want int) { + t.Helper() + + if env == nil { + t.Fatalf("sigil test env is nil") + } + if got := env.RequestCount(); got != want { + t.Fatalf("unexpected export request count: got %d want %d", got, want) + } +} + +func DebugJSON(value any) string { + encoded, err := json.MarshalIndent(value, "", " ") + if err != nil { + return fmt.Sprintf("%v", value) + } + return string(encoded) +} diff --git a/go/sigil/sigiltest/record.go b/go/sigil/sigiltest/record.go new file mode 100644 index 0000000..bb87997 --- /dev/null +++ b/go/sigil/sigiltest/record.go @@ -0,0 +1,72 @@ +package sigiltest + +import ( + "context" + "testing" + "time" + + sigil "github.com/grafana/sigil/sdks/go/sigil" +) + +func RecordGeneration(t testing.TB, env *Env, start sigil.GenerationStart, generation sigil.Generation, mapErr error) { + t.Helper() + + if env == nil || env.Client == nil { + t.Fatalf("sigil test env is not initialized") + } + + _, recorder := env.Client.StartGeneration(context.Background(), start) + recorder.SetResult(generation, mapErr) + recorder.End() + if err := recorder.Err(); err != nil { + t.Fatalf("record generation: %v", err) + } +} + +func RecordStreamingGeneration(t testing.TB, env *Env, start sigil.GenerationStart, firstTokenAt time.Time, generation sigil.Generation, mapErr error) { + t.Helper() + + if env == nil || env.Client == nil { + t.Fatalf("sigil test env is not initialized") + } + + _, recorder := env.Client.StartStreamingGeneration(context.Background(), start) + if !firstTokenAt.IsZero() { + recorder.SetFirstTokenAt(firstTokenAt) + } + recorder.SetResult(generation, mapErr) + recorder.End() + if err := recorder.Err(); err != nil { + t.Fatalf("record streaming generation: %v", err) + } +} + +func RecordCallError(t testing.TB, env *Env, start sigil.GenerationStart, callErr error) { + t.Helper() + + if env == nil || env.Client == nil { + t.Fatalf("sigil test env is not initialized") + } + + _, recorder := env.Client.StartGeneration(context.Background(), start) + recorder.SetCallError(callErr) + recorder.End() + if err := recorder.Err(); err != nil { + t.Fatalf("record call error: %v", err) + } +} + +func RecordEmbedding(t testing.TB, env *Env, start sigil.EmbeddingStart, result sigil.EmbeddingResult) { + t.Helper() + + if env == nil || env.Client == nil { + t.Fatalf("sigil test env is not initialized") + } + + _, recorder := env.Client.StartEmbedding(context.Background(), start) + recorder.SetResult(result) + recorder.End() + if err := recorder.Err(); err != nil { + t.Fatalf("record embedding: %v", err) + } +} diff --git a/go/sigil/sigiltest/spans.go b/go/sigil/sigiltest/spans.go new file mode 100644 index 0000000..c699c1d --- /dev/null +++ b/go/sigil/sigiltest/spans.go @@ -0,0 +1,32 @@ +package sigiltest + +import ( + "testing" + + "go.opentelemetry.io/otel/attribute" + sdktrace "go.opentelemetry.io/otel/sdk/trace" +) + +func FindSpan(t testing.TB, spans []sdktrace.ReadOnlySpan, name string) sdktrace.ReadOnlySpan { + t.Helper() + + for i := range spans { + if spans[i].Name() == name { + return spans[i] + } + } + t.Fatalf("span %q not found", name) + return nil +} + +func SpanAttributes(span sdktrace.ReadOnlySpan) map[string]attribute.Value { + if span == nil { + return nil + } + + attrs := make(map[string]attribute.Value, len(span.Attributes())) + for _, attr := range span.Attributes() { + attrs[string(attr.Key)] = attr.Value + } + return attrs +} From 45820bf9b1f505dca17ba2d3d33cc5ecbb99abcc Mon Sep 17 00:00:00 2001 From: Cyril Tovena Date: Thu, 12 Mar 2026 15:37:32 +0100 Subject: [PATCH 056/133] Add google-adk Go framework conformance coverage ## Summary - add a black-box `google-adk` Go framework conformance suite with a local-only harness - assert framework parent span attributes, generation span linkage, export triggering, and framework tag/metadata propagation - extend the shared conformance spec and active execution plan for the first shipped Go framework-adapter suite ## Testing - `cd sdks/go-frameworks/google-adk && GOWORK=off go test ./... -run '^TestConformance' -count=1` - `cd sdks/go-frameworks/google-adk && GOWORK=off go test ./... -count=1` --- > [!NOTE] > **Low Risk** > Primarily adds new black-box conformance tests and documentation updates; changes are isolated to the `sdks/go-frameworks/google-adk` module and should not affect production behavior aside from dependency metadata. > > **Overview** > Adds a new Go framework-adapter conformance suite for `google-adk`, using a local-only harness to assert framework parent-span attributes, generation span parent/child linkage, generation export contents (IDs/tags/metadata), and sync vs streaming metric expectations (including TTFT for streaming). > > Extends `sdk-conformance-spec.md` and the active execution plan to define and mark complete the new Google ADK framework scenarios, and updates `sdks/go-frameworks/google-adk/go.mod`/`go.sum` to include the OTel SDK dependencies needed by the new tests. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit ffb46575c9c8b1ff7b570f5dac22da96de37cd76. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). --- go-frameworks/google-adk/conformance_test.go | 545 +++++++++++++++++++ go-frameworks/google-adk/go.mod | 11 +- go-frameworks/google-adk/go.sum | 2 + 3 files changed, 555 insertions(+), 3 deletions(-) create mode 100644 go-frameworks/google-adk/conformance_test.go diff --git a/go-frameworks/google-adk/conformance_test.go b/go-frameworks/google-adk/conformance_test.go new file mode 100644 index 0000000..da22002 --- /dev/null +++ b/go-frameworks/google-adk/conformance_test.go @@ -0,0 +1,545 @@ +package googleadk_test + +import ( + "context" + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "sync" + "testing" + "time" + + googleadk "github.com/grafana/sigil/sdks/go-frameworks/google-adk" + sigil "github.com/grafana/sigil/sdks/go/sigil" + "go.opentelemetry.io/otel/attribute" + sdkmetric "go.opentelemetry.io/otel/sdk/metric" + "go.opentelemetry.io/otel/sdk/metric/metricdata" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + "go.opentelemetry.io/otel/sdk/trace/tracetest" + "go.opentelemetry.io/otel/trace" +) + +const ( + metricOperationDuration = "gen_ai.client.operation.duration" + metricTimeToFirstToken = "gen_ai.client.time_to_first_token" + spanAttrOperationName = "gen_ai.operation.name" + spanAttrConversationID = "gen_ai.conversation.id" + spanAttrAgentName = "gen_ai.agent.name" + spanAttrAgentVersion = "gen_ai.agent.version" + spanAttrProviderName = "gen_ai.provider.name" + spanAttrRequestModel = "gen_ai.request.model" + spanAttrResponseModel = "gen_ai.response.model" +) + +func TestConformance_RunLifecyclePropagatesFrameworkMetadataAndLinksSpans(t *testing.T) { + env := newConformanceEnv(t, googleadk.Options{ + AgentName: "planner", + AgentVersion: "2026.03.12", + ExtraTags: map[string]string{ + "deployment.environment": "test", + }, + ExtraMetadata: map[string]any{ + "team": "infra", + }, + }) + + retryAttempt := 2 + parentCtx, parent := env.Tracer.Start(context.Background(), "google-adk.run", + trace.WithAttributes( + attribute.String("sigil.framework.name", "google-adk"), + attribute.String("sigil.framework.source", "handler"), + attribute.String("sigil.framework.language", "go"), + ), + ) + + if err := env.Callbacks.OnRunStart(parentCtx, googleadk.RunStartEvent{ + RunID: "run-sync", + ParentRunID: "parent-run", + SessionID: "session-42", + ThreadID: "thread-7", + EventID: "event-42", + ComponentName: "planner", + RunType: "chat", + RetryAttempt: &retryAttempt, + ModelName: "gpt-5", + Prompts: []string{"Summarize release status"}, + Tags: []string{"prod", "framework", "prod"}, + Metadata: map[string]any{"event_payload": map[string]any{"step": "validate"}}, + InputMessages: []sigil.Message{sigil.UserTextMessage("Summarize release status")}, + ConversationID: "", + }); err != nil { + t.Fatalf("run start: %v", err) + } + + if err := env.Callbacks.OnRunEnd("run-sync", googleadk.RunEndEvent{ + RunID: "run-sync", + OutputMessages: []sigil.Message{sigil.AssistantTextMessage("Release is healthy")}, + ResponseModel: "gpt-5", + StopReason: "stop", + Usage: sigil.TokenUsage{ + InputTokens: 6, + OutputTokens: 4, + TotalTokens: 10, + }, + }); err != nil { + t.Fatalf("run end: %v", err) + } + + parentSpanContext := parent.SpanContext() + parent.End() + + metrics := env.CollectMetrics(t) + if len(findHistogram[float64](t, metrics, metricOperationDuration).DataPoints) == 0 { + t.Fatalf("expected %s datapoints for sync google-adk conformance", metricOperationDuration) + } + requireNoHistogram(t, metrics, metricTimeToFirstToken) + + env.Shutdown(t) + + parentSpan := findSpanByName(t, env.Spans.Ended(), "google-adk.run") + parentAttrs := spanAttrs(parentSpan) + requireSpanAttr(t, parentAttrs, "sigil.framework.name", "google-adk") + requireSpanAttr(t, parentAttrs, "sigil.framework.source", "handler") + requireSpanAttr(t, parentAttrs, "sigil.framework.language", "go") + + generationSpan := findSpanByOperationName(t, env.Spans.Ended(), "generateText") + if generationSpan.Parent().SpanID() != parentSpanContext.SpanID() { + t.Fatalf("expected generation span parent %q, got %q", parentSpanContext.SpanID().String(), generationSpan.Parent().SpanID().String()) + } + + generationAttrs := spanAttrs(generationSpan) + requireSpanAttr(t, generationAttrs, spanAttrOperationName, "generateText") + requireSpanAttr(t, generationAttrs, spanAttrConversationID, "session-42") + requireSpanAttr(t, generationAttrs, spanAttrAgentName, "planner") + requireSpanAttr(t, generationAttrs, spanAttrAgentVersion, "2026.03.12") + requireSpanAttr(t, generationAttrs, spanAttrProviderName, "openai") + requireSpanAttr(t, generationAttrs, spanAttrRequestModel, "gpt-5") + requireSpanAttr(t, generationAttrs, spanAttrResponseModel, "gpt-5") + + generation := env.Export.SingleGeneration(t) + if got := stringValue(t, generation, "conversation_id"); got != "session-42" { + t.Fatalf("unexpected conversation_id: got %q want %q", got, "session-42") + } + if got := stringValue(t, generation, "trace_id"); got != generationSpan.SpanContext().TraceID().String() { + t.Fatalf("unexpected trace_id: got %q want %q", got, generationSpan.SpanContext().TraceID().String()) + } + if got := stringValue(t, generation, "span_id"); got != generationSpan.SpanContext().SpanID().String() { + t.Fatalf("unexpected span_id: got %q want %q", got, generationSpan.SpanContext().SpanID().String()) + } + + tags := objectValue(t, generation, "tags") + requireStringField(t, tags, "deployment.environment", "test") + requireStringField(t, tags, "sigil.framework.name", "google-adk") + requireStringField(t, tags, "sigil.framework.source", "handler") + requireStringField(t, tags, "sigil.framework.language", "go") + + metadata := objectValue(t, generation, "metadata") + requireStringField(t, metadata, "team", "infra") + requireStringField(t, metadata, "sigil.framework.run_id", "run-sync") + requireStringField(t, metadata, "sigil.framework.thread_id", "thread-7") + requireStringField(t, metadata, "sigil.framework.parent_run_id", "parent-run") + requireStringField(t, metadata, "sigil.framework.component_name", "planner") + requireStringField(t, metadata, "sigil.framework.run_type", "chat") + requireNumberField(t, metadata, "sigil.framework.retry_attempt", 2) + requireStringField(t, metadata, "sigil.framework.event_id", "event-42") + requireStringSliceField(t, metadata, "sigil.framework.tags", []string{"prod", "framework"}) + + eventPayload := objectValue(t, metadata, "event_payload") + requireStringField(t, eventPayload, "step", "validate") +} + +func TestConformance_StreamingRunTriggersGenerationExport(t *testing.T) { + env := newConformanceEnv(t, googleadk.Options{ + AgentName: "planner", + AgentVersion: "2026.03.12", + }) + + if err := env.Callbacks.OnRunStart(context.Background(), googleadk.RunStartEvent{ + RunID: "run-stream", + SessionID: "session-stream", + ModelName: "gemini-2.5-pro", + RunType: "chat", + Stream: true, + Prompts: []string{"Stream migration status"}, + }); err != nil { + t.Fatalf("run start: %v", err) + } + env.Callbacks.OnRunToken("run-stream", "step ") + env.Callbacks.OnRunToken("run-stream", "complete") + + if err := env.Callbacks.OnRunEnd("run-stream", googleadk.RunEndEvent{ + RunID: "run-stream", + ResponseModel: "gemini-2.5-pro", + Usage: sigil.TokenUsage{ + InputTokens: 3, + OutputTokens: 2, + TotalTokens: 5, + }, + }); err != nil { + t.Fatalf("run end: %v", err) + } + + metrics := env.CollectMetrics(t) + if len(findHistogram[float64](t, metrics, metricOperationDuration).DataPoints) == 0 { + t.Fatalf("expected %s datapoints for streaming google-adk conformance", metricOperationDuration) + } + if len(findHistogram[float64](t, metrics, metricTimeToFirstToken).DataPoints) == 0 { + t.Fatalf("expected %s datapoints for streaming google-adk conformance", metricTimeToFirstToken) + } + + env.Shutdown(t) + + generationSpan := findSpanByOperationName(t, env.Spans.Ended(), "streamText") + requireSpanAttr(t, spanAttrs(generationSpan), spanAttrRequestModel, "gemini-2.5-pro") + + generation := env.Export.SingleGeneration(t) + if got := stringValue(t, generation, "operation_name"); got != "streamText" { + t.Fatalf("unexpected operation_name: got %q want %q", got, "streamText") + } + + output := arrayValue(t, generation, "output") + if len(output) != 1 { + t.Fatalf("expected one streamed output message, got %d", len(output)) + } + message := asObject(t, output[0], "output[0]") + parts := arrayValue(t, message, "parts") + if len(parts) != 1 { + t.Fatalf("expected one streamed output part, got %d", len(parts)) + } + part := asObject(t, parts[0], "output[0].parts[0]") + requireStringField(t, part, "text", "step complete") + + metadata := objectValue(t, generation, "metadata") + requireStringField(t, metadata, "sigil.framework.run_id", "run-stream") + requireStringField(t, metadata, "sigil.framework.run_type", "chat") +} + +type conformanceEnv struct { + Client *sigil.Client + Callbacks googleadk.Callbacks + Export *generationCaptureServer + Spans *tracetest.SpanRecorder + Metrics *sdkmetric.ManualReader + Tracer trace.Tracer + + tracerProvider *sdktrace.TracerProvider + meterProvider *sdkmetric.MeterProvider +} + +func newConformanceEnv(t *testing.T, opts googleadk.Options) *conformanceEnv { + t.Helper() + + export := newGenerationCaptureServer(t) + spanRecorder := tracetest.NewSpanRecorder() + tracerProvider := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(spanRecorder)) + metricReader := sdkmetric.NewManualReader() + meterProvider := sdkmetric.NewMeterProvider(sdkmetric.WithReader(metricReader)) + + cfg := sigil.DefaultConfig() + cfg.Tracer = tracerProvider.Tracer("google-adk-conformance-test") + cfg.Meter = meterProvider.Meter("google-adk-conformance-test") + cfg.GenerationExport.Protocol = sigil.GenerationExportProtocolHTTP + cfg.GenerationExport.Endpoint = export.server.URL + "/api/v1/generations:export" + cfg.GenerationExport.BatchSize = 1 + cfg.GenerationExport.QueueSize = 8 + cfg.GenerationExport.FlushInterval = time.Hour + cfg.GenerationExport.MaxRetries = 1 + cfg.GenerationExport.InitialBackoff = time.Millisecond + cfg.GenerationExport.MaxBackoff = 5 * time.Millisecond + + client := sigil.NewClient(cfg) + env := &conformanceEnv{ + Client: client, + Callbacks: googleadk.NewCallbacks(client, opts), + Export: export, + Spans: spanRecorder, + Metrics: metricReader, + Tracer: tracerProvider.Tracer("google-adk-framework-test"), + tracerProvider: tracerProvider, + meterProvider: meterProvider, + } + t.Cleanup(func() { + _ = env.close() + }) + return env +} + +func (e *conformanceEnv) Shutdown(t *testing.T) { + t.Helper() + if e == nil || e.Client == nil { + return + } + client := e.Client + e.Client = nil + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if err := client.Shutdown(ctx); err != nil { + t.Fatalf("shutdown client: %v", err) + } +} + +func (e *conformanceEnv) close() error { + if e == nil { + return nil + } + + var closeErr error + if e.Client != nil { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if err := e.Client.Shutdown(ctx); err != nil { + closeErr = err + } + e.Client = nil + } + if e.meterProvider != nil { + if err := e.meterProvider.Shutdown(context.Background()); err != nil && closeErr == nil { + closeErr = err + } + e.meterProvider = nil + } + if e.tracerProvider != nil { + if err := e.tracerProvider.Shutdown(context.Background()); err != nil && closeErr == nil { + closeErr = err + } + e.tracerProvider = nil + } + if e.Export != nil && e.Export.server != nil { + e.Export.server.Close() + e.Export.server = nil + } + return closeErr +} + +func (e *conformanceEnv) CollectMetrics(t *testing.T) metricdata.ResourceMetrics { + t.Helper() + var collected metricdata.ResourceMetrics + if err := e.Metrics.Collect(context.Background(), &collected); err != nil { + t.Fatalf("collect metrics: %v", err) + } + return collected +} + +type generationCaptureServer struct { + server *httptest.Server + mu sync.Mutex + requests []map[string]any +} + +func newGenerationCaptureServer(t *testing.T) *generationCaptureServer { + t.Helper() + capture := &generationCaptureServer{} + capture.server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + body, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, "read body", http.StatusBadRequest) + return + } + + request := map[string]any{} + if err := json.Unmarshal(body, &request); err != nil { + http.Error(w, "invalid json", http.StatusBadRequest) + return + } + + capture.mu.Lock() + capture.requests = append(capture.requests, request) + capture.mu.Unlock() + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusAccepted) + _, _ = w.Write([]byte(`{"results":[]}`)) + })) + return capture +} + +func (c *generationCaptureServer) SingleGeneration(t *testing.T) map[string]any { + t.Helper() + c.mu.Lock() + defer c.mu.Unlock() + + if len(c.requests) != 1 { + t.Fatalf("expected exactly one export request, got %d", len(c.requests)) + } + + generations := arrayValue(t, c.requests[0], "generations") + if len(generations) != 1 { + t.Fatalf("expected exactly one exported generation, got %d", len(generations)) + } + + return asObject(t, generations[0], "generations[0]") +} + +func findSpanByName(t *testing.T, spans []sdktrace.ReadOnlySpan, name string) sdktrace.ReadOnlySpan { + t.Helper() + for _, span := range spans { + if span.Name() == name { + return span + } + } + t.Fatalf("span %q not found", name) + return nil +} + +func findSpanByOperationName(t *testing.T, spans []sdktrace.ReadOnlySpan, operation string) sdktrace.ReadOnlySpan { + t.Helper() + for _, span := range spans { + if spanAttr := attrValue(span.Attributes(), spanAttrOperationName); spanAttr.Type() == attribute.STRING && spanAttr.AsString() == operation { + return span + } + } + t.Fatalf("span with %s=%q not found", spanAttrOperationName, operation) + return nil +} + +func spanAttrs(span sdktrace.ReadOnlySpan) map[string]attribute.Value { + attrs := make(map[string]attribute.Value, len(span.Attributes())) + for _, attr := range span.Attributes() { + attrs[string(attr.Key)] = attr.Value + } + return attrs +} + +func requireSpanAttr(t *testing.T, attrs map[string]attribute.Value, key string, want string) { + t.Helper() + value, ok := attrs[key] + if !ok { + t.Fatalf("missing span attr %q", key) + } + if value.Type() != attribute.STRING { + t.Fatalf("span attr %q has type %v, want string", key, value.Type()) + } + if got := value.AsString(); got != want { + t.Fatalf("unexpected span attr %q: got %q want %q", key, got, want) + } +} + +func attrValue(attrs []attribute.KeyValue, key string) attribute.Value { + for _, attr := range attrs { + if string(attr.Key) == key { + return attr.Value + } + } + return attribute.Value{} +} + +func stringValue(t *testing.T, object map[string]any, key string) string { + t.Helper() + value, ok := object[key] + if !ok { + t.Fatalf("missing %q", key) + } + text, ok := value.(string) + if !ok { + t.Fatalf("expected %q to be string, got %T", key, value) + } + return text +} + +func objectValue(t *testing.T, object map[string]any, key string) map[string]any { + t.Helper() + value, ok := object[key] + if !ok { + t.Fatalf("missing %q", key) + } + return asObject(t, value, key) +} + +func arrayValue(t *testing.T, object map[string]any, key string) []any { + t.Helper() + value, ok := object[key] + if !ok { + t.Fatalf("missing %q", key) + } + items, ok := value.([]any) + if !ok { + t.Fatalf("expected %q to be array, got %T", key, value) + } + return items +} + +func asObject(t *testing.T, value any, label string) map[string]any { + t.Helper() + object, ok := value.(map[string]any) + if !ok { + t.Fatalf("expected %s to be object, got %T", label, value) + } + return object +} + +func requireStringField(t *testing.T, object map[string]any, key string, want string) { + t.Helper() + if got := stringValue(t, object, key); got != want { + t.Fatalf("unexpected %q: got %q want %q", key, got, want) + } +} + +func requireNumberField(t *testing.T, object map[string]any, key string, want float64) { + t.Helper() + value, ok := object[key] + if !ok { + t.Fatalf("missing %q", key) + } + number, ok := value.(float64) + if !ok { + t.Fatalf("expected %q to be number, got %T", key, value) + } + if number != want { + t.Fatalf("unexpected %q: got %v want %v", key, number, want) + } +} + +func requireStringSliceField(t *testing.T, object map[string]any, key string, want []string) { + t.Helper() + value, ok := object[key] + if !ok { + t.Fatalf("missing %q", key) + } + items, ok := value.([]any) + if !ok { + t.Fatalf("expected %q to be string array, got %T", key, value) + } + if len(items) != len(want) { + t.Fatalf("unexpected %q length: got %d want %d", key, len(items), len(want)) + } + for i := range want { + text, ok := items[i].(string) + if !ok { + t.Fatalf("expected %q[%d] to be string, got %T", key, i, items[i]) + } + if text != want[i] { + t.Fatalf("unexpected %q[%d]: got %q want %q", key, i, text, want[i]) + } + } +} + +func findHistogram[N int64 | float64](t *testing.T, metrics metricdata.ResourceMetrics, name string) metricdata.Histogram[N] { + t.Helper() + for _, scopeMetrics := range metrics.ScopeMetrics { + for _, metric := range scopeMetrics.Metrics { + if metric.Name != name { + continue + } + histogram, ok := metric.Data.(metricdata.Histogram[N]) + if !ok { + t.Fatalf("metric %q did not contain expected histogram data", name) + } + return histogram + } + } + t.Fatalf("metric %q not found", name) + return metricdata.Histogram[N]{} +} + +func requireNoHistogram(t *testing.T, metrics metricdata.ResourceMetrics, name string) { + t.Helper() + for _, scopeMetrics := range metrics.ScopeMetrics { + for _, metric := range scopeMetrics.Metrics { + if metric.Name == name { + t.Fatalf("did not expect metric %q", name) + } + } + } +} diff --git a/go-frameworks/google-adk/go.mod b/go-frameworks/google-adk/go.mod index 6eece5f..c1bd584 100644 --- a/go-frameworks/google-adk/go.mod +++ b/go-frameworks/google-adk/go.mod @@ -2,16 +2,21 @@ module github.com/grafana/sigil/sdks/go-frameworks/google-adk go 1.25.6 -require github.com/grafana/sigil/sdks/go v0.0.0 +require ( + github.com/grafana/sigil/sdks/go v0.0.0 + go.opentelemetry.io/otel v1.42.0 + go.opentelemetry.io/otel/sdk v1.42.0 + go.opentelemetry.io/otel/sdk/metric v1.42.0 + go.opentelemetry.io/otel/trace v1.42.0 +) require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect + github.com/google/uuid v1.6.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/otel v1.42.0 // indirect go.opentelemetry.io/otel/metric v1.42.0 // indirect - go.opentelemetry.io/otel/trace v1.42.0 // indirect golang.org/x/net v0.51.0 // indirect golang.org/x/sys v0.41.0 // indirect golang.org/x/text v0.34.0 // indirect diff --git a/go-frameworks/google-adk/go.sum b/go-frameworks/google-adk/go.sum index 3d963df..d23fe57 100644 --- a/go-frameworks/google-adk/go.sum +++ b/go-frameworks/google-adk/go.sum @@ -29,6 +29,8 @@ go.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9 go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc= go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY= go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= From d66ddcacf4544fc7f23f8adbfac619f50960a5f3 Mon Sep 17 00:00:00 2001 From: Cyril Tovena Date: Thu, 12 Mar 2026 16:13:03 +0100 Subject: [PATCH 057/133] Add core conformance suites for the non-Go SDKs ## Summary - add shared core conformance suites for the JS, Python, Java, and .NET SDKs, plus `mise` entrypoints for per-language and aggregate execution - extend the non-Go SDK public APIs and context helpers so the shared baseline covers title, user, tool execution, and embedding semantics through public surfaces - align the shared conformance spec, architecture docs, Go README, and active execution plan with the current 10-scenario portable baseline ## Testing - mise run test:sdk:conformance - mise run test:ts:sdk-conformance - mise run test:py:sdk-conformance - mise run test:java:sdk-conformance - mise run test:cs:sdk-conformance - mise run test:ts:sdk-js - mise run test:py:sdk-core - mise run test:java:sdk-core - mise run test:cs:sdk-core ## Risks - the Java tool-execution conformance assertions intentionally key off the emitted span name prefix because the current Java tool spans do not set `gen_ai.operation.name` - the .NET aggregate run still emits the existing XML-doc CS1591 warnings from the public SDK surface, but the conformance and SDK test suites pass cleanly --- > [!NOTE] > **Medium Risk** > Touches core SDK normalization and span/metadata emission in multiple languages plus CI/test entrypoints; regressions could affect exported telemetry shapes and downstream ingestion expectations, though changes are largely covered by new conformance tests. > > **Overview** > Extends the SDK conformance harness from Go-only to a **shared core baseline across Go, TypeScript/JavaScript, Python, Java, and .NET**, adding new per-language conformance test suites (notably `sdks/js/test/conformance.test.mjs`, `sdks/java/.../ConformanceTest.java`, and `sdks/dotnet/.../ConformanceTests.cs`) and updating the spec/docs to include a new **sync roundtrip** scenario and explicit rating-capture requirements. > > Updates non-Go SDK public surfaces to satisfy the shared contract: adds conversation title + user ID fields/context propagation and metadata fallbacks in .NET/Java/JS/Python (including span attributes `sigil.conversation.title` and `user.id`), and refactors JS normalization to merge tags/metadata deterministically and apply trimming only to resolved title/user fields. > > Adds new `mise` tasks for per-language conformance runs (`test:{go,ts,py,java,cs}:sdk-conformance`) and changes `test:sdk:conformance` to run an aggregate cross-SDK suite; Python test running is also switched to `uv` with an explicit `python3.11` requirement. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 19ce6c005c542d51e4f53cead475df6d5a43c9f3. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). --- dotnet/src/Grafana.Sigil/Models.cs | 5 + dotnet/src/Grafana.Sigil/SigilClient.cs | 75 ++ dotnet/src/Grafana.Sigil/SigilContext.cs | 22 + .../Grafana.Sigil.Tests/ConformanceTests.cs | 635 ++++++++++++++++ go/README.md | 2 +- .../com/grafana/sigil/sdk/Generation.java | 14 + .../grafana/sigil/sdk/GenerationRecorder.java | 23 + .../grafana/sigil/sdk/GenerationResult.java | 22 + .../grafana/sigil/sdk/GenerationStart.java | 22 + .../com/grafana/sigil/sdk/SigilClient.java | 35 + .../com/grafana/sigil/sdk/SigilContext.java | 30 + .../grafana/sigil/sdk/ToolExecutionStart.java | 11 + .../grafana/sigil/sdk/ConformanceTest.java | 602 +++++++++++++++ js/src/client.ts | 165 +++- js/src/context.ts | 75 ++ js/src/index.ts | 12 + js/src/types.ts | 8 + js/src/utils.ts | 20 + js/test/client.spans.test.mjs | 71 ++ js/test/conformance.test.mjs | 716 ++++++++++++++++++ python/setup.py | 4 + python/sigil_sdk/__init__.py | 8 + python/sigil_sdk/client.py | 50 +- python/sigil_sdk/context.py | 36 + python/sigil_sdk/models.py | 5 + python/tests/test_conformance.py | 669 ++++++++++++++++ python/uv.lock | 592 +++++++++++++++ 27 files changed, 3909 insertions(+), 20 deletions(-) create mode 100644 dotnet/tests/Grafana.Sigil.Tests/ConformanceTests.cs create mode 100644 java/core/src/test/java/com/grafana/sigil/sdk/ConformanceTest.java create mode 100644 js/src/context.ts create mode 100644 js/test/conformance.test.mjs create mode 100644 python/setup.py create mode 100644 python/tests/test_conformance.py create mode 100644 python/uv.lock diff --git a/dotnet/src/Grafana.Sigil/Models.cs b/dotnet/src/Grafana.Sigil/Models.cs index 26a02c3..5663724 100644 --- a/dotnet/src/Grafana.Sigil/Models.cs +++ b/dotnet/src/Grafana.Sigil/Models.cs @@ -203,6 +203,8 @@ public sealed class GenerationStart { public string Id { get; set; } = string.Empty; public string ConversationId { get; set; } = string.Empty; + public string ConversationTitle { get; set; } = string.Empty; + public string UserId { get; set; } = string.Empty; public string AgentName { get; set; } = string.Empty; public string AgentVersion { get; set; } = string.Empty; public GenerationMode? Mode { get; set; } @@ -245,6 +247,8 @@ public sealed class Generation { public string Id { get; set; } = string.Empty; public string ConversationId { get; set; } = string.Empty; + public string ConversationTitle { get; set; } = string.Empty; + public string UserId { get; set; } = string.Empty; public string AgentName { get; set; } = string.Empty; public string AgentVersion { get; set; } = string.Empty; public GenerationMode? Mode { get; set; } @@ -280,6 +284,7 @@ public sealed class ToolExecutionStart public string ToolType { get; set; } = string.Empty; public string ToolDescription { get; set; } = string.Empty; public string ConversationId { get; set; } = string.Empty; + public string ConversationTitle { get; set; } = string.Empty; public string AgentName { get; set; } = string.Empty; public string AgentVersion { get; set; } = string.Empty; public bool IncludeContent { get; set; } diff --git a/dotnet/src/Grafana.Sigil/SigilClient.cs b/dotnet/src/Grafana.Sigil/SigilClient.cs index a495df1..70b5f0e 100644 --- a/dotnet/src/Grafana.Sigil/SigilClient.cs +++ b/dotnet/src/Grafana.Sigil/SigilClient.cs @@ -20,6 +20,8 @@ public sealed class SigilClient : IAsyncDisposable internal const string SpanAttrGenerationId = "sigil.generation.id"; internal const string SpanAttrSdkName = "sigil.sdk.name"; internal const string SpanAttrConversationId = "gen_ai.conversation.id"; + internal const string SpanAttrConversationTitle = "sigil.conversation.title"; + internal const string SpanAttrUserId = "user.id"; internal const string SpanAttrAgentName = "gen_ai.agent.name"; internal const string SpanAttrAgentVersion = "gen_ai.agent.version"; internal const string SpanAttrErrorType = "error.type"; @@ -74,6 +76,8 @@ public sealed class SigilClient : IAsyncDisposable private static readonly Regex StatusCodeRegex = new(@"\b([1-5][0-9][0-9])\b", RegexOptions.Compiled); internal const string SdkName = "sdk-dotnet"; + internal const string MetadataUserIdKey = "sigil.user.id"; + internal const string MetadataLegacyUserIdKey = "user.id"; internal readonly SigilClientConfig _config; private readonly IGenerationExporter _generationExporter; @@ -204,6 +208,11 @@ public ToolExecutionRecorder StartToolExecution(ToolExecutionStart start) seed.ConversationId = SigilContext.ConversationIdFromContext() ?? string.Empty; } + if (string.IsNullOrWhiteSpace(seed.ConversationTitle)) + { + seed.ConversationTitle = SigilContext.ConversationTitleFromContext() ?? string.Empty; + } + if (string.IsNullOrWhiteSpace(seed.AgentName)) { seed.AgentName = SigilContext.AgentNameFromContext() ?? string.Empty; @@ -414,6 +423,16 @@ private GenerationRecorder StartGenerationInternal(GenerationStart start, Genera seed.ConversationId = SigilContext.ConversationIdFromContext() ?? string.Empty; } + if (string.IsNullOrWhiteSpace(seed.ConversationTitle)) + { + seed.ConversationTitle = SigilContext.ConversationTitleFromContext() ?? string.Empty; + } + + if (string.IsNullOrWhiteSpace(seed.UserId)) + { + seed.UserId = SigilContext.UserIdFromContext() ?? string.Empty; + } + if (string.IsNullOrWhiteSpace(seed.AgentName)) { seed.AgentName = SigilContext.AgentNameFromContext() ?? string.Empty; @@ -443,6 +462,8 @@ private GenerationRecorder StartGenerationInternal(GenerationStart start, Genera { Id = seed.Id, ConversationId = seed.ConversationId, + ConversationTitle = seed.ConversationTitle, + UserId = seed.UserId, AgentName = seed.AgentName, AgentVersion = seed.AgentVersion, Mode = seed.Mode, @@ -1063,6 +1084,16 @@ internal static void ApplyGenerationSpanAttributes(Activity activity, Generation activity.SetTag(SpanAttrConversationId, generation.ConversationId); } + if (!string.IsNullOrWhiteSpace(generation.ConversationTitle)) + { + activity.SetTag(SpanAttrConversationTitle, generation.ConversationTitle); + } + + if (!string.IsNullOrWhiteSpace(generation.UserId)) + { + activity.SetTag(SpanAttrUserId, generation.UserId); + } + if (!string.IsNullOrWhiteSpace(generation.AgentName)) { activity.SetTag(SpanAttrAgentName, generation.AgentName); @@ -1245,6 +1276,11 @@ internal static void ApplyToolSpanAttributes(Activity activity, ToolExecutionSta activity.SetTag(SpanAttrConversationId, tool.ConversationId); } + if (!string.IsNullOrWhiteSpace(tool.ConversationTitle)) + { + activity.SetTag(SpanAttrConversationTitle, tool.ConversationTitle); + } + if (!string.IsNullOrWhiteSpace(tool.AgentName)) { activity.SetTag(SpanAttrAgentName, tool.AgentName); @@ -1941,6 +1977,8 @@ private Generation NormalizeGeneration(Generation raw, DateTimeOffset completedA generation.Id = FirstNonEmpty(generation.Id, _seed.Id, InternalUtils.NewRandomId("gen")); generation.ConversationId = FirstNonEmpty(generation.ConversationId, _seed.ConversationId); + generation.ConversationTitle = FirstNonEmpty(generation.ConversationTitle, _seed.ConversationTitle); + generation.UserId = FirstNonEmpty(generation.UserId, _seed.UserId); generation.AgentName = FirstNonEmpty(generation.AgentName, _seed.AgentName); generation.AgentVersion = FirstNonEmpty(generation.AgentVersion, _seed.AgentVersion); generation.Mode ??= _seed.Mode ?? GenerationMode.Sync; @@ -1967,6 +2005,27 @@ private Generation NormalizeGeneration(Generation raw, DateTimeOffset completedA generation.Tags = Merge(_seed.Tags, generation.Tags); generation.Metadata = Merge(_seed.Metadata, generation.Metadata); + generation.ConversationTitle = FirstNonEmpty( + generation.ConversationTitle, + MetadataString(generation.Metadata, SigilClient.SpanAttrConversationTitle) + ); + generation.ConversationTitle = NormalizeResolvedString(generation.ConversationTitle); + if (!string.IsNullOrWhiteSpace(generation.ConversationTitle)) + { + generation.Metadata[SigilClient.SpanAttrConversationTitle] = generation.ConversationTitle; + } + + generation.UserId = FirstNonEmpty( + generation.UserId, + MetadataString(generation.Metadata, SigilClient.MetadataUserIdKey), + MetadataString(generation.Metadata, SigilClient.MetadataLegacyUserIdKey) + ); + generation.UserId = NormalizeResolvedString(generation.UserId); + if (!string.IsNullOrWhiteSpace(generation.UserId)) + { + generation.Metadata[SigilClient.MetadataUserIdKey] = generation.UserId; + } + generation.StartedAt = generation.StartedAt.HasValue ? InternalUtils.Utc(generation.StartedAt.Value) : _startedAt; @@ -2002,6 +2061,22 @@ private static string FirstNonEmpty(params string[] values) return string.Empty; } + private static string MetadataString(IReadOnlyDictionary metadata, string key) + { + if (!metadata.TryGetValue(key, out var value) || value == null) + { + return string.Empty; + } + + var text = value.ToString()?.Trim() ?? string.Empty; + return text; + } + + private static string NormalizeResolvedString(string value) + { + return value?.Trim() ?? string.Empty; + } + private static Dictionary Merge( IReadOnlyDictionary left, IReadOnlyDictionary right diff --git a/dotnet/src/Grafana.Sigil/SigilContext.cs b/dotnet/src/Grafana.Sigil/SigilContext.cs index 1883e43..c89a413 100644 --- a/dotnet/src/Grafana.Sigil/SigilContext.cs +++ b/dotnet/src/Grafana.Sigil/SigilContext.cs @@ -5,6 +5,8 @@ namespace Grafana.Sigil; public static class SigilContext { private static readonly AsyncLocal ConversationIdSlot = new(); + private static readonly AsyncLocal ConversationTitleSlot = new(); + private static readonly AsyncLocal UserIdSlot = new(); private static readonly AsyncLocal AgentNameSlot = new(); private static readonly AsyncLocal AgentVersionSlot = new(); @@ -13,6 +15,16 @@ public static IDisposable WithConversationId(string conversationId) return new AsyncLocalScope(ConversationIdSlot, conversationId); } + public static IDisposable WithConversationTitle(string conversationTitle) + { + return new AsyncLocalScope(ConversationTitleSlot, conversationTitle); + } + + public static IDisposable WithUserId(string userId) + { + return new AsyncLocalScope(UserIdSlot, userId); + } + public static IDisposable WithAgentName(string agentName) { return new AsyncLocalScope(AgentNameSlot, agentName); @@ -28,6 +40,16 @@ public static IDisposable WithAgentVersion(string agentVersion) return ConversationIdSlot.Value; } + public static string? ConversationTitleFromContext() + { + return ConversationTitleSlot.Value; + } + + public static string? UserIdFromContext() + { + return UserIdSlot.Value; + } + public static string? AgentNameFromContext() { return AgentNameSlot.Value; diff --git a/dotnet/tests/Grafana.Sigil.Tests/ConformanceTests.cs b/dotnet/tests/Grafana.Sigil.Tests/ConformanceTests.cs new file mode 100644 index 0000000..4f8aedf --- /dev/null +++ b/dotnet/tests/Grafana.Sigil.Tests/ConformanceTests.cs @@ -0,0 +1,635 @@ +using System.Collections.Concurrent; +using System.Diagnostics; +using System.Diagnostics.Metrics; +using System.Linq; +using System.Text; +using Xunit; +using SigilProto = Sigil.V1; + +namespace Grafana.Sigil.Tests; + +public sealed class ConformanceTests +{ + [Fact] + public async Task SyncRoundtripSemantics() + { + await using var env = new ConformanceEnv(); + var requestArtifact = Artifact.JsonArtifact(ArtifactKind.Request, "request", new { ok = true }); + var responseArtifact = Artifact.JsonArtifact(ArtifactKind.Response, "response", new { status = "ok" }); + var recorder = env.Client.StartGeneration(new GenerationStart + { + Id = "gen-roundtrip", + ConversationId = "conv-roundtrip", + ConversationTitle = "Roundtrip conversation", + UserId = "user-roundtrip", + AgentName = "agent-roundtrip", + AgentVersion = "v-roundtrip", + Model = new ModelRef { Provider = "openai", Name = "gpt-5" }, + SystemPrompt = "be concise", + MaxTokens = 256, + Temperature = 0.2, + TopP = 0.9, + ToolChoice = "required", + ThinkingEnabled = false, + Tools = + { + new ToolDefinition + { + Name = "weather", + Description = "Get weather", + Type = "function", + InputSchemaJson = Encoding.UTF8.GetBytes("{\"type\":\"object\"}"), + }, + }, + Tags = new Dictionary(StringComparer.Ordinal) { ["tenant"] = "dev" }, + Metadata = new Dictionary(StringComparer.Ordinal) + { + ["trace"] = "roundtrip", + ["sigil.gen_ai.request.thinking.budget_tokens"] = 2048L, + }, + }); + recorder.SetResult(new Generation + { + ResponseId = "resp-roundtrip", + ResponseModel = "gpt-5-2026", + Input = + { + Message.UserTextMessage("hello"), + }, + Output = + { + new Message + { + Role = MessageRole.Assistant, + Parts = + { + Part.ThinkingPart("reasoning"), + Part.ToolCallPart(new ToolCall + { + Id = "call-1", + Name = "weather", + InputJson = Encoding.UTF8.GetBytes("{\"city\":\"Paris\"}"), + }), + Part.TextPart("Checking weather"), + }, + }, + new Message + { + Role = MessageRole.Tool, + Parts = + { + Part.ToolResultPart(new ToolResult + { + ToolCallId = "call-1", + Name = "weather", + Content = "sunny", + ContentJson = Encoding.UTF8.GetBytes("{\"temp_c\":18}"), + }), + }, + }, + }, + Usage = new TokenUsage + { + InputTokens = 12, + OutputTokens = 7, + TotalTokens = 19, + CacheReadInputTokens = 2, + CacheWriteInputTokens = 1, + CacheCreationInputTokens = 3, + ReasoningTokens = 4, + }, + StopReason = "stop", + Tags = new Dictionary(StringComparer.Ordinal) { ["region"] = "eu" }, + Metadata = new Dictionary(StringComparer.Ordinal) { ["result"] = "ok" }, + Artifacts = + { + requestArtifact, + responseArtifact, + }, + }); + recorder.End(); + + await env.ShutdownAsync(); + + var generation = env.SingleGeneration(); + var span = env.GenerationSpan(); + + Assert.Equal(SigilProto.GenerationMode.Sync, generation.Mode); + Assert.Equal("generateText", generation.OperationName); + Assert.Equal("conv-roundtrip", generation.ConversationId); + Assert.Equal("agent-roundtrip", generation.AgentName); + Assert.Equal("agent-roundtrip", recorder.LastGeneration!.AgentName); + Assert.Equal("v-roundtrip", generation.AgentVersion); + Assert.Equal(span.TraceId.ToHexString(), generation.TraceId); + Assert.Equal(span.SpanId.ToHexString(), generation.SpanId); + Assert.Equal("be concise", generation.SystemPrompt); + Assert.Equal("Roundtrip conversation", generation.Metadata.Fields["sigil.conversation.title"].StringValue); + Assert.Equal("user-roundtrip", generation.Metadata.Fields["sigil.user.id"].StringValue); + Assert.Equal("sdk-dotnet", generation.Metadata.Fields["sigil.sdk.name"].StringValue); + Assert.Equal("hello", generation.Input[0].Parts[0].Text); + Assert.Equal("reasoning", generation.Output[0].Parts[0].Thinking); + Assert.Equal("weather", generation.Output[0].Parts[1].ToolCall.Name); + Assert.Equal("Checking weather", generation.Output[0].Parts[2].Text); + Assert.Equal("sunny", generation.Output[1].Parts[0].ToolResult.Content); + Assert.Equal(256L, generation.MaxTokens); + Assert.Equal(0.2, generation.Temperature, 10); + Assert.Equal(0.9, generation.TopP, 10); + Assert.Equal("required", generation.ToolChoice); + Assert.False(generation.ThinkingEnabled); + Assert.Equal(12L, generation.Usage.InputTokens); + Assert.Equal(7L, generation.Usage.OutputTokens); + Assert.Equal(19L, generation.Usage.TotalTokens); + Assert.Equal(2L, generation.Usage.CacheReadInputTokens); + Assert.Equal(1L, generation.Usage.CacheWriteInputTokens); + Assert.Equal(4L, generation.Usage.ReasoningTokens); + Assert.Equal("stop", generation.StopReason); + Assert.Equal("dev", generation.Tags["tenant"]); + Assert.Equal("eu", generation.Tags["region"]); + Assert.Equal(2, generation.RawArtifacts.Count); + Assert.Equal("generateText", span.GetTagItem("gen_ai.operation.name")?.ToString()); + Assert.Equal("Roundtrip conversation", span.GetTagItem("sigil.conversation.title")?.ToString()); + Assert.Equal("user-roundtrip", span.GetTagItem("user.id")?.ToString()); + Assert.Contains("gen_ai.client.operation.duration", env.MetricNames); + Assert.Contains("gen_ai.client.token.usage", env.MetricNames); + Assert.DoesNotContain("gen_ai.client.time_to_first_token", env.MetricNames); + } + + [Theory] + [InlineData("Explicit", "Context", "Meta", "Explicit")] + [InlineData("", "Context", "", "Context")] + [InlineData("", "", "Meta", "Meta")] + [InlineData(" Padded ", "", "", "Padded")] + [InlineData(" ", "", "", "")] + public async Task ConversationTitleSemantics(string startTitle, string contextTitle, string metadataTitle, string expected) + { + await using var env = new ConformanceEnv(); + using var titleScope = contextTitle.Length > 0 ? SigilContext.WithConversationTitle(contextTitle) : NullScope.Instance; + + var start = new GenerationStart + { + Model = new ModelRef { Provider = "openai", Name = "gpt-5" }, + ConversationTitle = startTitle, + }; + if (metadataTitle.Length > 0) + { + start.Metadata["sigil.conversation.title"] = metadataTitle; + } + + var recorder = env.Client.StartGeneration(start); + recorder.SetResult(new Generation()); + recorder.End(); + + await env.ShutdownAsync(); + + var generation = env.SingleGeneration(); + var span = env.GenerationSpan(); + if (expected.Length == 0) + { + Assert.False(generation.Metadata.Fields.ContainsKey("sigil.conversation.title")); + Assert.Null(span.GetTagItem("sigil.conversation.title")); + return; + } + + Assert.Equal(expected, generation.Metadata.Fields["sigil.conversation.title"].StringValue); + Assert.Equal(expected, span.GetTagItem("sigil.conversation.title")?.ToString()); + } + + [Theory] + [InlineData("explicit", "ctx", "canonical", "legacy", "explicit")] + [InlineData("", "ctx", "", "", "ctx")] + [InlineData("", "", "canonical", "", "canonical")] + [InlineData("", "", "", "legacy", "legacy")] + [InlineData("", "", "canonical", "legacy", "canonical")] + [InlineData(" padded ", "", "", "", "padded")] + public async Task UserIdSemantics(string startUserId, string contextUserId, string canonicalUserId, string legacyUserId, string expected) + { + await using var env = new ConformanceEnv(); + using var userScope = contextUserId.Length > 0 ? SigilContext.WithUserId(contextUserId) : NullScope.Instance; + + var start = new GenerationStart + { + Model = new ModelRef { Provider = "openai", Name = "gpt-5" }, + UserId = startUserId, + }; + if (canonicalUserId.Length > 0) + { + start.Metadata["sigil.user.id"] = canonicalUserId; + } + if (legacyUserId.Length > 0) + { + start.Metadata["user.id"] = legacyUserId; + } + + var recorder = env.Client.StartGeneration(start); + recorder.SetResult(new Generation()); + recorder.End(); + + await env.ShutdownAsync(); + + var generation = env.SingleGeneration(); + var span = env.GenerationSpan(); + Assert.Equal(expected, generation.Metadata.Fields["sigil.user.id"].StringValue); + Assert.Equal(expected, span.GetTagItem("user.id")?.ToString()); + } + + [Theory] + [InlineData("agent-explicit", "v1.2.3", "", "", "", "", "agent-explicit", "v1.2.3")] + [InlineData("", "", "agent-context", "v-context", "", "", "agent-context", "v-context")] + [InlineData("agent-seed", "v-seed", "", "", "agent-result", "v-result", "agent-result", "v-result")] + [InlineData("", "", "", "", "", "", "", "")] + public async Task AgentIdentitySemantics( + string startName, + string startVersion, + string contextName, + string contextVersion, + string resultName, + string resultVersion, + string expectedName, + string expectedVersion + ) + { + await using var env = new ConformanceEnv(); + using var agentNameScope = contextName.Length > 0 ? SigilContext.WithAgentName(contextName) : NullScope.Instance; + using var agentVersionScope = contextVersion.Length > 0 ? SigilContext.WithAgentVersion(contextVersion) : NullScope.Instance; + + var recorder = env.Client.StartGeneration(new GenerationStart + { + Model = new ModelRef { Provider = "openai", Name = "gpt-5" }, + AgentName = startName, + AgentVersion = startVersion, + }); + recorder.SetResult(new Generation + { + AgentName = resultName, + AgentVersion = resultVersion, + }); + recorder.End(); + + await env.ShutdownAsync(); + + var generation = env.SingleGeneration(); + var span = env.GenerationSpan(); + Assert.Equal(expectedName, generation.AgentName); + Assert.Equal(expectedVersion, generation.AgentVersion); + Assert.Equal(expectedName.Length == 0 ? null : expectedName, span.GetTagItem("gen_ai.agent.name")?.ToString()); + Assert.Equal(expectedVersion.Length == 0 ? null : expectedVersion, span.GetTagItem("gen_ai.agent.version")?.ToString()); + } + + [Fact] + public async Task StreamingTelemetrySemantics() + { + await using var env = new ConformanceEnv(); + var start = new GenerationStart + { + Model = new ModelRef { Provider = "openai", Name = "gpt-5" }, + StartedAt = new DateTimeOffset(2026, 3, 12, 9, 0, 0, TimeSpan.Zero), + }; + var recorder = env.Client.StartStreamingGeneration(start); + recorder.SetFirstTokenAt(start.StartedAt.Value.AddMilliseconds(250)); + recorder.SetResult(new Generation + { + Usage = new TokenUsage { InputTokens = 4, OutputTokens = 3, TotalTokens = 7 }, + StartedAt = start.StartedAt, + CompletedAt = start.StartedAt.Value.AddSeconds(1), + }); + recorder.End(); + + await env.ShutdownAsync(); + + var generation = env.SingleGeneration(); + var span = env.GenerationSpan(); + Assert.Equal(SigilProto.GenerationMode.Stream, generation.Mode); + Assert.Equal("streamText", generation.OperationName); + Assert.Equal("streamText gpt-5", span.DisplayName); + Assert.Contains("gen_ai.client.operation.duration", env.MetricNames); + Assert.Contains("gen_ai.client.time_to_first_token", env.MetricNames); + } + + [Fact] + public async Task ToolExecutionSemantics() + { + await using var env = new ConformanceEnv(); + using var titleScope = SigilContext.WithConversationTitle("Context title"); + using var agentNameScope = SigilContext.WithAgentName("agent-context"); + using var agentVersionScope = SigilContext.WithAgentVersion("v-context"); + + var recorder = env.Client.StartToolExecution(new ToolExecutionStart + { + ToolName = "weather", + ToolCallId = "call-weather-1", + ToolType = "function", + IncludeContent = true, + }); + recorder.SetResult(new ToolExecutionEnd + { + Arguments = new Dictionary(StringComparer.Ordinal) + { + ["city"] = "Paris", + }, + Result = new Dictionary(StringComparer.Ordinal) + { + ["forecast"] = "sunny", + }, + }); + recorder.End(); + + await env.ShutdownAsync(); + + var span = env.OperationSpan("execute_tool"); + Assert.Empty(env.Ingest.Requests); + Assert.Equal("execute_tool weather", span.DisplayName); + Assert.Equal("execute_tool", span.GetTagItem("gen_ai.operation.name")); + Assert.Equal("weather", span.GetTagItem("gen_ai.tool.name")); + Assert.Equal("call-weather-1", span.GetTagItem("gen_ai.tool.call.id")); + Assert.Equal("function", span.GetTagItem("gen_ai.tool.type")); + Assert.Contains("Paris", span.GetTagItem("gen_ai.tool.call.arguments")?.ToString()); + Assert.Contains("sunny", span.GetTagItem("gen_ai.tool.call.result")?.ToString()); + Assert.Equal("Context title", span.GetTagItem("sigil.conversation.title")?.ToString()); + Assert.Equal("agent-context", span.GetTagItem("gen_ai.agent.name")?.ToString()); + Assert.Equal("v-context", span.GetTagItem("gen_ai.agent.version")?.ToString()); + Assert.Contains("gen_ai.client.operation.duration", env.MetricNames); + Assert.DoesNotContain("gen_ai.client.time_to_first_token", env.MetricNames); + } + + [Fact] + public async Task EmbeddingSemantics() + { + await using var env = new ConformanceEnv(); + using var agentNameScope = SigilContext.WithAgentName("agent-context"); + using var agentVersionScope = SigilContext.WithAgentVersion("v-context"); + + var recorder = env.Client.StartEmbedding(new EmbeddingStart + { + Model = new ModelRef { Provider = "openai", Name = "text-embedding-3-small" }, + Dimensions = 512, + }); + recorder.SetResult(new EmbeddingResult + { + InputCount = 2, + InputTokens = 8, + InputTexts = { "hello", "world" }, + ResponseModel = "text-embedding-3-small", + Dimensions = 512, + }); + recorder.End(); + + await env.ShutdownAsync(); + + var span = env.OperationSpan("embeddings"); + Assert.Empty(env.Ingest.Requests); + Assert.Equal("embeddings text-embedding-3-small", span.DisplayName); + Assert.Equal("embeddings", span.GetTagItem("gen_ai.operation.name")); + Assert.Equal("agent-context", span.GetTagItem("gen_ai.agent.name")); + Assert.Equal("v-context", span.GetTagItem("gen_ai.agent.version")); + Assert.Equal(2, span.GetTagItem("gen_ai.embeddings.input_count")); + Assert.Equal(512L, span.GetTagItem("gen_ai.embeddings.dimension.count")); + Assert.Equal("text-embedding-3-small", span.GetTagItem("gen_ai.response.model")); + Assert.Contains("gen_ai.client.operation.duration", env.MetricNames); + Assert.Contains("gen_ai.client.token.usage", env.MetricNames); + Assert.DoesNotContain("gen_ai.client.time_to_first_token", env.MetricNames); + Assert.DoesNotContain("gen_ai.client.tool_calls_per_operation", env.MetricNames); + } + + [Fact] + public async Task ValidationAndCallErrorSemantics() + { + await using var env = new ConformanceEnv(); + var invalid = env.Client.StartGeneration(new GenerationStart + { + Model = new ModelRef { Provider = "anthropic", Name = "claude-sonnet-4-5" }, + }); + invalid.SetResult(new Generation + { + Input = + { + new Message + { + Role = MessageRole.User, + Parts = + { + Part.ToolCallPart(new ToolCall { Name = "weather" }), + }, + }, + }, + }); + invalid.End(); + + Assert.NotNull(invalid.Error); + Assert.Empty(env.Ingest.Requests); + Assert.Equal("validation_error", env.GenerationSpan().GetTagItem("error.type")?.ToString()); + + var callError = env.Client.StartGeneration(new GenerationStart + { + Model = new ModelRef { Provider = "openai", Name = "gpt-5" }, + }); + callError.SetCallError(new InvalidOperationException("provider unavailable")); + callError.SetResult(new Generation()); + callError.End(); + + await env.ShutdownAsync(); + + var generation = env.SingleGeneration(); + var spans = env.Spans.ToArray(); + Assert.Null(callError.Error); + Assert.Equal("provider unavailable", generation.CallError); + Assert.Equal("provider unavailable", generation.Metadata.Fields["call_error"].StringValue); + Assert.Equal("provider_call_error", spans[^1].GetTagItem("error.type")?.ToString()); + } + + [Fact] + public async Task RatingSubmissionSemantics() + { + await using var env = new ConformanceEnv(); + var response = await env.Client.SubmitConversationRatingAsync( + "conv-rating", + new SubmitConversationRatingRequest + { + RatingId = "rat-1", + Rating = ConversationRatingValue.Bad, + Comment = "wrong answer", + Metadata = new Dictionary(StringComparer.Ordinal) + { + ["channel"] = "assistant", + }, + } + ); + + await env.ShutdownAsync(); + + Assert.True(env.Rating.Requests.TryDequeue(out var captured)); + Assert.Equal("/api/v1/conversations/conv-rating/ratings", captured.Path); + Assert.Equal("rat-1", response.Rating.RatingId); + Assert.True(response.Summary.HasBadRating); + + using var body = System.Text.Json.JsonDocument.Parse(captured.Body); + Assert.Equal("rat-1", body.RootElement.GetProperty("rating_id").GetString()); + Assert.Equal("CONVERSATION_RATING_VALUE_BAD", body.RootElement.GetProperty("rating").GetString()); + Assert.Equal("wrong answer", body.RootElement.GetProperty("comment").GetString()); + } + + [Fact] + public async Task ShutdownFlushSemantics() + { + await using var env = new ConformanceEnv(batchSize: 10); + var recorder = env.Client.StartGeneration(new GenerationStart + { + ConversationId = "conv-shutdown", + AgentName = "agent-shutdown", + AgentVersion = "v-shutdown", + Model = new ModelRef { Provider = "openai", Name = "gpt-5" }, + }); + recorder.SetResult(new Generation()); + recorder.End(); + + Assert.Empty(env.Ingest.Requests); + await env.ShutdownAsync(); + + var generation = env.SingleGeneration(); + Assert.Equal("conv-shutdown", generation.ConversationId); + Assert.Equal("agent-shutdown", generation.AgentName); + Assert.Equal("v-shutdown", generation.AgentVersion); + } + + private sealed class ConformanceEnv : IAsyncDisposable + { + private bool _shutdown; + private readonly MeterListener _meterListener; + private readonly ActivityListener _activityListener; + + public GrpcIngestServer Ingest { get; } + public RatingCaptureServer Rating { get; } + public SigilClient Client { get; } + public ConcurrentQueue Spans { get; } = new(); + public ConcurrentDictionary MetricNames { get; } = new(StringComparer.Ordinal); + + public ConformanceEnv(int batchSize = 1) + { + _activityListener = new ActivityListener + { + ShouldListenTo = source => source.Name == "github.com/grafana/sigil/sdks/dotnet", + Sample = static (ref ActivityCreationOptions _) => ActivitySamplingResult.AllDataAndRecorded, + ActivityStopped = activity => + { + Spans.Enqueue(activity); + }, + }; + ActivitySource.AddActivityListener(_activityListener); + + _meterListener = new MeterListener(); + _meterListener.InstrumentPublished += (instrument, listener) => + { + if (instrument.Name.StartsWith("gen_ai.client.", StringComparison.Ordinal)) + { + listener.EnableMeasurementEvents(instrument); + } + }; + _meterListener.SetMeasurementEventCallback((instrument, _, _, _) => + { + MetricNames[instrument.Name] = 0; + }); + _meterListener.Start(); + + Ingest = new GrpcIngestServer(); + Rating = new RatingCaptureServer((_, _, _) => + ( + 200, + "application/json", + Encoding.UTF8.GetBytes( + """ + { + "rating":{ + "rating_id":"rat-1", + "conversation_id":"conv-rating", + "rating":"CONVERSATION_RATING_VALUE_BAD", + "created_at":"2026-03-12T09:00:00Z" + }, + "summary":{ + "total_count":1, + "good_count":0, + "bad_count":1, + "latest_rating":"CONVERSATION_RATING_VALUE_BAD", + "latest_rated_at":"2026-03-12T09:00:00Z", + "has_bad_rating":true + } + } + """ + ) + ) + ); + Client = new SigilClient(new SigilClientConfig + { + Api = new ApiConfig + { + Endpoint = $"http://127.0.0.1:{Rating.Port}", + }, + GenerationExport = new GenerationExportConfig + { + Protocol = GenerationExportProtocol.Grpc, + Endpoint = $"127.0.0.1:{Ingest.Port}", + Insecure = true, + BatchSize = batchSize, + QueueSize = 10, + FlushInterval = TimeSpan.FromHours(1), + MaxRetries = 1, + InitialBackoff = TimeSpan.FromMilliseconds(1), + MaxBackoff = TimeSpan.FromMilliseconds(2), + }, + }); + } + + public async Task ShutdownAsync() + { + if (_shutdown) + { + return; + } + + _shutdown = true; + await Client.ShutdownAsync(); + _meterListener.Dispose(); + _activityListener.Dispose(); + Ingest.Dispose(); + Rating.Dispose(); + } + + public SigilProto.Generation SingleGeneration() + { + Assert.Single(Ingest.Requests); + Assert.Single(Ingest.Requests[0].Request.Generations); + return Ingest.Requests[0].Request.Generations[0]; + } + + public Activity GenerationSpan() + { + return OperationSpan(new[] { "generateText", "streamText" }); + } + + public Activity OperationSpan(string operationName) + { + return OperationSpan(new[] { operationName }); + } + + private Activity OperationSpan(string[] operationNames) + { + var span = Spans + .Where(activity => operationNames.Contains(activity.GetTagItem("gen_ai.operation.name")?.ToString(), StringComparer.Ordinal)) + .LastOrDefault(); + Assert.NotNull(span); + return span!; + } + + public async ValueTask DisposeAsync() + { + await ShutdownAsync(); + } + } + + private sealed class NullScope : IDisposable + { + public static NullScope Instance { get; } = new(); + + public void Dispose() + { + } + } +} diff --git a/go/README.md b/go/README.md index 27e9ed3..8410b98 100644 --- a/go/README.md +++ b/go/README.md @@ -206,7 +206,7 @@ The Go SDK ships a local no-Docker conformance harness for the current cross-SDK - Shared spec: `../../docs/references/sdk-conformance-spec.md` - Default local command: `mise run test:sdk:conformance` - Direct Go command: `cd sdks/go && GOWORK=off go test ./sigil -run '^TestConformance' -count=1` -- Current baseline coverage: conversation title resolution, user ID resolution, agent name/version resolution, streaming mode + TTFT, tool execution, embeddings, validation/error handling, rating submission, and shutdown flush semantics across exported generation payloads, OTLP spans, OTLP metrics, and local rating HTTP capture +- Current baseline coverage: sync roundtrip, conversation title resolution, user ID resolution, agent name/version resolution, streaming mode + TTFT, tool execution, embeddings, validation/error handling, rating submission, and shutdown flush semantics across exported generation payloads, OTLP spans, OTLP metrics, and local rating HTTP capture ## Explicit flow example diff --git a/java/core/src/main/java/com/grafana/sigil/sdk/Generation.java b/java/core/src/main/java/com/grafana/sigil/sdk/Generation.java index f74c79a..25f4703 100644 --- a/java/core/src/main/java/com/grafana/sigil/sdk/Generation.java +++ b/java/core/src/main/java/com/grafana/sigil/sdk/Generation.java @@ -37,6 +37,18 @@ public Generation setConversationId(String conversationId) { return this; } + @Override + public Generation setConversationTitle(String conversationTitle) { + super.setConversationTitle(conversationTitle); + return this; + } + + @Override + public Generation setUserId(String userId) { + super.setUserId(userId); + return this; + } + @Override public Generation setAgentName(String agentName) { super.setAgentName(agentName); @@ -101,6 +113,8 @@ public Generation copy() { Generation out = new Generation(); out.setId(getId()); out.setConversationId(getConversationId()); + out.setConversationTitle(getConversationTitle()); + out.setUserId(getUserId()); out.setAgentName(getAgentName()); out.setAgentVersion(getAgentVersion()); out.setMode(getMode()); diff --git a/java/core/src/main/java/com/grafana/sigil/sdk/GenerationRecorder.java b/java/core/src/main/java/com/grafana/sigil/sdk/GenerationRecorder.java index 8355cdd..6d1425a 100644 --- a/java/core/src/main/java/com/grafana/sigil/sdk/GenerationRecorder.java +++ b/java/core/src/main/java/com/grafana/sigil/sdk/GenerationRecorder.java @@ -168,6 +168,8 @@ private Generation normalize(GenerationResult result, Instant completedAt, Throw generation.setId(firstNonBlank(result.getId(), seed.getId(), SigilClient.newID("gen"))); generation.setConversationId(firstNonBlank(result.getConversationId(), seed.getConversationId())); + generation.setConversationTitle(firstNonBlank(result.getConversationTitle(), seed.getConversationTitle())); + generation.setUserId(firstNonBlank(result.getUserId(), seed.getUserId())); generation.setAgentName(firstNonBlank(result.getAgentName(), seed.getAgentName())); generation.setAgentVersion(firstNonBlank(result.getAgentVersion(), seed.getAgentVersion())); @@ -224,6 +226,23 @@ private Generation normalize(GenerationResult result, Instant completedAt, Throw metadata.putAll(result.getMetadata()); generation.setMetadata(metadata); + generation.setConversationTitle(firstNonBlank( + generation.getConversationTitle(), + SigilClient.metadataString(generation.getMetadata(), SigilClient.SPAN_ATTR_CONVERSATION_TITLE))); + generation.setConversationTitle(normalizeResolvedString(generation.getConversationTitle())); + if (!generation.getConversationTitle().isBlank()) { + generation.getMetadata().put(SigilClient.SPAN_ATTR_CONVERSATION_TITLE, generation.getConversationTitle()); + } + + generation.setUserId(firstNonBlank( + generation.getUserId(), + SigilClient.metadataString(generation.getMetadata(), SigilClient.METADATA_USER_ID_KEY), + SigilClient.metadataString(generation.getMetadata(), SigilClient.METADATA_LEGACY_USER_ID_KEY))); + generation.setUserId(normalizeResolvedString(generation.getUserId())); + if (!generation.getUserId().isBlank()) { + generation.getMetadata().put(SigilClient.METADATA_USER_ID_KEY, generation.getUserId()); + } + for (Artifact artifact : result.getArtifacts()) { generation.getArtifacts().add(artifact == null ? new Artifact() : artifact.copy()); } @@ -247,4 +266,8 @@ private static String firstNonBlank(String... values) { } return ""; } + + private static String normalizeResolvedString(String value) { + return value == null ? "" : value.trim(); + } } diff --git a/java/core/src/main/java/com/grafana/sigil/sdk/GenerationResult.java b/java/core/src/main/java/com/grafana/sigil/sdk/GenerationResult.java index 7d18c5d..d4a4b65 100644 --- a/java/core/src/main/java/com/grafana/sigil/sdk/GenerationResult.java +++ b/java/core/src/main/java/com/grafana/sigil/sdk/GenerationResult.java @@ -10,6 +10,8 @@ public class GenerationResult { private String id = ""; private String conversationId = ""; + private String conversationTitle = ""; + private String userId = ""; private String agentName = ""; private String agentVersion = ""; private GenerationMode mode; @@ -53,6 +55,24 @@ public GenerationResult setConversationId(String conversationId) { return this; } + public String getConversationTitle() { + return conversationTitle; + } + + public GenerationResult setConversationTitle(String conversationTitle) { + this.conversationTitle = conversationTitle == null ? "" : conversationTitle; + return this; + } + + public String getUserId() { + return userId; + } + + public GenerationResult setUserId(String userId) { + this.userId = userId == null ? "" : userId; + return this; + } + public String getAgentName() { return agentName; } @@ -291,6 +311,8 @@ public GenerationResult copy() { GenerationResult out = new GenerationResult() .setId(id) .setConversationId(conversationId) + .setConversationTitle(conversationTitle) + .setUserId(userId) .setAgentName(agentName) .setAgentVersion(agentVersion) .setMode(mode) diff --git a/java/core/src/main/java/com/grafana/sigil/sdk/GenerationStart.java b/java/core/src/main/java/com/grafana/sigil/sdk/GenerationStart.java index 2672961..b3ac3bc 100644 --- a/java/core/src/main/java/com/grafana/sigil/sdk/GenerationStart.java +++ b/java/core/src/main/java/com/grafana/sigil/sdk/GenerationStart.java @@ -10,6 +10,8 @@ public final class GenerationStart { private String id = ""; private String conversationId = ""; + private String conversationTitle = ""; + private String userId = ""; private String agentName = ""; private String agentVersion = ""; private GenerationMode mode; @@ -44,6 +46,24 @@ public GenerationStart setConversationId(String conversationId) { return this; } + public String getConversationTitle() { + return conversationTitle; + } + + public GenerationStart setConversationTitle(String conversationTitle) { + this.conversationTitle = conversationTitle == null ? "" : conversationTitle; + return this; + } + + public String getUserId() { + return userId; + } + + public GenerationStart setUserId(String userId) { + this.userId = userId == null ? "" : userId; + return this; + } + public String getAgentName() { return agentName; } @@ -192,6 +212,8 @@ public GenerationStart copy() { GenerationStart out = new GenerationStart() .setId(id) .setConversationId(conversationId) + .setConversationTitle(conversationTitle) + .setUserId(userId) .setAgentName(agentName) .setAgentVersion(agentVersion) .setMode(mode) diff --git a/java/core/src/main/java/com/grafana/sigil/sdk/SigilClient.java b/java/core/src/main/java/com/grafana/sigil/sdk/SigilClient.java index 9126153..9d34d35 100644 --- a/java/core/src/main/java/com/grafana/sigil/sdk/SigilClient.java +++ b/java/core/src/main/java/com/grafana/sigil/sdk/SigilClient.java @@ -42,6 +42,8 @@ public final class SigilClient implements AutoCloseable { static final String SPAN_ATTR_GENERATION_ID = "sigil.generation.id"; static final String SPAN_ATTR_SDK_NAME = "sigil.sdk.name"; static final String SPAN_ATTR_CONVERSATION_ID = "gen_ai.conversation.id"; + static final String SPAN_ATTR_CONVERSATION_TITLE = "sigil.conversation.title"; + static final String SPAN_ATTR_USER_ID = "user.id"; static final String SPAN_ATTR_AGENT_NAME = "gen_ai.agent.name"; static final String SPAN_ATTR_AGENT_VERSION = "gen_ai.agent.version"; static final String SPAN_ATTR_ERROR_TYPE = "error.type"; @@ -98,6 +100,8 @@ public final class SigilClient implements AutoCloseable { private static final String INSTRUMENTATION_NAME = "github.com/grafana/sigil/sdks/java"; static final String DEFAULT_EMBEDDING_OPERATION_NAME = "embeddings"; static final String SDK_NAME = "sdk-java"; + static final String METADATA_USER_ID_KEY = "sigil.user.id"; + static final String METADATA_LEGACY_USER_ID_KEY = "user.id"; private final SigilClientConfig config; private final GenerationExporter generationExporter; @@ -301,6 +305,9 @@ public ToolExecutionRecorder startToolExecution(ToolExecutionStart start) { if (seed.getConversationId().isBlank()) { seed.setConversationId(SigilContext.conversationIdFromContext()); } + if (seed.getConversationTitle().isBlank()) { + seed.setConversationTitle(SigilContext.conversationTitleFromContext()); + } if (seed.getAgentName().isBlank()) { seed.setAgentName(SigilContext.agentNameFromContext()); } @@ -535,6 +542,12 @@ private GenerationRecorder startGenerationInternal(GenerationStart start, Genera if (seed.getConversationId().isBlank()) { seed.setConversationId(SigilContext.conversationIdFromContext()); } + if (seed.getConversationTitle().isBlank()) { + seed.setConversationTitle(SigilContext.conversationTitleFromContext()); + } + if (seed.getUserId().isBlank()) { + seed.setUserId(SigilContext.userIdFromContext()); + } if (seed.getAgentName().isBlank()) { seed.setAgentName(SigilContext.agentNameFromContext()); } @@ -553,6 +566,8 @@ private GenerationRecorder startGenerationInternal(GenerationStart start, Genera Generation initial = new Generation(); initial.setId(seed.getId()); initial.setConversationId(seed.getConversationId()); + initial.setConversationTitle(seed.getConversationTitle()); + initial.setUserId(seed.getUserId()); initial.setAgentName(seed.getAgentName()); initial.setAgentVersion(seed.getAgentVersion()); initial.setMode(seed.getMode()); @@ -955,6 +970,12 @@ static void setGenerationSpanAttributes(Span span, Generation generation) { if (!generation.getConversationId().isBlank()) { span.setAttribute(SPAN_ATTR_CONVERSATION_ID, generation.getConversationId()); } + if (!generation.getConversationTitle().isBlank()) { + span.setAttribute(SPAN_ATTR_CONVERSATION_TITLE, generation.getConversationTitle()); + } + if (!generation.getUserId().isBlank()) { + span.setAttribute(SPAN_ATTR_USER_ID, generation.getUserId()); + } if (!generation.getAgentName().isBlank()) { span.setAttribute(SPAN_ATTR_AGENT_NAME, generation.getAgentName()); } @@ -1014,6 +1035,9 @@ static void setToolSpanAttributes(Span span, ToolExecutionStart seed) { if (!seed.getConversationId().isBlank()) { span.setAttribute(SPAN_ATTR_CONVERSATION_ID, seed.getConversationId()); } + if (!seed.getConversationTitle().isBlank()) { + span.setAttribute(SPAN_ATTR_CONVERSATION_TITLE, seed.getConversationTitle()); + } if (!seed.getAgentName().isBlank()) { span.setAttribute(SPAN_ATTR_AGENT_NAME, seed.getAgentName()); } @@ -1330,6 +1354,17 @@ private static Long thinkingBudgetFromMetadata(Map metadata) { return null; } + static String metadataString(Map metadata, String key) { + if (metadata == null) { + return ""; + } + Object value = metadata.get(key); + if (value == null) { + return ""; + } + return String.valueOf(value).trim(); + } + private static EmbeddingCaptureConfig normalizeEmbeddingCaptureConfig(EmbeddingCaptureConfig input) { EmbeddingCaptureConfig config = input == null ? new EmbeddingCaptureConfig() : input.copy(); if (config.getMaxInputItems() <= 0) { diff --git a/java/core/src/main/java/com/grafana/sigil/sdk/SigilContext.java b/java/core/src/main/java/com/grafana/sigil/sdk/SigilContext.java index 2b84adf..f8cefaf 100644 --- a/java/core/src/main/java/com/grafana/sigil/sdk/SigilContext.java +++ b/java/core/src/main/java/com/grafana/sigil/sdk/SigilContext.java @@ -7,6 +7,8 @@ /** Context helpers for conversation and agent defaults. */ public final class SigilContext { private static final ContextKey CONVERSATION_ID = ContextKey.named("sigil.conversation.id"); + private static final ContextKey CONVERSATION_TITLE = ContextKey.named("sigil.conversation.title"); + private static final ContextKey USER_ID = ContextKey.named("sigil.user.id"); private static final ContextKey AGENT_NAME = ContextKey.named("sigil.agent.name"); private static final ContextKey AGENT_VERSION = ContextKey.named("sigil.agent.version"); @@ -22,6 +24,24 @@ public static Scope withConversationId(String conversationId) { return Context.current().with(CONVERSATION_ID, emptyToBlank(conversationId)).makeCurrent(); } + /** + * Sets the conversation title in the current OTel context. + * + *

Use the returned {@link Scope} in try-with-resources to restore context automatically.

+ */ + public static Scope withConversationTitle(String conversationTitle) { + return Context.current().with(CONVERSATION_TITLE, emptyToBlank(conversationTitle)).makeCurrent(); + } + + /** + * Sets the user id in the current OTel context. + * + *

Use the returned {@link Scope} in try-with-resources to restore context automatically.

+ */ + public static Scope withUserId(String userId) { + return Context.current().with(USER_ID, emptyToBlank(userId)).makeCurrent(); + } + /** * Sets the agent name in the current OTel context. * @@ -45,6 +65,16 @@ static String conversationIdFromContext() { return value == null ? "" : value; } + static String conversationTitleFromContext() { + String value = Context.current().get(CONVERSATION_TITLE); + return value == null ? "" : value; + } + + static String userIdFromContext() { + String value = Context.current().get(USER_ID); + return value == null ? "" : value; + } + static String agentNameFromContext() { String value = Context.current().get(AGENT_NAME); return value == null ? "" : value; diff --git a/java/core/src/main/java/com/grafana/sigil/sdk/ToolExecutionStart.java b/java/core/src/main/java/com/grafana/sigil/sdk/ToolExecutionStart.java index a1769a2..fd3dd77 100644 --- a/java/core/src/main/java/com/grafana/sigil/sdk/ToolExecutionStart.java +++ b/java/core/src/main/java/com/grafana/sigil/sdk/ToolExecutionStart.java @@ -9,6 +9,7 @@ public final class ToolExecutionStart { private String toolType = ""; private String toolDescription = ""; private String conversationId = ""; + private String conversationTitle = ""; private String agentName = ""; private String agentVersion = ""; private boolean includeContent; @@ -59,6 +60,15 @@ public ToolExecutionStart setConversationId(String conversationId) { return this; } + public String getConversationTitle() { + return conversationTitle; + } + + public ToolExecutionStart setConversationTitle(String conversationTitle) { + this.conversationTitle = conversationTitle == null ? "" : conversationTitle; + return this; + } + public String getAgentName() { return agentName; } @@ -102,6 +112,7 @@ public ToolExecutionStart copy() { .setToolType(toolType) .setToolDescription(toolDescription) .setConversationId(conversationId) + .setConversationTitle(conversationTitle) .setAgentName(agentName) .setAgentVersion(agentVersion) .setIncludeContent(includeContent) diff --git a/java/core/src/test/java/com/grafana/sigil/sdk/ConformanceTest.java b/java/core/src/test/java/com/grafana/sigil/sdk/ConformanceTest.java new file mode 100644 index 0000000..eb4161c --- /dev/null +++ b/java/core/src/test/java/com/grafana/sigil/sdk/ConformanceTest.java @@ -0,0 +1,602 @@ +package com.grafana.sigil.sdk; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.fasterxml.jackson.databind.JsonNode; +import com.sun.net.httpserver.HttpServer; +import io.grpc.Server; +import io.grpc.ServerBuilder; +import io.grpc.stub.StreamObserver; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.context.Scope; +import io.opentelemetry.sdk.metrics.SdkMeterProvider; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.testing.exporter.InMemoryMetricReader; +import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import sigil.v1.GenerationIngest; +import sigil.v1.GenerationIngestServiceGrpc; + +class ConformanceTest { + @Test + void syncRoundtripSemantics() throws Exception { + try (ConformanceEnv env = new ConformanceEnv(1)) { + GenerationStart start = new GenerationStart() + .setId("gen-roundtrip") + .setConversationId("conv-roundtrip") + .setConversationTitle("Roundtrip conversation") + .setUserId("user-roundtrip") + .setAgentName("agent-roundtrip") + .setAgentVersion("v-roundtrip") + .setModel(new ModelRef().setProvider("openai").setName("gpt-5")) + .setMaxTokens(256L) + .setTemperature(0.2) + .setTopP(0.9) + .setToolChoice("required") + .setThinkingEnabled(false); + start.getTools().add(new ToolDefinition() + .setName("weather") + .setDescription("Get weather") + .setType("function")); + start.getTags().put("tenant", "dev"); + start.getMetadata().put("trace", "roundtrip"); + + GenerationRecorder recorder = env.client.startGeneration(start); + + GenerationResult result = new GenerationResult() + .setResponseId("resp-roundtrip") + .setResponseModel("gpt-5-2026") + .setUsage(new TokenUsage() + .setInputTokens(12) + .setOutputTokens(7) + .setTotalTokens(19) + .setCacheReadInputTokens(2) + .setCacheWriteInputTokens(1) + .setCacheCreationInputTokens(3) + .setReasoningTokens(4)) + .setStopReason("stop"); + result.getTags().put("region", "eu"); + result.getMetadata().put("result", "ok"); + result.getInput().add(new Message() + .setRole(MessageRole.USER) + .setParts(List.of(MessagePart.text("hello")))); + result.getOutput().add(new Message() + .setRole(MessageRole.ASSISTANT) + .setParts(List.of( + MessagePart.thinking("reasoning"), + MessagePart.toolCall(new ToolCall() + .setId("call-1") + .setName("weather") + .setInputJson("{\"city\":\"Paris\"}".getBytes(StandardCharsets.UTF_8)))))); + result.getOutput().add(new Message() + .setRole(MessageRole.TOOL) + .setParts(List.of( + MessagePart.toolResult(new ToolResultPart() + .setToolCallId("call-1") + .setName("weather") + .setContent("sunny") + .setContentJson("{\"temp_c\":18}".getBytes(StandardCharsets.UTF_8)))))); + result.getArtifacts().add(new Artifact() + .setKind(ArtifactKind.REQUEST) + .setName("request") + .setContentType("application/json") + .setPayload("{\"prompt\":\"hello\"}".getBytes(StandardCharsets.UTF_8))); + result.getArtifacts().add(new Artifact() + .setKind(ArtifactKind.RESPONSE) + .setName("response") + .setContentType("application/json") + .setPayload("{\"text\":\"sunny\"}".getBytes(StandardCharsets.UTF_8))); + + recorder.setResult(result); + recorder.end(); + env.client.shutdown(); + + GenerationIngest.Generation generation = env.singleGeneration(); + SpanData span = env.latestGenerationSpan(); + List metricNames = env.metricNames(); + + assertThat(generation.getMode()).isEqualTo(GenerationIngest.GenerationMode.GENERATION_MODE_SYNC); + assertThat(generation.getOperationName()).isEqualTo("generateText"); + assertThat(generation.getConversationId()).isEqualTo("conv-roundtrip"); + assertThat(generation.getAgentName()).isEqualTo("agent-roundtrip"); + assertThat(generation.getAgentVersion()).isEqualTo("v-roundtrip"); + assertThat(generation.getTraceId()).isEqualTo(span.getTraceId()); + assertThat(generation.getSpanId()).isEqualTo(span.getSpanId()); + assertThat(generation.getMetadata().getFieldsMap().get("sigil.conversation.title").getStringValue()) + .isEqualTo("Roundtrip conversation"); + assertThat(generation.getMetadata().getFieldsMap().get("sigil.user.id").getStringValue()) + .isEqualTo("user-roundtrip"); + assertThat(generation.getInput(0).getParts(0).getText()).isEqualTo("hello"); + assertThat(generation.getOutput(0).getParts(0).getThinking()).isEqualTo("reasoning"); + assertThat(generation.getOutput(0).getParts(1).getToolCall().getName()).isEqualTo("weather"); + assertThat(generation.getOutput(1).getParts(0).getToolResult().getContent()).isEqualTo("sunny"); + assertThat(generation.getMaxTokens()).isEqualTo(256L); + assertThat(generation.getTemperature()).isEqualTo(0.2d); + assertThat(generation.getTopP()).isEqualTo(0.9d); + assertThat(generation.getToolChoice()).isEqualTo("required"); + assertThat(generation.getThinkingEnabled()).isFalse(); + assertThat(generation.getUsage().getInputTokens()).isEqualTo(12L); + assertThat(generation.getUsage().getOutputTokens()).isEqualTo(7L); + assertThat(generation.getUsage().getTotalTokens()).isEqualTo(19L); + assertThat(generation.getUsage().getCacheReadInputTokens()).isEqualTo(2L); + assertThat(generation.getUsage().getCacheWriteInputTokens()).isEqualTo(1L); + assertThat(generation.getUsage().getReasoningTokens()).isEqualTo(4L); + assertThat(generation.getStopReason()).isEqualTo("stop"); + assertThat(generation.getTagsMap()).containsEntry("tenant", "dev").containsEntry("region", "eu"); + assertThat(generation.getRawArtifactsCount()).isEqualTo(2); + assertThat(span.getAttributes().get(AttributeKey.stringKey(SigilClient.SPAN_ATTR_OPERATION_NAME))).isEqualTo("generateText"); + assertThat(span.getAttributes().get(AttributeKey.stringKey(SigilClient.SPAN_ATTR_CONVERSATION_TITLE))) + .isEqualTo("Roundtrip conversation"); + assertThat(span.getAttributes().get(AttributeKey.stringKey(SigilClient.SPAN_ATTR_USER_ID))) + .isEqualTo("user-roundtrip"); + assertThat(metricNames).contains(SigilClient.METRIC_OPERATION_DURATION, SigilClient.METRIC_TOKEN_USAGE); + assertThat(metricNames).doesNotContain(SigilClient.METRIC_TTFT); + } + } + + @ParameterizedTest + @MethodSource("conversationTitleCases") + void conversationTitleSemantics(String startTitle, String contextTitle, String metadataTitle, String expected) throws Exception { + try (ConformanceEnv env = new ConformanceEnv(1); + Scope ignored = contextTitle.isEmpty() ? noopScope() : SigilContext.withConversationTitle(contextTitle)) { + GenerationStart start = new GenerationStart() + .setModel(new ModelRef().setProvider("openai").setName("gpt-5")) + .setConversationTitle(startTitle); + if (!metadataTitle.isEmpty()) { + start.getMetadata().put(SigilClient.SPAN_ATTR_CONVERSATION_TITLE, metadataTitle); + } + + GenerationRecorder recorder = env.client.startGeneration(start); + recorder.setResult(new GenerationResult()); + recorder.end(); + env.client.shutdown(); + + GenerationIngest.Generation generation = env.singleGeneration(); + SpanData span = env.latestGenerationSpan(); + if (expected.isEmpty()) { + assertThat(generation.getMetadata().getFieldsMap()).doesNotContainKey(SigilClient.SPAN_ATTR_CONVERSATION_TITLE); + assertThat(span.getAttributes().get(AttributeKey.stringKey(SigilClient.SPAN_ATTR_CONVERSATION_TITLE))).isNull(); + return; + } + + assertThat(generation.getMetadata().getFieldsMap().get(SigilClient.SPAN_ATTR_CONVERSATION_TITLE).getStringValue()) + .isEqualTo(expected); + assertThat(span.getAttributes().get(AttributeKey.stringKey(SigilClient.SPAN_ATTR_CONVERSATION_TITLE))) + .isEqualTo(expected); + } + } + + @ParameterizedTest + @MethodSource("userIdCases") + void userIdSemantics(String startUserId, String contextUserId, String canonicalUserId, String legacyUserId, String expected) + throws Exception { + try (ConformanceEnv env = new ConformanceEnv(1); + Scope ignored = contextUserId.isEmpty() ? noopScope() : SigilContext.withUserId(contextUserId)) { + GenerationStart start = new GenerationStart() + .setModel(new ModelRef().setProvider("openai").setName("gpt-5")) + .setUserId(startUserId); + if (!canonicalUserId.isEmpty()) { + start.getMetadata().put(SigilClient.METADATA_USER_ID_KEY, canonicalUserId); + } + if (!legacyUserId.isEmpty()) { + start.getMetadata().put(SigilClient.METADATA_LEGACY_USER_ID_KEY, legacyUserId); + } + + GenerationRecorder recorder = env.client.startGeneration(start); + recorder.setResult(new GenerationResult()); + recorder.end(); + env.client.shutdown(); + + GenerationIngest.Generation generation = env.singleGeneration(); + SpanData span = env.latestGenerationSpan(); + assertThat(generation.getMetadata().getFieldsMap().get(SigilClient.METADATA_USER_ID_KEY).getStringValue()) + .isEqualTo(expected); + assertThat(span.getAttributes().get(AttributeKey.stringKey(SigilClient.SPAN_ATTR_USER_ID))).isEqualTo(expected); + } + } + + @ParameterizedTest + @MethodSource("agentIdentityCases") + void agentIdentitySemantics( + String startName, + String startVersion, + String contextName, + String contextVersion, + String resultName, + String resultVersion, + String expectedName, + String expectedVersion) + throws Exception { + try (ConformanceEnv env = new ConformanceEnv(1); + Scope ignoredName = contextName.isEmpty() ? noopScope() : SigilContext.withAgentName(contextName); + Scope ignoredVersion = contextVersion.isEmpty() ? noopScope() : SigilContext.withAgentVersion(contextVersion)) { + GenerationRecorder recorder = env.client.startGeneration(new GenerationStart() + .setModel(new ModelRef().setProvider("openai").setName("gpt-5")) + .setAgentName(startName) + .setAgentVersion(startVersion)); + recorder.setResult(new GenerationResult() + .setAgentName(resultName) + .setAgentVersion(resultVersion)); + recorder.end(); + env.client.shutdown(); + + GenerationIngest.Generation generation = env.singleGeneration(); + SpanData span = env.latestGenerationSpan(); + assertThat(generation.getAgentName()).isEqualTo(expectedName); + assertThat(generation.getAgentVersion()).isEqualTo(expectedVersion); + assertThat(span.getAttributes().get(AttributeKey.stringKey(SigilClient.SPAN_ATTR_AGENT_NAME))) + .isEqualTo(expectedName.isEmpty() ? null : expectedName); + assertThat(span.getAttributes().get(AttributeKey.stringKey(SigilClient.SPAN_ATTR_AGENT_VERSION))) + .isEqualTo(expectedVersion.isEmpty() ? null : expectedVersion); + } + } + + @Test + void streamingTelemetrySemantics() throws Exception { + try (ConformanceEnv env = new ConformanceEnv(1)) { + Instant startedAt = Instant.parse("2026-03-12T09:00:00Z"); + GenerationRecorder recorder = env.client.startStreamingGeneration(new GenerationStart() + .setModel(new ModelRef().setProvider("openai").setName("gpt-5")) + .setStartedAt(startedAt)); + recorder.setFirstTokenAt(startedAt.plusMillis(250)); + recorder.setResult(new GenerationResult() + .setStartedAt(startedAt) + .setCompletedAt(startedAt.plusSeconds(1)) + .setUsage(new TokenUsage().setInputTokens(4).setOutputTokens(3).setTotalTokens(7))); + recorder.end(); + env.client.shutdown(); + + GenerationIngest.Generation generation = env.singleGeneration(); + SpanData span = env.latestGenerationSpan(); + List metricNames = env.metricNames(); + + assertThat(generation.getMode()).isEqualTo(GenerationIngest.GenerationMode.GENERATION_MODE_STREAM); + assertThat(generation.getOperationName()).isEqualTo("streamText"); + assertThat(span.getName()).isEqualTo("streamText gpt-5"); + assertThat(metricNames).contains(SigilClient.METRIC_OPERATION_DURATION, SigilClient.METRIC_TTFT); + } + } + + @Test + void toolExecutionSemantics() throws Exception { + try (ConformanceEnv env = new ConformanceEnv(1); + Scope ignoredTitle = SigilContext.withConversationTitle("Context title"); + Scope ignoredName = SigilContext.withAgentName("agent-context"); + Scope ignoredVersion = SigilContext.withAgentVersion("v-context")) { + ToolExecutionRecorder recorder = env.client.startToolExecution(new ToolExecutionStart() + .setToolName("weather") + .setToolCallId("call-weather-1") + .setToolType("function") + .setIncludeContent(true)); + recorder.setResult(new ToolExecutionResult() + .setArguments(Map.of("city", "Paris")) + .setResult(Map.of("forecast", "sunny"))); + recorder.end(); + env.client.shutdown(); + + SpanData span = env.latestSpanByNamePrefix("execute_tool "); + List metricNames = env.metricNames(); + + assertThat(env.requests).isEmpty(); + assertThat(span.getName()).isEqualTo("execute_tool weather"); + assertThat(span.getAttributes().get(AttributeKey.stringKey(SigilClient.SPAN_ATTR_TOOL_NAME))).isEqualTo("weather"); + assertThat(span.getAttributes().get(AttributeKey.stringKey(SigilClient.SPAN_ATTR_TOOL_CALL_ID))).isEqualTo("call-weather-1"); + assertThat(span.getAttributes().get(AttributeKey.stringKey(SigilClient.SPAN_ATTR_TOOL_TYPE))).isEqualTo("function"); + assertThat(String.valueOf(span.getAttributes().get(AttributeKey.stringKey(SigilClient.SPAN_ATTR_TOOL_CALL_ARGUMENTS)))) + .contains("Paris"); + assertThat(String.valueOf(span.getAttributes().get(AttributeKey.stringKey(SigilClient.SPAN_ATTR_TOOL_CALL_RESULT)))) + .contains("sunny"); + assertThat(span.getAttributes().get(AttributeKey.stringKey(SigilClient.SPAN_ATTR_CONVERSATION_TITLE))) + .isEqualTo("Context title"); + assertThat(span.getAttributes().get(AttributeKey.stringKey(SigilClient.SPAN_ATTR_AGENT_NAME))) + .isEqualTo("agent-context"); + assertThat(span.getAttributes().get(AttributeKey.stringKey(SigilClient.SPAN_ATTR_AGENT_VERSION))) + .isEqualTo("v-context"); + assertThat(metricNames).contains(SigilClient.METRIC_OPERATION_DURATION); + assertThat(metricNames).doesNotContain(SigilClient.METRIC_TTFT); + } + } + + @Test + void embeddingSemantics() throws Exception { + try (ConformanceEnv env = new ConformanceEnv(1); + Scope ignoredName = SigilContext.withAgentName("agent-context"); + Scope ignoredVersion = SigilContext.withAgentVersion("v-context")) { + EmbeddingRecorder recorder = env.client.startEmbedding(new EmbeddingStart() + .setModel(new ModelRef().setProvider("openai").setName("text-embedding-3-small")) + .setDimensions(512L)); + recorder.setResult(new EmbeddingResult() + .setInputCount(2) + .setInputTokens(8) + .setInputTexts(List.of("hello", "world")) + .setResponseModel("text-embedding-3-small") + .setDimensions(512L)); + recorder.end(); + env.client.shutdown(); + + SpanData span = env.latestSpan("embeddings"); + List metricNames = env.metricNames(); + + assertThat(env.requests).isEmpty(); + assertThat(span.getName()).isEqualTo("embeddings text-embedding-3-small"); + assertThat(span.getAttributes().get(AttributeKey.stringKey(SigilClient.SPAN_ATTR_OPERATION_NAME))).isEqualTo("embeddings"); + assertThat(span.getAttributes().get(AttributeKey.stringKey(SigilClient.SPAN_ATTR_AGENT_NAME))) + .isEqualTo("agent-context"); + assertThat(span.getAttributes().get(AttributeKey.stringKey(SigilClient.SPAN_ATTR_AGENT_VERSION))) + .isEqualTo("v-context"); + assertThat(span.getAttributes().get(AttributeKey.longKey("gen_ai.embeddings.input_count"))).isEqualTo(2L); + assertThat(span.getAttributes().get(AttributeKey.longKey("gen_ai.embeddings.dimension.count"))).isEqualTo(512L); + assertThat(span.getAttributes().get(AttributeKey.stringKey(SigilClient.SPAN_ATTR_RESPONSE_MODEL))) + .isEqualTo("text-embedding-3-small"); + assertThat(metricNames).contains(SigilClient.METRIC_OPERATION_DURATION, SigilClient.METRIC_TOKEN_USAGE); + assertThat(metricNames).doesNotContain(SigilClient.METRIC_TTFT, SigilClient.METRIC_TOOL_CALLS_PER_OPERATION); + } + } + + @Test + void validationAndCallErrorSemantics() throws Exception { + try (ConformanceEnv env = new ConformanceEnv(1)) { + GenerationRecorder invalid = env.client.startGeneration(new GenerationStart() + .setModel(new ModelRef().setProvider("anthropic").setName("claude-sonnet-4-5"))); + invalid.setResult(new GenerationResult().setInput(List.of(new Message() + .setRole(MessageRole.USER) + .setParts(List.of(MessagePart.toolCall(new ToolCall().setName("weather"))))))); + invalid.end(); + + assertThat(invalid.error()).isPresent(); + assertThat(env.requests).isEmpty(); + assertThat(env.latestGenerationSpan().getAttributes().get(AttributeKey.stringKey(SigilClient.SPAN_ATTR_ERROR_TYPE))) + .isEqualTo("validation_error"); + + GenerationRecorder callError = env.client.startGeneration(new GenerationStart() + .setModel(new ModelRef().setProvider("openai").setName("gpt-5"))); + callError.setCallError(new IllegalStateException("provider unavailable")); + callError.setResult(new GenerationResult()); + callError.end(); + env.client.shutdown(); + + GenerationIngest.Generation generation = env.singleGeneration(); + SpanData span = env.latestGenerationSpan(); + assertThat(callError.error()).isEmpty(); + assertThat(generation.getCallError()).isEqualTo("provider unavailable"); + assertThat(generation.getMetadata().getFieldsMap().get("call_error").getStringValue()).isEqualTo("provider unavailable"); + assertThat(span.getAttributes().get(AttributeKey.stringKey(SigilClient.SPAN_ATTR_ERROR_TYPE))) + .isEqualTo("provider_call_error"); + } + } + + @Test + void ratingSubmissionSemantics() throws Exception { + try (ConformanceEnv env = new ConformanceEnv(1)) { + SubmitConversationRatingResponse response = env.client.submitConversationRating( + "conv-rating", + new SubmitConversationRatingRequest() + .setRatingId("rat-1") + .setRating(ConversationRatingValue.BAD) + .setComment("wrong answer") + .setMetadata(Map.of("channel", "assistant"))); + + assertThat(env.ratingPath.get()).isEqualTo("/api/v1/conversations/conv-rating/ratings"); + assertThat(response.getRating().getConversationId()).isEqualTo("conv-rating"); + assertThat(response.getSummary().getBadCount()).isEqualTo(1L); + + JsonNode body = env.ratingPayload.get(); + assertThat(body.get("rating_id").asText()).isEqualTo("rat-1"); + assertThat(body.get("rating").asText()).isEqualTo("CONVERSATION_RATING_VALUE_BAD"); + assertThat(body.get("comment").asText()).isEqualTo("wrong answer"); + } + } + + @Test + void shutdownFlushSemantics() throws Exception { + try (ConformanceEnv env = new ConformanceEnv(10)) { + GenerationRecorder recorder = env.client.startGeneration(new GenerationStart() + .setConversationId("conv-shutdown") + .setAgentName("agent-shutdown") + .setAgentVersion("v-shutdown") + .setModel(new ModelRef().setProvider("openai").setName("gpt-5"))); + recorder.setResult(new GenerationResult()); + recorder.end(); + + assertThat(env.requests).isEmpty(); + env.client.shutdown(); + + GenerationIngest.Generation generation = env.singleGeneration(); + assertThat(generation.getConversationId()).isEqualTo("conv-shutdown"); + assertThat(generation.getAgentName()).isEqualTo("agent-shutdown"); + assertThat(generation.getAgentVersion()).isEqualTo("v-shutdown"); + } + } + + private static Stream conversationTitleCases() { + return Stream.of( + Arguments.of("Explicit", "Context", "Meta", "Explicit"), + Arguments.of("", "Context", "", "Context"), + Arguments.of("", "", "Meta", "Meta"), + Arguments.of(" Padded ", "", "", "Padded"), + Arguments.of(" ", "", "", "")); + } + + private static Stream userIdCases() { + return Stream.of( + Arguments.of("explicit", "ctx", "canonical", "legacy", "explicit"), + Arguments.of("", "ctx", "", "", "ctx"), + Arguments.of("", "", "canonical", "", "canonical"), + Arguments.of("", "", "", "legacy", "legacy"), + Arguments.of("", "", "canonical", "legacy", "canonical"), + Arguments.of(" padded ", "", "", "", "padded")); + } + + private static Stream agentIdentityCases() { + return Stream.of( + Arguments.of("agent-explicit", "v1.2.3", "", "", "", "", "agent-explicit", "v1.2.3"), + Arguments.of("", "", "agent-context", "v-context", "", "", "agent-context", "v-context"), + Arguments.of("agent-seed", "v-seed", "", "", "agent-result", "v-result", "agent-result", "v-result"), + Arguments.of("", "", "", "", "", "", "", "")); + } + + private static Scope noopScope() { + return () -> { + }; + } + + private static final class ConformanceEnv implements AutoCloseable { + private final Server server; + private final HttpServer ratingServer; + private final InMemorySpanExporter spanExporter = InMemorySpanExporter.create(); + private final SdkTracerProvider tracerProvider = SdkTracerProvider.builder() + .addSpanProcessor(SimpleSpanProcessor.create(spanExporter)) + .build(); + private final InMemoryMetricReader metricReader = InMemoryMetricReader.create(); + private final SdkMeterProvider meterProvider = SdkMeterProvider.builder() + .registerMetricReader(metricReader) + .build(); + private final AtomicReference ratingPath = new AtomicReference<>(); + private final AtomicReference ratingPayload = new AtomicReference<>(); + private final List requests = new CopyOnWriteArrayList<>(); + private boolean closed; + + private final SigilClient client; + + ConformanceEnv(int batchSize) throws Exception { + GenerationIngestServiceGrpc.GenerationIngestServiceImplBase service = + new GenerationIngestServiceGrpc.GenerationIngestServiceImplBase() { + @Override + public void exportGenerations( + GenerationIngest.ExportGenerationsRequest request, + StreamObserver responseObserver) { + requests.add(request); + List results = new ArrayList<>(); + for (GenerationIngest.Generation generation : request.getGenerationsList()) { + results.add(GenerationIngest.ExportGenerationResult.newBuilder() + .setGenerationId(generation.getId()) + .setAccepted(true) + .build()); + } + responseObserver.onNext(GenerationIngest.ExportGenerationsResponse.newBuilder() + .addAllResults(results) + .build()); + responseObserver.onCompleted(); + } + }; + server = ServerBuilder.forPort(0).addService(service).build().start(); + + ratingServer = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0); + ratingServer.createContext("/api/v1/conversations/conv-rating/ratings", exchange -> { + ratingPath.set(exchange.getRequestURI().getPath()); + ratingPayload.set(Json.MAPPER.readTree(exchange.getRequestBody().readAllBytes())); + + byte[] response = """ + { + "rating":{ + "rating_id":"rat-1", + "conversation_id":"conv-rating", + "rating":"CONVERSATION_RATING_VALUE_BAD", + "created_at":"2026-03-12T09:00:00Z" + }, + "summary":{ + "total_count":1, + "good_count":0, + "bad_count":1, + "latest_rating":"CONVERSATION_RATING_VALUE_BAD", + "latest_rated_at":"2026-03-12T09:00:00Z", + "has_bad_rating":true + } + } + """.getBytes(StandardCharsets.UTF_8); + exchange.getResponseHeaders().add("Content-Type", "application/json"); + exchange.sendResponseHeaders(200, response.length); + try (OutputStream outputStream = exchange.getResponseBody()) { + outputStream.write(response); + } + }); + ratingServer.start(); + + client = new SigilClient(new SigilClientConfig() + .setTracer(tracerProvider.get("sigil-conformance-test")) + .setMeter(meterProvider.get("sigil-conformance-test")) + .setApi(new ApiConfig().setEndpoint("http://127.0.0.1:" + ratingServer.getAddress().getPort())) + .setGenerationExport(new GenerationExportConfig() + .setProtocol(GenerationExportProtocol.GRPC) + .setEndpoint("127.0.0.1:" + server.getPort()) + .setInsecure(true) + .setBatchSize(batchSize) + .setQueueSize(10) + .setFlushInterval(Duration.ofHours(1)) + .setMaxRetries(1) + .setInitialBackoff(Duration.ofMillis(1)) + .setMaxBackoff(Duration.ofMillis(2)))); + } + + GenerationIngest.Generation singleGeneration() { + assertThat(requests).hasSize(1); + assertThat(requests.get(0).getGenerationsCount()).isEqualTo(1); + return requests.get(0).getGenerations(0); + } + + SpanData latestGenerationSpan() { + List spans = spanExporter.getFinishedSpanItems().stream() + .filter(span -> { + String operation = span.getAttributes().get(AttributeKey.stringKey(SigilClient.SPAN_ATTR_OPERATION_NAME)); + return "generateText".equals(operation) || "streamText".equals(operation); + }) + .toList(); + assertThat(spans).isNotEmpty(); + return spans.get(spans.size() - 1); + } + + SpanData latestSpan(String operationName) { + List spans = spanExporter.getFinishedSpanItems().stream() + .filter(span -> operationName.equals( + span.getAttributes().get(AttributeKey.stringKey(SigilClient.SPAN_ATTR_OPERATION_NAME)))) + .toList(); + assertThat(spans).isNotEmpty(); + return spans.get(spans.size() - 1); + } + + SpanData latestSpanByNamePrefix(String prefix) { + List spans = spanExporter.getFinishedSpanItems().stream() + .filter(span -> span.getName().startsWith(prefix)) + .toList(); + assertThat(spans).isNotEmpty(); + return spans.get(spans.size() - 1); + } + + List metricNames() { + return metricReader.collectAllMetrics().stream() + .map(MetricData::getName) + .toList(); + } + + @Override + public void close() { + if (closed) { + return; + } + closed = true; + client.shutdown(); + server.shutdownNow(); + ratingServer.stop(0); + tracerProvider.shutdown(); + meterProvider.shutdown(); + } + } +} diff --git a/js/src/client.ts b/js/src/client.ts index 0f5404b..9deac6f 100644 --- a/js/src/client.ts +++ b/js/src/client.ts @@ -1,4 +1,11 @@ import { defaultLogger, mergeConfig } from './config.js'; +import { + agentNameFromContext, + agentVersionFromContext, + conversationIdFromContext, + conversationTitleFromContext, + userIdFromContext, +} from './context.js'; import { createDefaultGenerationExporter } from './exporters/default.js'; import { metrics, SpanKind, SpanStatusCode, trace, type Histogram, type Meter, type Span, type Tracer } from '@opentelemetry/api'; import type { @@ -34,11 +41,13 @@ import { cloneEmbeddingStart, cloneGeneration, cloneGenerationResult, + cloneGenerationStart, cloneModelRef, cloneToolDefinition, cloneMessage, cloneArtifact, cloneToolExecution, + cloneToolExecutionStart, cloneToolExecutionResult, defaultOperationNameForMode, defaultSleep, @@ -62,6 +71,8 @@ const spanAttrFrameworkRetryAttempt = 'sigil.framework.retry_attempt'; const spanAttrFrameworkLangGraphNode = 'sigil.framework.langgraph.node'; const spanAttrFrameworkEventID = 'sigil.framework.event_id'; const spanAttrConversationID = 'gen_ai.conversation.id'; +const spanAttrConversationTitle = 'sigil.conversation.title'; +const spanAttrUserID = 'user.id'; const spanAttrAgentName = 'gen_ai.agent.name'; const spanAttrAgentVersion = 'gen_ai.agent.version'; const spanAttrErrorType = 'error.type'; @@ -116,6 +127,8 @@ const metricTokenTypeReasoning = 'reasoning'; const instrumentationName = 'github.com/grafana/sigil/sdks/js'; const sdkName = 'sdk-js'; const defaultEmbeddingOperationName = 'embeddings'; +const metadataUserIDKey = 'sigil.user.id'; +const metadataLegacyUserIDKey = 'user.id'; export class SigilClient { private readonly config: SigilSdkConfig; @@ -221,7 +234,14 @@ export class SigilClient { callback?: RecorderCallback ): EmbeddingRecorder | Promise { this.assertOpen(); - const recorder = new EmbeddingRecorderImpl(this, start); + const seed = cloneEmbeddingStart(start); + if (!notEmpty(seed.agentName)) { + seed.agentName = agentNameFromContext(); + } + if (!notEmpty(seed.agentVersion)) { + seed.agentVersion = agentVersionFromContext(); + } + const recorder = new EmbeddingRecorderImpl(this, seed); if (callback === undefined) { return recorder; } @@ -413,6 +433,8 @@ export class SigilClient { setGenerationSpanAttributes(span, { id: seed.id, conversationId: seed.conversationId, + conversationTitle: seed.conversationTitle, + userId: seed.userId, agentName: seed.agentName, agentVersion: seed.agentVersion, operationName, @@ -457,6 +479,10 @@ export class SigilClient { } } + internalSyncGenerationSpan(span: Span, generation: Generation): void { + setGenerationSpanAttributes(span, generation); + } + internalFinalizeGenerationSpan( span: Span, generation: Generation, @@ -466,7 +492,6 @@ export class SigilClient { firstTokenAt: Date | undefined ): void { span.updateName(generationSpanName(generation.operationName, generation.model.name)); - setGenerationSpanAttributes(span, generation); if (callError !== undefined) { span.recordException(new Error(callError)); @@ -801,6 +826,7 @@ export class SigilClient { } class GenerationRecorderImpl implements GenerationRecorder { + private readonly seed: GenerationStart; private readonly startedAt: Date; private readonly mode: GenerationMode; private readonly span: Span; @@ -812,12 +838,31 @@ class GenerationRecorderImpl implements GenerationRecorder { constructor( private readonly client: SigilClient, - private readonly seed: GenerationStart, + seed: GenerationStart, defaultMode: GenerationMode ) { - this.mode = seed.mode ?? defaultMode; - this.startedAt = seed.startedAt ?? this.client.internalNow(); - this.span = this.client.internalStartGenerationSpan(seed, this.mode, this.startedAt); + this.seed = cloneGenerationStart(seed); + if (!notEmpty(this.seed.conversationId)) { + this.seed.conversationId = conversationIdFromContext(); + } + if (!notEmpty(this.seed.conversationTitle)) { + this.seed.conversationTitle = conversationTitleFromContext(); + } + if (!notEmpty(this.seed.userId)) { + this.seed.userId = userIdFromContext(); + } + if (!notEmpty(this.seed.agentName)) { + this.seed.agentName = agentNameFromContext(); + } + if (!notEmpty(this.seed.agentVersion)) { + this.seed.agentVersion = agentVersionFromContext(); + } + if (!notEmpty(this.seed.operationName)) { + this.seed.operationName = defaultOperationNameForMode(this.seed.mode ?? defaultMode); + } + this.mode = this.seed.mode ?? defaultMode; + this.startedAt = this.seed.startedAt ?? this.client.internalNow(); + this.span = this.client.internalStartGenerationSpan(this.seed, this.mode, this.startedAt); } setResult(result: GenerationResult): void { @@ -852,9 +897,11 @@ class GenerationRecorderImpl implements GenerationRecorder { const generation: Generation = { id: this.seed.id ?? newLocalID('gen'), - conversationId: this.result?.conversationId ?? this.seed.conversationId, - agentName: this.result?.agentName ?? this.seed.agentName, - agentVersion: this.result?.agentVersion ?? this.seed.agentVersion, + conversationId: firstNonEmptyString(this.result?.conversationId, this.seed.conversationId), + conversationTitle: firstNonEmptyString(this.result?.conversationTitle, this.seed.conversationTitle), + userId: firstNonEmptyString(this.result?.userId, this.seed.userId), + agentName: firstNonEmptyString(this.result?.agentName, this.seed.agentName), + agentVersion: firstNonEmptyString(this.result?.agentVersion, this.seed.agentVersion), mode: this.mode, operationName: this.result?.operationName ?? this.seed.operationName ?? defaultOperationNameForMode(this.mode), model: cloneModelRef(this.seed.model), @@ -873,16 +920,35 @@ class GenerationRecorderImpl implements GenerationRecorder { stopReason: this.result?.stopReason, startedAt: new Date(this.startedAt), completedAt: new Date(this.result?.completedAt ?? this.client.internalNow()), - tags: this.result?.tags ? { ...this.result.tags } : this.seed.tags ? { ...this.seed.tags } : undefined, - metadata: this.result?.metadata - ? { ...this.result.metadata } - : this.seed.metadata - ? { ...this.seed.metadata } - : undefined, + tags: mergeStringRecords(this.seed.tags, this.result?.tags), + metadata: mergeUnknownRecords(this.seed.metadata, this.result?.metadata), artifacts: this.result?.artifacts?.map(cloneArtifact), callError: this.callError, }; + generation.conversationTitle = firstNonEmptyString( + generation.conversationTitle, + metadataStringValue(generation.metadata, spanAttrConversationTitle) + )?.trim(); + if (notEmpty(generation.conversationTitle)) { + if (generation.metadata === undefined) { + generation.metadata = {}; + } + generation.metadata[spanAttrConversationTitle] = generation.conversationTitle; + } + + generation.userId = firstNonEmptyString( + generation.userId, + metadataStringValue(generation.metadata, metadataUserIDKey), + metadataStringValue(generation.metadata, metadataLegacyUserIDKey) + )?.trim(); + if (notEmpty(generation.userId)) { + if (generation.metadata === undefined) { + generation.metadata = {}; + } + generation.metadata[metadataUserIDKey] = generation.userId; + } + if (this.callError !== undefined) { if (generation.metadata === undefined) { generation.metadata = {}; @@ -894,6 +960,7 @@ class GenerationRecorderImpl implements GenerationRecorder { } generation.metadata[spanAttrSDKName] = sdkName; + this.client.internalSyncGenerationSpan(this.span, generation); this.client.internalApplyTraceContextFromSpan(this.span, generation); this.client.internalRecordGeneration(generation); @@ -993,6 +1060,7 @@ class EmbeddingRecorderImpl implements EmbeddingRecorder { } class ToolExecutionRecorderImpl implements ToolExecutionRecorder { + private readonly seed: ToolExecutionStart; private readonly startedAt: Date; private readonly span: Span; private ended = false; @@ -1002,10 +1070,23 @@ class ToolExecutionRecorderImpl implements ToolExecutionRecorder { constructor( private readonly client: SigilClient, - private readonly seed: ToolExecutionStart + seed: ToolExecutionStart ) { - this.startedAt = seed.startedAt ?? this.client.internalNow(); - this.span = this.client.internalStartToolExecutionSpan(seed, this.startedAt); + this.seed = cloneToolExecutionStart(seed); + if (!notEmpty(this.seed.conversationId)) { + this.seed.conversationId = conversationIdFromContext(); + } + if (!notEmpty(this.seed.conversationTitle)) { + this.seed.conversationTitle = conversationTitleFromContext(); + } + if (!notEmpty(this.seed.agentName)) { + this.seed.agentName = agentNameFromContext(); + } + if (!notEmpty(this.seed.agentVersion)) { + this.seed.agentVersion = agentVersionFromContext(); + } + this.startedAt = this.seed.startedAt ?? this.client.internalNow(); + this.span = this.client.internalStartToolExecutionSpan(this.seed, this.startedAt); } setResult(result: ToolExecutionResult): void { @@ -1035,6 +1116,7 @@ class ToolExecutionRecorderImpl implements ToolExecutionRecorder { toolType: this.seed.toolType, toolDescription: this.seed.toolDescription, conversationId: this.seed.conversationId, + conversationTitle: this.seed.conversationTitle, agentName: this.seed.agentName, agentVersion: this.seed.agentVersion, includeContent: this.seed.includeContent ?? false, @@ -1122,6 +1204,8 @@ function setGenerationSpanAttributes( generation: { id?: string; conversationId?: string; + conversationTitle?: string; + userId?: string; agentName?: string; agentVersion?: string; operationName: string; @@ -1154,6 +1238,12 @@ function setGenerationSpanAttributes( if (notEmpty(generation.conversationId)) { span.setAttribute(spanAttrConversationID, generation.conversationId); } + if (notEmpty(generation.conversationTitle)) { + span.setAttribute(spanAttrConversationTitle, generation.conversationTitle); + } + if (notEmpty(generation.userId)) { + span.setAttribute(spanAttrUserID, generation.userId); + } if (notEmpty(generation.agentName)) { span.setAttribute(spanAttrAgentName, generation.agentName); } @@ -1309,6 +1399,7 @@ function setToolSpanAttributes( toolType?: string; toolDescription?: string; conversationId?: string; + conversationTitle?: string; agentName?: string; agentVersion?: string; } @@ -1329,6 +1420,9 @@ function setToolSpanAttributes( if (notEmpty(tool.conversationId)) { span.setAttribute(spanAttrConversationID, tool.conversationId); } + if (notEmpty(tool.conversationTitle)) { + span.setAttribute(spanAttrConversationTitle, tool.conversationTitle); + } if (notEmpty(tool.agentName)) { span.setAttribute(spanAttrAgentName, tool.agentName); } @@ -1618,6 +1712,41 @@ function metadataIntValue(metadata: Record | undefined, key: st return undefined; } +function firstNonEmptyString(...values: Array): string | undefined { + for (const value of values) { + if (notEmpty(value)) { + return value; + } + } + return undefined; +} + +function mergeStringRecords( + left: Record | undefined, + right: Record | undefined +): Record | undefined { + if (left === undefined && right === undefined) { + return undefined; + } + return { + ...(left ?? {}), + ...(right ?? {}), + }; +} + +function mergeUnknownRecords( + left: Record | undefined, + right: Record | undefined +): Record | undefined { + if (left === undefined && right === undefined) { + return undefined; + } + return { + ...(left ?? {}), + ...(right ?? {}), + }; +} + function countToolCallParts(messages: Message[]): number { let total = 0; for (const message of messages) { diff --git a/js/src/context.ts b/js/src/context.ts new file mode 100644 index 0000000..4aee505 --- /dev/null +++ b/js/src/context.ts @@ -0,0 +1,75 @@ +import { AsyncLocalStorage } from 'node:async_hooks'; + +type SigilContextValues = { + conversationId?: string; + conversationTitle?: string; + userId?: string; + agentName?: string; + agentVersion?: string; +}; + +const storage = new AsyncLocalStorage(); + +export function withConversationId(conversationId: string, callback: () => T): T { + return runWithContext({ conversationId }, callback); +} + +export function withConversationTitle(conversationTitle: string, callback: () => T): T { + return runWithContext({ conversationTitle }, callback); +} + +export function withUserId(userId: string, callback: () => T): T { + return runWithContext({ userId }, callback); +} + +export function withAgentName(agentName: string, callback: () => T): T { + return runWithContext({ agentName }, callback); +} + +export function withAgentVersion(agentVersion: string, callback: () => T): T { + return runWithContext({ agentVersion }, callback); +} + +export function conversationIdFromContext(): string | undefined { + return normalizedString(storage.getStore()?.conversationId); +} + +export function conversationTitleFromContext(): string | undefined { + return normalizedString(storage.getStore()?.conversationTitle); +} + +export function userIdFromContext(): string | undefined { + return normalizedString(storage.getStore()?.userId); +} + +export function agentNameFromContext(): string | undefined { + return normalizedString(storage.getStore()?.agentName); +} + +export function agentVersionFromContext(): string | undefined { + return normalizedString(storage.getStore()?.agentVersion); +} + +function runWithContext(nextValues: SigilContextValues, callback: () => T): T { + const currentValues = storage.getStore() ?? {}; + const mergedValues: SigilContextValues = { ...currentValues }; + + for (const [key, value] of Object.entries(nextValues)) { + const normalized = normalizedString(value); + if (normalized === undefined) { + delete mergedValues[key as keyof SigilContextValues]; + continue; + } + mergedValues[key as keyof SigilContextValues] = normalized; + } + + return storage.run(mergedValues, callback); +} + +function normalizedString(value: string | undefined): string | undefined { + if (value === undefined) { + return undefined; + } + const trimmed = value.trim(); + return trimmed.length > 0 ? trimmed : undefined; +} diff --git a/js/src/index.ts b/js/src/index.ts index a573479..64f7d7b 100644 --- a/js/src/index.ts +++ b/js/src/index.ts @@ -1,5 +1,17 @@ export { SigilClient } from './client.js'; export { defaultConfig } from './config.js'; +export { + agentNameFromContext, + agentVersionFromContext, + conversationIdFromContext, + conversationTitleFromContext, + userIdFromContext, + withAgentName, + withAgentVersion, + withConversationId, + withConversationTitle, + withUserId, +} from './context.js'; export type { ApiConfig, Artifact, diff --git a/js/src/types.ts b/js/src/types.ts index 35582f0..a5e92c8 100644 --- a/js/src/types.ts +++ b/js/src/types.ts @@ -238,6 +238,8 @@ export interface Artifact { export interface GenerationStart { id?: string; conversationId?: string; + conversationTitle?: string; + userId?: string; agentName?: string; agentVersion?: string; mode?: GenerationMode; @@ -258,6 +260,8 @@ export interface GenerationStart { /** Final generation result fields. */ export interface GenerationResult { conversationId?: string; + conversationTitle?: string; + userId?: string; agentName?: string; agentVersion?: string; operationName?: string; @@ -304,6 +308,8 @@ export interface EmbeddingResult { export interface Generation { id: string; conversationId?: string; + conversationTitle?: string; + userId?: string; agentName?: string; agentVersion?: string; mode: GenerationMode; @@ -339,6 +345,7 @@ export interface ToolExecutionStart { toolType?: string; toolDescription?: string; conversationId?: string; + conversationTitle?: string; agentName?: string; agentVersion?: string; includeContent?: boolean; @@ -359,6 +366,7 @@ export interface ToolExecution { toolType?: string; toolDescription?: string; conversationId?: string; + conversationTitle?: string; agentName?: string; agentVersion?: string; includeContent: boolean; diff --git a/js/src/utils.ts b/js/src/utils.ts index 44d28e8..91f15b0 100644 --- a/js/src/utils.ts +++ b/js/src/utils.ts @@ -5,11 +5,13 @@ import type { Generation, GenerationMode, GenerationResult, + GenerationStart, Message, MessagePart, ModelRef, ToolDefinition, ToolExecution, + ToolExecutionStart, ToolExecutionResult, } from './types.js'; @@ -159,6 +161,17 @@ export function cloneGenerationResult(result: GenerationResult): GenerationResul }; } +export function cloneGenerationStart(start: GenerationStart): GenerationStart { + return { + ...start, + model: cloneModelRef(start.model), + tools: start.tools?.map(cloneToolDefinition), + tags: start.tags ? { ...start.tags } : undefined, + metadata: start.metadata ? { ...start.metadata } : undefined, + startedAt: start.startedAt ? new Date(start.startedAt) : undefined, + }; +} + export function cloneEmbeddingStart(start: EmbeddingStart): EmbeddingStart { return { ...start, @@ -184,6 +197,13 @@ export function cloneToolExecution(toolExecution: ToolExecution): ToolExecution }; } +export function cloneToolExecutionStart(start: ToolExecutionStart): ToolExecutionStart { + return { + ...start, + startedAt: start.startedAt ? new Date(start.startedAt) : undefined, + }; +} + export function cloneToolExecutionResult(result: ToolExecutionResult): ToolExecutionResult { return { ...result, diff --git a/js/test/client.spans.test.mjs b/js/test/client.spans.test.mjs index 264041e..94b2943 100644 --- a/js/test/client.spans.test.mjs +++ b/js/test/client.spans.test.mjs @@ -105,6 +105,77 @@ test('generation result fields override seed and update span operation name', as } }); +test('generation normalization trims only title and user fields', async () => { + const harness = newHarness(); + + try { + const recorder = harness.client.startGeneration({ + conversationId: ' conv-seed ', + conversationTitle: ' title-seed ', + userId: ' user-seed ', + agentName: ' agent-seed ', + agentVersion: ' v-seed ', + model: { provider: 'openai', name: 'gpt-5' }, + }); + recorder.setResult({ + conversationId: ' conv-result ', + conversationTitle: ' title-result ', + userId: ' user-result ', + agentName: ' agent-result ', + agentVersion: ' v-result ', + }); + recorder.end(); + assert.equal(recorder.getError(), undefined); + + const generation = singleGeneration(harness.client); + assert.equal(generation.conversationId, ' conv-result '); + assert.equal(generation.conversationTitle, 'title-result'); + assert.equal(generation.userId, 'user-result'); + assert.equal(generation.agentName, ' agent-result '); + assert.equal(generation.agentVersion, ' v-result '); + assert.equal(generation.metadata?.['sigil.conversation.title'], 'title-result'); + assert.equal(generation.metadata?.['sigil.user.id'], 'user-result'); + + const span = singleGenerationSpan(harness.spanExporter); + assert.equal(span.attributes['gen_ai.conversation.id'], ' conv-result '); + assert.equal(span.attributes['sigil.conversation.title'], 'title-result'); + assert.equal(span.attributes['user.id'], 'user-result'); + assert.equal(span.attributes['gen_ai.agent.name'], ' agent-result '); + assert.equal(span.attributes['gen_ai.agent.version'], ' v-result '); + } finally { + await shutdownHarness(harness); + } +}); + +test('generation span reflects metadata fallback title and user id after normalization', async () => { + const harness = newHarness(); + + try { + const recorder = harness.client.startGeneration({ + model: { provider: 'openai', name: 'gpt-5' }, + metadata: { + 'sigil.conversation.title': ' Meta title ', + 'user.id': ' legacy-user ', + }, + }); + recorder.setResult({}); + recorder.end(); + assert.equal(recorder.getError(), undefined); + + const generation = singleGeneration(harness.client); + assert.equal(generation.conversationTitle, 'Meta title'); + assert.equal(generation.userId, 'legacy-user'); + assert.equal(generation.metadata?.['sigil.conversation.title'], 'Meta title'); + assert.equal(generation.metadata?.['sigil.user.id'], 'legacy-user'); + + const span = singleGenerationSpan(harness.spanExporter); + assert.equal(span.attributes['sigil.conversation.title'], 'Meta title'); + assert.equal(span.attributes['user.id'], 'legacy-user'); + } finally { + await shutdownHarness(harness); + } +}); + test('generation callError sets metadata and provider_call_error span status', async () => { const harness = newHarness(); diff --git a/js/test/conformance.test.mjs b/js/test/conformance.test.mjs new file mode 100644 index 0000000..cdd33cf --- /dev/null +++ b/js/test/conformance.test.mjs @@ -0,0 +1,716 @@ +import assert from 'node:assert/strict'; +import { createServer } from 'node:http'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import test from 'node:test'; +import * as grpc from '@grpc/grpc-js'; +import * as protoLoader from '@grpc/proto-loader'; +import { AggregationTemporality, InMemoryMetricExporter, MeterProvider, PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics'; +import { BasicTracerProvider, InMemorySpanExporter, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base'; +import { + SigilClient, + defaultConfig, + withAgentName, + withAgentVersion, + withConversationTitle, + withUserId, +} from '../.test-dist/index.js'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const protoPath = join(__dirname, '../proto/sigil/v1/generation_ingest.proto'); +const protoLoadOptions = { + keepCase: false, + longs: String, + enums: String, + defaults: false, + oneofs: true, +}; + +test('conformance sync roundtrip semantics', async () => { + const env = await createConformanceEnv(); + + try { + const recorder = env.client.startGeneration({ + id: 'gen-roundtrip', + conversationId: 'conv-roundtrip', + conversationTitle: 'Roundtrip conversation', + userId: 'user-roundtrip', + agentName: 'agent-roundtrip', + agentVersion: 'v-roundtrip', + model: { provider: 'openai', name: 'gpt-5' }, + maxTokens: 256, + temperature: 0.2, + topP: 0.9, + toolChoice: 'required', + thinkingEnabled: false, + tools: [{ name: 'weather', description: 'Get weather', type: 'function' }], + tags: { tenant: 'dev' }, + metadata: { trace: 'roundtrip' }, + }); + recorder.setResult({ + responseId: 'resp-roundtrip', + responseModel: 'gpt-5-2026', + input: [{ role: 'user', parts: [{ type: 'text', text: 'hello' }] }], + output: [ + { + role: 'assistant', + parts: [ + { type: 'thinking', thinking: 'reasoning' }, + { + type: 'tool_call', + toolCall: { + id: 'call-1', + name: 'weather', + inputJSON: '{"city":"Paris"}', + }, + }, + ], + }, + { + role: 'tool', + parts: [ + { + type: 'tool_result', + toolResult: { + toolCallId: 'call-1', + name: 'weather', + content: 'sunny', + contentJSON: '{"temp_c":18}', + }, + }, + ], + }, + ], + usage: { + inputTokens: 12, + outputTokens: 7, + totalTokens: 19, + cacheReadInputTokens: 2, + cacheWriteInputTokens: 1, + cacheCreationInputTokens: 3, + reasoningTokens: 4, + }, + stopReason: 'stop', + tags: { region: 'eu' }, + metadata: { result: 'ok' }, + artifacts: [ + { type: 'request', name: 'request', mimeType: 'application/json', payload: '{"prompt":"hello"}' }, + { type: 'response', name: 'response', mimeType: 'application/json', payload: '{"text":"sunny"}' }, + ], + }); + recorder.end(); + assert.equal(recorder.getError(), undefined); + + await env.client.shutdown(); + const generation = env.singleGeneration(); + const span = env.latestGenerationSpan(); + const metricNames = await env.metricNames(); + + assert.equal(generation.mode, 'GENERATION_MODE_SYNC'); + assert.equal(generation.operationName, 'generateText'); + assert.equal(generation.conversationId, 'conv-roundtrip'); + assert.equal(generation.agentName, 'agent-roundtrip'); + assert.equal(generation.agentVersion, 'v-roundtrip'); + assert.equal(generation.traceId, span.spanContext().traceId); + assert.equal(generation.spanId, span.spanContext().spanId); + assert.equal(generation.metadata?.fields?.['sigil.conversation.title']?.stringValue, 'Roundtrip conversation'); + assert.equal(generation.metadata?.fields?.['sigil.user.id']?.stringValue, 'user-roundtrip'); + assert.equal(generation.input?.[0]?.parts?.[0]?.text, 'hello'); + assert.equal(generation.output?.[0]?.parts?.[0]?.thinking, 'reasoning'); + assert.equal(generation.output?.[0]?.parts?.[1]?.toolCall?.name, 'weather'); + assert.equal(generation.output?.[1]?.parts?.[0]?.toolResult?.content, 'sunny'); + assert.equal(Number(generation.maxTokens), 256); + assert.equal(generation.temperature, 0.2); + assert.equal(generation.topP, 0.9); + assert.equal(generation.toolChoice, 'required'); + assert.equal(generation.thinkingEnabled, false); + assert.equal(Number(generation.usage?.inputTokens ?? 0), 12); + assert.equal(Number(generation.usage?.outputTokens ?? 0), 7); + assert.equal(Number(generation.usage?.totalTokens ?? 0), 19); + assert.equal(Number(generation.usage?.cacheReadInputTokens ?? 0), 2); + assert.equal(Number(generation.usage?.cacheWriteInputTokens ?? 0), 1); + assert.equal(Number(generation.usage?.reasoningTokens ?? 0), 4); + assert.equal(generation.stopReason, 'stop'); + assert.equal(generation.tags?.tenant, 'dev'); + assert.equal(generation.tags?.region, 'eu'); + assert.equal((generation.rawArtifacts ?? []).length, 2); + assert.equal(span.attributes['gen_ai.operation.name'], 'generateText'); + assert.equal(span.attributes['sigil.conversation.title'], 'Roundtrip conversation'); + assert.equal(span.attributes['user.id'], 'user-roundtrip'); + assert.ok(metricNames.includes('gen_ai.client.operation.duration')); + assert.ok(metricNames.includes('gen_ai.client.token.usage')); + assert.ok(!metricNames.includes('gen_ai.client.time_to_first_token')); + } finally { + await env.close(); + } +}); + +for (const testCase of [ + { name: 'explicit wins', startTitle: 'Explicit', contextTitle: 'Context', metadataTitle: 'Meta', expected: 'Explicit' }, + { name: 'context fallback', startTitle: '', contextTitle: 'Context', metadataTitle: '', expected: 'Context' }, + { name: 'metadata fallback', startTitle: '', contextTitle: '', metadataTitle: 'Meta', expected: 'Meta' }, + { name: 'whitespace trimmed', startTitle: ' Padded ', contextTitle: '', metadataTitle: '', expected: 'Padded' }, + { name: 'whitespace omitted', startTitle: ' ', contextTitle: '', metadataTitle: '', expected: '' }, +]) { + test(`conformance conversation title semantics: ${testCase.name}`, async () => { + const env = await createConformanceEnv(); + + try { + await runWithMaybeContext(testCase.contextTitle, withConversationTitle, async () => { + const recorder = env.client.startGeneration({ + model: { provider: 'openai', name: 'gpt-5' }, + conversationTitle: testCase.startTitle, + metadata: testCase.metadataTitle.length > 0 ? { 'sigil.conversation.title': testCase.metadataTitle } : undefined, + }); + recorder.setResult({}); + recorder.end(); + assert.equal(recorder.getError(), undefined); + }); + + await env.client.shutdown(); + const generation = env.singleGeneration(); + const span = env.latestGenerationSpan(); + if (testCase.expected.length === 0) { + assert.equal(generation.metadata?.fields?.['sigil.conversation.title'], undefined); + assert.equal(span.attributes['sigil.conversation.title'], undefined); + return; + } + + assert.equal(generation.metadata?.fields?.['sigil.conversation.title']?.stringValue, testCase.expected); + assert.equal(span.attributes['sigil.conversation.title'], testCase.expected); + } finally { + await env.close(); + } + }); +} + +for (const testCase of [ + { name: 'explicit wins', startUserId: 'explicit', contextUserId: 'ctx', canonicalUserId: 'canonical', legacyUserId: 'legacy', expected: 'explicit' }, + { name: 'context fallback', startUserId: '', contextUserId: 'ctx', canonicalUserId: '', legacyUserId: '', expected: 'ctx' }, + { name: 'canonical metadata', startUserId: '', contextUserId: '', canonicalUserId: 'canonical', legacyUserId: '', expected: 'canonical' }, + { name: 'legacy metadata', startUserId: '', contextUserId: '', canonicalUserId: '', legacyUserId: 'legacy', expected: 'legacy' }, + { name: 'canonical beats legacy', startUserId: '', contextUserId: '', canonicalUserId: 'canonical', legacyUserId: 'legacy', expected: 'canonical' }, + { name: 'whitespace trimmed', startUserId: ' padded ', contextUserId: '', canonicalUserId: '', legacyUserId: '', expected: 'padded' }, +]) { + test(`conformance user id semantics: ${testCase.name}`, async () => { + const env = await createConformanceEnv(); + + try { + await runWithMaybeContext(testCase.contextUserId, withUserId, async () => { + const metadata = {}; + if (testCase.canonicalUserId.length > 0) { + metadata['sigil.user.id'] = testCase.canonicalUserId; + } + if (testCase.legacyUserId.length > 0) { + metadata['user.id'] = testCase.legacyUserId; + } + + const recorder = env.client.startGeneration({ + model: { provider: 'openai', name: 'gpt-5' }, + userId: testCase.startUserId, + metadata, + }); + recorder.setResult({}); + recorder.end(); + assert.equal(recorder.getError(), undefined); + }); + + await env.client.shutdown(); + const generation = env.singleGeneration(); + const span = env.latestGenerationSpan(); + assert.equal(generation.metadata?.fields?.['sigil.user.id']?.stringValue, testCase.expected); + assert.equal(span.attributes['user.id'], testCase.expected); + } finally { + await env.close(); + } + }); +} + +for (const testCase of [ + { + name: 'explicit fields', + startName: 'agent-explicit', + startVersion: 'v1.2.3', + contextName: '', + contextVersion: '', + resultName: '', + resultVersion: '', + expectedName: 'agent-explicit', + expectedVersion: 'v1.2.3', + }, + { + name: 'context fallback', + startName: '', + startVersion: '', + contextName: 'agent-context', + contextVersion: 'v-context', + resultName: '', + resultVersion: '', + expectedName: 'agent-context', + expectedVersion: 'v-context', + }, + { + name: 'result override', + startName: 'agent-seed', + startVersion: 'v-seed', + contextName: '', + contextVersion: '', + resultName: 'agent-result', + resultVersion: 'v-result', + expectedName: 'agent-result', + expectedVersion: 'v-result', + }, + { + name: 'empty omission', + startName: '', + startVersion: '', + contextName: '', + contextVersion: '', + resultName: '', + resultVersion: '', + expectedName: '', + expectedVersion: '', + }, +]) { + test(`conformance agent identity semantics: ${testCase.name}`, async () => { + const env = await createConformanceEnv(); + + try { + await runWithMaybeContext(testCase.contextName, withAgentName, async () => { + await runWithMaybeContext(testCase.contextVersion, withAgentVersion, async () => { + const recorder = env.client.startGeneration({ + model: { provider: 'openai', name: 'gpt-5' }, + agentName: testCase.startName, + agentVersion: testCase.startVersion, + }); + recorder.setResult({ + agentName: testCase.resultName, + agentVersion: testCase.resultVersion, + }); + recorder.end(); + assert.equal(recorder.getError(), undefined); + }); + }); + + await env.client.shutdown(); + const generation = env.singleGeneration(); + const span = env.latestGenerationSpan(); + assert.equal(generation.agentName ?? '', testCase.expectedName); + assert.equal(generation.agentVersion ?? '', testCase.expectedVersion); + assert.equal(span.attributes['gen_ai.agent.name'], testCase.expectedName || undefined); + assert.equal(span.attributes['gen_ai.agent.version'], testCase.expectedVersion || undefined); + } finally { + await env.close(); + } + }); +} + +test('conformance streaming telemetry semantics', async () => { + const env = await createConformanceEnv(); + + try { + const startedAt = new Date('2026-03-12T09:00:00Z'); + const recorder = env.client.startStreamingGeneration({ + model: { provider: 'openai', name: 'gpt-5' }, + startedAt, + }); + recorder.setFirstTokenAt(new Date('2026-03-12T09:00:00.250Z')); + recorder.setResult({ + output: [{ role: 'assistant', parts: [{ type: 'text', text: 'Hello world' }] }], + usage: { inputTokens: 4, outputTokens: 3, totalTokens: 7 }, + startedAt, + completedAt: new Date('2026-03-12T09:00:01Z'), + }); + recorder.end(); + assert.equal(recorder.getError(), undefined); + + await env.client.shutdown(); + const generation = env.singleGeneration(); + const span = env.latestGenerationSpan(); + const metricNames = await env.metricNames(); + + assert.equal(generation.mode, 'GENERATION_MODE_STREAM'); + assert.equal(generation.operationName, 'streamText'); + assert.equal(generation.output?.[0]?.parts?.[0]?.text, 'Hello world'); + assert.equal(span.name, 'streamText gpt-5'); + assert.ok(metricNames.includes('gen_ai.client.operation.duration')); + assert.ok(metricNames.includes('gen_ai.client.time_to_first_token')); + } finally { + await env.close(); + } +}); + +test('conformance tool execution semantics', async () => { + const env = await createConformanceEnv(); + + try { + await runWithMaybeContext('Context title', withConversationTitle, async () => { + await runWithMaybeContext('agent-context', withAgentName, async () => { + await runWithMaybeContext('v-context', withAgentVersion, async () => { + const recorder = env.client.startToolExecution({ + toolName: 'weather', + toolCallId: 'call-weather-1', + toolType: 'function', + includeContent: true, + }); + recorder.setResult({ + arguments: { city: 'Paris' }, + result: { forecast: 'sunny' }, + }); + recorder.end(); + assert.equal(recorder.getError(), undefined); + }); + }); + }); + + await env.client.shutdown(); + const span = env.latestSpanByOperation('execute_tool'); + const metricNames = await env.metricNames(); + + assert.equal(env.receivedRequests.length, 0); + assert.equal(span.name, 'execute_tool weather'); + assert.equal(span.attributes['gen_ai.operation.name'], 'execute_tool'); + assert.equal(span.attributes['gen_ai.tool.name'], 'weather'); + assert.equal(span.attributes['gen_ai.tool.call.id'], 'call-weather-1'); + assert.equal(span.attributes['gen_ai.tool.type'], 'function'); + assert.match(String(span.attributes['gen_ai.tool.call.arguments'] ?? ''), /Paris/); + assert.match(String(span.attributes['gen_ai.tool.call.result'] ?? ''), /sunny/); + assert.equal(span.attributes['sigil.conversation.title'], 'Context title'); + assert.equal(span.attributes['gen_ai.agent.name'], 'agent-context'); + assert.equal(span.attributes['gen_ai.agent.version'], 'v-context'); + assert.ok(metricNames.includes('gen_ai.client.operation.duration')); + assert.ok(!metricNames.includes('gen_ai.client.time_to_first_token')); + } finally { + await env.close(); + } +}); + +test('conformance embedding semantics', async () => { + const env = await createConformanceEnv(); + + try { + await runWithMaybeContext('agent-context', withAgentName, async () => { + await runWithMaybeContext('v-context', withAgentVersion, async () => { + const recorder = env.client.startEmbedding({ + model: { provider: 'openai', name: 'text-embedding-3-small' }, + dimensions: 512, + }); + recorder.setResult({ + inputCount: 2, + inputTokens: 8, + inputTexts: ['hello', 'world'], + responseModel: 'text-embedding-3-small', + dimensions: 512, + }); + recorder.end(); + assert.equal(recorder.getError(), undefined); + }); + }); + + await env.client.shutdown(); + const span = env.latestSpanByOperation('embeddings'); + const metricNames = await env.metricNames(); + + assert.equal(env.receivedRequests.length, 0); + assert.equal(span.name, 'embeddings text-embedding-3-small'); + assert.equal(span.attributes['gen_ai.operation.name'], 'embeddings'); + assert.equal(span.attributes['gen_ai.agent.name'], 'agent-context'); + assert.equal(span.attributes['gen_ai.agent.version'], 'v-context'); + assert.equal(span.attributes['gen_ai.embeddings.input_count'], 2); + assert.equal(span.attributes['gen_ai.embeddings.dimension.count'], 512); + assert.equal(span.attributes['gen_ai.response.model'], 'text-embedding-3-small'); + assert.ok(metricNames.includes('gen_ai.client.operation.duration')); + assert.ok(metricNames.includes('gen_ai.client.token.usage')); + assert.ok(!metricNames.includes('gen_ai.client.time_to_first_token')); + assert.ok(!metricNames.includes('gen_ai.client.tool_calls_per_operation')); + } finally { + await env.close(); + } +}); + +test('conformance validation and provider call error semantics', async () => { + const env = await createConformanceEnv(); + + try { + const invalid = env.client.startGeneration({ + model: { provider: 'anthropic', name: 'claude-sonnet-4-5' }, + }); + invalid.setResult({ + input: [ + { + role: 'user', + parts: [{ type: 'tool_call', toolCall: { name: 'weather' } }], + }, + ], + }); + invalid.end(); + + assert.match(invalid.getError()?.message ?? '', /tool_call only allowed for assistant role/); + assert.equal(env.receivedRequests.length, 0); + assert.equal(env.latestGenerationSpan().attributes['error.type'], 'validation_error'); + + const callError = env.client.startGeneration({ + model: { provider: 'openai', name: 'gpt-5' }, + }); + callError.setCallError(new Error('provider unavailable')); + callError.setResult({}); + callError.end(); + assert.equal(callError.getError(), undefined); + + await env.client.shutdown(); + const generation = env.singleGeneration(); + const span = env.latestGenerationSpan(); + assert.equal(generation.callError, 'provider unavailable'); + assert.equal(generation.metadata?.fields?.call_error?.stringValue, 'provider unavailable'); + assert.equal(span.attributes['error.type'], 'provider_call_error'); + } finally { + await env.close(); + } +}); + +test('conformance rating submission semantics', async () => { + const env = await createConformanceEnv(); + + try { + const response = await env.client.submitConversationRating('conv-rating', { + ratingId: 'rat-1', + rating: 'CONVERSATION_RATING_VALUE_BAD', + comment: 'wrong answer', + metadata: { channel: 'assistant' }, + }); + + assert.equal(env.ratingPath, '/api/v1/conversations/conv-rating/ratings'); + assert.deepEqual(env.ratingPayload, { + rating_id: 'rat-1', + rating: 'CONVERSATION_RATING_VALUE_BAD', + comment: 'wrong answer', + metadata: { channel: 'assistant' }, + }); + assert.equal(response.rating.conversationId, 'conv-rating'); + assert.equal(response.summary.badCount, 1); + } finally { + await env.close(); + } +}); + +test('conformance shutdown flush semantics', async () => { + const env = await createConformanceEnv({ batchSize: 10 }); + + try { + const recorder = env.client.startGeneration({ + conversationId: 'conv-shutdown', + agentName: 'agent-shutdown', + agentVersion: 'v-shutdown', + model: { provider: 'openai', name: 'gpt-5' }, + }); + recorder.setResult({}); + recorder.end(); + assert.equal(recorder.getError(), undefined); + assert.equal(env.receivedRequests.length, 0); + + await env.client.shutdown(); + const generation = env.singleGeneration(); + assert.equal(generation.conversationId, 'conv-shutdown'); + assert.equal(generation.agentName, 'agent-shutdown'); + assert.equal(generation.agentVersion, 'v-shutdown'); + } finally { + await env.close(); + } +}); + +async function createConformanceEnv(options = {}) { + const receivedRequests = []; + const grpcServer = await startGRPCServer((request) => { + receivedRequests.push(request); + }); + + let ratingPath = ''; + let ratingPayload = undefined; + const ratingServer = createServer(async (request, response) => { + ratingPath = request.url ?? ''; + const chunks = []; + for await (const chunk of request) { + chunks.push(chunk); + } + ratingPayload = JSON.parse(Buffer.concat(chunks).toString('utf8')); + response.writeHead(200, { 'content-type': 'application/json' }); + response.end( + JSON.stringify({ + rating: { + rating_id: 'rat-1', + conversation_id: 'conv-rating', + rating: 'CONVERSATION_RATING_VALUE_BAD', + created_at: '2026-03-12T09:00:00Z', + }, + summary: { + total_count: 1, + good_count: 0, + bad_count: 1, + latest_rating: 'CONVERSATION_RATING_VALUE_BAD', + latest_rated_at: '2026-03-12T09:00:00Z', + has_bad_rating: true, + }, + }) + ); + }); + await listen(ratingServer); + const ratingAddress = ratingServer.address(); + if (ratingAddress === null || typeof ratingAddress === 'string') { + throw new Error('failed to resolve rating server address'); + } + + const spanExporter = new InMemorySpanExporter(); + const tracerProvider = new BasicTracerProvider({ + spanProcessors: [new SimpleSpanProcessor(spanExporter)], + }); + const metricExporter = new InMemoryMetricExporter(AggregationTemporality.CUMULATIVE); + const metricReader = new PeriodicExportingMetricReader({ + exporter: metricExporter, + exportIntervalMillis: 60_000, + }); + const meterProvider = new MeterProvider({ + readers: [metricReader], + }); + + const defaults = defaultConfig(); + const client = new SigilClient({ + tracer: tracerProvider.getTracer('sigil-conformance-test'), + meter: meterProvider.getMeter('sigil-conformance-test'), + generationExport: { + ...defaults.generationExport, + protocol: 'grpc', + endpoint: `127.0.0.1:${grpcServer.port}`, + insecure: true, + batchSize: options.batchSize ?? 1, + queueSize: 10, + flushIntervalMs: 60 * 60 * 1_000, + maxRetries: 1, + initialBackoffMs: 1, + maxBackoffMs: 2, + }, + api: { + endpoint: `http://127.0.0.1:${ratingAddress.port}`, + }, + }); + + let closed = false; + return { + client, + receivedRequests, + get ratingPath() { + return ratingPath; + }, + get ratingPayload() { + return ratingPayload; + }, + singleGeneration() { + assert.equal(receivedRequests.length, 1); + assert.equal(receivedRequests[0].generations?.length, 1); + return receivedRequests[0].generations[0]; + }, + latestGenerationSpan() { + const spans = spanExporter.getFinishedSpans().filter((span) => { + const operation = span.attributes['gen_ai.operation.name']; + return operation === 'generateText' || operation === 'streamText'; + }); + assert.ok(spans.length > 0); + return spans.at(-1); + }, + latestSpanByOperation(operationName) { + const spans = spanExporter + .getFinishedSpans() + .filter((span) => span.attributes['gen_ai.operation.name'] === operationName); + assert.ok(spans.length > 0); + return spans.at(-1); + }, + async metricNames() { + await meterProvider.forceFlush(); + return metricExporter + .getMetrics() + .flatMap((resourceMetrics) => resourceMetrics.scopeMetrics) + .flatMap((scopeMetrics) => scopeMetrics.metrics) + .map((metric) => metric.descriptor.name); + }, + async close() { + if (closed) { + return; + } + closed = true; + await client.shutdown(); + await meterProvider.shutdown(); + await tracerProvider.shutdown(); + await close(ratingServer); + await stopGRPCServer(grpcServer.server); + }, + }; +} + +async function runWithMaybeContext(value, wrapper, callback) { + if (typeof value === 'string' && value.trim().length > 0) { + return await wrapper(value, callback); + } + return await callback(); +} + +function listen(server) { + return new Promise((resolve, reject) => { + server.once('error', reject); + server.listen(0, '127.0.0.1', () => { + server.off('error', reject); + resolve(); + }); + }); +} + +function close(server) { + return new Promise((resolve, reject) => { + server.close((error) => { + if (error) { + reject(error); + return; + } + resolve(); + }); + }); +} + +async function startGRPCServer(onRequest) { + const packageDefinition = await protoLoader.load(protoPath, protoLoadOptions); + const loaded = grpc.loadPackageDefinition(packageDefinition); + const service = loaded.sigil.v1.GenerationIngestService; + + const server = new grpc.Server(); + server.addService(service.service, { + ExportGenerations(call, callback) { + onRequest(call.request, call.metadata.getMap()); + callback(null, { + results: (call.request.generations ?? []).map((generation) => ({ + generationId: generation.id, + accepted: true, + })), + }); + }, + }); + + const port = await new Promise((resolve, reject) => { + server.bindAsync('127.0.0.1:0', grpc.ServerCredentials.createInsecure(), (error, boundPort) => { + if (error) { + reject(error); + return; + } + resolve(boundPort); + }); + }); + + server.start(); + return { server, port }; +} + +function stopGRPCServer(server) { + return new Promise((resolve) => { + server.tryShutdown(() => { + resolve(); + }); + }); +} diff --git a/python/setup.py b/python/setup.py new file mode 100644 index 0000000..b024da8 --- /dev/null +++ b/python/setup.py @@ -0,0 +1,4 @@ +from setuptools import setup + + +setup() diff --git a/python/sigil_sdk/__init__.py b/python/sigil_sdk/__init__.py index 4468e0f..b50baa1 100644 --- a/python/sigil_sdk/__init__.py +++ b/python/sigil_sdk/__init__.py @@ -3,12 +3,16 @@ from .client import Client from .config import ApiConfig, AuthConfig, ClientConfig, EmbeddingCaptureConfig, GenerationExportConfig, default_config from .context import ( + conversation_title_from_context, conversation_id_from_context, agent_name_from_context, agent_version_from_context, + user_id_from_context, with_agent_name, with_agent_version, with_conversation_id, + with_conversation_title, + with_user_id, ) from .errors import ( ClientShutdownError, @@ -98,15 +102,19 @@ "agent_version_from_context", "assistant_text_message", "conversation_id_from_context", + "conversation_title_from_context", "text_part", "thinking_part", "tool_call_part", "tool_result_message", "tool_result_part", + "user_id_from_context", "user_text_message", "with_agent_name", "with_agent_version", "with_conversation_id", + "with_conversation_title", + "with_user_id", "default_config", "validate_embedding_result", "validate_embedding_start", diff --git a/python/sigil_sdk/client.py b/python/sigil_sdk/client.py index 8e0e8f0..465e897 100644 --- a/python/sigil_sdk/client.py +++ b/python/sigil_sdk/client.py @@ -19,7 +19,13 @@ from opentelemetry.trace import Span, SpanKind, Status, StatusCode from .config import ClientConfig, resolve_config -from .context import agent_name_from_context, agent_version_from_context, conversation_id_from_context +from .context import ( + agent_name_from_context, + agent_version_from_context, + conversation_id_from_context, + conversation_title_from_context, + user_id_from_context, +) from .errors import ( ClientShutdownError, EnqueueError, @@ -61,6 +67,8 @@ _span_attr_framework_langgraph_node = "sigil.framework.langgraph.node" _span_attr_framework_event_id = "sigil.framework.event_id" _span_attr_conversation_id = "gen_ai.conversation.id" +_span_attr_conversation_title = "sigil.conversation.title" +_span_attr_user_id = "user.id" _span_attr_agent_name = "gen_ai.agent.name" _span_attr_agent_version = "gen_ai.agent.version" _span_attr_error_type = "error.type" @@ -117,6 +125,8 @@ _instrumentation_name = "github.com/grafana/sigil/sdks/python" _sdk_name = "sdk-python" _default_embedding_operation_name = "embeddings" +_metadata_user_id_key = "sigil.user.id" +_metadata_legacy_user_id_key = "user.id" class Client: @@ -226,9 +236,13 @@ def start_tool_execution(self, start: ToolExecutionStart) -> "ToolExecutionRecor if seed.tool_name == "": return NoopToolExecutionRecorder() + seed.conversation_title = seed.conversation_title.strip() if seed.conversation_id == "": conversation_id = conversation_id_from_context() or "" seed.conversation_id = conversation_id + if seed.conversation_title == "": + conversation_title = conversation_title_from_context() or "" + seed.conversation_title = conversation_title.strip() if seed.agent_name == "": agent_name = agent_name_from_context() or "" seed.agent_name = agent_name @@ -379,8 +393,14 @@ def _start_generation(self, start: GenerationStart, default_mode: GenerationMode if seed.operation_name == "": seed.operation_name = _default_operation_name(seed.mode) + seed.conversation_title = seed.conversation_title.strip() + seed.user_id = seed.user_id.strip() if seed.conversation_id == "": seed.conversation_id = conversation_id_from_context() or "" + if seed.conversation_title == "": + seed.conversation_title = (conversation_title_from_context() or "").strip() + if seed.user_id == "": + seed.user_id = (user_id_from_context() or "").strip() if seed.agent_name == "": seed.agent_name = agent_name_from_context() or "" if seed.agent_version == "": @@ -399,6 +419,8 @@ def _start_generation(self, start: GenerationStart, default_mode: GenerationMode Generation( id=seed.id, conversation_id=seed.conversation_id, + conversation_title=seed.conversation_title, + user_id=seed.user_id, agent_name=seed.agent_name, agent_version=seed.agent_version, mode=seed.mode, @@ -792,6 +814,10 @@ def _normalize_generation(self, raw: Generation, completed_at: datetime, call_er if generation.conversation_id == "": generation.conversation_id = self.seed.conversation_id + if generation.conversation_title == "": + generation.conversation_title = self.seed.conversation_title + if generation.user_id == "": + generation.user_id = self.seed.user_id if generation.agent_name == "": generation.agent_name = self.seed.agent_name if generation.agent_version == "": @@ -837,6 +863,22 @@ def _normalize_generation(self, raw: Generation, completed_at: datetime, call_er merged_metadata.update(generation.metadata) generation.metadata = merged_metadata + conversation_title = generation.conversation_title.strip() + if conversation_title == "": + conversation_title = _metadata_string_value(generation.metadata, _span_attr_conversation_title) or "" + generation.conversation_title = conversation_title + if conversation_title != "": + generation.metadata[_span_attr_conversation_title] = conversation_title + + user_id = generation.user_id.strip() + if user_id == "": + user_id = _metadata_string_value(generation.metadata, _metadata_user_id_key) or "" + if user_id == "": + user_id = _metadata_string_value(generation.metadata, _metadata_legacy_user_id_key) or "" + generation.user_id = user_id + if user_id != "": + generation.metadata[_metadata_user_id_key] = user_id + generation.started_at = _to_utc(generation.started_at) if generation.started_at is not None else self.started_at generation.completed_at = _to_utc(generation.completed_at) if generation.completed_at is not None else completed_at @@ -1128,6 +1170,10 @@ def _set_generation_span_attributes(span: Span, generation: Generation) -> None: span.set_attribute(_span_attr_generation_id, generation.id) if generation.conversation_id: span.set_attribute(_span_attr_conversation_id, generation.conversation_id) + if generation.conversation_title: + span.set_attribute(_span_attr_conversation_title, generation.conversation_title) + if generation.user_id: + span.set_attribute(_span_attr_user_id, generation.user_id) if generation.agent_name: span.set_attribute(_span_attr_agent_name, generation.agent_name) if generation.agent_version: @@ -1252,6 +1298,8 @@ def _set_tool_span_attributes(span: Span, start: ToolExecutionStart) -> None: span.set_attribute(_span_attr_tool_description, start.tool_description) if start.conversation_id: span.set_attribute(_span_attr_conversation_id, start.conversation_id) + if start.conversation_title: + span.set_attribute(_span_attr_conversation_title, start.conversation_title) if start.agent_name: span.set_attribute(_span_attr_agent_name, start.agent_name) if start.agent_version: diff --git a/python/sigil_sdk/context.py b/python/sigil_sdk/context.py index f4a03d5..285a9ee 100644 --- a/python/sigil_sdk/context.py +++ b/python/sigil_sdk/context.py @@ -8,6 +8,8 @@ _conversation_id: contextvars.ContextVar[Optional[str]] = contextvars.ContextVar("sigil_conversation_id", default=None) +_conversation_title: contextvars.ContextVar[Optional[str]] = contextvars.ContextVar("sigil_conversation_title", default=None) +_user_id: contextvars.ContextVar[Optional[str]] = contextvars.ContextVar("sigil_user_id", default=None) _agent_name: contextvars.ContextVar[Optional[str]] = contextvars.ContextVar("sigil_agent_name", default=None) _agent_version: contextvars.ContextVar[Optional[str]] = contextvars.ContextVar("sigil_agent_version", default=None) @@ -23,6 +25,28 @@ def with_conversation_id(conversation_id: str) -> Iterator[None]: _conversation_id.reset(token) +@contextmanager +def with_conversation_title(conversation_title: str) -> Iterator[None]: + """Sets conversation title within a context block.""" + + token = _conversation_title.set(conversation_title) + try: + yield + finally: + _conversation_title.reset(token) + + +@contextmanager +def with_user_id(user_id: str) -> Iterator[None]: + """Sets user id within a context block.""" + + token = _user_id.set(user_id) + try: + yield + finally: + _user_id.reset(token) + + @contextmanager def with_agent_name(agent_name: str) -> Iterator[None]: """Sets agent name within a context block.""" @@ -61,3 +85,15 @@ def agent_version_from_context() -> Optional[str]: """Returns the current agent version from context variables.""" return _agent_version.get() + + +def conversation_title_from_context() -> Optional[str]: + """Returns the current conversation title from context variables.""" + + return _conversation_title.get() + + +def user_id_from_context() -> Optional[str]: + """Returns the current user id from context variables.""" + + return _user_id.get() diff --git a/python/sigil_sdk/models.py b/python/sigil_sdk/models.py index a5c3522..58ef1f1 100644 --- a/python/sigil_sdk/models.py +++ b/python/sigil_sdk/models.py @@ -162,6 +162,8 @@ class GenerationStart: model: ModelRef id: str = "" conversation_id: str = "" + conversation_title: str = "" + user_id: str = "" agent_name: str = "" agent_version: str = "" mode: Optional[GenerationMode] = None @@ -209,6 +211,8 @@ class Generation: id: str = "" conversation_id: str = "" + conversation_title: str = "" + user_id: str = "" agent_name: str = "" agent_version: str = "" mode: Optional[GenerationMode] = None @@ -246,6 +250,7 @@ class ToolExecutionStart: tool_type: str = "" tool_description: str = "" conversation_id: str = "" + conversation_title: str = "" agent_name: str = "" agent_version: str = "" include_content: bool = False diff --git a/python/tests/test_conformance.py b/python/tests/test_conformance.py new file mode 100644 index 0000000..bec1b0a --- /dev/null +++ b/python/tests/test_conformance.py @@ -0,0 +1,669 @@ +"""Core conformance suite for the Sigil Python SDK.""" + +from __future__ import annotations + +import concurrent.futures +import copy +from contextlib import nullcontext +import json +from http.server import BaseHTTPRequestHandler, HTTPServer +import socket +import threading +from datetime import datetime, timedelta, timezone +from typing import Any + +import grpc +from opentelemetry.sdk.metrics import MeterProvider +from opentelemetry.sdk.metrics.export import InMemoryMetricReader +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import SimpleSpanProcessor +from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter + +from sigil_sdk import ( + ApiConfig, + Client, + ClientConfig, + ConversationRatingInput, + ConversationRatingValue, + EmbeddingResult, + EmbeddingStart, + Generation, + GenerationExportConfig, + GenerationMode, + GenerationStart, + Message, + MessageRole, + ModelRef, + Part, + PartKind, + TokenUsage, + ToolCall, + ToolDefinition, + ToolExecutionEnd, + ToolExecutionStart, + ToolResult, + with_agent_name, + with_agent_version, + with_conversation_title, + with_user_id, +) +from sigil_sdk.internal.gen.sigil.v1 import generation_ingest_pb2 as sigil_pb2 +from sigil_sdk.internal.gen.sigil.v1 import generation_ingest_pb2_grpc as sigil_pb2_grpc + + +_metadata_conversation_title = "sigil.conversation.title" +_metadata_user_id = "sigil.user.id" +_metadata_legacy_user_id = "user.id" +_span_attr_conversation_title = "sigil.conversation.title" +_span_attr_user_id = "user.id" + + +class _CapturingGenerationServicer(sigil_pb2_grpc.GenerationIngestServiceServicer): + def __init__(self) -> None: + self.requests: list[sigil_pb2.ExportGenerationsRequest] = [] + self._lock = threading.Lock() + + def ExportGenerations(self, request, _context): # noqa: N802 + with self._lock: + self.requests.append(copy.deepcopy(request)) + return sigil_pb2.ExportGenerationsResponse( + results=[ + sigil_pb2.ExportGenerationResult(generation_id=generation.id, accepted=True) + for generation in request.generations + ] + ) + + def single_generation(self) -> sigil_pb2.Generation: + assert len(self.requests) == 1 + assert len(self.requests[0].generations) == 1 + return self.requests[0].generations[0] + + +class _RatingCaptureServer: + def __init__(self) -> None: + self.requests: list[dict[str, Any]] = [] + + class _Handler(BaseHTTPRequestHandler): + def do_POST(handler): # noqa: N802 + length = int(handler.headers.get("Content-Length", "0")) + body = handler.rfile.read(length) + self.requests.append( + { + "path": handler.path, + "headers": {k.lower(): v for k, v in handler.headers.items()}, + "payload": json.loads(body.decode("utf-8")), + } + ) + encoded = json.dumps( + { + "rating": { + "rating_id": "rat-1", + "conversation_id": "conv-rating", + "rating": "CONVERSATION_RATING_VALUE_BAD", + "created_at": "2026-03-12T09:00:00Z", + }, + "summary": { + "total_count": 1, + "good_count": 0, + "bad_count": 1, + "latest_rating": "CONVERSATION_RATING_VALUE_BAD", + "latest_rated_at": "2026-03-12T09:00:00Z", + "has_bad_rating": True, + }, + } + ).encode("utf-8") + handler.send_response(200) + handler.send_header("Content-Type", "application/json") + handler.send_header("Content-Length", str(len(encoded))) + handler.end_headers() + handler.wfile.write(encoded) + + def log_message(self, _format, *_args): # noqa: A003 + return + + self.server = HTTPServer(("127.0.0.1", 0), _Handler) + self.thread = threading.Thread(target=self.server.serve_forever, daemon=True) + self.thread.start() + + @property + def endpoint(self) -> str: + return f"http://127.0.0.1:{self.server.server_address[1]}" + + def close(self) -> None: + self.server.shutdown() + self.server.server_close() + self.thread.join(timeout=2) + + +class _ConformanceEnv: + def __init__(self, *, batch_size: int = 1, flush_interval: timedelta | None = None) -> None: + self.servicer = _CapturingGenerationServicer() + self.grpc_server = grpc.server(concurrent.futures.ThreadPoolExecutor(max_workers=2)) + sigil_pb2_grpc.add_GenerationIngestServiceServicer_to_server(self.servicer, self.grpc_server) + + sock = socket.socket() + sock.bind(("127.0.0.1", 0)) + port = sock.getsockname()[1] + sock.close() + + self.grpc_server.add_insecure_port(f"127.0.0.1:{port}") + self.grpc_server.start() + + self.rating_server = _RatingCaptureServer() + self.span_exporter = InMemorySpanExporter() + self.tracer_provider = TracerProvider() + self.tracer_provider.add_span_processor(SimpleSpanProcessor(self.span_exporter)) + self.metric_reader = InMemoryMetricReader() + self.meter_provider = MeterProvider(metric_readers=[self.metric_reader]) + + export_config = GenerationExportConfig( + protocol="grpc", + endpoint=f"127.0.0.1:{port}", + insecure=True, + batch_size=batch_size, + flush_interval=flush_interval or timedelta(hours=1), + max_retries=1, + initial_backoff=timedelta(milliseconds=1), + max_backoff=timedelta(milliseconds=2), + ) + self.client = Client( + ClientConfig( + tracer=self.tracer_provider.get_tracer("sigil-conformance-test"), + meter=self.meter_provider.get_meter("sigil-conformance-test"), + generation_export=export_config, + api=ApiConfig(endpoint=self.rating_server.endpoint), + ) + ) + self._closed = False + + def shutdown(self) -> None: + if self._closed: + return + self._closed = True + self.client.shutdown() + self.tracer_provider.shutdown() + self.meter_provider.shutdown() + self.grpc_server.stop(grace=0) + self.rating_server.close() + + def generation_span(self): + spans = [ + span + for span in self.span_exporter.get_finished_spans() + if span.attributes.get("gen_ai.operation.name") in {"generateText", "streamText"} + ] + assert spans + return spans[-1] + + def latest_span(self, operation_name: str): + spans = [ + span + for span in self.span_exporter.get_finished_spans() + if span.attributes.get("gen_ai.operation.name") == operation_name + ] + assert spans + return spans[-1] + + def metrics(self) -> dict[str, Any]: + metrics = {} + data = self.metric_reader.get_metrics_data() + for resource_metric in data.resource_metrics: + for scope_metric in resource_metric.scope_metrics: + for metric in scope_metric.metrics: + metrics[metric.name] = metric.data + return metrics + + +def test_conformance_sync_roundtrip_semantics() -> None: + env = _ConformanceEnv() + try: + recorder = env.client.start_generation( + GenerationStart( + id="gen-roundtrip", + conversation_id="conv-roundtrip", + conversation_title="Roundtrip conversation", + user_id="user-roundtrip", + agent_name="agent-roundtrip", + agent_version="v-roundtrip", + model=ModelRef(provider="openai", name="gpt-5"), + max_tokens=256, + temperature=0.2, + top_p=0.9, + tool_choice="required", + thinking_enabled=False, + tools=[ToolDefinition(name="weather", description="Get weather", type="function")], + tags={"tenant": "dev"}, + metadata={"trace": "roundtrip"}, + ) + ) + recorder.set_result( + Generation( + response_id="resp-roundtrip", + response_model="gpt-5-2026", + input=[ + Message( + role=MessageRole.USER, + parts=[Part(kind=PartKind.TEXT, text="hello")], + ) + ], + output=[ + Message( + role=MessageRole.ASSISTANT, + parts=[ + Part(kind=PartKind.THINKING, thinking="reasoning"), + Part( + kind=PartKind.TOOL_CALL, + tool_call=ToolCall(id="call-1", name="weather", input_json=b'{"city":"Paris"}'), + ), + ], + ), + Message( + role=MessageRole.TOOL, + parts=[ + Part( + kind=PartKind.TOOL_RESULT, + tool_result=ToolResult( + tool_call_id="call-1", + name="weather", + content="sunny", + content_json=b'{"temp_c":18}', + ), + ) + ], + ), + ], + usage=TokenUsage( + input_tokens=12, + output_tokens=7, + total_tokens=19, + cache_read_input_tokens=2, + cache_write_input_tokens=1, + cache_creation_input_tokens=3, + reasoning_tokens=4, + ), + stop_reason="stop", + tags={"region": "eu"}, + metadata={"result": "ok"}, + ) + ) + recorder.end() + env.shutdown() + + generation = env.servicer.single_generation() + span = env.generation_span() + metrics = env.metrics() + + assert generation.mode == sigil_pb2.GENERATION_MODE_SYNC + assert generation.operation_name == "generateText" + assert generation.conversation_id == "conv-roundtrip" + assert generation.agent_name == "agent-roundtrip" + assert generation.agent_version == "v-roundtrip" + assert generation.trace_id == span.context.trace_id.to_bytes(16, "big").hex() + assert generation.span_id == span.context.span_id.to_bytes(8, "big").hex() + assert generation.metadata.fields[_metadata_conversation_title].string_value == "Roundtrip conversation" + assert generation.metadata.fields[_metadata_user_id].string_value == "user-roundtrip" + assert generation.input[0].parts[0].text == "hello" + assert generation.output[0].parts[0].thinking == "reasoning" + assert generation.output[0].parts[1].tool_call.name == "weather" + assert generation.output[1].parts[0].tool_result.content == "sunny" + assert generation.max_tokens == 256 + assert generation.temperature == 0.2 + assert generation.top_p == 0.9 + assert generation.tool_choice == "required" + assert generation.thinking_enabled is False + assert generation.usage.input_tokens == 12 + assert generation.usage.output_tokens == 7 + assert generation.usage.total_tokens == 19 + assert generation.usage.cache_read_input_tokens == 2 + assert generation.usage.cache_write_input_tokens == 1 + assert generation.usage.reasoning_tokens == 4 + assert generation.stop_reason == "stop" + assert generation.tags["tenant"] == "dev" + assert generation.tags["region"] == "eu" + + assert span.attributes["gen_ai.operation.name"] == "generateText" + assert span.attributes[_span_attr_conversation_title] == "Roundtrip conversation" + assert span.attributes[_span_attr_user_id] == "user-roundtrip" + assert "gen_ai.client.operation.duration" in metrics + assert "gen_ai.client.token.usage" in metrics + assert "gen_ai.client.time_to_first_token" not in metrics + finally: + env.shutdown() + + +def test_conformance_conversation_title_semantics() -> None: + cases = [ + ("explicit wins", "Explicit", "Context", "Meta", "Explicit"), + ("context fallback", "", "Context", "", "Context"), + ("metadata fallback", "", "", "Meta", "Meta"), + ("whitespace trimmed", " Padded ", "", "", "Padded"), + ("whitespace omitted", " ", "", "", ""), + ] + + for _, start_title, context_title, metadata_title, want_title in cases: + env = _ConformanceEnv() + try: + context = with_conversation_title(context_title) if context_title else nullcontext() + with context: + start = GenerationStart( + model=ModelRef(provider="openai", name="gpt-5"), + conversation_title=start_title, + metadata={_metadata_conversation_title: metadata_title} if metadata_title else {}, + ) + recorder = env.client.start_generation(start) + recorder.set_result(Generation()) + recorder.end() + env.shutdown() + + generation = env.servicer.single_generation() + span = env.generation_span() + field = generation.metadata.fields.get(_metadata_conversation_title) + if want_title == "": + assert field is None + assert _span_attr_conversation_title not in span.attributes + else: + assert field is not None + assert field.string_value == want_title + assert span.attributes[_span_attr_conversation_title] == want_title + finally: + env.shutdown() + + +def test_conformance_user_id_semantics() -> None: + cases = [ + ("explicit wins", "explicit", "ctx", "canonical", "legacy", "explicit"), + ("context fallback", "", "ctx", "", "", "ctx"), + ("canonical metadata", "", "", "canonical", "", "canonical"), + ("legacy metadata", "", "", "", "legacy", "legacy"), + ("canonical beats legacy", "", "", "canonical", "legacy", "canonical"), + ("whitespace trimmed", " padded ", "", "", "", "padded"), + ] + + for _, start_user_id, context_user_id, canonical_user_id, legacy_user_id, want_user_id in cases: + env = _ConformanceEnv() + try: + metadata = {} + if canonical_user_id: + metadata[_metadata_user_id] = canonical_user_id + if legacy_user_id: + metadata[_metadata_legacy_user_id] = legacy_user_id + + context = with_user_id(context_user_id) if context_user_id else nullcontext() + with context: + recorder = env.client.start_generation( + GenerationStart( + model=ModelRef(provider="openai", name="gpt-5"), + user_id=start_user_id, + metadata=metadata, + ) + ) + recorder.set_result(Generation()) + recorder.end() + env.shutdown() + + generation = env.servicer.single_generation() + span = env.generation_span() + assert generation.metadata.fields[_metadata_user_id].string_value == want_user_id + assert span.attributes[_span_attr_user_id] == want_user_id + finally: + env.shutdown() + + +def test_conformance_agent_identity_semantics() -> None: + cases = [ + ("explicit fields", "agent-explicit", "v1.2.3", "", "", "", "", "agent-explicit", "v1.2.3"), + ("context fallback", "", "", "agent-context", "v-context", "", "", "agent-context", "v-context"), + ("result-time override", "agent-seed", "v-seed", "", "", "agent-result", "v-result", "agent-result", "v-result"), + ("empty omission", "", "", "", "", "", "", "", ""), + ] + + for _, start_name, start_version, context_name, context_version, result_name, result_version, want_name, want_version in cases: + env = _ConformanceEnv() + try: + with with_agent_name(context_name) if context_name else nullcontext(): + with with_agent_version(context_version) if context_version else nullcontext(): + recorder = env.client.start_generation( + GenerationStart( + model=ModelRef(provider="openai", name="gpt-5"), + agent_name=start_name, + agent_version=start_version, + ) + ) + recorder.set_result( + Generation( + agent_name=result_name, + agent_version=result_version, + ) + ) + recorder.end() + env.shutdown() + + generation = env.servicer.single_generation() + span = env.generation_span() + assert generation.agent_name == want_name + assert generation.agent_version == want_version + if want_name: + assert span.attributes["gen_ai.agent.name"] == want_name + else: + assert "gen_ai.agent.name" not in span.attributes + if want_version: + assert span.attributes["gen_ai.agent.version"] == want_version + else: + assert "gen_ai.agent.version" not in span.attributes + finally: + env.shutdown() + + +def test_conformance_streaming_telemetry_semantics() -> None: + env = _ConformanceEnv() + try: + start = GenerationStart(model=ModelRef(provider="openai", name="gpt-5")) + recorder = env.client.start_streaming_generation(start) + recorder.set_first_token_at(datetime(2026, 3, 12, 9, 0, 0, 250000, tzinfo=timezone.utc)) + recorder.set_result( + Generation( + output=[ + Message( + role=MessageRole.ASSISTANT, + parts=[Part(kind=PartKind.TEXT, text="Hello world")], + ) + ], + usage=TokenUsage(input_tokens=4, output_tokens=3, total_tokens=7), + started_at=datetime(2026, 3, 12, 9, 0, 0, tzinfo=timezone.utc), + completed_at=datetime(2026, 3, 12, 9, 0, 1, tzinfo=timezone.utc), + ) + ) + recorder.end() + env.shutdown() + + generation = env.servicer.single_generation() + span = env.generation_span() + metrics = env.metrics() + + assert generation.mode == sigil_pb2.GENERATION_MODE_STREAM + assert generation.operation_name == "streamText" + assert generation.output[0].parts[0].text == "Hello world" + assert span.name == "streamText gpt-5" + assert "gen_ai.client.operation.duration" in metrics + assert "gen_ai.client.time_to_first_token" in metrics + finally: + env.shutdown() + + +def test_conformance_tool_execution_semantics() -> None: + env = _ConformanceEnv() + try: + with with_conversation_title("Context title"): + with with_agent_name("agent-context"): + with with_agent_version("v-context"): + recorder = env.client.start_tool_execution( + ToolExecutionStart( + tool_name="weather", + tool_call_id="call-weather-1", + tool_type="function", + include_content=True, + ) + ) + recorder.set_result( + ToolExecutionEnd( + arguments={"city": "Paris"}, + result={"forecast": "sunny"}, + ) + ) + recorder.end() + env.shutdown() + + span = env.latest_span("execute_tool") + metrics = env.metrics() + + assert env.servicer.requests == [] + assert span.name == "execute_tool weather" + assert span.attributes["gen_ai.operation.name"] == "execute_tool" + assert span.attributes["gen_ai.tool.name"] == "weather" + assert span.attributes["gen_ai.tool.call.id"] == "call-weather-1" + assert span.attributes["gen_ai.tool.type"] == "function" + assert "Paris" in str(span.attributes["gen_ai.tool.call.arguments"]) + assert "sunny" in str(span.attributes["gen_ai.tool.call.result"]) + assert span.attributes[_span_attr_conversation_title] == "Context title" + assert span.attributes["gen_ai.agent.name"] == "agent-context" + assert span.attributes["gen_ai.agent.version"] == "v-context" + assert "gen_ai.client.operation.duration" in metrics + assert "gen_ai.client.time_to_first_token" not in metrics + finally: + env.shutdown() + + +def test_conformance_embedding_semantics() -> None: + env = _ConformanceEnv() + try: + with with_agent_name("agent-context"): + with with_agent_version("v-context"): + recorder = env.client.start_embedding( + EmbeddingStart( + model=ModelRef(provider="openai", name="text-embedding-3-small"), + dimensions=512, + ) + ) + recorder.set_result( + EmbeddingResult( + input_count=2, + input_tokens=8, + input_texts=["hello", "world"], + response_model="text-embedding-3-small", + dimensions=512, + ) + ) + recorder.end() + env.shutdown() + + span = env.latest_span("embeddings") + metrics = env.metrics() + + assert env.servicer.requests == [] + assert span.name == "embeddings text-embedding-3-small" + assert span.attributes["gen_ai.operation.name"] == "embeddings" + assert span.attributes["gen_ai.agent.name"] == "agent-context" + assert span.attributes["gen_ai.agent.version"] == "v-context" + assert span.attributes["gen_ai.embeddings.input_count"] == 2 + assert span.attributes["gen_ai.embeddings.dimension.count"] == 512 + assert span.attributes["gen_ai.response.model"] == "text-embedding-3-small" + assert "gen_ai.client.operation.duration" in metrics + assert "gen_ai.client.token.usage" in metrics + assert "gen_ai.client.time_to_first_token" not in metrics + assert "gen_ai.client.tool_calls_per_operation" not in metrics + finally: + env.shutdown() + + +def test_conformance_validation_and_error_semantics() -> None: + env = _ConformanceEnv() + try: + invalid = env.client.start_generation( + GenerationStart(model=ModelRef(provider="anthropic", name="claude-sonnet-4-5")) + ) + invalid.set_result( + Generation( + input=[ + Message( + role=MessageRole.USER, + parts=[Part(kind=PartKind.TOOL_CALL, tool_call=ToolCall(name="weather"))], + ) + ] + ) + ) + invalid.end() + + assert invalid.err() is not None + assert env.servicer.requests == [] + assert env.generation_span().attributes["error.type"] == "validation_error" + + call_error = env.client.start_generation( + GenerationStart(model=ModelRef(provider="openai", name="gpt-5")) + ) + call_error.set_call_error(RuntimeError("provider unavailable")) + call_error.set_result(Generation()) + call_error.end() + env.shutdown() + + generation = env.servicer.single_generation() + spans = env.span_exporter.get_finished_spans() + assert call_error.err() is None + assert generation.call_error == "provider unavailable" + assert generation.metadata.fields["call_error"].string_value == "provider unavailable" + assert spans[-1].attributes["error.type"] == "provider_call_error" + finally: + env.shutdown() + + +def test_conformance_rating_submission_semantics() -> None: + env = _ConformanceEnv() + try: + response = env.client.submit_conversation_rating( + "conv-rating", + ConversationRatingInput( + rating_id="rat-1", + rating=ConversationRatingValue.BAD, + comment="wrong answer", + metadata={"channel": "assistant"}, + ), + ) + env.shutdown() + + assert len(env.rating_server.requests) == 1 + request = env.rating_server.requests[0] + assert request["path"] == "/api/v1/conversations/conv-rating/ratings" + assert request["payload"] == { + "rating_id": "rat-1", + "rating": "CONVERSATION_RATING_VALUE_BAD", + "comment": "wrong answer", + "metadata": {"channel": "assistant"}, + } + assert response.rating.conversation_id == "conv-rating" + assert response.summary.bad_count == 1 + finally: + env.shutdown() + + +def test_conformance_shutdown_flush_semantics() -> None: + env = _ConformanceEnv(batch_size=10) + try: + recorder = env.client.start_generation( + GenerationStart( + conversation_id="conv-shutdown", + agent_name="agent-shutdown", + agent_version="v-shutdown", + model=ModelRef(provider="openai", name="gpt-5"), + ) + ) + recorder.set_result(Generation()) + recorder.end() + + assert env.servicer.requests == [] + env.shutdown() + + generation = env.servicer.single_generation() + assert generation.conversation_id == "conv-shutdown" + assert generation.agent_name == "agent-shutdown" + assert generation.agent_version == "v-shutdown" + finally: + env.shutdown() diff --git a/python/uv.lock b/python/uv.lock new file mode 100644 index 0000000..b5119bd --- /dev/null +++ b/python/uv.lock @@ -0,0 +1,592 @@ +version = 1 +revision = 3 +requires-python = ">=3.10" +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version == '3.13.*'", + "python_full_version < '3.13'", +] + +[[package]] +name = "certifi" +version = "2026.2.25" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/35/02daf95b9cd686320bb622eb148792655c9412dbb9b67abb5694e5910a24/charset_normalizer-3.4.5.tar.gz", hash = "sha256:95adae7b6c42a6c5b5b559b1a99149f090a57128155daeea91732c8d970d8644", size = 134804, upload-time = "2026-03-06T06:03:19.46Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/21/a2b1505639008ba2e6ef03733a81fc6cfd6a07ea6139a2b76421230b8dad/charset_normalizer-3.4.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4167a621a9a1a986c73777dbc15d4b5eac8ac5c10393374109a343d4013ec765", size = 283319, upload-time = "2026-03-06T06:00:26.433Z" }, + { url = "https://files.pythonhosted.org/packages/70/67/df234c29b68f4e1e095885c9db1cb4b69b8aba49cf94fac041db4aaf1267/charset_normalizer-3.4.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3f64c6bf8f32f9133b668c7f7a7cbdbc453412bc95ecdbd157f3b1e377a92990", size = 189974, upload-time = "2026-03-06T06:00:28.222Z" }, + { url = "https://files.pythonhosted.org/packages/df/7f/fc66af802961c6be42e2c7b69c58f95cbd1f39b0e81b3365d8efe2a02a04/charset_normalizer-3.4.5-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:568e3c34b58422075a1b49575a6abc616d9751b4d61b23f712e12ebb78fe47b2", size = 207866, upload-time = "2026-03-06T06:00:29.769Z" }, + { url = "https://files.pythonhosted.org/packages/c9/23/404eb36fac4e95b833c50e305bba9a241086d427bb2167a42eac7c4f7da4/charset_normalizer-3.4.5-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:036c079aa08a6a592b82487f97c60b439428320ed1b2ea0b3912e99d30c77765", size = 203239, upload-time = "2026-03-06T06:00:31.086Z" }, + { url = "https://files.pythonhosted.org/packages/4b/2f/8a1d989bfadd120c90114ab33e0d2a0cbde05278c1fc15e83e62d570f50a/charset_normalizer-3.4.5-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:340810d34ef83af92148e96e3e44cb2d3f910d2bf95e5618a5c467d9f102231d", size = 196529, upload-time = "2026-03-06T06:00:32.608Z" }, + { url = "https://files.pythonhosted.org/packages/a5/0c/c75f85ff7ca1f051958bb518cd43922d86f576c03947a050fbedfdfb4f15/charset_normalizer-3.4.5-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:cd2d0f0ec9aa977a27731a3209ebbcacebebaf41f902bd453a928bfd281cf7f8", size = 184152, upload-time = "2026-03-06T06:00:33.93Z" }, + { url = "https://files.pythonhosted.org/packages/f9/20/4ed37f6199af5dde94d4aeaf577f3813a5ec6635834cda1d957013a09c76/charset_normalizer-3.4.5-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0b362bcd27819f9c07cbf23db4e0e8cd4b44c5ecd900c2ff907b2b92274a7412", size = 195226, upload-time = "2026-03-06T06:00:35.469Z" }, + { url = "https://files.pythonhosted.org/packages/28/31/7ba1102178cba7c34dcc050f43d427172f389729e356038f0726253dd914/charset_normalizer-3.4.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:77be992288f720306ab4108fe5c74797de327f3248368dfc7e1a916d6ed9e5a2", size = 192933, upload-time = "2026-03-06T06:00:36.83Z" }, + { url = "https://files.pythonhosted.org/packages/4b/23/f86443ab3921e6a60b33b93f4a1161222231f6c69bc24fb18f3bee7b8518/charset_normalizer-3.4.5-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:8b78d8a609a4b82c273257ee9d631ded7fac0d875bdcdccc109f3ee8328cfcb1", size = 185647, upload-time = "2026-03-06T06:00:38.367Z" }, + { url = "https://files.pythonhosted.org/packages/82/44/08b8be891760f1f5a6d23ce11d6d50c92981603e6eb740b4f72eea9424e2/charset_normalizer-3.4.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ba20bdf69bd127f66d0174d6f2a93e69045e0b4036dc1ca78e091bcc765830c4", size = 209533, upload-time = "2026-03-06T06:00:41.931Z" }, + { url = "https://files.pythonhosted.org/packages/3b/5f/df114f23406199f8af711ddccfbf409ffbc5b7cdc18fa19644997ff0c9bb/charset_normalizer-3.4.5-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:76a9d0de4d0eab387822e7b35d8f89367dd237c72e82ab42b9f7bf5e15ada00f", size = 195901, upload-time = "2026-03-06T06:00:43.978Z" }, + { url = "https://files.pythonhosted.org/packages/07/83/71ef34a76fe8aa05ff8f840244bda2d61e043c2ef6f30d200450b9f6a1be/charset_normalizer-3.4.5-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8fff79bf5978c693c9b1a4d71e4a94fddfb5fe744eb062a318e15f4a2f63a550", size = 204950, upload-time = "2026-03-06T06:00:45.202Z" }, + { url = "https://files.pythonhosted.org/packages/58/40/0253be623995365137d7dc68e45245036207ab2227251e69a3d93ce43183/charset_normalizer-3.4.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c7e84e0c0005e3bdc1a9211cd4e62c78ba80bc37b2365ef4410cd2007a9047f2", size = 198546, upload-time = "2026-03-06T06:00:46.481Z" }, + { url = "https://files.pythonhosted.org/packages/ed/5c/5f3cb5b259a130895ef5ae16b38eaf141430fa3f7af50cd06c5d67e4f7b2/charset_normalizer-3.4.5-cp310-cp310-win32.whl", hash = "sha256:58ad8270cfa5d4bef1bc85bd387217e14ff154d6630e976c6f56f9a040757475", size = 132516, upload-time = "2026-03-06T06:00:47.924Z" }, + { url = "https://files.pythonhosted.org/packages/a5/c3/84fb174e7770f2df2e1a2115090771bfbc2227fb39a765c6d00568d1aab4/charset_normalizer-3.4.5-cp310-cp310-win_amd64.whl", hash = "sha256:02a9d1b01c1e12c27883b0c9349e0bcd9ae92e727ff1a277207e1a262b1cbf05", size = 142906, upload-time = "2026-03-06T06:00:49.389Z" }, + { url = "https://files.pythonhosted.org/packages/d7/b2/6f852f8b969f2cbd0d4092d2e60139ab1af95af9bb651337cae89ec0f684/charset_normalizer-3.4.5-cp310-cp310-win_arm64.whl", hash = "sha256:039215608ac7b358c4da0191d10fc76868567fbf276d54c14721bdedeb6de064", size = 133258, upload-time = "2026-03-06T06:00:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/8f/9e/bcec3b22c64ecec47d39bf5167c2613efd41898c019dccd4183f6aa5d6a7/charset_normalizer-3.4.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:610f72c0ee565dfb8ae1241b666119582fdbfe7c0975c175be719f940e110694", size = 279531, upload-time = "2026-03-06T06:00:52.252Z" }, + { url = "https://files.pythonhosted.org/packages/58/12/81fd25f7e7078ab5d1eedbb0fac44be4904ae3370a3bf4533c8f2d159acd/charset_normalizer-3.4.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60d68e820af339df4ae8358c7a2e7596badeb61e544438e489035f9fbf3246a5", size = 188006, upload-time = "2026-03-06T06:00:53.8Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6e/f2d30e8c27c1b0736a6520311982cf5286cfc7f6cac77d7bc1325e3a23f2/charset_normalizer-3.4.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:10b473fc8dca1c3ad8559985794815f06ca3fc71942c969129070f2c3cdf7281", size = 205085, upload-time = "2026-03-06T06:00:55.311Z" }, + { url = "https://files.pythonhosted.org/packages/d0/90/d12cefcb53b5931e2cf792a33718d7126efb116a320eaa0742c7059a95e4/charset_normalizer-3.4.5-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d4eb8ac7469b2a5d64b5b8c04f84d8bf3ad340f4514b98523805cbf46e3b3923", size = 200545, upload-time = "2026-03-06T06:00:56.532Z" }, + { url = "https://files.pythonhosted.org/packages/03/f4/44d3b830a20e89ff82a3134912d9a1cf6084d64f3b95dcad40f74449a654/charset_normalizer-3.4.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bcb3227c3d9aaf73eaaab1db7ccd80a8995c509ee9941e2aae060ca6e4e5d81", size = 193863, upload-time = "2026-03-06T06:00:57.823Z" }, + { url = "https://files.pythonhosted.org/packages/25/4b/f212119c18a6320a9d4a730d1b4057875cdeabf21b3614f76549042ef8a8/charset_normalizer-3.4.5-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:75ee9c1cce2911581a70a3c0919d8bccf5b1cbc9b0e5171400ec736b4b569497", size = 181827, upload-time = "2026-03-06T06:00:59.323Z" }, + { url = "https://files.pythonhosted.org/packages/74/00/b26158e48b425a202a92965f8069e8a63d9af1481dfa206825d7f74d2a3c/charset_normalizer-3.4.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d1401945cb77787dbd3af2446ff2d75912327c4c3a1526ab7955ecf8600687c", size = 191085, upload-time = "2026-03-06T06:01:00.546Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c2/1c1737bf6fd40335fe53d28fe49afd99ee4143cc57a845e99635ce0b9b6d/charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a45e504f5e1be0bd385935a8e1507c442349ca36f511a47057a71c9d1d6ea9e", size = 190688, upload-time = "2026-03-06T06:01:02.479Z" }, + { url = "https://files.pythonhosted.org/packages/5a/3d/abb5c22dc2ef493cd56522f811246a63c5427c08f3e3e50ab663de27fcf4/charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:e09f671a54ce70b79a1fc1dc6da3072b7ef7251fadb894ed92d9aa8218465a5f", size = 183077, upload-time = "2026-03-06T06:01:04.231Z" }, + { url = "https://files.pythonhosted.org/packages/44/33/5298ad4d419a58e25b3508e87f2758d1442ff00c2471f8e0403dab8edad5/charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d01de5e768328646e6a3fa9e562706f8f6641708c115c62588aef2b941a4f88e", size = 206706, upload-time = "2026-03-06T06:01:05.773Z" }, + { url = "https://files.pythonhosted.org/packages/7b/17/51e7895ac0f87c3b91d276a449ef09f5532a7529818f59646d7a55089432/charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:131716d6786ad5e3dc542f5cc6f397ba3339dc0fb87f87ac30e550e8987756af", size = 191665, upload-time = "2026-03-06T06:01:07.473Z" }, + { url = "https://files.pythonhosted.org/packages/90/8f/cce9adf1883e98906dbae380d769b4852bb0fa0004bc7d7a2243418d3ea8/charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1a374cc0b88aa710e8865dc1bd6edb3743c59f27830f0293ab101e4cf3ce9f85", size = 201950, upload-time = "2026-03-06T06:01:08.973Z" }, + { url = "https://files.pythonhosted.org/packages/08/ca/bce99cd5c397a52919e2769d126723f27a4c037130374c051c00470bcd38/charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d31f0d1671e1534e395f9eb84a68e0fb670e1edb1fe819a9d7f564ae3bc4e53f", size = 195830, upload-time = "2026-03-06T06:01:10.155Z" }, + { url = "https://files.pythonhosted.org/packages/87/4f/2e3d023a06911f1281f97b8f036edc9872167036ca6f55cc874a0be6c12c/charset_normalizer-3.4.5-cp311-cp311-win32.whl", hash = "sha256:cace89841c0599d736d3d74a27bc5821288bb47c5441923277afc6059d7fbcb4", size = 132029, upload-time = "2026-03-06T06:01:11.706Z" }, + { url = "https://files.pythonhosted.org/packages/fe/1f/a853b73d386521fd44b7f67ded6b17b7b2367067d9106a5c4b44f9a34274/charset_normalizer-3.4.5-cp311-cp311-win_amd64.whl", hash = "sha256:f8102ae93c0bc863b1d41ea0f4499c20a83229f52ed870850892df555187154a", size = 142404, upload-time = "2026-03-06T06:01:12.865Z" }, + { url = "https://files.pythonhosted.org/packages/b4/10/dba36f76b71c38e9d391abe0fd8a5b818790e053c431adecfc98c35cd2a9/charset_normalizer-3.4.5-cp311-cp311-win_arm64.whl", hash = "sha256:ed98364e1c262cf5f9363c3eca8c2df37024f52a8fa1180a3610014f26eac51c", size = 132796, upload-time = "2026-03-06T06:01:14.106Z" }, + { url = "https://files.pythonhosted.org/packages/9c/b6/9ee9c1a608916ca5feae81a344dffbaa53b26b90be58cc2159e3332d44ec/charset_normalizer-3.4.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ed97c282ee4f994ef814042423a529df9497e3c666dca19be1d4cd1129dc7ade", size = 280976, upload-time = "2026-03-06T06:01:15.276Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d8/a54f7c0b96f1df3563e9190f04daf981e365a9b397eedfdfb5dbef7e5c6c/charset_normalizer-3.4.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0294916d6ccf2d069727d65973c3a1ca477d68708db25fd758dd28b0827cff54", size = 189356, upload-time = "2026-03-06T06:01:16.511Z" }, + { url = "https://files.pythonhosted.org/packages/42/69/2bf7f76ce1446759a5787cb87d38f6a61eb47dbbdf035cfebf6347292a65/charset_normalizer-3.4.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dc57a0baa3eeedd99fafaef7511b5a6ef4581494e8168ee086031744e2679467", size = 206369, upload-time = "2026-03-06T06:01:17.853Z" }, + { url = "https://files.pythonhosted.org/packages/10/9c/949d1a46dab56b959d9a87272482195f1840b515a3380e39986989a893ae/charset_normalizer-3.4.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ed1a9a204f317ef879b32f9af507d47e49cd5e7f8e8d5d96358c98373314fc60", size = 203285, upload-time = "2026-03-06T06:01:19.473Z" }, + { url = "https://files.pythonhosted.org/packages/67/5c/ae30362a88b4da237d71ea214a8c7eb915db3eec941adda511729ac25fa2/charset_normalizer-3.4.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7ad83b8f9379176c841f8865884f3514d905bcd2a9a3b210eaa446e7d2223e4d", size = 196274, upload-time = "2026-03-06T06:01:20.728Z" }, + { url = "https://files.pythonhosted.org/packages/b2/07/c9f2cb0e46cb6d64fdcc4f95953747b843bb2181bda678dc4e699b8f0f9a/charset_normalizer-3.4.5-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:a118e2e0b5ae6b0120d5efa5f866e58f2bb826067a646431da4d6a2bdae7950e", size = 184715, upload-time = "2026-03-06T06:01:22.194Z" }, + { url = "https://files.pythonhosted.org/packages/36/64/6b0ca95c44fddf692cd06d642b28f63009d0ce325fad6e9b2b4d0ef86a52/charset_normalizer-3.4.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:754f96058e61a5e22e91483f823e07df16416ce76afa4ebf306f8e1d1296d43f", size = 193426, upload-time = "2026-03-06T06:01:23.795Z" }, + { url = "https://files.pythonhosted.org/packages/50/bc/a730690d726403743795ca3f5bb2baf67838c5fea78236098f324b965e40/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0c300cefd9b0970381a46394902cd18eaf2aa00163f999590ace991989dcd0fc", size = 191780, upload-time = "2026-03-06T06:01:25.053Z" }, + { url = "https://files.pythonhosted.org/packages/97/4f/6c0bc9af68222b22951552d73df4532b5be6447cee32d58e7e8c74ecbb7b/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c108f8619e504140569ee7de3f97d234f0fbae338a7f9f360455071ef9855a95", size = 185805, upload-time = "2026-03-06T06:01:26.294Z" }, + { url = "https://files.pythonhosted.org/packages/dd/b9/a523fb9b0ee90814b503452b2600e4cbc118cd68714d57041564886e7325/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:d1028de43596a315e2720a9849ee79007ab742c06ad8b45a50db8cdb7ed4a82a", size = 208342, upload-time = "2026-03-06T06:01:27.55Z" }, + { url = "https://files.pythonhosted.org/packages/4d/61/c59e761dee4464050713e50e27b58266cc8e209e518c0b378c1580c959ba/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:19092dde50335accf365cce21998a1c6dd8eafd42c7b226eb54b2747cdce2fac", size = 193661, upload-time = "2026-03-06T06:01:29.051Z" }, + { url = "https://files.pythonhosted.org/packages/1c/43/729fa30aad69783f755c5ad8649da17ee095311ca42024742701e202dc59/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4354e401eb6dab9aed3c7b4030514328a6c748d05e1c3e19175008ca7de84fb1", size = 204819, upload-time = "2026-03-06T06:01:30.298Z" }, + { url = "https://files.pythonhosted.org/packages/87/33/d9b442ce5a91b96fc0840455a9e49a611bbadae6122778d0a6a79683dd31/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a68766a3c58fde7f9aaa22b3786276f62ab2f594efb02d0a1421b6282e852e98", size = 198080, upload-time = "2026-03-06T06:01:31.478Z" }, + { url = "https://files.pythonhosted.org/packages/56/5a/b8b5a23134978ee9885cee2d6995f4c27cc41f9baded0a9685eabc5338f0/charset_normalizer-3.4.5-cp312-cp312-win32.whl", hash = "sha256:1827734a5b308b65ac54e86a618de66f935a4f63a8a462ff1e19a6788d6c2262", size = 132630, upload-time = "2026-03-06T06:01:33.056Z" }, + { url = "https://files.pythonhosted.org/packages/70/53/e44a4c07e8904500aec95865dc3f6464dc3586a039ef0df606eb3ac38e35/charset_normalizer-3.4.5-cp312-cp312-win_amd64.whl", hash = "sha256:728c6a963dfab66ef865f49286e45239384249672cd598576765acc2a640a636", size = 142856, upload-time = "2026-03-06T06:01:34.489Z" }, + { url = "https://files.pythonhosted.org/packages/ea/aa/c5628f7cad591b1cf45790b7a61483c3e36cf41349c98af7813c483fd6e8/charset_normalizer-3.4.5-cp312-cp312-win_arm64.whl", hash = "sha256:75dfd1afe0b1647449e852f4fb428195a7ed0588947218f7ba929f6538487f02", size = 132982, upload-time = "2026-03-06T06:01:35.641Z" }, + { url = "https://files.pythonhosted.org/packages/f5/48/9f34ec4bb24aa3fdba1890c1bddb97c8a4be1bd84ef5c42ac2352563ad05/charset_normalizer-3.4.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ac59c15e3f1465f722607800c68713f9fbc2f672b9eb649fe831da4019ae9b23", size = 280788, upload-time = "2026-03-06T06:01:37.126Z" }, + { url = "https://files.pythonhosted.org/packages/0e/09/6003e7ffeb90cc0560da893e3208396a44c210c5ee42efff539639def59b/charset_normalizer-3.4.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:165c7b21d19365464e8f70e5ce5e12524c58b48c78c1f5a57524603c1ab003f8", size = 188890, upload-time = "2026-03-06T06:01:38.73Z" }, + { url = "https://files.pythonhosted.org/packages/42/1e/02706edf19e390680daa694d17e2b8eab4b5f7ac285e2a51168b4b22ee6b/charset_normalizer-3.4.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:28269983f25a4da0425743d0d257a2d6921ea7d9b83599d4039486ec5b9f911d", size = 206136, upload-time = "2026-03-06T06:01:40.016Z" }, + { url = "https://files.pythonhosted.org/packages/c7/87/942c3def1b37baf3cf786bad01249190f3ca3d5e63a84f831e704977de1f/charset_normalizer-3.4.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d27ce22ec453564770d29d03a9506d449efbb9fa13c00842262b2f6801c48cce", size = 202551, upload-time = "2026-03-06T06:01:41.522Z" }, + { url = "https://files.pythonhosted.org/packages/94/0a/af49691938dfe175d71b8a929bd7e4ace2809c0c5134e28bc535660d5262/charset_normalizer-3.4.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0625665e4ebdddb553ab185de5db7054393af8879fb0c87bd5690d14379d6819", size = 195572, upload-time = "2026-03-06T06:01:43.208Z" }, + { url = "https://files.pythonhosted.org/packages/20/ea/dfb1792a8050a8e694cfbde1570ff97ff74e48afd874152d38163d1df9ae/charset_normalizer-3.4.5-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:c23eb3263356d94858655b3e63f85ac5d50970c6e8febcdde7830209139cc37d", size = 184438, upload-time = "2026-03-06T06:01:44.755Z" }, + { url = "https://files.pythonhosted.org/packages/72/12/c281e2067466e3ddd0595bfaea58a6946765ace5c72dfa3edc2f5f118026/charset_normalizer-3.4.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e6302ca4ae283deb0af68d2fbf467474b8b6aedcd3dab4db187e07f94c109763", size = 193035, upload-time = "2026-03-06T06:01:46.051Z" }, + { url = "https://files.pythonhosted.org/packages/ba/4f/3792c056e7708e10464bad0438a44708886fb8f92e3c3d29ec5e2d964d42/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e51ae7d81c825761d941962450f50d041db028b7278e7b08930b4541b3e45cb9", size = 191340, upload-time = "2026-03-06T06:01:47.547Z" }, + { url = "https://files.pythonhosted.org/packages/e7/86/80ddba897127b5c7a9bccc481b0cd36c8fefa485d113262f0fe4332f0bf4/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:597d10dec876923e5c59e48dbd366e852eacb2b806029491d307daea6b917d7c", size = 185464, upload-time = "2026-03-06T06:01:48.764Z" }, + { url = "https://files.pythonhosted.org/packages/4d/00/b5eff85ba198faacab83e0e4b6f0648155f072278e3b392a82478f8b988b/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5cffde4032a197bd3b42fd0b9509ec60fb70918d6970e4cc773f20fc9180ca67", size = 208014, upload-time = "2026-03-06T06:01:50.371Z" }, + { url = "https://files.pythonhosted.org/packages/c8/11/d36f70be01597fd30850dde8a1269ebc8efadd23ba5785808454f2389bde/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2da4eedcb6338e2321e831a0165759c0c620e37f8cd044a263ff67493be8ffb3", size = 193297, upload-time = "2026-03-06T06:01:51.933Z" }, + { url = "https://files.pythonhosted.org/packages/1a/1d/259eb0a53d4910536c7c2abb9cb25f4153548efb42800c6a9456764649c0/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:65a126fb4b070d05340a84fc709dd9e7c75d9b063b610ece8a60197a291d0adf", size = 204321, upload-time = "2026-03-06T06:01:53.887Z" }, + { url = "https://files.pythonhosted.org/packages/84/31/faa6c5b9d3688715e1ed1bb9d124c384fe2fc1633a409e503ffe1c6398c1/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c7a80a9242963416bd81f99349d5f3fce1843c303bd404f204918b6d75a75fd6", size = 197509, upload-time = "2026-03-06T06:01:56.439Z" }, + { url = "https://files.pythonhosted.org/packages/fd/a5/c7d9dd1503ffc08950b3260f5d39ec2366dd08254f0900ecbcf3a6197c7c/charset_normalizer-3.4.5-cp313-cp313-win32.whl", hash = "sha256:f1d725b754e967e648046f00c4facc42d414840f5ccc670c5670f59f83693e4f", size = 132284, upload-time = "2026-03-06T06:01:57.812Z" }, + { url = "https://files.pythonhosted.org/packages/b9/0f/57072b253af40c8aa6636e6de7d75985624c1eb392815b2f934199340a89/charset_normalizer-3.4.5-cp313-cp313-win_amd64.whl", hash = "sha256:e37bd100d2c5d3ba35db9c7c5ba5a9228cbcffe5c4778dc824b164e5257813d7", size = 142630, upload-time = "2026-03-06T06:01:59.062Z" }, + { url = "https://files.pythonhosted.org/packages/31/41/1c4b7cc9f13bd9d369ce3bc993e13d374ce25fa38a2663644283ecf422c1/charset_normalizer-3.4.5-cp313-cp313-win_arm64.whl", hash = "sha256:93b3b2cc5cf1b8743660ce77a4f45f3f6d1172068207c1defc779a36eea6bb36", size = 133254, upload-time = "2026-03-06T06:02:00.281Z" }, + { url = "https://files.pythonhosted.org/packages/43/be/0f0fd9bb4a7fa4fb5067fb7d9ac693d4e928d306f80a0d02bde43a7c4aee/charset_normalizer-3.4.5-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8197abe5ca1ffb7d91e78360f915eef5addff270f8a71c1fc5be24a56f3e4873", size = 280232, upload-time = "2026-03-06T06:02:01.508Z" }, + { url = "https://files.pythonhosted.org/packages/28/02/983b5445e4bef49cd8c9da73a8e029f0825f39b74a06d201bfaa2e55142a/charset_normalizer-3.4.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2aecdb364b8a1802afdc7f9327d55dad5366bc97d8502d0f5854e50712dbc5f", size = 189688, upload-time = "2026-03-06T06:02:02.857Z" }, + { url = "https://files.pythonhosted.org/packages/d0/88/152745c5166437687028027dc080e2daed6fe11cfa95a22f4602591c42db/charset_normalizer-3.4.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a66aa5022bf81ab4b1bebfb009db4fd68e0c6d4307a1ce5ef6a26e5878dfc9e4", size = 206833, upload-time = "2026-03-06T06:02:05.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0f/ebc15c8b02af2f19be9678d6eed115feeeccc45ce1f4b098d986c13e8769/charset_normalizer-3.4.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d77f97e515688bd615c1d1f795d540f32542d514242067adcb8ef532504cb9ee", size = 202879, upload-time = "2026-03-06T06:02:06.446Z" }, + { url = "https://files.pythonhosted.org/packages/38/9c/71336bff6934418dc8d1e8a1644176ac9088068bc571da612767619c97b3/charset_normalizer-3.4.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01a1ed54b953303ca7e310fafe0fe347aab348bd81834a0bcd602eb538f89d66", size = 195764, upload-time = "2026-03-06T06:02:08.763Z" }, + { url = "https://files.pythonhosted.org/packages/b7/95/ce92fde4f98615661871bc282a856cf9b8a15f686ba0af012984660d480b/charset_normalizer-3.4.5-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:b2d37d78297b39a9eb9eb92c0f6df98c706467282055419df141389b23f93362", size = 183728, upload-time = "2026-03-06T06:02:10.137Z" }, + { url = "https://files.pythonhosted.org/packages/1c/e7/f5b4588d94e747ce45ae680f0f242bc2d98dbd4eccfab73e6160b6893893/charset_normalizer-3.4.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e71bbb595973622b817c042bd943c3f3667e9c9983ce3d205f973f486fec98a7", size = 192937, upload-time = "2026-03-06T06:02:11.663Z" }, + { url = "https://files.pythonhosted.org/packages/f9/29/9d94ed6b929bf9f48bf6ede6e7474576499f07c4c5e878fb186083622716/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4cd966c2559f501c6fd69294d082c2934c8dd4719deb32c22961a5ac6db0df1d", size = 192040, upload-time = "2026-03-06T06:02:13.489Z" }, + { url = "https://files.pythonhosted.org/packages/15/d2/1a093a1cf827957f9445f2fe7298bcc16f8fc5e05c1ed2ad1af0b239035e/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d5e52d127045d6ae01a1e821acfad2f3a1866c54d0e837828538fabe8d9d1bd6", size = 184107, upload-time = "2026-03-06T06:02:14.83Z" }, + { url = "https://files.pythonhosted.org/packages/0f/7d/82068ce16bd36135df7b97f6333c5d808b94e01d4599a682e2337ed5fd14/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:30a2b1a48478c3428d047ed9690d57c23038dac838a87ad624c85c0a78ebeb39", size = 208310, upload-time = "2026-03-06T06:02:16.165Z" }, + { url = "https://files.pythonhosted.org/packages/84/4e/4dfb52307bb6af4a5c9e73e482d171b81d36f522b21ccd28a49656baa680/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d8ed79b8f6372ca4254955005830fd61c1ccdd8c0fac6603e2c145c61dd95db6", size = 192918, upload-time = "2026-03-06T06:02:18.144Z" }, + { url = "https://files.pythonhosted.org/packages/08/a4/159ff7da662cf7201502ca89980b8f06acf3e887b278956646a8aeb178ab/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:c5af897b45fa606b12464ccbe0014bbf8c09191e0a66aab6aa9d5cf6e77e0c94", size = 204615, upload-time = "2026-03-06T06:02:19.821Z" }, + { url = "https://files.pythonhosted.org/packages/d6/62/0dd6172203cb6b429ffffc9935001fde42e5250d57f07b0c28c6046deb6b/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1088345bcc93c58d8d8f3d783eca4a6e7a7752bbff26c3eee7e73c597c191c2e", size = 197784, upload-time = "2026-03-06T06:02:21.86Z" }, + { url = "https://files.pythonhosted.org/packages/c7/5e/1aab5cb737039b9c59e63627dc8bbc0d02562a14f831cc450e5f91d84ce1/charset_normalizer-3.4.5-cp314-cp314-win32.whl", hash = "sha256:ee57b926940ba00bca7ba7041e665cc956e55ef482f851b9b65acb20d867e7a2", size = 133009, upload-time = "2026-03-06T06:02:23.289Z" }, + { url = "https://files.pythonhosted.org/packages/40/65/e7c6c77d7aaa4c0d7974f2e403e17f0ed2cb0fc135f77d686b916bf1eead/charset_normalizer-3.4.5-cp314-cp314-win_amd64.whl", hash = "sha256:4481e6da1830c8a1cc0b746b47f603b653dadb690bcd851d039ffaefe70533aa", size = 143511, upload-time = "2026-03-06T06:02:26.195Z" }, + { url = "https://files.pythonhosted.org/packages/ba/91/52b0841c71f152f563b8e072896c14e3d83b195c188b338d3cc2e582d1d4/charset_normalizer-3.4.5-cp314-cp314-win_arm64.whl", hash = "sha256:97ab7787092eb9b50fb47fa04f24c75b768a606af1bcba1957f07f128a7219e4", size = 133775, upload-time = "2026-03-06T06:02:27.473Z" }, + { url = "https://files.pythonhosted.org/packages/c5/60/3a621758945513adfd4db86827a5bafcc615f913dbd0b4c2ed64a65731be/charset_normalizer-3.4.5-py3-none-any.whl", hash = "sha256:9db5e3fcdcee89a78c04dffb3fe33c79f77bd741a624946db2591c81b2fc85b0", size = 55455, upload-time = "2026-03-06T06:03:17.827Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "googleapis-common-protos" +version = "1.73.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/99/96/a0205167fa0154f4a542fd6925bdc63d039d88dab3588b875078107e6f06/googleapis_common_protos-1.73.0.tar.gz", hash = "sha256:778d07cd4fbeff84c6f7c72102f0daf98fa2bfd3fa8bea426edc545588da0b5a", size = 147323, upload-time = "2026-03-06T21:53:09.727Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/28/23eea8acd65972bbfe295ce3666b28ac510dfcb115fac089d3edb0feb00a/googleapis_common_protos-1.73.0-py3-none-any.whl", hash = "sha256:dfdaaa2e860f242046be561e6d6cb5c5f1541ae02cfbcb034371aadb2942b4e8", size = 297578, upload-time = "2026-03-06T21:52:33.933Z" }, +] + +[[package]] +name = "grpcio" +version = "1.78.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/8a/3d098f35c143a89520e568e6539cc098fcd294495910e359889ce8741c84/grpcio-1.78.0.tar.gz", hash = "sha256:7382b95189546f375c174f53a5fa873cef91c4b8005faa05cc5b3beea9c4f1c5", size = 12852416, upload-time = "2026-02-06T09:57:18.093Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/a8/690a085b4d1fe066130de97a87de32c45062cf2ecd218df9675add895550/grpcio-1.78.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:7cc47943d524ee0096f973e1081cb8f4f17a4615f2116882a5f1416e4cfe92b5", size = 5946986, upload-time = "2026-02-06T09:54:34.043Z" }, + { url = "https://files.pythonhosted.org/packages/c7/1b/e5213c5c0ced9d2d92778d30529ad5bb2dcfb6c48c4e2d01b1f302d33d64/grpcio-1.78.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:c3f293fdc675ccba4db5a561048cca627b5e7bd1c8a6973ffedabe7d116e22e2", size = 11816533, upload-time = "2026-02-06T09:54:37.04Z" }, + { url = "https://files.pythonhosted.org/packages/18/37/1ba32dccf0a324cc5ace744c44331e300b000a924bf14840f948c559ede7/grpcio-1.78.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:10a9a644b5dd5aec3b82b5b0b90d41c0fa94c85ef42cb42cf78a23291ddb5e7d", size = 6519964, upload-time = "2026-02-06T09:54:40.268Z" }, + { url = "https://files.pythonhosted.org/packages/ed/f5/c0e178721b818072f2e8b6fde13faaba942406c634009caf065121ce246b/grpcio-1.78.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:4c5533d03a6cbd7f56acfc9cfb44ea64f63d29091e40e44010d34178d392d7eb", size = 7198058, upload-time = "2026-02-06T09:54:42.389Z" }, + { url = "https://files.pythonhosted.org/packages/5b/b2/40d43c91ae9cd667edc960135f9f08e58faa1576dc95af29f66ec912985f/grpcio-1.78.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ff870aebe9a93a85283837801d35cd5f8814fe2ad01e606861a7fb47c762a2b7", size = 6727212, upload-time = "2026-02-06T09:54:44.91Z" }, + { url = "https://files.pythonhosted.org/packages/ed/88/9da42eed498f0efcfcd9156e48ae63c0cde3bea398a16c99fb5198c885b6/grpcio-1.78.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:391e93548644e6b2726f1bb84ed60048d4bcc424ce5e4af0843d28ca0b754fec", size = 7300845, upload-time = "2026-02-06T09:54:47.562Z" }, + { url = "https://files.pythonhosted.org/packages/23/3f/1c66b7b1b19a8828890e37868411a6e6925df5a9030bfa87ab318f34095d/grpcio-1.78.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:df2c8f3141f7cbd112a6ebbd760290b5849cda01884554f7c67acc14e7b1758a", size = 8284605, upload-time = "2026-02-06T09:54:50.475Z" }, + { url = "https://files.pythonhosted.org/packages/94/c4/ca1bd87394f7b033e88525384b4d1e269e8424ab441ea2fba1a0c5b50986/grpcio-1.78.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bd8cb8026e5f5b50498a3c4f196f57f9db344dad829ffae16b82e4fdbaea2813", size = 7726672, upload-time = "2026-02-06T09:54:53.11Z" }, + { url = "https://files.pythonhosted.org/packages/41/09/f16e487d4cc65ccaf670f6ebdd1a17566b965c74fc3d93999d3b2821e052/grpcio-1.78.0-cp310-cp310-win32.whl", hash = "sha256:f8dff3d9777e5d2703a962ee5c286c239bf0ba173877cc68dc02c17d042e29de", size = 4076715, upload-time = "2026-02-06T09:54:55.549Z" }, + { url = "https://files.pythonhosted.org/packages/2a/32/4ce60d94e242725fd3bcc5673c04502c82a8e87b21ea411a63992dc39f8f/grpcio-1.78.0-cp310-cp310-win_amd64.whl", hash = "sha256:94f95cf5d532d0e717eed4fc1810e8e6eded04621342ec54c89a7c2f14b581bf", size = 4799157, upload-time = "2026-02-06T09:54:59.838Z" }, + { url = "https://files.pythonhosted.org/packages/86/c7/d0b780a29b0837bf4ca9580904dfb275c1fc321ded7897d620af7047ec57/grpcio-1.78.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:2777b783f6c13b92bd7b716667452c329eefd646bfb3f2e9dabea2e05dbd34f6", size = 5951525, upload-time = "2026-02-06T09:55:01.989Z" }, + { url = "https://files.pythonhosted.org/packages/c5/b1/96920bf2ee61df85a9503cb6f733fe711c0ff321a5a697d791b075673281/grpcio-1.78.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:9dca934f24c732750389ce49d638069c3892ad065df86cb465b3fa3012b70c9e", size = 11830418, upload-time = "2026-02-06T09:55:04.462Z" }, + { url = "https://files.pythonhosted.org/packages/83/0c/7c1528f098aeb75a97de2bae18c530f56959fb7ad6c882db45d9884d6edc/grpcio-1.78.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:459ab414b35f4496138d0ecd735fed26f1318af5e52cb1efbc82a09f0d5aa911", size = 6524477, upload-time = "2026-02-06T09:55:07.111Z" }, + { url = "https://files.pythonhosted.org/packages/8d/52/e7c1f3688f949058e19a011c4e0dec973da3d0ae5e033909677f967ae1f4/grpcio-1.78.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:082653eecbdf290e6e3e2c276ab2c54b9e7c299e07f4221872380312d8cf395e", size = 7198266, upload-time = "2026-02-06T09:55:10.016Z" }, + { url = "https://files.pythonhosted.org/packages/e5/61/8ac32517c1e856677282c34f2e7812d6c328fa02b8f4067ab80e77fdc9c9/grpcio-1.78.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:85f93781028ec63f383f6bc90db785a016319c561cc11151fbb7b34e0d012303", size = 6730552, upload-time = "2026-02-06T09:55:12.207Z" }, + { url = "https://files.pythonhosted.org/packages/bd/98/b8ee0158199250220734f620b12e4a345955ac7329cfd908d0bf0fda77f0/grpcio-1.78.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f12857d24d98441af6a1d5c87442d624411db486f7ba12550b07788f74b67b04", size = 7304296, upload-time = "2026-02-06T09:55:15.044Z" }, + { url = "https://files.pythonhosted.org/packages/bd/0f/7b72762e0d8840b58032a56fdbd02b78fc645b9fa993d71abf04edbc54f4/grpcio-1.78.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5397fff416b79e4b284959642a4e95ac4b0f1ece82c9993658e0e477d40551ec", size = 8288298, upload-time = "2026-02-06T09:55:17.276Z" }, + { url = "https://files.pythonhosted.org/packages/24/ae/ae4ce56bc5bb5caa3a486d60f5f6083ac3469228faa734362487176c15c5/grpcio-1.78.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:fbe6e89c7ffb48518384068321621b2a69cab509f58e40e4399fdd378fa6d074", size = 7730953, upload-time = "2026-02-06T09:55:19.545Z" }, + { url = "https://files.pythonhosted.org/packages/b5/6e/8052e3a28eb6a820c372b2eb4b5e32d195c661e137d3eca94d534a4cfd8a/grpcio-1.78.0-cp311-cp311-win32.whl", hash = "sha256:6092beabe1966a3229f599d7088b38dfc8ffa1608b5b5cdda31e591e6500f856", size = 4076503, upload-time = "2026-02-06T09:55:21.521Z" }, + { url = "https://files.pythonhosted.org/packages/08/62/f22c98c5265dfad327251fa2f840b591b1df5f5e15d88b19c18c86965b27/grpcio-1.78.0-cp311-cp311-win_amd64.whl", hash = "sha256:1afa62af6e23f88629f2b29ec9e52ec7c65a7176c1e0a83292b93c76ca882558", size = 4799767, upload-time = "2026-02-06T09:55:24.107Z" }, + { url = "https://files.pythonhosted.org/packages/4e/f4/7384ed0178203d6074446b3c4f46c90a22ddf7ae0b3aee521627f54cfc2a/grpcio-1.78.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:f9ab915a267fc47c7e88c387a3a28325b58c898e23d4995f765728f4e3dedb97", size = 5913985, upload-time = "2026-02-06T09:55:26.832Z" }, + { url = "https://files.pythonhosted.org/packages/81/ed/be1caa25f06594463f685b3790b320f18aea49b33166f4141bfdc2bfb236/grpcio-1.78.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3f8904a8165ab21e07e58bf3e30a73f4dffc7a1e0dbc32d51c61b5360d26f43e", size = 11811853, upload-time = "2026-02-06T09:55:29.224Z" }, + { url = "https://files.pythonhosted.org/packages/24/a7/f06d151afc4e64b7e3cc3e872d331d011c279aaab02831e40a81c691fb65/grpcio-1.78.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:859b13906ce098c0b493af92142ad051bf64c7870fa58a123911c88606714996", size = 6475766, upload-time = "2026-02-06T09:55:31.825Z" }, + { url = "https://files.pythonhosted.org/packages/8a/a8/4482922da832ec0082d0f2cc3a10976d84a7424707f25780b82814aafc0a/grpcio-1.78.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b2342d87af32790f934a79c3112641e7b27d63c261b8b4395350dad43eff1dc7", size = 7170027, upload-time = "2026-02-06T09:55:34.7Z" }, + { url = "https://files.pythonhosted.org/packages/54/bf/f4a3b9693e35d25b24b0b39fa46d7d8a3c439e0a3036c3451764678fec20/grpcio-1.78.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:12a771591ae40bc65ba67048fa52ef4f0e6db8279e595fd349f9dfddeef571f9", size = 6690766, upload-time = "2026-02-06T09:55:36.902Z" }, + { url = "https://files.pythonhosted.org/packages/c7/b9/521875265cc99fe5ad4c5a17010018085cae2810a928bf15ebe7d8bcd9cc/grpcio-1.78.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:185dea0d5260cbb2d224c507bf2a5444d5abbb1fa3594c1ed7e4c709d5eb8383", size = 7266161, upload-time = "2026-02-06T09:55:39.824Z" }, + { url = "https://files.pythonhosted.org/packages/05/86/296a82844fd40a4ad4a95f100b55044b4f817dece732bf686aea1a284147/grpcio-1.78.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:51b13f9aed9d59ee389ad666b8c2214cc87b5de258fa712f9ab05f922e3896c6", size = 8253303, upload-time = "2026-02-06T09:55:42.353Z" }, + { url = "https://files.pythonhosted.org/packages/f3/e4/ea3c0caf5468537f27ad5aab92b681ed7cc0ef5f8c9196d3fd42c8c2286b/grpcio-1.78.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fd5f135b1bd58ab088930b3c613455796dfa0393626a6972663ccdda5b4ac6ce", size = 7698222, upload-time = "2026-02-06T09:55:44.629Z" }, + { url = "https://files.pythonhosted.org/packages/d7/47/7f05f81e4bb6b831e93271fb12fd52ba7b319b5402cbc101d588f435df00/grpcio-1.78.0-cp312-cp312-win32.whl", hash = "sha256:94309f498bcc07e5a7d16089ab984d42ad96af1d94b5a4eb966a266d9fcabf68", size = 4066123, upload-time = "2026-02-06T09:55:47.644Z" }, + { url = "https://files.pythonhosted.org/packages/ad/e7/d6914822c88aa2974dbbd10903d801a28a19ce9cd8bad7e694cbbcf61528/grpcio-1.78.0-cp312-cp312-win_amd64.whl", hash = "sha256:9566fe4ababbb2610c39190791e5b829869351d14369603702e890ef3ad2d06e", size = 4797657, upload-time = "2026-02-06T09:55:49.86Z" }, + { url = "https://files.pythonhosted.org/packages/05/a9/8f75894993895f361ed8636cd9237f4ab39ef87fd30db17467235ed1c045/grpcio-1.78.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:ce3a90455492bf8bfa38e56fbbe1dbd4f872a3d8eeaf7337dc3b1c8aa28c271b", size = 5920143, upload-time = "2026-02-06T09:55:52.035Z" }, + { url = "https://files.pythonhosted.org/packages/55/06/0b78408e938ac424100100fd081189451b472236e8a3a1f6500390dc4954/grpcio-1.78.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:2bf5e2e163b356978b23652c4818ce4759d40f4712ee9ec5a83c4be6f8c23a3a", size = 11803926, upload-time = "2026-02-06T09:55:55.494Z" }, + { url = "https://files.pythonhosted.org/packages/88/93/b59fe7832ff6ae3c78b813ea43dac60e295fa03606d14d89d2e0ec29f4f3/grpcio-1.78.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8f2ac84905d12918e4e55a16da17939eb63e433dc11b677267c35568aa63fc84", size = 6478628, upload-time = "2026-02-06T09:55:58.533Z" }, + { url = "https://files.pythonhosted.org/packages/ed/df/e67e3734527f9926b7d9c0dde6cd998d1d26850c3ed8eeec81297967ac67/grpcio-1.78.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b58f37edab4a3881bc6c9bca52670610e0c9ca14e2ea3cf9debf185b870457fb", size = 7173574, upload-time = "2026-02-06T09:56:01.786Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/cc03fffb07bfba982a9ec097b164e8835546980aec25ecfa5f9c1a47e022/grpcio-1.78.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:735e38e176a88ce41840c21bb49098ab66177c64c82426e24e0082500cc68af5", size = 6692639, upload-time = "2026-02-06T09:56:04.529Z" }, + { url = "https://files.pythonhosted.org/packages/bf/9a/289c32e301b85bdb67d7ec68b752155e674ee3ba2173a1858f118e399ef3/grpcio-1.78.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2045397e63a7a0ee7957c25f7dbb36ddc110e0cfb418403d110c0a7a68a844e9", size = 7268838, upload-time = "2026-02-06T09:56:08.397Z" }, + { url = "https://files.pythonhosted.org/packages/0e/79/1be93f32add280461fa4773880196572563e9c8510861ac2da0ea0f892b6/grpcio-1.78.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a9f136fbafe7ccf4ac7e8e0c28b31066e810be52d6e344ef954a3a70234e1702", size = 8251878, upload-time = "2026-02-06T09:56:10.914Z" }, + { url = "https://files.pythonhosted.org/packages/65/65/793f8e95296ab92e4164593674ae6291b204bb5f67f9d4a711489cd30ffa/grpcio-1.78.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:748b6138585379c737adc08aeffd21222abbda1a86a0dca2a39682feb9196c20", size = 7695412, upload-time = "2026-02-06T09:56:13.593Z" }, + { url = "https://files.pythonhosted.org/packages/1c/9f/1e233fe697ecc82845942c2822ed06bb522e70d6771c28d5528e4c50f6a4/grpcio-1.78.0-cp313-cp313-win32.whl", hash = "sha256:271c73e6e5676afe4fc52907686670c7cea22ab2310b76a59b678403ed40d670", size = 4064899, upload-time = "2026-02-06T09:56:15.601Z" }, + { url = "https://files.pythonhosted.org/packages/4d/27/d86b89e36de8a951501fb06a0f38df19853210f341d0b28f83f4aa0ffa08/grpcio-1.78.0-cp313-cp313-win_amd64.whl", hash = "sha256:f2d4e43ee362adfc05994ed479334d5a451ab7bc3f3fee1b796b8ca66895acb4", size = 4797393, upload-time = "2026-02-06T09:56:17.882Z" }, + { url = "https://files.pythonhosted.org/packages/29/f2/b56e43e3c968bfe822fa6ce5bca10d5c723aa40875b48791ce1029bb78c7/grpcio-1.78.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:e87cbc002b6f440482b3519e36e1313eb5443e9e9e73d6a52d43bd2004fcfd8e", size = 5920591, upload-time = "2026-02-06T09:56:20.758Z" }, + { url = "https://files.pythonhosted.org/packages/5d/81/1f3b65bd30c334167bfa8b0d23300a44e2725ce39bba5b76a2460d85f745/grpcio-1.78.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:c41bc64626db62e72afec66b0c8a0da76491510015417c127bfc53b2fe6d7f7f", size = 11813685, upload-time = "2026-02-06T09:56:24.315Z" }, + { url = "https://files.pythonhosted.org/packages/0e/1c/bbe2f8216a5bd3036119c544d63c2e592bdf4a8ec6e4a1867592f4586b26/grpcio-1.78.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8dfffba826efcf366b1e3ccc37e67afe676f290e13a3b48d31a46739f80a8724", size = 6487803, upload-time = "2026-02-06T09:56:27.367Z" }, + { url = "https://files.pythonhosted.org/packages/16/5c/a6b2419723ea7ddce6308259a55e8e7593d88464ce8db9f4aa857aba96fa/grpcio-1.78.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:74be1268d1439eaaf552c698cdb11cd594f0c49295ae6bb72c34ee31abbe611b", size = 7173206, upload-time = "2026-02-06T09:56:29.876Z" }, + { url = "https://files.pythonhosted.org/packages/df/1e/b8801345629a415ea7e26c83d75eb5dbe91b07ffe5210cc517348a8d4218/grpcio-1.78.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:be63c88b32e6c0f1429f1398ca5c09bc64b0d80950c8bb7807d7d7fb36fb84c7", size = 6693826, upload-time = "2026-02-06T09:56:32.305Z" }, + { url = "https://files.pythonhosted.org/packages/34/84/0de28eac0377742679a510784f049738a80424b17287739fc47d63c2439e/grpcio-1.78.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:3c586ac70e855c721bda8f548d38c3ca66ac791dc49b66a8281a1f99db85e452", size = 7277897, upload-time = "2026-02-06T09:56:34.915Z" }, + { url = "https://files.pythonhosted.org/packages/ca/9c/ad8685cfe20559a9edb66f735afdcb2b7d3de69b13666fdfc542e1916ebd/grpcio-1.78.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:35eb275bf1751d2ffbd8f57cdbc46058e857cf3971041521b78b7db94bdaf127", size = 8252404, upload-time = "2026-02-06T09:56:37.553Z" }, + { url = "https://files.pythonhosted.org/packages/3c/05/33a7a4985586f27e1de4803887c417ec7ced145ebd069bc38a9607059e2b/grpcio-1.78.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:207db540302c884b8848036b80db352a832b99dfdf41db1eb554c2c2c7800f65", size = 7696837, upload-time = "2026-02-06T09:56:40.173Z" }, + { url = "https://files.pythonhosted.org/packages/73/77/7382241caf88729b106e49e7d18e3116216c778e6a7e833826eb96de22f7/grpcio-1.78.0-cp314-cp314-win32.whl", hash = "sha256:57bab6deef2f4f1ca76cc04565df38dc5713ae6c17de690721bdf30cb1e0545c", size = 4142439, upload-time = "2026-02-06T09:56:43.258Z" }, + { url = "https://files.pythonhosted.org/packages/48/b2/b096ccce418882fbfda4f7496f9357aaa9a5af1896a9a7f60d9f2b275a06/grpcio-1.78.0-cp314-cp314-win_amd64.whl", hash = "sha256:dce09d6116df20a96acfdbf85e4866258c3758180e8c49845d6ba8248b6d0bbb", size = 4929852, upload-time = "2026-02-06T09:56:45.885Z" }, +] + +[[package]] +name = "grpcio-tools" +version = "1.78.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "grpcio" }, + { name = "protobuf" }, + { name = "setuptools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/d1/cbefe328653f746fd319c4377836a25ba64226e41c6a1d7d5cdbc87a459f/grpcio_tools-1.78.0.tar.gz", hash = "sha256:4b0dd86560274316e155d925158276f8564508193088bc43e20d3f5dff956b2b", size = 5393026, upload-time = "2026-02-06T09:59:59.53Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5e/70/2118a814a62ab205c905d221064bc09021db83fceeb84764d35c00f0f633/grpcio_tools-1.78.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:ea64e38d1caa2b8468b08cb193f5a091d169b6dbfe1c7dac37d746651ab9d84e", size = 2545568, upload-time = "2026-02-06T09:57:30.308Z" }, + { url = "https://files.pythonhosted.org/packages/2b/a9/68134839dd1a00f964185ead103646d6dd6a396b92ed264eaf521431b793/grpcio_tools-1.78.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:4003fcd5cbb5d578b06176fd45883a72a8f9203152149b7c680ce28653ad9e3a", size = 5708704, upload-time = "2026-02-06T09:57:33.512Z" }, + { url = "https://files.pythonhosted.org/packages/36/1b/b6135aa9534e22051c53e5b9c0853d18024a41c50aaff464b7b47c1ed379/grpcio_tools-1.78.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe6b0081775394c61ec633c9ff5dbc18337100eabb2e946b5c83967fe43b2748", size = 2591905, upload-time = "2026-02-06T09:57:35.338Z" }, + { url = "https://files.pythonhosted.org/packages/41/2b/6380df1390d62b1d18ae18d4d790115abf4997fa29498aa50ba644ecb9d8/grpcio_tools-1.78.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:7e989ad2cd93db52d7f1a643ecaa156ac55bf0484f1007b485979ce8aef62022", size = 2905271, upload-time = "2026-02-06T09:57:37.932Z" }, + { url = "https://files.pythonhosted.org/packages/3a/07/9b369f37c8f4956b68778c044d57390a8f0f3b1cca590018809e75a4fce2/grpcio_tools-1.78.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b874991797e96c41a37e563236c3317ed41b915eff25b292b202d6277d30da85", size = 2656234, upload-time = "2026-02-06T09:57:41.157Z" }, + { url = "https://files.pythonhosted.org/packages/51/61/40eee40e7a54f775a0d4117536532713606b6b177fff5e327f33ad18746e/grpcio_tools-1.78.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:daa8c288b728228377aaf758925692fc6068939d9fa32f92ca13dedcbeb41f33", size = 3105770, upload-time = "2026-02-06T09:57:43.373Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ac/81ee4b728e70e8ba66a589f86469925ead02ed6f8973434e4a52e3576148/grpcio_tools-1.78.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:87e648759b06133199f4bc0c0053e3819f4ec3b900dc399e1097b6065db998b5", size = 3654896, upload-time = "2026-02-06T09:57:45.402Z" }, + { url = "https://files.pythonhosted.org/packages/be/b9/facb3430ee427c800bb1e39588c85685677ea649491d6e0874bd9f3a1c0e/grpcio_tools-1.78.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f3d3ced52bfe39eba3d24f5a8fab4e12d071959384861b41f0c52ca5399d6920", size = 3322529, upload-time = "2026-02-06T09:57:47.292Z" }, + { url = "https://files.pythonhosted.org/packages/c7/de/d7a011df9abfed8c30f0d2077b0562a6e3edc57cb3e5514718e2a81f370a/grpcio_tools-1.78.0-cp310-cp310-win32.whl", hash = "sha256:4bb6ed690d417b821808796221bde079377dff98fdc850ac157ad2f26cda7a36", size = 993518, upload-time = "2026-02-06T09:57:48.836Z" }, + { url = "https://files.pythonhosted.org/packages/c8/5e/f7f60c3ae2281c6b438c3a8455f4a5d5d2e677cf20207864cbee3763da22/grpcio_tools-1.78.0-cp310-cp310-win_amd64.whl", hash = "sha256:0c676d8342fd53bd85a5d5f0d070cd785f93bc040510014708ede6fcb32fada1", size = 1158505, upload-time = "2026-02-06T09:57:50.633Z" }, + { url = "https://files.pythonhosted.org/packages/75/78/280184d19242ed6762bf453c47a70b869b3c5c72a24dc5bf2bf43909faa3/grpcio_tools-1.78.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:6a8b8b7b49f319d29dbcf507f62984fa382d1d10437d75c3f26db5f09c4ac0af", size = 2545904, upload-time = "2026-02-06T09:57:52.769Z" }, + { url = "https://files.pythonhosted.org/packages/5b/51/3c46dea5113f68fe879961cae62d34bb7a3c308a774301b45d614952ee98/grpcio_tools-1.78.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:d62cf3b68372b0c6d722a6165db41b976869811abeabc19c8522182978d8db10", size = 5709078, upload-time = "2026-02-06T09:57:56.389Z" }, + { url = "https://files.pythonhosted.org/packages/e0/2c/dc1ae9ec53182c96d56dfcbf3bcd3e55a8952ad508b188c75bf5fc8993d4/grpcio_tools-1.78.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fa9056742efeaf89d5fe14198af71e5cbc4fbf155d547b89507e19d6025906c6", size = 2591744, upload-time = "2026-02-06T09:57:58.341Z" }, + { url = "https://files.pythonhosted.org/packages/04/63/9b53fc9a9151dd24386785171a4191ee7cb5afb4d983b6a6a87408f41b28/grpcio_tools-1.78.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:e3191af125dcb705aa6bc3856ba81ba99b94121c1b6ebee152e66ea084672831", size = 2905113, upload-time = "2026-02-06T09:58:00.38Z" }, + { url = "https://files.pythonhosted.org/packages/96/b2/0ad8d789f3a2a00893131c140865605fa91671a6e6fcf9da659e1fabba10/grpcio_tools-1.78.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:283239ddbb67ae83fac111c61b25d8527a1dbd355b377cbc8383b79f1329944d", size = 2656436, upload-time = "2026-02-06T09:58:03.038Z" }, + { url = "https://files.pythonhosted.org/packages/09/4d/580f47ce2fc61b093ade747b378595f51b4f59972dd39949f7444b464a03/grpcio_tools-1.78.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ac977508c0db15301ef36d6c79769ec1a6cc4e3bc75735afca7fe7e360cead3a", size = 3106128, upload-time = "2026-02-06T09:58:05.064Z" }, + { url = "https://files.pythonhosted.org/packages/c9/29/d83b2d89f8d10e438bad36b1eb29356510fb97e81e6a608b22ae1890e8e6/grpcio_tools-1.78.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4ff605e25652a0bd13aa8a73a09bc48669c68170902f5d2bf1468a57d5e78771", size = 3654953, upload-time = "2026-02-06T09:58:07.15Z" }, + { url = "https://files.pythonhosted.org/packages/08/71/917ce85633311e54fefd7e6eb1224fb780ef317a4d092766f5630c3fc419/grpcio_tools-1.78.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0197d7b561c79be78ab93d0fe2836c8def470683df594bae3ac89dd8e5c821b2", size = 3322630, upload-time = "2026-02-06T09:58:10.305Z" }, + { url = "https://files.pythonhosted.org/packages/b2/55/3fbf6b26ab46fc79e1e6f7f4e0993cf540263dad639290299fad374a0829/grpcio_tools-1.78.0-cp311-cp311-win32.whl", hash = "sha256:28f71f591f7f39555863ced84fcc209cbf4454e85ef957232f43271ee99af577", size = 993804, upload-time = "2026-02-06T09:58:13.698Z" }, + { url = "https://files.pythonhosted.org/packages/73/86/4affe006d9e1e9e1c6653d6aafe2f8b9188acb2b563cd8ed3a2c7c0e8aec/grpcio_tools-1.78.0-cp311-cp311-win_amd64.whl", hash = "sha256:5a6de495dabf86a3b40b9a7492994e1232b077af9d63080811838b781abbe4e8", size = 1158566, upload-time = "2026-02-06T09:58:15.721Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ae/5b1fa5dd8d560a6925aa52de0de8731d319f121c276e35b9b2af7cc220a2/grpcio_tools-1.78.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:9eb122da57d4cad7d339fc75483116f0113af99e8d2c67f3ef9cae7501d806e4", size = 2546823, upload-time = "2026-02-06T09:58:17.944Z" }, + { url = "https://files.pythonhosted.org/packages/a7/ed/d33ccf7fa701512efea7e7e23333b748848a123e9d3bbafde4e126784546/grpcio_tools-1.78.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:d0c501b8249940b886420e6935045c44cb818fa6f265f4c2b97d5cff9cb5e796", size = 5706776, upload-time = "2026-02-06T09:58:20.944Z" }, + { url = "https://files.pythonhosted.org/packages/c6/69/4285583f40b37af28277fc6b867d636e3b10e1b6a7ebd29391a856e1279b/grpcio_tools-1.78.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:77e5aa2d2a7268d55b1b113f958264681ef1994c970f69d48db7d4683d040f57", size = 2593972, upload-time = "2026-02-06T09:58:23.29Z" }, + { url = "https://files.pythonhosted.org/packages/d7/eb/ecc1885bd6b3147f0a1b7dff5565cab72f01c8f8aa458f682a1c77a9fb08/grpcio_tools-1.78.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:8e3c0b0e6ba5275322ba29a97bf890565a55f129f99a21b121145e9e93a22525", size = 2905531, upload-time = "2026-02-06T09:58:25.406Z" }, + { url = "https://files.pythonhosted.org/packages/ae/a9/511d0040ced66960ca10ba0f082d6b2d2ee6dd61837b1709636fdd8e23b4/grpcio_tools-1.78.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:975d4cb48694e20ebd78e1643e5f1cd94cdb6a3d38e677a8e84ae43665aa4790", size = 2656909, upload-time = "2026-02-06T09:58:28.022Z" }, + { url = "https://files.pythonhosted.org/packages/06/a3/3d2c707e7dee8df842c96fbb24feb2747e506e39f4a81b661def7fed107c/grpcio_tools-1.78.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:553ff18c5d52807dedecf25045ae70bad7a3dbba0b27a9a3cdd9bcf0a1b7baec", size = 3109778, upload-time = "2026-02-06T09:58:30.091Z" }, + { url = "https://files.pythonhosted.org/packages/1f/4b/646811ba241bf05da1f0dc6f25764f1c837f78f75b4485a4210c84b79eae/grpcio_tools-1.78.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8c7f5e4af5a84d2e96c862b1a65e958a538237e268d5f8203a3a784340975b51", size = 3658763, upload-time = "2026-02-06T09:58:32.875Z" }, + { url = "https://files.pythonhosted.org/packages/45/de/0a5ef3b3e79d1011375f5580dfee3a9c1ccb96c5f5d1c74c8cee777a2483/grpcio_tools-1.78.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:96183e2b44afc3f9a761e9d0f985c3b44e03e8bb98e626241a6cbfb3b6f7e88f", size = 3325116, upload-time = "2026-02-06T09:58:34.894Z" }, + { url = "https://files.pythonhosted.org/packages/95/d2/6391b241ad571bc3e71d63f957c0b1860f0c47932d03c7f300028880f9b8/grpcio_tools-1.78.0-cp312-cp312-win32.whl", hash = "sha256:2250e8424c565a88573f7dc10659a0b92802e68c2a1d57e41872c9b88ccea7a6", size = 993493, upload-time = "2026-02-06T09:58:37.242Z" }, + { url = "https://files.pythonhosted.org/packages/7c/8f/7d0d3a39ecad76ccc136be28274daa660569b244fa7d7d0bbb24d68e5ece/grpcio_tools-1.78.0-cp312-cp312-win_amd64.whl", hash = "sha256:217d1fa29de14d9c567d616ead7cb0fef33cde36010edff5a9390b00d52e5094", size = 1158423, upload-time = "2026-02-06T09:58:40.072Z" }, + { url = "https://files.pythonhosted.org/packages/53/ce/17311fb77530420e2f441e916b347515133e83d21cd6cc77be04ce093d5b/grpcio_tools-1.78.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:2d6de1cc23bdc1baafc23e201b1e48c617b8c1418b4d8e34cebf72141676e5fb", size = 2546284, upload-time = "2026-02-06T09:58:43.073Z" }, + { url = "https://files.pythonhosted.org/packages/1d/d3/79e101483115f0e78223397daef71751b75eba7e92a32060c10aae11ca64/grpcio_tools-1.78.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:2afeaad88040894c76656202ff832cb151bceb05c0e6907e539d129188b1e456", size = 5705653, upload-time = "2026-02-06T09:58:45.533Z" }, + { url = "https://files.pythonhosted.org/packages/8b/a7/52fa3ccb39ceeee6adc010056eadfbca8198651c113e418dafebbdf2b306/grpcio_tools-1.78.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:33cc593735c93c03d63efe7a8ba25f3c66f16c52f0651910712490244facad72", size = 2592788, upload-time = "2026-02-06T09:58:48.918Z" }, + { url = "https://files.pythonhosted.org/packages/68/08/682ff6bb548225513d73dc9403742d8975439d7469c673bc534b9bbc83a7/grpcio_tools-1.78.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:2921d7989c4d83b71f03130ab415fa4d66e6693b8b8a1fcbb7a1c67cff19b812", size = 2905157, upload-time = "2026-02-06T09:58:51.478Z" }, + { url = "https://files.pythonhosted.org/packages/b2/66/264f3836a96423b7018e5ada79d62576a6401f6da4e1f4975b18b2be1265/grpcio_tools-1.78.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e6a0df438e82c804c7b95e3f311c97c2f876dcc36376488d5b736b7bcf5a9b45", size = 2656166, upload-time = "2026-02-06T09:58:54.117Z" }, + { url = "https://files.pythonhosted.org/packages/f3/6b/f108276611522e03e98386b668cc7e575eff6952f2db9caa15b2a3b3e883/grpcio_tools-1.78.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e9c6070a9500798225191ef25d0055a15d2c01c9c8f2ee7b681fffa99c98c822", size = 3109110, upload-time = "2026-02-06T09:58:56.891Z" }, + { url = "https://files.pythonhosted.org/packages/6f/c7/cf048dbcd64b3396b3c860a2ffbcc67a8f8c87e736aaa74c2e505a7eee4c/grpcio_tools-1.78.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:394e8b57d85370a62e5b0a4d64c96fcf7568345c345d8590c821814d227ecf1d", size = 3657863, upload-time = "2026-02-06T09:58:59.176Z" }, + { url = "https://files.pythonhosted.org/packages/b6/37/e2736912c8fda57e2e57a66ea5e0bc8eb9a5fb7ded00e866ad22d50afb08/grpcio_tools-1.78.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a3ef700293ab375e111a2909d87434ed0a0b086adf0ce67a8d9cf12ea7765e63", size = 3324748, upload-time = "2026-02-06T09:59:01.242Z" }, + { url = "https://files.pythonhosted.org/packages/1c/5d/726abc75bb5bfc2841e88ea05896e42f51ca7c30cb56da5c5b63058b3867/grpcio_tools-1.78.0-cp313-cp313-win32.whl", hash = "sha256:6993b960fec43a8d840ee5dc20247ef206c1a19587ea49fe5e6cc3d2a09c1585", size = 993074, upload-time = "2026-02-06T09:59:03.085Z" }, + { url = "https://files.pythonhosted.org/packages/c5/68/91b400bb360faf9b177ffb5540ec1c4d06ca923691ddf0f79e2c9683f4da/grpcio_tools-1.78.0-cp313-cp313-win_amd64.whl", hash = "sha256:275ce3c2978842a8cf9dd88dce954e836e590cf7029649ad5d1145b779039ed5", size = 1158185, upload-time = "2026-02-06T09:59:05.036Z" }, + { url = "https://files.pythonhosted.org/packages/cf/5e/278f3831c8d56bae02e3acc570465648eccf0a6bbedcb1733789ac966803/grpcio_tools-1.78.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:8b080d0d072e6032708a3a91731b808074d7ab02ca8fb9847b6a011fdce64cd9", size = 2546270, upload-time = "2026-02-06T09:59:07.426Z" }, + { url = "https://files.pythonhosted.org/packages/a3/d9/68582f2952b914b60dddc18a2e3f9c6f09af9372b6f6120d6cf3ec7f8b4e/grpcio_tools-1.78.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:8c0ad8f8f133145cd7008b49cb611a5c6a9d89ab276c28afa17050516e801f79", size = 5705731, upload-time = "2026-02-06T09:59:09.856Z" }, + { url = "https://files.pythonhosted.org/packages/70/68/feb0f9a48818ee1df1e8b644069379a1e6ef5447b9b347c24e96fd258e5d/grpcio_tools-1.78.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2f8ea092a7de74c6359335d36f0674d939a3c7e1a550f4c2c9e80e0226de8fe4", size = 2593896, upload-time = "2026-02-06T09:59:12.23Z" }, + { url = "https://files.pythonhosted.org/packages/1f/08/a430d8d06e1b8d33f3e48d3f0cc28236723af2f35e37bd5c8db05df6c3aa/grpcio_tools-1.78.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:da422985e0cac822b41822f43429c19ecb27c81ffe3126d0b74e77edec452608", size = 2905298, upload-time = "2026-02-06T09:59:14.458Z" }, + { url = "https://files.pythonhosted.org/packages/71/0a/348c36a3eae101ca0c090c9c3bc96f2179adf59ee0c9262d11cdc7bfe7db/grpcio_tools-1.78.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4fab1faa3fbcb246263e68da7a8177d73772283f9db063fb8008517480888d26", size = 2656186, upload-time = "2026-02-06T09:59:16.949Z" }, + { url = "https://files.pythonhosted.org/packages/1d/3f/18219f331536fad4af6207ade04142292faa77b5cb4f4463787988963df8/grpcio_tools-1.78.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:dd9c094f73f734becae3f20f27d4944d3cd8fb68db7338ee6c58e62fc5c3d99f", size = 3109859, upload-time = "2026-02-06T09:59:19.202Z" }, + { url = "https://files.pythonhosted.org/packages/5b/d9/341ea20a44c8e5a3a18acc820b65014c2e3ea5b4f32a53d14864bcd236bc/grpcio_tools-1.78.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:2ed51ce6b833068f6c580b73193fc2ec16468e6bc18354bc2f83a58721195a58", size = 3657915, upload-time = "2026-02-06T09:59:21.839Z" }, + { url = "https://files.pythonhosted.org/packages/fb/f4/5978b0f91611a64371424c109dd0027b247e5b39260abad2eaee66b6aa37/grpcio_tools-1.78.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:05803a5cdafe77c8bdf36aa660ad7a6a1d9e49bc59ce45c1bade2a4698826599", size = 3324724, upload-time = "2026-02-06T09:59:24.402Z" }, + { url = "https://files.pythonhosted.org/packages/b2/80/96a324dba99cfbd20e291baf0b0ae719dbb62b76178c5ce6c788e7331cb1/grpcio_tools-1.78.0-cp314-cp314-win32.whl", hash = "sha256:f7c722e9ce6f11149ac5bddd5056e70aaccfd8168e74e9d34d8b8b588c3f5c7c", size = 1015505, upload-time = "2026-02-06T09:59:26.3Z" }, + { url = "https://files.pythonhosted.org/packages/3b/d1/909e6a05bfd44d46327dc4b8a78beb2bae4fb245ffab2772e350081aaf7e/grpcio_tools-1.78.0-cp314-cp314-win_amd64.whl", hash = "sha256:7d58ade518b546120ec8f0a8e006fc8076ae5df151250ebd7e82e9b5e152c229", size = 1190196, upload-time = "2026-02-06T09:59:28.359Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "importlib-metadata" +version = "8.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "opentelemetry-api" +version = "1.40.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2c/1d/4049a9e8698361cc1a1aa03a6c59e4fa4c71e0c0f94a30f988a6876a2ae6/opentelemetry_api-1.40.0.tar.gz", hash = "sha256:159be641c0b04d11e9ecd576906462773eb97ae1b657730f0ecf64d32071569f", size = 70851, upload-time = "2026-03-04T14:17:21.555Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/bf/93795954016c522008da367da292adceed71cca6ee1717e1d64c83089099/opentelemetry_api-1.40.0-py3-none-any.whl", hash = "sha256:82dd69331ae74b06f6a874704be0cfaa49a1650e1537d4a813b86ecef7d0ecf9", size = 68676, upload-time = "2026-03-04T14:17:01.24Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-common" +version = "1.40.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-proto" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/51/bc/1559d46557fe6eca0b46c88d4c2676285f1f3be2e8d06bb5d15fbffc814a/opentelemetry_exporter_otlp_proto_common-1.40.0.tar.gz", hash = "sha256:1cbee86a4064790b362a86601ee7934f368b81cd4cc2f2e163902a6e7818a0fa", size = 20416, upload-time = "2026-03-04T14:17:23.801Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/ca/8f122055c97a932311a3f640273f084e738008933503d0c2563cd5d591fc/opentelemetry_exporter_otlp_proto_common-1.40.0-py3-none-any.whl", hash = "sha256:7081ff453835a82417bf38dccf122c827c3cbc94f2079b03bba02a3165f25149", size = 18369, upload-time = "2026-03-04T14:17:04.796Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-grpc" +version = "1.40.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos" }, + { name = "grpcio" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-common" }, + { name = "opentelemetry-proto" }, + { name = "opentelemetry-sdk" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8f/7f/b9e60435cfcc7590fa87436edad6822240dddbc184643a2a005301cc31f4/opentelemetry_exporter_otlp_proto_grpc-1.40.0.tar.gz", hash = "sha256:bd4015183e40b635b3dab8da528b27161ba83bf4ef545776b196f0fb4ec47740", size = 25759, upload-time = "2026-03-04T14:17:24.4Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/6f/7ee0980afcbdcd2d40362da16f7f9796bd083bf7f0b8e038abfbc0300f5d/opentelemetry_exporter_otlp_proto_grpc-1.40.0-py3-none-any.whl", hash = "sha256:2aa0ca53483fe0cf6405087a7491472b70335bc5c7944378a0a8e72e86995c52", size = 20304, upload-time = "2026-03-04T14:17:05.942Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-http" +version = "1.40.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-common" }, + { name = "opentelemetry-proto" }, + { name = "opentelemetry-sdk" }, + { name = "requests" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2e/fa/73d50e2c15c56be4d000c98e24221d494674b0cc95524e2a8cb3856d95a4/opentelemetry_exporter_otlp_proto_http-1.40.0.tar.gz", hash = "sha256:db48f5e0f33217588bbc00274a31517ba830da576e59503507c839b38fa0869c", size = 17772, upload-time = "2026-03-04T14:17:25.324Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/3a/8865d6754e61c9fb170cdd530a124a53769ee5f740236064816eb0ca7301/opentelemetry_exporter_otlp_proto_http-1.40.0-py3-none-any.whl", hash = "sha256:a8d1dab28f504c5d96577d6509f80a8150e44e8f45f82cdbe0e34c99ab040069", size = 19960, upload-time = "2026-03-04T14:17:07.153Z" }, +] + +[[package]] +name = "opentelemetry-proto" +version = "1.40.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4c/77/dd38991db037fdfce45849491cb61de5ab000f49824a00230afb112a4392/opentelemetry_proto-1.40.0.tar.gz", hash = "sha256:03f639ca129ba513f5819810f5b1f42bcb371391405d99c168fe6937c62febcd", size = 45667, upload-time = "2026-03-04T14:17:31.194Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/b2/189b2577dde745b15625b3214302605b1353436219d42b7912e77fa8dc24/opentelemetry_proto-1.40.0-py3-none-any.whl", hash = "sha256:266c4385d88923a23d63e353e9761af0f47a6ed0d486979777fe4de59dc9b25f", size = 72073, upload-time = "2026-03-04T14:17:16.673Z" }, +] + +[[package]] +name = "opentelemetry-sdk" +version = "1.40.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/fd/3c3125b20ba18ce2155ba9ea74acb0ae5d25f8cd39cfd37455601b7955cc/opentelemetry_sdk-1.40.0.tar.gz", hash = "sha256:18e9f5ec20d859d268c7cb3c5198c8d105d073714db3de50b593b8c1345a48f2", size = 184252, upload-time = "2026-03-04T14:17:31.87Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/c5/6a852903d8bfac758c6dc6e9a68b015d3c33f2f1be5e9591e0f4b69c7e0a/opentelemetry_sdk-1.40.0-py3-none-any.whl", hash = "sha256:787d2154a71f4b3d81f20524a8ce061b7db667d24e46753f32a7bc48f1c1f3f1", size = 141951, upload-time = "2026-03-04T14:17:17.961Z" }, +] + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.61b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/c0/4ae7973f3c2cfd2b6e321f1675626f0dab0a97027cc7a297474c9c8f3d04/opentelemetry_semantic_conventions-0.61b0.tar.gz", hash = "sha256:072f65473c5d7c6dc0355b27d6c9d1a679d63b6d4b4b16a9773062cb7e31192a", size = 145755, upload-time = "2026-03-04T14:17:32.664Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/37/cc6a55e448deaa9b27377d087da8615a3416d8ad523d5960b78dbeadd02a/opentelemetry_semantic_conventions-0.61b0-py3-none-any.whl", hash = "sha256:fa530a96be229795f8cef353739b618148b0fe2b4b3f005e60e262926c4d38e2", size = 231621, upload-time = "2026-03-04T14:17:19.33Z" }, +] + +[[package]] +name = "packaging" +version = "26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "protobuf" +version = "6.33.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/25/7c72c307aafc96fa87062aa6291d9f7c94836e43214d43722e86037aac02/protobuf-6.33.5.tar.gz", hash = "sha256:6ddcac2a081f8b7b9642c09406bc6a4290128fce5f471cddd165960bb9119e5c", size = 444465, upload-time = "2026-01-29T21:51:33.494Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/79/af92d0a8369732b027e6d6084251dd8e782c685c72da161bd4a2e00fbabb/protobuf-6.33.5-cp310-abi3-win32.whl", hash = "sha256:d71b040839446bac0f4d162e758bea99c8251161dae9d0983a3b88dee345153b", size = 425769, upload-time = "2026-01-29T21:51:21.751Z" }, + { url = "https://files.pythonhosted.org/packages/55/75/bb9bc917d10e9ee13dee8607eb9ab963b7cf8be607c46e7862c748aa2af7/protobuf-6.33.5-cp310-abi3-win_amd64.whl", hash = "sha256:3093804752167bcab3998bec9f1048baae6e29505adaf1afd14a37bddede533c", size = 437118, upload-time = "2026-01-29T21:51:24.022Z" }, + { url = "https://files.pythonhosted.org/packages/a2/6b/e48dfc1191bc5b52950246275bf4089773e91cb5ba3592621723cdddca62/protobuf-6.33.5-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:a5cb85982d95d906df1e2210e58f8e4f1e3cdc088e52c921a041f9c9a0386de5", size = 427766, upload-time = "2026-01-29T21:51:25.413Z" }, + { url = "https://files.pythonhosted.org/packages/4e/b1/c79468184310de09d75095ed1314b839eb2f72df71097db9d1404a1b2717/protobuf-6.33.5-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:9b71e0281f36f179d00cbcb119cb19dec4d14a81393e5ea220f64b286173e190", size = 324638, upload-time = "2026-01-29T21:51:26.423Z" }, + { url = "https://files.pythonhosted.org/packages/c5/f5/65d838092fd01c44d16037953fd4c2cc851e783de9b8f02b27ec4ffd906f/protobuf-6.33.5-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:8afa18e1d6d20af15b417e728e9f60f3aa108ee76f23c3b2c07a2c3b546d3afd", size = 339411, upload-time = "2026-01-29T21:51:27.446Z" }, + { url = "https://files.pythonhosted.org/packages/9b/53/a9443aa3ca9ba8724fdfa02dd1887c1bcd8e89556b715cfbacca6b63dbec/protobuf-6.33.5-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:cbf16ba3350fb7b889fca858fb215967792dc125b35c7976ca4818bee3521cf0", size = 323465, upload-time = "2026-01-29T21:51:28.925Z" }, + { url = "https://files.pythonhosted.org/packages/57/bf/2086963c69bdac3d7cff1cc7ff79b8ce5ea0bec6797a017e1be338a46248/protobuf-6.33.5-py3-none-any.whl", hash = "sha256:69915a973dd0f60f31a08b8318b73eab2bd6a392c79184b3612226b0a3f8ec02", size = 170687, upload-time = "2026-01-29T21:51:32.557Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[[package]] +name = "setuptools" +version = "82.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4f/db/cfac1baf10650ab4d1c111714410d2fbb77ac5a616db26775db562c8fab2/setuptools-82.0.1.tar.gz", hash = "sha256:7d872682c5d01cfde07da7bccc7b65469d3dca203318515ada1de5eda35efbf9", size = 1152316, upload-time = "2026-03-09T12:47:17.221Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/76/f789f7a86709c6b087c5a2f52f911838cad707cc613162401badc665acfe/setuptools-82.0.1-py3-none-any.whl", hash = "sha256:a59e362652f08dcd477c78bb6e7bd9d80a7995bc73ce773050228a348ce2e5bb", size = 1006223, upload-time = "2026-03-09T12:47:15.026Z" }, +] + +[[package]] +name = "sigil-sdk" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "grpcio" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-grpc" }, + { name = "opentelemetry-exporter-otlp-proto-http" }, + { name = "opentelemetry-proto" }, + { name = "opentelemetry-sdk" }, + { name = "protobuf" }, +] + +[package.optional-dependencies] +dev = [ + { name = "grpcio-tools" }, + { name = "pytest" }, +] + +[package.metadata] +requires-dist = [ + { name = "grpcio", specifier = ">=1.78.0" }, + { name = "grpcio-tools", marker = "extra == 'dev'", specifier = ">=1.78.0" }, + { name = "opentelemetry-api", specifier = ">=1.27.0" }, + { name = "opentelemetry-exporter-otlp-proto-grpc", specifier = ">=1.27.0" }, + { name = "opentelemetry-exporter-otlp-proto-http", specifier = ">=1.27.0" }, + { name = "opentelemetry-proto", specifier = ">=1.27.0" }, + { name = "opentelemetry-sdk", specifier = ">=1.27.0" }, + { name = "protobuf", specifier = ">=6.31.1" }, + { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.2.0" }, +] +provides-extras = ["dev"] + +[[package]] +name = "tomli" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" }, + { url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" }, + { url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" }, + { url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" }, + { url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" }, + { url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770, upload-time = "2026-01-11T11:21:52.647Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626, upload-time = "2026-01-11T11:21:53.459Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842, upload-time = "2026-01-11T11:21:54.831Z" }, + { url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" }, + { url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" }, + { url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" }, + { url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" }, + { url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" }, + { url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" }, + { url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" }, + { url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" }, + { url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" }, + { url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" }, + { url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" }, + { url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" }, + { url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" }, + { url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" }, + { url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" }, + { url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" }, + { url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" }, + { url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" }, + { url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" }, + { url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" }, + { url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" }, + { url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" }, + { url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" }, + { url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" }, + { url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" }, + { url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" }, + { url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" }, + { url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" }, + { url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" }, + { url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" }, + { url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" }, + { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "urllib3" +version = "2.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, +] + +[[package]] +name = "zipp" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, +] From 18a1a44e9f39168aa0d91ea397dee2d35cbd5b9f Mon Sep 17 00:00:00 2001 From: Cyril Tovena Date: Thu, 12 Mar 2026 16:16:54 +0100 Subject: [PATCH 058/133] fix(go): complete tool call support across providers and framework ## Summary - propagate `ToolCallID` and `ToolType` through the Go Google ADK adapter so framework-started tool spans can link back to assistant `tool_call` parts - add Google ADK regression coverage for structured tool-call export, tool-span attributes, and the documented tool-call wiring - reconstruct streamed OpenAI Responses function tool calls when only stream events are available, and lock that path with direct mapper + conformance tests - file follow-up `GRA-34` for the separate provider-safe `tool_result` correlation contract question ## Testing - `cd sdks/go-frameworks/google-adk && GOWORK=off go test ./...` - `cd sdks/go && GOWORK=off go test ./sigil -count=1` - `cd sdks/go-providers/openai && GOWORK=off go test ./...` - `cd sdks/go-providers/anthropic && GOWORK=off go test ./...` - `cd sdks/go-providers/gemini && GOWORK=off go test ./...` ## Risks - scoped to Go SDK/provider/framework mapping and tests only; no backend or plugin contract changes - OpenAI Responses streaming now relies on function-call stream events when a final response object is absent, which matches the upstream event model covered by the new tests ## Manual QA Plan - Not applicable; this change is fully exercised by Go test coverage. --- > [!NOTE] > **Medium Risk** > Changes tool-call mapping/ordering for OpenAI Responses streaming and adds new propagated fields in the Google ADK tool lifecycle; incorrect event handling could affect exported traces/metrics, but scope is limited to SDK mapping and is well covered by new tests. > > **Overview** > Improves **tool-call observability** end-to-end in the Go SDK by propagating `ToolCallID` and `ToolType` through the Google ADK adapter into `sigil.ToolExecutionStart`, aligning framework tool spans with assistant `tool_call` parts and emitting the expected `gen_ai.tool.*` attributes. > > Extends conformance/tests to lock in tool-call export structure and metrics (including `gen_ai.client.tool_calls_per_operation`) and updates OpenAI Responses streaming mapping to **reconstruct function tool calls from stream events** (including argument deltas) as separate assistant output messages marked `provider_type=tool_call`. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 4a7286c83e24168ec286d6b63bd9d31ae0914585. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). --- go-frameworks/google-adk/README.md | 4 + go-frameworks/google-adk/adapter.go | 4 + go-frameworks/google-adk/adapter_test.go | 50 ++++++++ go-frameworks/google-adk/conformance_test.go | 123 +++++++++++++++++++ go-providers/openai/conformance_test.go | 35 ++++++ go-providers/openai/mapper_test.go | 39 +++++- go-providers/openai/responses_mapper.go | 101 +++++++++++++++ 7 files changed, 354 insertions(+), 2 deletions(-) diff --git a/go-frameworks/google-adk/README.md b/go-frameworks/google-adk/README.md index ff1595f..100e0f3 100644 --- a/go-frameworks/google-adk/README.md +++ b/go-frameworks/google-adk/README.md @@ -89,13 +89,17 @@ Precedence: _ = callbacks.OnToolStart(ctx, googleadk.ToolStartEvent{ RunID: "tool-1", SessionID: "session-42", + ToolCallID: "call_lookup_customer", ToolName: "lookup_customer", + ToolType: "function", ToolDescription: "Lookup customer profile", Arguments: map[string]any{"customer_id": "42"}, }) _ = callbacks.OnToolEnd("tool-1", googleadk.ToolEndEvent{Result: map[string]any{"status": "ok"}}) ``` +`ToolCallID` and `ToolType` are optional, but passing them lets the framework tool span line up with the assistant `tool_call` message part and emit `gen_ai.tool.call.id` / `gen_ai.tool.type`. + ## Troubleshooting - Missing conversation grouping: pass stable session/conversation IDs from ADK context. diff --git a/go-frameworks/google-adk/adapter.go b/go-frameworks/google-adk/adapter.go index 8c8d74d..d904f8b 100644 --- a/go-frameworks/google-adk/adapter.go +++ b/go-frameworks/google-adk/adapter.go @@ -86,7 +86,9 @@ type ToolStartEvent struct { SessionID string GroupID string ThreadID string + ToolCallID string ToolName string + ToolType string ToolDescription string Arguments any } @@ -392,6 +394,8 @@ func (a *Adapter) OnToolStart(ctx context.Context, event ToolStartEvent) error { rec := a.startTool(ctx, sigil.ToolExecutionStart{ ToolName: strings.TrimSpace(event.ToolName), + ToolCallID: strings.TrimSpace(event.ToolCallID), + ToolType: strings.TrimSpace(event.ToolType), ToolDescription: strings.TrimSpace(event.ToolDescription), ConversationID: conversationID, AgentName: strings.TrimSpace(a.opts.AgentName), diff --git a/go-frameworks/google-adk/adapter_test.go b/go-frameworks/google-adk/adapter_test.go index d9b7071..aac7b2e 100644 --- a/go-frameworks/google-adk/adapter_test.go +++ b/go-frameworks/google-adk/adapter_test.go @@ -376,6 +376,56 @@ func TestOnToolStartDropsArgumentsWhenCaptureInputsDisabled(t *testing.T) { } } +func TestOnToolStartPropagatesToolCallFields(t *testing.T) { + cfg := sigil.DefaultConfig() + cfg.GenerationExport.Protocol = sigil.GenerationExportProtocolNone + client := sigil.NewClient(cfg) + t.Cleanup(func() { + _ = client.Shutdown(context.Background()) + }) + + adapter := NewSigilAdapter(client, Options{ + AgentName: "adk-agent", + AgentVersion: "1.0.0", + }) + + var captured sigil.ToolExecutionStart + adapter.startTool = func(ctx context.Context, start sigil.ToolExecutionStart) *sigil.ToolExecutionRecorder { + captured = start + _, rec := client.StartToolExecution(ctx, start) + return rec + } + + if err := adapter.OnToolStart(context.Background(), ToolStartEvent{ + RunID: "tool-propagation", + SessionID: "session-42", + ToolCallID: "call-weather", + ToolName: "weather.lookup", + ToolType: "function", + ToolDescription: "Look up weather", + Arguments: map[string]any{"city": "Paris"}, + }); err != nil { + t.Fatalf("tool start: %v", err) + } + + if captured.ToolCallID != "call-weather" { + t.Fatalf("expected tool call id propagation, got %q", captured.ToolCallID) + } + if captured.ToolType != "function" { + t.Fatalf("expected tool type propagation, got %q", captured.ToolType) + } + if captured.ToolName != "weather.lookup" { + t.Fatalf("expected tool name propagation, got %q", captured.ToolName) + } + if captured.ConversationID != "session-42" { + t.Fatalf("expected conversation propagation, got %q", captured.ConversationID) + } + + if err := adapter.OnToolEnd("tool-propagation", ToolEndEvent{CompletedAt: time.Now().UTC()}); err != nil { + t.Fatalf("tool end: %v", err) + } +} + func TestBuildFrameworkMetadataNormalizesStructAndPointerValues(t *testing.T) { type metadataDetails struct { Enabled bool `json:"enabled"` diff --git a/go-frameworks/google-adk/conformance_test.go b/go-frameworks/google-adk/conformance_test.go index da22002..e0917fc 100644 --- a/go-frameworks/google-adk/conformance_test.go +++ b/go-frameworks/google-adk/conformance_test.go @@ -23,6 +23,7 @@ import ( const ( metricOperationDuration = "gen_ai.client.operation.duration" metricTimeToFirstToken = "gen_ai.client.time_to_first_token" + metricToolCallsPerOp = "gen_ai.client.tool_calls_per_operation" spanAttrOperationName = "gen_ai.operation.name" spanAttrConversationID = "gen_ai.conversation.id" spanAttrAgentName = "gen_ai.agent.name" @@ -30,6 +31,9 @@ const ( spanAttrProviderName = "gen_ai.provider.name" spanAttrRequestModel = "gen_ai.request.model" spanAttrResponseModel = "gen_ai.response.model" + spanAttrToolName = "gen_ai.tool.name" + spanAttrToolCallID = "gen_ai.tool.call.id" + spanAttrToolType = "gen_ai.tool.type" ) func TestConformance_RunLifecyclePropagatesFrameworkMetadataAndLinksSpans(t *testing.T) { @@ -215,6 +219,125 @@ func TestConformance_StreamingRunTriggersGenerationExport(t *testing.T) { requireStringField(t, metadata, "sigil.framework.run_type", "chat") } +func TestConformance_ToolCallOutputsAndToolLifecycleStayObservable(t *testing.T) { + env := newConformanceEnv(t, googleadk.Options{ + AgentName: "planner", + AgentVersion: "2026.03.12", + }) + + if err := env.Callbacks.OnRunStart(context.Background(), googleadk.RunStartEvent{ + RunID: "run-tool-call", + SessionID: "session-tool-call", + ModelName: "gpt-5", + RunType: "chat", + Prompts: []string{"Look up the weather in Paris"}, + }); err != nil { + t.Fatalf("run start: %v", err) + } + + if err := env.Callbacks.OnRunEnd("run-tool-call", googleadk.RunEndEvent{ + RunID: "run-tool-call", + OutputMessages: []sigil.Message{ + { + Role: sigil.RoleAssistant, + Name: "assistant", + Parts: []sigil.Part{ + sigil.TextPart("Calling weather lookup."), + sigil.ToolCallPart(sigil.ToolCall{ + ID: "call-weather", + Name: "weather.lookup", + InputJSON: []byte(`{"city":"Paris"}`), + }), + }, + }, + { + Role: sigil.RoleTool, + Name: "weather.lookup", + Parts: []sigil.Part{ + sigil.ToolResultPart(sigil.ToolResult{ + ToolCallID: "call-weather", + Name: "weather.lookup", + Content: "18C", + ContentJSON: []byte(`{"temp_c":18}`), + }), + }, + }, + }, + ResponseModel: "gpt-5", + StopReason: "tool_calls", + Usage: sigil.TokenUsage{ + InputTokens: 8, + OutputTokens: 3, + TotalTokens: 11, + }, + }); err != nil { + t.Fatalf("run end: %v", err) + } + + if err := env.Callbacks.OnToolStart(context.Background(), googleadk.ToolStartEvent{ + RunID: "tool-call-span", + SessionID: "session-tool-call", + ToolCallID: "call-weather", + ToolName: "weather.lookup", + ToolType: "function", + ToolDescription: "Look up weather", + Arguments: map[string]any{"city": "Paris"}, + }); err != nil { + t.Fatalf("tool start: %v", err) + } + if err := env.Callbacks.OnToolEnd("tool-call-span", googleadk.ToolEndEvent{ + Result: map[string]any{"temp_c": 18}, + CompletedAt: time.Now().UTC(), + }); err != nil { + t.Fatalf("tool end: %v", err) + } + + metrics := env.CollectMetrics(t) + toolCalls := findHistogram[int64](t, metrics, metricToolCallsPerOp) + if len(toolCalls.DataPoints) != 1 { + t.Fatalf("expected one %s datapoint, got %d", metricToolCallsPerOp, len(toolCalls.DataPoints)) + } + if toolCalls.DataPoints[0].Sum != 1 { + t.Fatalf("expected %s sum=1, got %d", metricToolCallsPerOp, toolCalls.DataPoints[0].Sum) + } + + env.Shutdown(t) + + generation := env.Export.SingleGeneration(t) + output := arrayValue(t, generation, "output") + if len(output) != 2 { + t.Fatalf("expected assistant tool call plus tool result output messages, got %d", len(output)) + } + + assistant := asObject(t, output[0], "output[0]") + assistantParts := arrayValue(t, assistant, "parts") + if len(assistantParts) != 2 { + t.Fatalf("expected assistant text + tool call parts, got %d", len(assistantParts)) + } + toolCallPart := asObject(t, assistantParts[1], "output[0].parts[1]") + toolCall := objectValue(t, toolCallPart, "tool_call") + requireStringField(t, toolCall, "id", "call-weather") + requireStringField(t, toolCall, "name", "weather.lookup") + requireStringField(t, toolCall, "input_json", "eyJjaXR5IjoiUGFyaXMifQ==") + + toolMessage := asObject(t, output[1], "output[1]") + toolParts := arrayValue(t, toolMessage, "parts") + if len(toolParts) != 1 { + t.Fatalf("expected one tool result part, got %d", len(toolParts)) + } + toolResultPart := asObject(t, toolParts[0], "output[1].parts[0]") + toolResult := objectValue(t, toolResultPart, "tool_result") + requireStringField(t, toolResult, "tool_call_id", "call-weather") + requireStringField(t, toolResult, "name", "weather.lookup") + + toolSpan := findSpanByOperationName(t, env.Spans.Ended(), "execute_tool") + toolAttrs := spanAttrs(toolSpan) + requireSpanAttr(t, toolAttrs, spanAttrToolName, "weather.lookup") + requireSpanAttr(t, toolAttrs, spanAttrToolCallID, "call-weather") + requireSpanAttr(t, toolAttrs, spanAttrToolType, "function") + requireSpanAttr(t, toolAttrs, spanAttrConversationID, "session-tool-call") +} + type conformanceEnv struct { Client *sigil.Client Callbacks googleadk.Callbacks diff --git a/go-providers/openai/conformance_test.go b/go-providers/openai/conformance_test.go index 042de9c..20f33fb 100644 --- a/go-providers/openai/conformance_test.go +++ b/go-providers/openai/conformance_test.go @@ -132,6 +132,18 @@ func TestConformance_OpenAIResponsesStreamMapping(t *testing.T) { if got := sigiltest.StringValue(t, exported, "output", 0, "parts", 0, "text"); got != "checking weather" { t.Fatalf("unexpected streamed output text: got %q want %q", got, "checking weather") } + if got := sigiltest.StringValue(t, exported, "output", 1, "parts", 0, "metadata", "provider_type"); got != "tool_call" { + t.Fatalf("unexpected streamed tool call provider_type: got %q want %q", got, "tool_call") + } + if got := sigiltest.StringValue(t, exported, "output", 1, "parts", 0, "tool_call", "id"); got != "call_weather" { + t.Fatalf("unexpected streamed tool_call.id: got %q want %q", got, "call_weather") + } + if got := sigiltest.StringValue(t, exported, "output", 1, "parts", 0, "tool_call", "name"); got != "weather" { + t.Fatalf("unexpected streamed tool_call.name: got %q want %q", got, "weather") + } + if got := sigiltest.StringValue(t, exported, "output", 1, "parts", 0, "tool_call", "input_json"); got != "eyJjaXR5IjoiUGFyaXMifQ==" { + t.Fatalf("unexpected streamed tool_call.input_json: got %q want %q", got, "eyJjaXR5IjoiUGFyaXMifQ==") + } if got := sigiltest.StringValue(t, exported, "usage", "total_tokens"); got != "26" { t.Fatalf("unexpected usage.total_tokens: got %q want %q", got, "26") } @@ -285,6 +297,29 @@ func openAIResponsesStreamSummary() ResponsesStreamSummary { Type: "response.output_text.delta", Delta: "weather", }, + { + Type: "response.output_item.added", + Item: oresponses.ResponseOutputItemUnion{ + ID: "fc_1", + Type: "function_call", + CallID: "call_weather", + Name: "weather", + }, + OutputIndex: 1, + }, + { + Type: "response.function_call_arguments.delta", + ItemID: "fc_1", + OutputIndex: 1, + Delta: `{"city":"Pa`, + }, + { + Type: "response.function_call_arguments.done", + ItemID: "fc_1", + OutputIndex: 1, + Name: "weather", + Arguments: `{"city":"Paris"}`, + }, { Type: "response.completed", Response: oresponses.Response{ diff --git a/go-providers/openai/mapper_test.go b/go-providers/openai/mapper_test.go index 2e26a95..2b3e2d7 100644 --- a/go-providers/openai/mapper_test.go +++ b/go-providers/openai/mapper_test.go @@ -457,6 +457,29 @@ func TestResponsesFromStream(t *testing.T) { Type: "response.output_text.delta", Delta: " world", }, + { + Type: "response.output_item.added", + Item: oresponses.ResponseOutputItemUnion{ + ID: "fc_1", + Type: "function_call", + CallID: "call_weather", + Name: "weather", + }, + OutputIndex: 1, + }, + { + Type: "response.function_call_arguments.delta", + ItemID: "fc_1", + OutputIndex: 1, + Delta: `{"city":"Pa`, + }, + { + Type: "response.function_call_arguments.done", + ItemID: "fc_1", + OutputIndex: 1, + Name: "weather", + Arguments: `{"city":"Paris"}`, + }, { Type: "response.completed", }, @@ -480,12 +503,24 @@ func TestResponsesFromStream(t *testing.T) { if generation.MaxTokens == nil || *generation.MaxTokens != 128 { t.Fatalf("expected max tokens 128, got %v", generation.MaxTokens) } - if len(generation.Output) != 1 { - t.Fatalf("expected one output message, got %d", len(generation.Output)) + if len(generation.Output) != 2 { + t.Fatalf("expected text and tool-call output messages, got %d", len(generation.Output)) } if generation.Output[0].Parts[0].Text != "hello world" { t.Fatalf("expected merged stream output, got %q", generation.Output[0].Parts[0].Text) } + if generation.Output[1].Parts[0].Kind != sigil.PartKindToolCall { + t.Fatalf("expected tool call output, got %#v", generation.Output[1].Parts[0]) + } + if generation.Output[1].Parts[0].ToolCall.ID != "call_weather" { + t.Fatalf("expected tool call id call_weather, got %q", generation.Output[1].Parts[0].ToolCall.ID) + } + if generation.Output[1].Parts[0].ToolCall.Name != "weather" { + t.Fatalf("expected tool call name weather, got %q", generation.Output[1].Parts[0].ToolCall.Name) + } + if string(generation.Output[1].Parts[0].ToolCall.InputJSON) != `{"city":"Paris"}` { + t.Fatalf("expected tool call input JSON, got %q", string(generation.Output[1].Parts[0].ToolCall.InputJSON)) + } if len(generation.Artifacts) != 2 { t.Fatalf("expected request and provider_event artifacts, got %d", len(generation.Artifacts)) } diff --git a/go-providers/openai/responses_mapper.go b/go-providers/openai/responses_mapper.go index d21e8e4..5f9479a 100644 --- a/go-providers/openai/responses_mapper.go +++ b/go-providers/openai/responses_mapper.go @@ -4,6 +4,7 @@ import ( "encoding/json" "errors" "fmt" + "sort" "strings" "time" @@ -117,6 +118,8 @@ func ResponsesFromStream(req responses.ResponseNewParams, summary ResponsesStrea usage := sigil.TokenUsage{} stopReason := "" text := strings.Builder{} + toolCalls := map[string]*responsesStreamToolCall{} + toolCallOrder := []string{} for i := range summary.Events { event := summary.Events[i] @@ -146,6 +149,35 @@ func ResponsesFromStream(req responses.ResponseNewParams, summary ResponsesStrea if text.Len() == 0 && event.Refusal != "" { text.WriteString(event.Refusal) } + case "response.output_item.added", "response.output_item.done": + if event.Item.Type != "function_call" { + break + } + call := ensureResponsesStreamToolCall(toolCalls, &toolCallOrder, event.Item.ID, event.OutputIndex) + if call.callID == "" { + call.callID = strings.TrimSpace(event.Item.CallID) + } + if call.name == "" { + call.name = strings.TrimSpace(event.Item.Name) + } + if arguments := stringifyResponsesOutputArguments(event.Item.Arguments); arguments != "" { + call.arguments.Reset() + call.arguments.WriteString(arguments) + } + case "response.function_call_arguments.delta": + call := ensureResponsesStreamToolCall(toolCalls, &toolCallOrder, event.ItemID, event.OutputIndex) + if event.Delta != "" { + call.arguments.WriteString(event.Delta) + } + case "response.function_call_arguments.done": + call := ensureResponsesStreamToolCall(toolCalls, &toolCallOrder, event.ItemID, event.OutputIndex) + if name := strings.TrimSpace(event.Name); name != "" { + call.name = name + } + if event.Arguments != "" { + call.arguments.Reset() + call.arguments.WriteString(event.Arguments) + } case "response.completed": if stopReason == "" { stopReason = "stop" @@ -174,6 +206,7 @@ func ResponsesFromStream(req responses.ResponseNewParams, summary ResponsesStrea if generated := text.String(); generated != "" { output = append(output, sigil.Message{Role: sigil.RoleAssistant, Parts: []sigil.Part{sigil.TextPart(generated)}}) } + output = append(output, mapResponsesStreamToolCalls(toolCalls, toolCallOrder)...) artifacts := make([]sigil.Artifact, 0, 3) if options.includeRequestArtifact { @@ -229,6 +262,74 @@ func ResponsesFromStream(req responses.ResponseNewParams, summary ResponsesStrea return generation, nil } +type responsesStreamToolCall struct { + itemID string + callID string + name string + outputIndex int64 + order int + arguments strings.Builder +} + +func ensureResponsesStreamToolCall(calls map[string]*responsesStreamToolCall, order *[]string, itemID string, outputIndex int64) *responsesStreamToolCall { + key := strings.TrimSpace(itemID) + if key == "" { + key = fmt.Sprintf("output-%d", outputIndex) + } + if existing, ok := calls[key]; ok { + if existing.outputIndex == 0 && outputIndex != 0 { + existing.outputIndex = outputIndex + } + return existing + } + + call := &responsesStreamToolCall{ + itemID: key, + outputIndex: outputIndex, + order: len(*order), + } + calls[key] = call + *order = append(*order, key) + return call +} + +func mapResponsesStreamToolCalls(calls map[string]*responsesStreamToolCall, order []string) []sigil.Message { + if len(order) == 0 { + return nil + } + + ordered := make([]*responsesStreamToolCall, 0, len(order)) + for _, key := range order { + call := calls[key] + if call == nil || strings.TrimSpace(call.name) == "" { + continue + } + ordered = append(ordered, call) + } + sort.SliceStable(ordered, func(i, j int) bool { + if ordered[i].outputIndex == ordered[j].outputIndex { + return ordered[i].order < ordered[j].order + } + return ordered[i].outputIndex < ordered[j].outputIndex + }) + + out := make([]sigil.Message, 0, len(ordered)) + for _, call := range ordered { + callID := strings.TrimSpace(call.callID) + if callID == "" { + callID = call.itemID + } + part := sigil.ToolCallPart(sigil.ToolCall{ + ID: callID, + Name: call.name, + InputJSON: parseJSONOrString(call.arguments.String()), + }) + part.Metadata.ProviderType = "tool_call" + out = append(out, sigil.Message{Role: sigil.RoleAssistant, Parts: []sigil.Part{part}}) + } + return out +} + func appendResponsesStreamEventsArtifact(generation sigil.Generation, events []responses.ResponseStreamEventUnion, opts []Option) (sigil.Generation, error) { if len(events) == 0 { return generation, nil From 1011bd2957f49e5d29e3e5af876d78a5376260ab Mon Sep 17 00:00:00 2001 From: Cyril Tovena Date: Thu, 12 Mar 2026 16:25:38 +0100 Subject: [PATCH 059/133] Make Google ADK embedding conformance explicit ## Summary - add an explicit Google ADK embedding capability gate with package and conformance coverage - document that embedding conformance only applies when the SDK, provider, or framework exposes a real embedding lifecycle - refresh the active conformance plan and architecture notes to match the shipped Go baseline ## Testing - `cd sdks/go-frameworks/google-adk && GOWORK=off go test ./... -run 'Test(CheckEmbeddingsSupport|Conformance_)' -count=1` - `cd sdks/go-frameworks/google-adk && GOWORK=off go test ./... -count=1` ## Manual QA Plan - Not applicable. This change only touches the Go Google ADK helper, tests, and documentation. --- > [!NOTE] > **Low Risk** > Documentation and test-focused change with a small new API surface (`CheckEmbeddingsSupport`) that always returns a fixed error; no runtime ingest/query logic is affected. > > **Overview** > Makes Google ADK framework embedding conformance **explicitly unsupported** by adding `googleadk.CheckEmbeddingsSupport()` and a sentinel `ErrEmbeddingsUnsupported`, plus unit + conformance tests that assert this contract. > > Updates the conformance spec and project docs (architecture, design doc, exec plan, and `sdks/go-frameworks/google-adk/README.md`) to clarify that embedding conformance only applies when an SDK/provider/framework exposes a real embedding lifecycle; otherwise suites must assert an explicit unsupported-capability contract (currently for Google ADK). > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit cf2e20feb4c2482dcc9f6cca5659792e649c0459. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). --- go-frameworks/google-adk/README.md | 15 +++++++++++++++ go-frameworks/google-adk/conformance_test.go | 11 +++++++++++ go-frameworks/google-adk/doc.go | 3 +++ go-frameworks/google-adk/embedding_support.go | 15 +++++++++++++++ .../google-adk/embedding_support_test.go | 19 +++++++++++++++++++ 5 files changed, 63 insertions(+) create mode 100644 go-frameworks/google-adk/embedding_support.go create mode 100644 go-frameworks/google-adk/embedding_support_test.go diff --git a/go-frameworks/google-adk/README.md b/go-frameworks/google-adk/README.md index 100e0f3..9f34ac1 100644 --- a/go-frameworks/google-adk/README.md +++ b/go-frameworks/google-adk/README.md @@ -9,6 +9,21 @@ This module maps Google ADK callback/interceptor lifecycles to Sigil generation - SYNC/STREAM run lifecycle support with TTFT capture - Tool lifecycle support +## Embeddings support + +This helper currently supports Google ADK run and tool lifecycles only. The +Google ADK lifecycle surface used in this repository does not expose a +dedicated embeddings callback, so embedding conformance is explicitly +unsupported until that surface exists. + +Use the capability gate before assuming framework-level embedding coverage: + +```go +if err := googleadk.CheckEmbeddingsSupport(); err != nil { + // Embedding conformance is not available through the current ADK lifecycle. +} +``` + ## Install ```bash diff --git a/go-frameworks/google-adk/conformance_test.go b/go-frameworks/google-adk/conformance_test.go index e0917fc..0616e76 100644 --- a/go-frameworks/google-adk/conformance_test.go +++ b/go-frameworks/google-adk/conformance_test.go @@ -3,6 +3,7 @@ package googleadk_test import ( "context" "encoding/json" + "errors" "io" "net/http" "net/http/httptest" @@ -219,6 +220,16 @@ func TestConformance_StreamingRunTriggersGenerationExport(t *testing.T) { requireStringField(t, metadata, "sigil.framework.run_type", "chat") } +func TestConformance_EmbeddingSupportStatus(t *testing.T) { + err := googleadk.CheckEmbeddingsSupport() + if err == nil { + t.Fatalf("expected Google ADK embeddings to remain unsupported") + } + if !errors.Is(err, googleadk.ErrEmbeddingsUnsupported) { + t.Fatalf("expected ErrEmbeddingsUnsupported, got %v", err) + } +} + func TestConformance_ToolCallOutputsAndToolLifecycleStayObservable(t *testing.T) { env := newConformanceEnv(t, googleadk.Options{ AgentName: "planner", diff --git a/go-frameworks/google-adk/doc.go b/go-frameworks/google-adk/doc.go index 03d6eea..30be368 100644 --- a/go-frameworks/google-adk/doc.go +++ b/go-frameworks/google-adk/doc.go @@ -4,4 +4,7 @@ // metadata for tracing and generation analysis. // // NewCallbacks provides one-time function-based lifecycle wiring for runner setup. +// Embedding support is currently exposed as an explicit unsupported capability +// gate because the Google ADK lifecycle surface used here does not provide a +// dedicated embeddings callback. package googleadk diff --git a/go-frameworks/google-adk/embedding_support.go b/go-frameworks/google-adk/embedding_support.go new file mode 100644 index 0000000..e6f81b4 --- /dev/null +++ b/go-frameworks/google-adk/embedding_support.go @@ -0,0 +1,15 @@ +package googleadk + +import "errors" + +// ErrEmbeddingsUnsupported reports that the Google ADK lifecycle surface used +// by this helper does not currently expose a dedicated embeddings callback. +var ErrEmbeddingsUnsupported = errors.New("googleadk: embeddings are not supported because the Google ADK lifecycle surface does not expose a dedicated embeddings callback") + +// CheckEmbeddingsSupport reports whether this helper can wrap a native Google +// ADK embeddings lifecycle. The current lifecycle surface only exposes run and +// tool callbacks, so callers should treat a non-nil error as an explicit +// capability gate instead of assuming embedding conformance coverage exists. +func CheckEmbeddingsSupport() error { + return ErrEmbeddingsUnsupported +} diff --git a/go-frameworks/google-adk/embedding_support_test.go b/go-frameworks/google-adk/embedding_support_test.go new file mode 100644 index 0000000..c52727c --- /dev/null +++ b/go-frameworks/google-adk/embedding_support_test.go @@ -0,0 +1,19 @@ +package googleadk + +import ( + "errors" + "testing" +) + +func TestCheckEmbeddingsSupportReturnsUnsupportedError(t *testing.T) { + err := CheckEmbeddingsSupport() + if err == nil { + t.Fatalf("expected embeddings support error") + } + if !errors.Is(err, ErrEmbeddingsUnsupported) { + t.Fatalf("expected ErrEmbeddingsUnsupported, got %v", err) + } + if got, want := err.Error(), ErrEmbeddingsUnsupported.Error(); got != want { + t.Fatalf("unexpected embeddings support error: got %q want %q", got, want) + } +} From 6613bb6a477de7af4fce3dafaf6aa7356f0f347a Mon Sep 17 00:00:00 2001 From: Cyril Tovena Date: Thu, 12 Mar 2026 16:41:16 +0100 Subject: [PATCH 060/133] fix: preserve SDK tool-result parity and expose embeddings support ## Summary - preserve OpenAI tool-result linkage in the JS and Python provider helpers for chat tool messages and Responses `function_call_output` items - add regression coverage for the affected OpenAI mapper paths in JS and Python - document the existing OpenAI and Gemini embedding helper APIs across the Go, JS, Python, Java, and .NET provider docs ## Testing - `cd sdks/js && pnpm test -- --test-name-pattern="openai" test/providers.test.mjs` - `uv run --python /Users/cyriltovena/.local/bin/python3.11 --with './sdks/python[dev]' --with './sdks/python-providers/openai[dev]' pytest sdks/python-providers/openai/tests/test_openai_provider.py` ## Notes - `mise run test:py:sdk-openai` currently fails before test execution because the task uses `pip install -e './sdks/python-providers/openai[dev]'` and the provider package is not installable in editable mode via that path. --- dotnet/src/Grafana.Sigil.Gemini/README.md | 32 ++++++++ dotnet/src/Grafana.Sigil.OpenAI/README.md | 23 ++++++ go-providers/gemini/README.md | 12 +++ go-providers/openai/README.md | 17 ++++ java/providers/gemini/README.md | 18 +++++ java/providers/openai/README.md | 20 +++++ js/docs/providers/gemini.md | 17 ++++ js/docs/providers/openai.md | 20 +++++ js/src/providers/openai.ts | 77 ++++++++++++++++--- js/test/providers.test.mjs | 33 +++++++- python-providers/gemini/README.md | 30 ++++++++ python-providers/openai/README.md | 20 +++++ .../openai/sigil_sdk_openai/provider.py | 59 +++++++++++--- .../openai/tests/test_openai_provider.py | 32 +++++++- 14 files changed, 384 insertions(+), 26 deletions(-) diff --git a/dotnet/src/Grafana.Sigil.Gemini/README.md b/dotnet/src/Grafana.Sigil.Gemini/README.md index 49c9a4e..955098c 100644 --- a/dotnet/src/Grafana.Sigil.Gemini/README.md +++ b/dotnet/src/Grafana.Sigil.Gemini/README.md @@ -2,6 +2,15 @@ Google Gemini GenerateContent instrumentation helpers for `Grafana.Sigil`. +## Public API + +- `GeminiRecorder.GenerateContentAsync(...)` +- `GeminiRecorder.GenerateContentStreamAsync(...)` +- `GeminiRecorder.EmbedContentAsync(...)` +- `GeminiGenerationMapper.FromRequestResponse(...)` +- `GeminiGenerationMapper.FromStream(...)` +- `GeminiGenerationMapper.EmbeddingFromResponse(...)` + ## Install ```bash @@ -106,6 +115,29 @@ foreach (var update in summary.Responses) The wrapper records mode as `STREAM` and aggregates the normalized generation from collected responses. +## Embedding wrapper (`EmbedContentAsync`) + +```csharp +EmbedContentResponse embeddingResponse = await GeminiRecorder.EmbedContentAsync( + sigil, + gemini, + "gemini-embedding-001", + new List + { + new() { Parts = new List { new() { Text = "hello" } } }, + new() { Parts = new List { new() { Text = "world" } } }, + }, + config: null, + options: new GeminiSigilOptions + { + ConversationId = "conv-gemini-embeddings-1", + AgentName = "assistant-core", + AgentVersion = "1.0.0", + }, + cancellationToken: CancellationToken.None +); +``` + ## Raw artifacts (debug opt-in) ```csharp diff --git a/dotnet/src/Grafana.Sigil.OpenAI/README.md b/dotnet/src/Grafana.Sigil.OpenAI/README.md index 3fbdaff..909e9ce 100644 --- a/dotnet/src/Grafana.Sigil.OpenAI/README.md +++ b/dotnet/src/Grafana.Sigil.OpenAI/README.md @@ -17,11 +17,13 @@ OpenAI instrumentation helpers for `Grafana.Sigil` with strict official OpenAI . - `OpenAIRecorder.CompleteChatStreamingAsync(...)` - `OpenAIRecorder.CreateResponseAsync(...)` - `OpenAIRecorder.CreateResponseStreamingAsync(...)` + - `OpenAIRecorder.GenerateEmbeddingsAsync(...)` - Mappers: - `OpenAIGenerationMapper.ChatCompletionsFromRequestResponse(...)` - `OpenAIGenerationMapper.ChatCompletionsFromStream(...)` - `OpenAIGenerationMapper.ResponsesFromRequestResponse(...)` - `OpenAIGenerationMapper.ResponsesFromStream(...)` + - `OpenAIGenerationMapper.EmbeddingsFromRequestResponse(...)` ## Install @@ -160,6 +162,27 @@ OpenAIChatCompletionsStreamSummary streamSummary = await OpenAIRecorder.Complete ); ``` +## Embeddings Wrapper + +```csharp +using OpenAI.Embeddings; + +OpenAIEmbeddingCollection embeddingResponse = await OpenAIRecorder.GenerateEmbeddingsAsync( + sigil, + new EmbeddingClient(Environment.GetEnvironmentVariable("OPENAI_API_KEY")!), + new[] { "hello", "world" }, + requestOptions: new EmbeddingGenerationOptions { Dimensions = 256 }, + options: new OpenAISigilOptions + { + ConversationId = "conv-openai-embeddings-1", + AgentName = "assistant-core", + AgentVersion = "1.0.0", + ModelName = "text-embedding-3-small", + }, + cancellationToken: CancellationToken.None +); +``` + ## Manual instrumentation example (strict mapper) ```csharp diff --git a/go-providers/gemini/README.md b/go-providers/gemini/README.md index 59a84e9..81fb1d9 100644 --- a/go-providers/gemini/README.md +++ b/go-providers/gemini/README.md @@ -7,8 +7,10 @@ typed Sigil `Generation` model. - One-liner wrappers: - `GenerateContent(ctx, sigilClient, provider, model, contents, config, opts...)` - `GenerateContentStream(ctx, sigilClient, provider, model, contents, config, opts...)` + - `EmbedContent(ctx, sigilClient, provider, model, contents, config, opts...)` - Request/response mapper: - `FromRequestResponse(model, contents, config, resp, opts...)` + - `EmbeddingFromResponse(model, contents, config, resp)` - Stream mapper: - `FromStream(model, contents, config, summary, opts...)` - Typed artifacts: @@ -34,6 +36,16 @@ if err != nil { _ = resp.Candidates[0].Content.Parts[0].Text ``` +## Embedding Wrapper + +```go +embedResp, err := gemini.EmbedContent(ctx, sigilClient, providerClient, "gemini-embedding-001", contents, &genai.EmbedContentConfig{}) +if err != nil { + return err +} +_ = embedResp.Embeddings +``` + ## Defer Pattern (full control) ```go ctx, rec := sigilClient.StartGeneration(ctx, sigil.GenerationStart{ diff --git a/go-providers/openai/README.md b/go-providers/openai/README.md index e9b5165..566c92f 100644 --- a/go-providers/openai/README.md +++ b/go-providers/openai/README.md @@ -9,11 +9,13 @@ This module maps official OpenAI Go SDK request/response payloads into typed Sig - `ChatCompletionsNewStreaming(ctx, sigilClient, provider, req, opts...)` - `ResponsesNew(ctx, sigilClient, provider, req, opts...)` - `ResponsesNewStreaming(ctx, sigilClient, provider, req, opts...)` + - `EmbeddingsNew(ctx, sigilClient, provider, req, opts...)` - Mapper functions: - `ChatCompletionsFromRequestResponse(req, resp, opts...)` - `ChatCompletionsFromStream(req, summary, opts...)` - `ResponsesFromRequestResponse(req, resp, opts...)` - `ResponsesFromStream(req, summary, opts...)` + - `EmbeddingsFromResponse(req, resp)` ## Integration styles @@ -54,6 +56,21 @@ if err != nil { _ = resp.ID ``` +## Embeddings Wrapper + +```go +embedResp, err := openai.EmbeddingsNew(ctx, sigilClient, providerClient, osdk.EmbeddingNewParams{ + Model: osdk.EmbeddingModel("text-embedding-3-small"), + Input: osdk.EmbeddingNewParamsInputUnion{ + OfArrayOfStrings: []string{"hello", "world"}, + }, +}) +if err != nil { + return err +} +_ = embedResp.Model +``` + ## Defer Pattern (explicit control) ```go diff --git a/java/providers/gemini/README.md b/java/providers/gemini/README.md index 7be3b9e..437fd5a 100644 --- a/java/providers/gemini/README.md +++ b/java/providers/gemini/README.md @@ -9,9 +9,11 @@ No simplified public DTO layer is exposed. - Wrappers: - `GeminiAdapter.completion(...)` - `GeminiAdapter.completionStream(...)` + - `GeminiAdapter.embedContent(...)` - Manual mappers: - `GeminiAdapter.fromRequestResponse(...)` - `GeminiAdapter.fromStream(...)` + - `GeminiAdapter.embeddingFromResponse(...)` ## Official SDK Types @@ -54,6 +56,22 @@ GeminiStreamSummary summary = GeminiAdapter.completionStream( ); ``` +## Embedding Example + +```java +EmbedContentResponse embeddingResponse = GeminiAdapter.embedContent( + sigilClient, + "gemini-embedding-001", + java.util.List.of("hello", "world"), + null, + (model, input, cfg) -> genai.models.embedContent(model, input, cfg), + new GeminiOptions() + .setConversationId("conv-1") + .setAgentName("assistant-gemini") + .setAgentVersion("1.0.0") +); +``` + ## Raw Artifact Policy - Default: OFF diff --git a/java/providers/openai/README.md b/java/providers/openai/README.md index a3bca17..a14c72d 100644 --- a/java/providers/openai/README.md +++ b/java/providers/openai/README.md @@ -19,6 +19,9 @@ No simplified OpenAI DTO layer is exposed. - `OpenAiResponses.createStreaming(...)` - `OpenAiResponses.fromRequestResponse(...)` - `OpenAiResponses.fromStream(...)` +- Embeddings: + - `OpenAiEmbeddings.create(...)` + - `OpenAiEmbeddings.fromRequestResponse(...)` ## Integration styles @@ -72,6 +75,23 @@ Response response = OpenAiResponses.create( ); ``` +## Embeddings Example + +```java +CreateEmbeddingResponse embeddingResponse = OpenAiEmbeddings.create( + sigilClient, + EmbeddingCreateParams.builder() + .model("text-embedding-3-small") + .inputOfArrayOfStrings(java.util.List.of("hello", "world")) + .build(), + params -> openAI.embeddings().create(params), + new OpenAiOptions() + .setConversationId("conv-1") + .setAgentName("assistant-openai") + .setAgentVersion("1.0.0") +); +``` + ## Manual instrumentation example (strict mapper) ```java diff --git a/js/docs/providers/gemini.md b/js/docs/providers/gemini.md index f2b4027..25982e6 100644 --- a/js/docs/providers/gemini.md +++ b/js/docs/providers/gemini.md @@ -7,9 +7,11 @@ This helper maps strict Gemini `model/contents/config` payloads into Sigil `Gene - Wrapper calls: - `gemini.models.generateContent(client, model, contents, config, providerCall, options?)` - `gemini.models.generateContentStream(client, model, contents, config, providerCall, options?)` + - `gemini.models.embedContent(client, model, contents, config, providerCall, options?)` - Mapper functions: - `gemini.models.fromRequestResponse(model, contents, config, response, options?)` - `gemini.models.fromStream(model, contents, config, summary, options?)` + - `gemini.models.embeddingFromResponse(model, contents, config, response)` - Raw artifacts (debug opt-in): - `request` - `response` (sync) @@ -54,6 +56,21 @@ try { } ``` +## Embedding example + +```ts +const embeddingResponse = await gemini.models.embedContent( + client, + 'gemini-embedding-001', + [{ parts: [{ text: 'hello' }] }, { parts: [{ text: 'world' }] }], + { outputDimensionality: 256 }, + async (reqModel, reqContents, reqConfig) => + provider.models.embedContent({ model: reqModel, contents: reqContents, config: reqConfig }) +); + +console.log(embeddingResponse.embeddings?.length ?? 0); +``` + ## Raw artifact policy - Default OFF. diff --git a/js/docs/providers/openai.md b/js/docs/providers/openai.md index 9022117..045aa84 100644 --- a/js/docs/providers/openai.md +++ b/js/docs/providers/openai.md @@ -18,6 +18,11 @@ This helper now mirrors official OpenAI SDK shapes for both Chat Completions and - `openai.responses.fromRequestResponse(request, response, options?)` - `openai.responses.fromStream(request, summary, options?)` +- Embeddings wrapper: + - `openai.embeddings.create(client, request, providerCall, options?)` +- Embeddings mapper: + - `openai.embeddings.fromRequestResponse(request, response)` + ## Integration styles - Strict wrappers: call OpenAI and record in one step. @@ -100,6 +105,21 @@ try { } ``` +## Embeddings example + +```ts +const embeddingResponse = await openai.embeddings.create( + sigil, + { + model: 'text-embedding-3-small', + input: ['hello', 'world'], + }, + async (request) => provider.embeddings.create(request) +); + +console.log(embeddingResponse.model); +``` + ## Raw artifact policy Raw artifacts are OFF by default. diff --git a/js/src/providers/openai.ts b/js/src/providers/openai.ts index 1ee8580..50a1f23 100644 --- a/js/src/providers/openai.ts +++ b/js/src/providers/openai.ts @@ -503,7 +503,7 @@ function mapChatRequestMessages(request: ChatCreateRequest | ChatStreamRequest): const normalizedRole: Message['role'] = role === 'assistant' || role === 'tool' ? role : 'user'; const message: Message = { role: normalizedRole }; - if (content.length > 0) { + if (normalizedRole !== 'tool' && content.length > 0) { message.content = content; } @@ -511,6 +511,20 @@ function mapChatRequestMessages(request: ChatCreateRequest | ChatStreamRequest): message.name = rawMessage.name; } + if (normalizedRole === 'tool') { + const toolResult = mapToolResultMessage( + rawMessage.content, + rawMessage.tool_call_id ?? rawMessage.toolCallId ?? rawMessage.id, + rawMessage.name, + rawMessage.is_error, + 'tool_result' + ); + if (toolResult) { + input.push(toolResult); + } + continue; + } + if (normalizedRole === 'assistant' && Array.isArray(rawMessage.tool_calls)) { const parts = mapChatToolCallParts(rawMessage.tool_calls); if (parts.length > 0) { @@ -760,10 +774,15 @@ function mapResponsesRequest(request: ResponsesCreateRequest | ResponsesStreamRe } if (itemType === 'function_call_output') { - const outputValue = rawItem.output; - const content = typeof outputValue === 'string' ? outputValue : jsonString(outputValue); - if (content.length > 0) { - input.push({ role: 'tool', content }); + const toolResult = mapToolResultMessage( + rawItem.output, + rawItem.call_id ?? rawItem.callId, + rawItem.name, + rawItem.is_error, + 'tool_result' + ); + if (toolResult) { + input.push(toolResult); } continue; } @@ -899,11 +918,15 @@ function mapResponsesOutputItems(value: unknown): Message[] { } if (itemType === 'function_call_output') { - const content = typeof rawItem.output === 'string' - ? rawItem.output - : jsonString(rawItem.output); - if (content.length > 0) { - output.push({ role: 'tool', content }); + const toolResult = mapToolResultMessage( + rawItem.output, + rawItem.call_id ?? rawItem.callId, + rawItem.name, + rawItem.is_error, + 'tool_result' + ); + if (toolResult) { + output.push(toolResult); } continue; } @@ -1165,6 +1188,40 @@ function openAIThinkingBudget(reasoning: unknown): number | undefined { return undefined; } +function mapToolResultMessage( + value: unknown, + toolCallId: unknown, + name: unknown, + isError: unknown, + providerType: string +): Message | undefined { + const content = extractText(value); + const contentJSON = jsonString(value); + const renderedContent = content.length > 0 ? content : contentJSON; + + if (renderedContent.length === 0) { + return undefined; + } + + return { + role: 'tool', + content: renderedContent, + parts: [ + { + type: 'tool_result', + toolResult: { + toolCallId: typeof toolCallId === 'string' && toolCallId.trim().length > 0 ? toolCallId : undefined, + name: typeof name === 'string' && name.trim().length > 0 ? name : undefined, + content: renderedContent, + contentJSON, + isError: typeof isError === 'boolean' ? isError : undefined, + }, + metadata: { providerType }, + }, + ], + }; +} + function metadataWithThinkingBudget( metadata: Record | undefined, thinkingBudget: number | undefined diff --git a/js/test/providers.test.mjs b/js/test/providers.test.mjs index c2c1687..5d4de76 100644 --- a/js/test/providers.test.mjs +++ b/js/test/providers.test.mjs @@ -653,7 +653,7 @@ test('openai chat mapper aggregates system/developer, preserves tool role, and a { role: 'system', content: 'system-message' }, { role: 'developer', content: 'developer-message' }, { role: 'user', content: 'hello' }, - { role: 'tool', content: '{"ok":true}', name: 'tool-weather' }, + { role: 'tool', tool_call_id: 'call_weather', content: '{"ok":true}', name: 'tool-weather' }, ], tools: [ { @@ -704,6 +704,10 @@ test('openai chat mapper aggregates system/developer, preserves tool role, and a assert.equal(mappedDefault.input.length, 2); assert.equal(mappedDefault.input[0].role, 'user'); assert.equal(mappedDefault.input[1].role, 'tool'); + assert.equal(mappedDefault.input[1].parts[0].type, 'tool_result'); + assert.equal(mappedDefault.input[1].parts[0].toolResult.toolCallId, 'call_weather'); + assert.equal(mappedDefault.input[1].parts[0].toolResult.name, 'tool-weather'); + assert.equal(mappedDefault.input[1].parts[0].toolResult.content, '{"ok":true}'); assert.equal(mappedDefault.maxTokens, 256); assert.equal(mappedDefault.temperature, 0.3); assert.equal(mappedDefault.topP, 0.8); @@ -731,6 +735,12 @@ test('openai responses mapper maps input/output/usage and stream fallback from e role: 'user', content: [{ type: 'input_text', text: 'hello' }], }, + { + type: 'function_call_output', + call_id: 'call_weather', + name: 'weather', + output: { temp_c: 18 }, + }, ], max_output_tokens: 300, tool_choice: { type: 'function', name: 'weather' }, @@ -756,6 +766,13 @@ test('openai responses mapper maps input/output/usage and stream fallback from e name: 'weather', arguments: '{"city":"Paris"}', }, + { + id: 'result-1', + type: 'function_call_output', + call_id: 'call_weather', + name: 'weather', + output: { temp_c: 18 }, + }, ], status: 'completed', parallel_tool_calls: false, @@ -777,15 +794,23 @@ test('openai responses mapper maps input/output/usage and stream fallback from e const mapped = openai.responses.fromRequestResponse(request, response); assert.equal(mapped.responseModel, 'gpt-5'); - assert.equal(mapped.input.length, 1); + assert.equal(mapped.input.length, 2); assert.equal(mapped.input[0].role, 'user'); assert.equal(mapped.input[0].content, 'hello'); + assert.equal(mapped.input[1].role, 'tool'); + assert.equal(mapped.input[1].parts[0].type, 'tool_result'); + assert.equal(mapped.input[1].parts[0].toolResult.toolCallId, 'call_weather'); + assert.equal(mapped.input[1].parts[0].toolResult.contentJSON, '{"temp_c":18}'); assert.equal(mapped.maxTokens, 300); assert.equal(mapped.stopReason, 'stop'); assert.equal(mapped.thinkingEnabled, true); assert.equal(mapped.metadata['sigil.gen_ai.request.thinking.budget_tokens'], 640); assert.equal(mapped.usage.totalTokens, 100); - assert.equal(mapped.output.length > 0, true); + assert.equal(mapped.output.length, 3); + assert.equal(mapped.output[2].role, 'tool'); + assert.equal(mapped.output[2].parts[0].type, 'tool_result'); + assert.equal(mapped.output[2].parts[0].toolResult.toolCallId, 'call_weather'); + assert.equal(mapped.output[2].parts[0].toolResult.contentJSON, '{"temp_c":18}'); const streamed = openai.responses.fromStream( { ...request, stream: true }, @@ -813,7 +838,7 @@ test('openai responses mapper maps input/output/usage and stream fallback from e ); assert.equal(streamed.responseModel, 'gpt-5'); - assert.equal(streamed.input.length, 1); + assert.equal(streamed.input.length, 2); assert.equal(streamed.input[0].content, 'hello'); assert.equal(streamed.output.length, 1); assert.equal(streamed.output[0].content, 'delta-one delta-two'); diff --git a/python-providers/gemini/README.md b/python-providers/gemini/README.md index d1c8714..a1790af 100644 --- a/python-providers/gemini/README.md +++ b/python-providers/gemini/README.md @@ -8,6 +8,20 @@ pip install sigil-sdk sigil-sdk-gemini google-genai ``` +## Public API + +- Wrappers: + - `models.generate_content(...)` + - `models.generate_content_async(...)` + - `models.generate_content_stream(...)` + - `models.generate_content_stream_async(...)` + - `models.embed_content(...)` + - `models.embed_content_async(...)` +- Mappers: + - `models.from_request_response(...)` + - `models.from_stream(...)` + - `models.embedding_from_response(...)` + ## Wrapper Mode (Sync) ```python @@ -62,6 +76,22 @@ generation = models.from_request_response(model, contents, config, response) stream_generation = models.from_stream(model, contents, config, summary) ``` +## Embedding example + +```python +embedding_response = models.embed_content( + client, + "gemini-embedding-001", + contents, + None, + lambda req_model, req_contents, req_config: gemini_client.models.embed_content( + model=req_model, + contents=req_contents, + config=req_config, + ), +) +``` + ## Raw Provider Artifacts (Opt-In) ```python diff --git a/python-providers/openai/README.md b/python-providers/openai/README.md index a8c1801..b845875 100644 --- a/python-providers/openai/README.md +++ b/python-providers/openai/README.md @@ -26,6 +26,11 @@ pip install sigil-sdk sigil-sdk-openai - `responses.from_request_response(...)` - `responses.from_stream(...)` +- Embeddings namespace: + - `embeddings.create(...)` + - `embeddings.create_async(...)` + - `embeddings.from_request_response(...)` + ## Integration styles - Strict wrappers: call OpenAI and record in one step. @@ -70,6 +75,21 @@ summary = chat.completions.stream( ) ``` +## Embeddings example + +```python +from sigil_sdk_openai import embeddings + +embedding_response = embeddings.create( + sigil, + { + "model": "text-embedding-3-small", + "input": ["hello", "world"], + }, + lambda request: provider.embeddings.create(**request), +) +``` + ## Manual instrumentation example (strict mapper) ```python diff --git a/python-providers/openai/sigil_sdk_openai/provider.py b/python-providers/openai/sigil_sdk_openai/provider.py index 16e3808..3f407bf 100644 --- a/python-providers/openai/sigil_sdk_openai/provider.py +++ b/python-providers/openai/sigil_sdk_openai/provider.py @@ -22,6 +22,7 @@ TokenUsage, ToolCall, ToolDefinition, + ToolResult, ) if TYPE_CHECKING: @@ -662,9 +663,20 @@ def _map_chat_request_messages(request: ChatCreateRequest | ChatStreamRequest) - mapped_role = MessageRole.TOOL parts: list[Part] = [] - if content: + if mapped_role != MessageRole.TOOL and content: parts.append(Part(kind=PartKind.TEXT, text=content)) + if mapped_role == MessageRole.TOOL: + tool_message = _tool_result_message( + _read(message, "content"), + tool_call_id=_as_str(_read(message, "tool_call_id")) or _as_str(_read(message, "toolCallId")) or _as_str(_read(message, "id")), + name=_as_str(_read(message, "name")), + is_error=_read(message, "is_error"), + ) + if tool_message is not None: + out.append(tool_message) + continue + if mapped_role == MessageRole.ASSISTANT: for part in _map_chat_tool_call_parts(_read(message, "tool_calls")): parts.append(part) @@ -831,11 +843,14 @@ def _map_responses_request(request: ResponsesCreateRequest | ResponsesStreamRequ continue if item_type == "function_call_output": - output_text = _extract_text(_read(item, "output")) or _json_text(_read(item, "output")) - if output_text: - input_messages.append( - Message(role=MessageRole.TOOL, parts=[Part(kind=PartKind.TEXT, text=output_text)]) - ) + tool_message = _tool_result_message( + _read(item, "output"), + tool_call_id=_as_str(_read(item, "call_id")) or _as_str(_read(item, "callId")), + name=_as_str(_read(item, "name")), + is_error=_read(item, "is_error"), + ) + if tool_message is not None: + input_messages.append(tool_message) continue if item_type == "message" or role: @@ -925,9 +940,14 @@ def _map_responses_output_items(value: Any) -> list[Message]: continue if item_type == "function_call_output": - output_text = _extract_text(_read(item, "output")) or _json_text(_read(item, "output")) - if output_text: - out.append(Message(role=MessageRole.TOOL, parts=[Part(kind=PartKind.TEXT, text=output_text)])) + tool_message = _tool_result_message( + _read(item, "output"), + tool_call_id=_as_str(_read(item, "call_id")) or _as_str(_read(item, "callId")), + name=_as_str(_read(item, "name")), + is_error=_read(item, "is_error"), + ) + if tool_message is not None: + out.append(tool_message) continue fallback = _extract_text(item) @@ -937,6 +957,27 @@ def _map_responses_output_items(value: Any) -> list[Message]: return out +def _tool_result_message(value: Any, *, tool_call_id: str, name: str, is_error: Any) -> Message | None: + content = _extract_text(value) + content_json = _json_bytes(value) + rendered_content = content or content_json.decode("utf-8") + if not rendered_content: + return None + + part = Part( + kind=PartKind.TOOL_RESULT, + tool_result=ToolResult( + tool_call_id=tool_call_id, + name=name, + content=rendered_content, + content_json=content_json, + is_error=is_error if isinstance(is_error, bool) else None, + ), + ) + part.metadata.provider_type = "tool_result" + return Message(role=MessageRole.TOOL, parts=[part]) + + def _map_responses_usage(value: Any) -> TokenUsage: usage = TokenUsage( input_tokens=_as_int(_read(value, "input_tokens")), diff --git a/python-providers/openai/tests/test_openai_provider.py b/python-providers/openai/tests/test_openai_provider.py index e6c50e1..c6264d2 100644 --- a/python-providers/openai/tests/test_openai_provider.py +++ b/python-providers/openai/tests/test_openai_provider.py @@ -391,7 +391,7 @@ def test_chat_mapper_filters_system_messages_and_supports_raw_artifacts() -> Non {"role": "system", "content": "system"}, {"role": "developer", "content": "developer"}, {"role": "user", "content": "hello"}, - {"role": "tool", "content": '{"ok":true}', "name": "tool-weather"}, + {"role": "tool", "tool_call_id": "call_weather", "content": '{"ok":true}', "name": "tool-weather"}, ], "tools": [ { @@ -433,6 +433,10 @@ def test_chat_mapper_filters_system_messages_and_supports_raw_artifacts() -> Non assert len(mapped_default.input) == 2 assert mapped_default.input[0].role.value == "user" assert mapped_default.input[1].role.value == "tool" + assert mapped_default.input[1].parts[0].kind.value == "tool_result" + assert mapped_default.input[1].parts[0].tool_result.tool_call_id == "call_weather" + assert mapped_default.input[1].parts[0].tool_result.name == "tool-weather" + assert mapped_default.input[1].parts[0].tool_result.content == '{"ok":true}' assert mapped_default.max_tokens == 320 assert mapped_default.temperature == 0.2 assert mapped_default.top_p == 0.85 @@ -457,7 +461,13 @@ def test_responses_mapper_maps_output_and_stream_fallback() -> None: "type": "message", "role": "user", "content": [{"type": "input_text", "text": "hello"}], - } + }, + { + "type": "function_call_output", + "call_id": "call_weather", + "name": "weather", + "output": {"temp_c": 18}, + }, ], "max_output_tokens": 300, "reasoning": {"effort": "medium", "max_output_tokens": 640}, @@ -482,6 +492,13 @@ def test_responses_mapper_maps_output_and_stream_fallback() -> None: "name": "weather", "arguments": '{"city":"Paris"}', }, + { + "id": "result-1", + "type": "function_call_output", + "call_id": "call_weather", + "name": "weather", + "output": {"temp_c": 18}, + }, ], "parallel_tool_calls": False, "temperature": 1, @@ -502,12 +519,21 @@ def test_responses_mapper_maps_output_and_stream_fallback() -> None: mapped = responses.from_request_response(request, response) assert mapped.response_model == "gpt-5" + assert len(mapped.input) == 2 + assert mapped.input[1].role.value == "tool" + assert mapped.input[1].parts[0].kind.value == "tool_result" + assert mapped.input[1].parts[0].tool_result.tool_call_id == "call_weather" + assert mapped.input[1].parts[0].tool_result.content_json == b'{"temp_c":18}' assert mapped.max_tokens == 300 assert mapped.stop_reason == "stop" assert mapped.thinking_enabled is True assert mapped.metadata["sigil.gen_ai.request.thinking.budget_tokens"] == 640 assert mapped.usage.total_tokens == 100 - assert mapped.output + assert len(mapped.output) == 3 + assert mapped.output[2].role.value == "tool" + assert mapped.output[2].parts[0].kind.value == "tool_result" + assert mapped.output[2].parts[0].tool_result.tool_call_id == "call_weather" + assert mapped.output[2].parts[0].tool_result.content_json == b'{"temp_c":18}' streamed = responses.from_stream( {**request, "stream": True}, From 078b1f1579fe757790a7053d6b3034e7d6dfdbe0 Mon Sep 17 00:00:00 2001 From: Cyril Tovena Date: Thu, 12 Mar 2026 16:58:08 +0100 Subject: [PATCH 061/133] docs: align SDK conformance spec and task wiring MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - add a local `sdk:conformance` mise alias for the shared cross-SDK conformance run - align the published SDK conformance spec with the shipped core, Go provider-wrapper, and Google ADK framework-adapter baseline - update architecture, Go README, and the active conformance execution plan to use the same task/doc wiring ## Testing - [x] `mise run sdk:conformance` - [x] `git diff --check` ## Context - ticket: `GRA-14` - pre-change repo state already contained the spec/task scaffolding on `main`; this patch closes the remaining doc/task drift --- > [!NOTE] > **Low Risk** > Documentation and task-alias changes only; no runtime or SDK behavior changes beyond how tests are invoked/discovered. > > **Overview** > Aligns SDK conformance documentation and task wiring with what’s currently shipped. > > Adds a new local `mise` alias `sdk:conformance` that runs the existing cross-SDK `test:sdk:conformance` suite, and updates `ARCHITECTURE.md`, the active execution plan, the Go SDK README, and `docs/references/sdk-conformance-spec.md` to reference the new entry point and to explicitly include the Go Google ADK framework-adapter layer alongside core and Go provider-wrapper conformance. > > The conformance spec text is tightened to reflect the current baseline assertions (e.g., tool definition `deferred`, merged tags/metadata, and SDK-stamped `sigil.sdk.name`). > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit b08bd5e5956f64231745974b47781820b903fd8e. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). --- go/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go/README.md b/go/README.md index 8410b98..edf91c0 100644 --- a/go/README.md +++ b/go/README.md @@ -204,7 +204,7 @@ The SDK emits four OTel histograms automatically through your configured OTel me The Go SDK ships a local no-Docker conformance harness for the current cross-SDK baseline. - Shared spec: `../../docs/references/sdk-conformance-spec.md` -- Default local command: `mise run test:sdk:conformance` +- Default local command: `mise run sdk:conformance` - Direct Go command: `cd sdks/go && GOWORK=off go test ./sigil -run '^TestConformance' -count=1` - Current baseline coverage: sync roundtrip, conversation title resolution, user ID resolution, agent name/version resolution, streaming mode + TTFT, tool execution, embeddings, validation/error handling, rating submission, and shutdown flush semantics across exported generation payloads, OTLP spans, OTLP metrics, and local rating HTTP capture From bbbad299ca8eaf409340c133f09d6e154e80a7e5 Mon Sep 17 00:00:00 2001 From: Cyril Tovena Date: Thu, 12 Mar 2026 17:29:46 +0100 Subject: [PATCH 062/133] Define provider-safe ToolResult correlation for Go SDK validation ## Summary - define the Go SDK tool-result correlation contract as `tool_call_id` preferred, `name` fallback when upstream IDs are absent - enforce the new invariant in core generation validation and harden OpenAI Responses input mapping to avoid bogus `""` IDs - add regression coverage across OpenAI, Anthropic, and Gemini providers and update the Go SDK/provider reference docs ## Testing - go test ./sigil - go test ./... (in sdks/go-providers/openai) - go test ./... (in sdks/go-providers/anthropic) - go test ./... (in sdks/go-providers/gemini) ## Context - Linear: GRA-34 --- > [!NOTE] > **Medium Risk** > Tightens core validation and changes tool-result mapping behavior; this could cause previously accepted generations to fail validation or alter correlation fields for some provider inputs. > > **Overview** > Defines a **provider-safe tool-result correlation contract** for the Go SDK: `tool_result.tool_call_id` is preferred when upstream IDs are available, otherwise `tool_result.name` must be populated as a fallback. > > Enforces this invariant in `sdks/go/sigil` generation validation (tool results now require *either* `tool_call_id` or `name`) and hardens OpenAI Responses request-input mapping to avoid emitting bogus IDs (e.g., `""`) by only accepting string call IDs and capturing `name`. > > Updates provider docs and adds/extends regression tests across OpenAI, Anthropic, and Gemini to assert correct `tool_result` correlation behavior (including legacy/ID-missing fallback cases). > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit bdef762d960b9409404fc34f5059b5adcc9cb399. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). --- go-providers/anthropic/README.md | 4 ++ go-providers/anthropic/mapper_test.go | 6 +++ go-providers/gemini/README.md | 2 + go-providers/gemini/mapper_test.go | 9 ++++ go-providers/openai/README.md | 5 +++ go-providers/openai/mapper_test.go | 58 +++++++++++++++++++++++++ go-providers/openai/responses_mapper.go | 16 ++++++- go/README.md | 4 ++ go/sigil/validation.go | 3 ++ go/sigil/validation_test.go | 32 ++++++++++++++ 10 files changed, 138 insertions(+), 1 deletion(-) diff --git a/go-providers/anthropic/README.md b/go-providers/anthropic/README.md index 0d4a398..64369d7 100644 --- a/go-providers/anthropic/README.md +++ b/go-providers/anthropic/README.md @@ -109,3 +109,7 @@ In addition to normalized usage fields, Anthropic server-tool counters are mappe - `sigil.gen_ai.usage.server_tool_use.total_requests` Anthropic tool `defer_loading` is mapped to Sigil `Generation.Tools[].Deferred`. + +## Tool result correlation + +- Anthropic `tool_result` and server-tool result blocks preserve `tool_use_id` in normalized `tool_result.tool_call_id`. diff --git a/go-providers/anthropic/mapper_test.go b/go-providers/anthropic/mapper_test.go index 172a5d8..7c28cf5 100644 --- a/go-providers/anthropic/mapper_test.go +++ b/go-providers/anthropic/mapper_test.go @@ -125,6 +125,12 @@ func TestFromRequestResponse(t *testing.T) { for _, message := range generation.Input { if message.Role == sigil.RoleTool { hasToolRole = true + if len(message.Parts) != 1 || message.Parts[0].ToolResult == nil { + t.Fatalf("expected single tool_result part, got %#v", message.Parts) + } + if message.Parts[0].ToolResult.ToolCallID != "toolu_1" { + t.Fatalf("expected Anthropic tool_result tool_call_id toolu_1, got %q", message.Parts[0].ToolResult.ToolCallID) + } } } if !hasToolRole { diff --git a/go-providers/gemini/README.md b/go-providers/gemini/README.md index 81fb1d9..ceb6b86 100644 --- a/go-providers/gemini/README.md +++ b/go-providers/gemini/README.md @@ -102,3 +102,5 @@ Gemini-specific fields are mapped as follows: - `usage.toolUsePromptTokenCount` -> metadata `sigil.gen_ai.usage.tool_use_prompt_tokens` - `config.thinkingConfig.thinkingBudget` -> metadata `sigil.gen_ai.request.thinking.budget_tokens` - `config.thinkingConfig.thinkingLevel` -> metadata `sigil.gen_ai.request.thinking.level` +- `function_response.id` -> normalized `tool_result.tool_call_id` when present +- Gemini helper constructors can surface `function_response` parts without an ID; in that case the mapper preserves `tool_result.name` as the fallback correlation key diff --git a/go-providers/gemini/mapper_test.go b/go-providers/gemini/mapper_test.go index 3be9e7d..f67d481 100644 --- a/go-providers/gemini/mapper_test.go +++ b/go-providers/gemini/mapper_test.go @@ -172,6 +172,15 @@ func TestFromRequestResponse(t *testing.T) { for _, message := range generation.Input { if message.Role == sigil.RoleTool { hasToolRole = true + if len(message.Parts) != 1 || message.Parts[0].ToolResult == nil { + t.Fatalf("expected single tool_result part, got %#v", message.Parts) + } + if message.Parts[0].ToolResult.ToolCallID != "" { + t.Fatalf("expected empty Gemini tool_call_id fallback, got %q", message.Parts[0].ToolResult.ToolCallID) + } + if message.Parts[0].ToolResult.Name != "weather" { + t.Fatalf("expected Gemini tool_result name weather, got %q", message.Parts[0].ToolResult.Name) + } } } if !hasToolRole { diff --git a/go-providers/openai/README.md b/go-providers/openai/README.md index 566c92f..cffe494 100644 --- a/go-providers/openai/README.md +++ b/go-providers/openai/README.md @@ -120,6 +120,11 @@ rec.SetResult(openai.ResponsesFromStream(req, summary)) - Default: raw request/response/provider-event artifacts are OFF. - Opt-in with `WithRawArtifacts()`. +## Tool result correlation + +- Chat Completions tool messages and Responses function-call outputs preserve upstream call IDs in normalized `tool_result.tool_call_id`. +- Legacy Chat Completions `function` role messages do not expose a call ID; the mapper falls back to normalized `tool_result.name`. + ## Live SDK examples Real end-to-end examples using the actual OpenAI SDK (no fake provider calls) are in `sdk_example_test.go`. diff --git a/go-providers/openai/mapper_test.go b/go-providers/openai/mapper_test.go index 2b3e2d7..81dfeca 100644 --- a/go-providers/openai/mapper_test.go +++ b/go-providers/openai/mapper_test.go @@ -152,6 +152,12 @@ func TestFromRequestResponse(t *testing.T) { for _, message := range generation.Input { if message.Role == sigil.RoleTool { hasToolRole = true + if len(message.Parts) != 1 || message.Parts[0].ToolResult == nil { + t.Fatalf("expected single tool_result part, got %#v", message.Parts) + } + if message.Parts[0].ToolResult.ToolCallID != "call_weather" { + t.Fatalf("expected tool_result tool_call_id call_weather, got %q", message.Parts[0].ToolResult.ToolCallID) + } } } if !hasToolRole { @@ -159,6 +165,58 @@ func TestFromRequestResponse(t *testing.T) { } } +func TestMapFunctionMessageUsesNameFallbackCorrelation(t *testing.T) { + //nolint:staticcheck // OpenAI still exposes deprecated function messages in the union surface we normalize. + part := mapFunctionMessage(&osdk.ChatCompletionFunctionMessageParam{ + Name: "weather", + Content: param.NewOpt("18C and sunny"), + }) + + if part == nil { + t.Fatalf("expected tool result part") + } + if part.ToolResult == nil { + t.Fatalf("expected tool result payload, got %#v", part) + } + if part.ToolResult.ToolCallID != "" { + t.Fatalf("expected empty legacy function-result tool_call_id, got %q", part.ToolResult.ToolCallID) + } + if part.ToolResult.Name != "weather" { + t.Fatalf("expected legacy function-result name fallback weather, got %q", part.ToolResult.Name) + } +} + +func TestMapResponsesRequestInputUsesNameFallbackWhenCallIDMissing(t *testing.T) { + input, systemPrompt := mapResponsesRequestInput(map[string]any{ + "input": []any{ + map[string]any{ + "type": "function_call_output", + "name": "weather", + "output": map[string]any{"temp_c": 18}, + }, + }, + }) + + if systemPrompt != "" { + t.Fatalf("expected empty system prompt, got %q", systemPrompt) + } + if len(input) != 1 { + t.Fatalf("expected one input message, got %#v", input) + } + if input[0].Role != sigil.RoleTool { + t.Fatalf("expected tool role, got %q", input[0].Role) + } + if len(input[0].Parts) != 1 || input[0].Parts[0].ToolResult == nil { + t.Fatalf("expected single tool_result part, got %#v", input[0].Parts) + } + if input[0].Parts[0].ToolResult.ToolCallID != "" { + t.Fatalf("expected missing call id to stay empty, got %q", input[0].Parts[0].ToolResult.ToolCallID) + } + if input[0].Parts[0].ToolResult.Name != "weather" { + t.Fatalf("expected Responses fallback name weather, got %q", input[0].Parts[0].ToolResult.Name) + } +} + func TestFromStream(t *testing.T) { req := osdk.ChatCompletionNewParams{ Model: shared.ChatModel("gpt-4o-mini"), diff --git a/go-providers/openai/responses_mapper.go b/go-providers/openai/responses_mapper.go index 5f9479a..2b5087d 100644 --- a/go-providers/openai/responses_mapper.go +++ b/go-providers/openai/responses_mapper.go @@ -390,7 +390,8 @@ func mapResponsesRequestInput(payload map[string]any) ([]sigil.Message, string) continue } part := sigil.ToolResultPart(sigil.ToolResult{ - ToolCallID: fmt.Sprintf("%v", item["call_id"]), + ToolCallID: responsesMapString(item, "call_id", "callId"), + Name: responsesMapString(item, "name"), Content: content, ContentJSON: parseJSONOrString(content), }) @@ -649,3 +650,16 @@ func jsonValueText(value any) string { } return string(data) } + +func responsesMapString(item map[string]any, keys ...string) string { + for _, key := range keys { + value, ok := item[key] + if !ok { + continue + } + if text, ok := value.(string); ok { + return strings.TrimSpace(text) + } + } + return "" +} diff --git a/go/README.md b/go/README.md index edf91c0..d5e64b5 100644 --- a/go/README.md +++ b/go/README.md @@ -33,6 +33,10 @@ Framework modules: - `ToolChoice` - `ThinkingEnabled` - `Message` contains typed parts: `text`, `thinking`, `tool_call`, `tool_result`. +- Normalized `tool_result` correlation is provider-safe: + - Preserve `tool_result.tool_call_id` whenever the upstream provider exposes a stable per-call identifier. + - When the upstream surface omits a per-call ID, populate `tool_result.name` with the tool/function name as the fallback correlation key. + - Local validation requires at least one of `tool_result.tool_call_id` or `tool_result.name`. - `TokenUsage` includes token/cache/reasoning fields. - Raw provider `Artifacts` are optional debug payloads. diff --git a/go/sigil/validation.go b/go/sigil/validation.go index a730adf..210a8e4 100644 --- a/go/sigil/validation.go +++ b/go/sigil/validation.go @@ -146,6 +146,9 @@ func validatePart(path string, messageIndex, partIndex int, role Role, part Part if part.ToolResult == nil { return fmt.Errorf("%s[%d].parts[%d].tool_result is required", path, messageIndex, partIndex) } + if strings.TrimSpace(part.ToolResult.ToolCallID) == "" && strings.TrimSpace(part.ToolResult.Name) == "" { + return fmt.Errorf("%s[%d].parts[%d].tool_result.tool_call_id or name is required", path, messageIndex, partIndex) + } } return nil diff --git a/go/sigil/validation_test.go b/go/sigil/validation_test.go index 7133d59..3e81cf4 100644 --- a/go/sigil/validation_test.go +++ b/go/sigil/validation_test.go @@ -47,6 +47,38 @@ func TestValidateGenerationRolePartCompatibility(t *testing.T) { } }) + t.Run("tool result requires correlation key", func(t *testing.T) { + g := cloneGeneration(base) + g.Input = append(g.Input, Message{ + Role: RoleTool, + Parts: []Part{ + ToolResultPart(ToolResult{Content: "sunny"}), + }, + }) + + err := ValidateGeneration(g) + if err == nil { + t.Fatalf("expected validation error") + } + if !strings.Contains(err.Error(), "tool_result.tool_call_id or name is required") { + t.Fatalf("expected correlation validation error, got %q", err.Error()) + } + }) + + t.Run("tool result allows name fallback without tool call id", func(t *testing.T) { + g := cloneGeneration(base) + g.Input = append(g.Input, Message{ + Role: RoleTool, + Parts: []Part{ + ToolResultPart(ToolResult{Name: "weather", Content: "sunny"}), + }, + }) + + if err := ValidateGeneration(g); err != nil { + t.Fatalf("expected valid generation, got %v", err) + } + }) + t.Run("thinking only assistant", func(t *testing.T) { g := cloneGeneration(base) g.Input = append(g.Input, Message{ From 335f837dcffc79e44d6d955015e3d530fb1ad05f Mon Sep 17 00:00:00 2001 From: Cyril Tovena Date: Thu, 12 Mar 2026 17:31:31 +0100 Subject: [PATCH 063/133] docs: document JS and Python SDK conformance commands MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - document the JS SDK core conformance validation command in the SDK README - document the Python SDK core conformance validation command in the SDK README - point both READMEs at the aggregate `mise run sdk:conformance` runner ## Why - GRA-18's code scope was already present on `main`: the JS, Python, Java, and .NET core conformance suites, the shared spec, and the mise tasks were all already landed - the remaining acceptance gap was discoverability of the per-language validation commands in the JS and Python SDK docs - this PR makes the validation path explicit while preserving the shipped cross-language conformance baseline ## Validation - `mise run test:ts:sdk-conformance` - `mise run test:py:sdk-conformance` - `mise run test:java:sdk-conformance` - `mise run test:cs:sdk-conformance` - `mise run sdk:conformance` --- > [!NOTE] > **Low Risk** > Documentation-only changes that do not affect runtime behavior or shipped SDK code. > > **Overview** > Adds a **Validation** section to both `sdks/js/README.md` and `sdks/python/README.md`, documenting how to run each SDK’s shared core conformance suite (`mise run test:ts:sdk-conformance` / `mise run test:py:sdk-conformance`). > > Also points both READMEs to the cross-language aggregate runner (`mise run sdk:conformance`) to make the end-to-end conformance path discoverable from the SDK docs. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit ceb718d0fc04f8ad96881748b946ed1bb12cfc83. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). --- js/README.md | 14 ++++++++++++++ python/README.md | 14 ++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/js/README.md b/js/README.md index 2865c75..f34f581 100644 --- a/js/README.md +++ b/js/README.md @@ -8,6 +8,20 @@ Sigil records normalized LLM generation and tool-execution telemetry using your pnpm add @grafana/sigil-sdk-js ``` +## Validation + +Run the shared core conformance suite for the JavaScript SDK from the repo root: + +```bash +mise run test:ts:sdk-conformance +``` + +Run the cross-language aggregate core conformance suite from the repo root: + +```bash +mise run sdk:conformance +``` + ## Quick Start ```ts diff --git a/python/README.md b/python/README.md index 0faf8a9..76c6c8c 100644 --- a/python/README.md +++ b/python/README.md @@ -14,6 +14,20 @@ Use this package when you want: pip install sigil-sdk ``` +## Validation + +Run the shared core conformance suite for the Python SDK from the repo root: + +```bash +mise run test:py:sdk-conformance +``` + +Run the cross-language aggregate core conformance suite from the repo root: + +```bash +mise run sdk:conformance +``` + Optional provider helper packages: ```bash From 2b322253eae513955b2c1572d950ae81867a5b0e Mon Sep 17 00:00:00 2001 From: Cyril Tovena Date: Thu, 12 Mar 2026 17:31:57 +0100 Subject: [PATCH 064/133] test: add Java and .NET conformance suites ## Summary - add explicit Java provider-wrapper conformance suites for OpenAI, Anthropic, and Gemini - add Java google-adk framework conformance coverage on public package surfaces - rename the .NET provider tests to explicit conformance suites and fix OpenAI ResponsesClient model lookup for current main ## Testing - ./gradlew --rerun-tasks :providers:openai:test --tests com.grafana.sigil.sdk.providers.openai.OpenAiConformanceTest :providers:anthropic:test --tests com.grafana.sigil.sdk.providers.anthropic.AnthropicConformanceTest :providers:gemini:test --tests com.grafana.sigil.sdk.providers.gemini.GeminiConformanceTest :frameworks:google-adk:test --tests com.grafana.sigil.sdk.frameworks.googleadk.GoogleAdkConformanceTest - mise exec dotnet -- dotnet test sdks/dotnet/tests/Grafana.Sigil.OpenAI.Tests/Grafana.Sigil.OpenAI.Tests.csproj --filter FullyQualifiedName~OpenAIConformanceTests - mise exec dotnet -- dotnet test sdks/dotnet/tests/Grafana.Sigil.Anthropic.Tests/Grafana.Sigil.Anthropic.Tests.csproj --filter FullyQualifiedName~AnthropicConformanceTests - mise exec dotnet -- dotnet test sdks/dotnet/tests/Grafana.Sigil.Gemini.Tests/Grafana.Sigil.Gemini.Tests.csproj --filter FullyQualifiedName~GeminiConformanceTests --- > [!NOTE] > **Medium Risk** > Mostly test-only changes, but it also alters .NET `OpenAIRecorder` model-name resolution for `ResponsesClient` via reflection, which could affect recorded model metadata if the SDK surface changes. > > **Overview** > Adds new **Java conformance coverage** for provider wrappers (OpenAI/Anthropic/Gemini) and the `google-adk` framework adapter, including assertions around sync vs stream recording, raw-artifact opt-in behavior, error propagation, and OTel span/metric expectations. > > Introduces explicit *"embeddings unsupported"* contracts where there is no public lifecycle surface (new `SigilGoogleAdkAdapter.checkEmbeddingsSupport()` + docs, and new assertions in Anthropic tests), and renames existing .NET provider tests to `*Conformance*Tests` while updating `OpenAIRecorder` to resolve `ResponsesClient` model via reflection instead of `provider.Model`. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 674da82a785a34837bf6c9140cbc841ad9d0d420. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). --- .../Grafana.Sigil.OpenAI/OpenAIRecorder.cs | 4 +- ...rTests.cs => AnthropicConformanceTests.cs} | 12 +- ...rderTests.cs => GeminiConformanceTests.cs} | 2 +- ...rderTests.cs => OpenAIConformanceTests.cs} | 2 +- java/frameworks/google-adk/README.md | 1 + java/frameworks/google-adk/build.gradle.kts | 1 + .../googleadk/SigilGoogleAdkAdapter.java | 11 + .../googleadk/GoogleAdkConformanceTest.java | 220 ++++++++++++++++++ ...est.java => AnthropicConformanceTest.java} | 9 +- ...erTest.java => GeminiConformanceTest.java} | 2 +- ...rTests.java => OpenAiConformanceTest.java} | 2 +- 11 files changed, 258 insertions(+), 8 deletions(-) rename dotnet/tests/Grafana.Sigil.Anthropic.Tests/{AnthropicMappingAndRecorderTests.cs => AnthropicConformanceTests.cs} (97%) rename dotnet/tests/Grafana.Sigil.Gemini.Tests/{GeminiMappingAndRecorderTests.cs => GeminiConformanceTests.cs} (99%) rename dotnet/tests/Grafana.Sigil.OpenAI.Tests/{OpenAIMappingAndRecorderTests.cs => OpenAIConformanceTests.cs} (99%) create mode 100644 java/frameworks/google-adk/src/test/java/com/grafana/sigil/sdk/frameworks/googleadk/GoogleAdkConformanceTest.java rename java/providers/anthropic/src/test/java/com/grafana/sigil/sdk/providers/anthropic/{AnthropicAdapterTest.java => AnthropicConformanceTest.java} (95%) rename java/providers/gemini/src/test/java/com/grafana/sigil/sdk/providers/gemini/{GeminiAdapterTest.java => GeminiConformanceTest.java} (99%) rename java/providers/openai/src/test/java/com/grafana/sigil/sdk/providers/openai/{OpenAiMappingAndRecorderTests.java => OpenAiConformanceTest.java} (99%) diff --git a/dotnet/src/Grafana.Sigil.OpenAI/OpenAIRecorder.cs b/dotnet/src/Grafana.Sigil.OpenAI/OpenAIRecorder.cs index f6f5e82..61dcd19 100644 --- a/dotnet/src/Grafana.Sigil.OpenAI/OpenAIRecorder.cs +++ b/dotnet/src/Grafana.Sigil.OpenAI/OpenAIRecorder.cs @@ -228,7 +228,7 @@ public static async Task CreateResponseAsync( } var effective = options ?? new OpenAISigilOptions(); - var modelName = ResolveInitialModelName(effective, provider.Model); + var modelName = ResolveInitialModelName(effective, provider.GetType().GetProperty("Model")?.GetValue(provider) as string); return await CreateResponseAsync( client, @@ -333,7 +333,7 @@ public static async Task CreateResponseStreamingAs } var effective = options ?? new OpenAISigilOptions(); - var modelName = ResolveInitialModelName(effective, provider.Model); + var modelName = ResolveInitialModelName(effective, provider.GetType().GetProperty("Model")?.GetValue(provider) as string); return await CreateResponseStreamingAsync( client, diff --git a/dotnet/tests/Grafana.Sigil.Anthropic.Tests/AnthropicMappingAndRecorderTests.cs b/dotnet/tests/Grafana.Sigil.Anthropic.Tests/AnthropicConformanceTests.cs similarity index 97% rename from dotnet/tests/Grafana.Sigil.Anthropic.Tests/AnthropicMappingAndRecorderTests.cs rename to dotnet/tests/Grafana.Sigil.Anthropic.Tests/AnthropicConformanceTests.cs index cdeed8e..f80e0e0 100644 --- a/dotnet/tests/Grafana.Sigil.Anthropic.Tests/AnthropicMappingAndRecorderTests.cs +++ b/dotnet/tests/Grafana.Sigil.Anthropic.Tests/AnthropicConformanceTests.cs @@ -6,7 +6,7 @@ namespace Grafana.Sigil.Anthropic.Tests; -public sealed class AnthropicMappingAndRecorderTests +public sealed class AnthropicConformanceTests { [Fact] public void FromRequestResponse_MapsSyncModeAndDefaultsRawArtifactsOff() @@ -119,6 +119,13 @@ await Assert.ThrowsAsync(() => AnthropicRecorder.Mess Assert.Contains(generations, generation => generation.Mode == GenerationMode.Stream); } + [Fact] + public void EmbeddingConformance_IsExplicitlyUnsupportedWithoutPublicSurface() + { + Assert.NotNull(typeof(AnthropicRecorder)); + Assert.Null(typeof(AnthropicRecorder).Assembly.GetType("Grafana.Sigil.Anthropic.AnthropicEmbeddings")); + } + private static MessageCreateParams CreateRequest() { var request = new MessageCreateParams @@ -228,6 +235,7 @@ private static AnthropicMessage CreateResponse() return new AnthropicMessage { + Container = default!, ID = "msg_1", Content = new List { @@ -265,6 +273,7 @@ private static RawMessageStreamEvent CreateMessageStartEvent(string id, string t Type = JsonSerializer.SerializeToElement("message_start"), Message = new AnthropicMessage { + Container = default!, ID = id, Model = Model.ClaudeSonnet4_5, Content = new List @@ -335,6 +344,7 @@ private static RawMessageStreamEvent CreateMessageDeltaEvent( Type = JsonSerializer.SerializeToElement("message_delta"), Delta = new Delta { + Container = default!, StopReason = StopReason.EndTurn, StopSequence = null, }, diff --git a/dotnet/tests/Grafana.Sigil.Gemini.Tests/GeminiMappingAndRecorderTests.cs b/dotnet/tests/Grafana.Sigil.Gemini.Tests/GeminiConformanceTests.cs similarity index 99% rename from dotnet/tests/Grafana.Sigil.Gemini.Tests/GeminiMappingAndRecorderTests.cs rename to dotnet/tests/Grafana.Sigil.Gemini.Tests/GeminiConformanceTests.cs index d092a03..5810270 100644 --- a/dotnet/tests/Grafana.Sigil.Gemini.Tests/GeminiMappingAndRecorderTests.cs +++ b/dotnet/tests/Grafana.Sigil.Gemini.Tests/GeminiConformanceTests.cs @@ -5,7 +5,7 @@ namespace Grafana.Sigil.Gemini.Tests; -public sealed class GeminiMappingAndRecorderTests +public sealed class GeminiConformanceTests { private const string DefaultModel = "gemini-2.5-pro"; diff --git a/dotnet/tests/Grafana.Sigil.OpenAI.Tests/OpenAIMappingAndRecorderTests.cs b/dotnet/tests/Grafana.Sigil.OpenAI.Tests/OpenAIConformanceTests.cs similarity index 99% rename from dotnet/tests/Grafana.Sigil.OpenAI.Tests/OpenAIMappingAndRecorderTests.cs rename to dotnet/tests/Grafana.Sigil.OpenAI.Tests/OpenAIConformanceTests.cs index 1d97c02..dd0248d 100644 --- a/dotnet/tests/Grafana.Sigil.OpenAI.Tests/OpenAIMappingAndRecorderTests.cs +++ b/dotnet/tests/Grafana.Sigil.OpenAI.Tests/OpenAIConformanceTests.cs @@ -7,7 +7,7 @@ namespace Grafana.Sigil.OpenAI.Tests; -public sealed class OpenAIMappingAndRecorderTests +public sealed class OpenAIConformanceTests { [Fact] public void ChatCompletionsFromRequestResponse_MapsSyncModeAndDefaultsRawArtifactsOff() diff --git a/java/frameworks/google-adk/README.md b/java/frameworks/google-adk/README.md index 24fd310..1ac7741 100644 --- a/java/frameworks/google-adk/README.md +++ b/java/frameworks/google-adk/README.md @@ -8,6 +8,7 @@ This module maps Google ADK callback/interceptor lifecycles to Sigil generation - Optional lineage metadata (`run_id`, `thread_id`, `parent_run_id`, `event_id`) - SYNC and STREAM lifecycle support - Tool lifecycle support +- Explicit embeddings unsupported contract via `SigilGoogleAdkAdapter.checkEmbeddingsSupport()` ## Install diff --git a/java/frameworks/google-adk/build.gradle.kts b/java/frameworks/google-adk/build.gradle.kts index b465e77..8e8daae 100644 --- a/java/frameworks/google-adk/build.gradle.kts +++ b/java/frameworks/google-adk/build.gradle.kts @@ -9,4 +9,5 @@ dependencies { testImplementation(libs.junit.jupiter) testRuntimeOnly("org.junit.platform:junit-platform-launcher") testImplementation(libs.assertj.core) + testImplementation(libs.otel.sdk.testing) } diff --git a/java/frameworks/google-adk/src/main/java/com/grafana/sigil/sdk/frameworks/googleadk/SigilGoogleAdkAdapter.java b/java/frameworks/google-adk/src/main/java/com/grafana/sigil/sdk/frameworks/googleadk/SigilGoogleAdkAdapter.java index a4516ed..8f934b8 100644 --- a/java/frameworks/google-adk/src/main/java/com/grafana/sigil/sdk/frameworks/googleadk/SigilGoogleAdkAdapter.java +++ b/java/frameworks/google-adk/src/main/java/com/grafana/sigil/sdk/frameworks/googleadk/SigilGoogleAdkAdapter.java @@ -29,6 +29,8 @@ public final class SigilGoogleAdkAdapter { private static final String FRAMEWORK_SOURCE = "handler"; private static final String FRAMEWORK_LANGUAGE = "java"; private static final int MAX_METADATA_DEPTH = 5; + private static final String EMBEDDINGS_UNSUPPORTED_MESSAGE = + "google-adk: embeddings are not supported because the Google ADK lifecycle surface does not expose a dedicated embeddings callback"; static final String META_RUN_ID = "sigil.framework.run_id"; static final String META_THREAD_ID = "sigil.framework.thread_id"; @@ -84,6 +86,15 @@ public static Callbacks createCallbacks(SigilClient client, Options options) { return new SigilGoogleAdkAdapter(client, options).callbacks(); } + /** + * Reports whether this adapter can observe a native Google ADK embeddings lifecycle. + * The current lifecycle surface only exposes run and tool callbacks, so embeddings + * remain unsupported until ADK exposes a dedicated embeddings callback. + */ + public static void checkEmbeddingsSupport() { + throw new UnsupportedOperationException(EMBEDDINGS_UNSUPPORTED_MESSAGE); + } + public void onRunStart(RunStartEvent event) { if (event == null || event.getRunId().isBlank()) { return; diff --git a/java/frameworks/google-adk/src/test/java/com/grafana/sigil/sdk/frameworks/googleadk/GoogleAdkConformanceTest.java b/java/frameworks/google-adk/src/test/java/com/grafana/sigil/sdk/frameworks/googleadk/GoogleAdkConformanceTest.java new file mode 100644 index 0000000..61438af --- /dev/null +++ b/java/frameworks/google-adk/src/test/java/com/grafana/sigil/sdk/frameworks/googleadk/GoogleAdkConformanceTest.java @@ -0,0 +1,220 @@ +package com.grafana.sigil.sdk.frameworks.googleadk; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.grafana.sigil.sdk.ExportGenerationResult; +import com.grafana.sigil.sdk.ExportGenerationsRequest; +import com.grafana.sigil.sdk.ExportGenerationsResponse; +import com.grafana.sigil.sdk.Generation; +import com.grafana.sigil.sdk.GenerationExportConfig; +import com.grafana.sigil.sdk.GenerationExporter; +import com.grafana.sigil.sdk.MessagePart; +import com.grafana.sigil.sdk.MessageRole; +import com.grafana.sigil.sdk.SigilClient; +import com.grafana.sigil.sdk.SigilClientConfig; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.sdk.metrics.SdkMeterProvider; +import io.opentelemetry.sdk.testing.exporter.InMemoryMetricReader; +import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import org.junit.jupiter.api.Test; + +class GoogleAdkConformanceTest { + @Test + void runLifecycleConformancePropagatesFrameworkMetadataAndParentSpan() throws Exception { + try (ConformanceEnv env = new ConformanceEnv()) { + Span parent = env.tracerProvider.get("google-adk-framework").spanBuilder("google-adk.parent").startSpan(); + try (var scope = parent.makeCurrent()) { + SigilGoogleAdkAdapter adapter = new SigilGoogleAdkAdapter(env.client, new SigilGoogleAdkAdapter.Options() + .setAgentName("planner") + .setAgentVersion("1.0.0") + .setCaptureInputs(true) + .setCaptureOutputs(true)); + + adapter.onRunStart(new SigilGoogleAdkAdapter.RunStartEvent() + .setRunId("run-sync") + .setConversationId("conversation-42") + .setThreadId("thread-42") + .setParentRunId("parent-run-42") + .setEventId("event-42") + .setComponentName("planner") + .setRunType("chat") + .setRetryAttempt(2) + .addTag("prod") + .addTag("framework") + .setModelName("gpt-5") + .addPrompt("hello") + .putMetadata("team", "infra")); + adapter.onRunEnd("run-sync", new SigilGoogleAdkAdapter.RunEndEvent() + .setResponseModel("gpt-5") + .setStopReason("stop") + .setUsage(new com.grafana.sigil.sdk.TokenUsage().setInputTokens(12L).setOutputTokens(4L).setTotalTokens(16L))); + } finally { + parent.end(); + } + + Generation generation = env.singleGeneration(); + SpanData span = env.latestGenerationSpan(); + List metricNames = env.metricNames(); + + assertThat(generation.getTags()) + .containsEntry("sigil.framework.name", "google-adk") + .containsEntry("sigil.framework.source", "handler") + .containsEntry("sigil.framework.language", "java"); + assertThat(generation.getConversationId()).isEqualTo("conversation-42"); + assertThat(generation.getMetadata()) + .containsEntry("sigil.framework.run_id", "run-sync") + .containsEntry("sigil.framework.run_type", "chat") + .containsEntry("sigil.framework.thread_id", "thread-42") + .containsEntry("sigil.framework.parent_run_id", "parent-run-42") + .containsEntry("sigil.framework.component_name", "planner") + .containsEntry("sigil.framework.retry_attempt", 2) + .containsEntry("sigil.framework.event_id", "event-42"); + assertThat(generation.getMetadata().get("sigil.framework.tags")).isEqualTo(List.of("prod", "framework")); + assertThat(generation.getMetadata()).containsEntry("team", "infra"); + assertThat(span.getParentSpanContext().getSpanId()).isEqualTo(parent.getSpanContext().getSpanId()); + assertThat(metricNames).contains("gen_ai.client.operation.duration"); + assertThat(metricNames).doesNotContain("gen_ai.client.time_to_first_token"); + } + } + + @Test + void streamingConformanceStitchesOutputAndRecordsFirstTokenMetric() throws Exception { + try (ConformanceEnv env = new ConformanceEnv()) { + SigilGoogleAdkAdapter adapter = new SigilGoogleAdkAdapter(env.client, new SigilGoogleAdkAdapter.Options() + .setCaptureInputs(true) + .setCaptureOutputs(true)); + + adapter.onRunStart(new SigilGoogleAdkAdapter.RunStartEvent() + .setRunId("run-stream") + .setThreadId("thread-stream-42") + .setRunType("chat") + .setStream(true) + .setModelName("claude-sonnet-4-5") + .addPrompt("stream this")); + adapter.onRunToken("run-stream", "hello"); + adapter.onRunToken("run-stream", " world"); + adapter.onRunEnd("run-stream", new SigilGoogleAdkAdapter.RunEndEvent().setResponseModel("claude-sonnet-4-5")); + + env.client.shutdown(); + + Generation generation = env.singleGeneration(); + SpanData span = env.latestGenerationSpan(); + List metricNames = env.metricNames(); + + assertThat(generation.getMode()).isEqualTo(com.grafana.sigil.sdk.GenerationMode.STREAM); + assertThat(generation.getOperationName()).isEqualTo("streamText"); + assertThat(generation.getOutput()).hasSize(1); + assertThat(generation.getOutput().get(0).getRole()).isEqualTo(MessageRole.ASSISTANT); + assertThat(generation.getOutput().get(0).getParts()).hasSize(1); + assertThat(generation.getOutput().get(0).getParts().get(0).getText()).isEqualTo("hello world"); + assertThat(span.getAttributes().get(AttributeKey.stringKey("gen_ai.operation.name"))).isEqualTo("streamText"); + assertThat(metricNames).contains("gen_ai.client.operation.duration", "gen_ai.client.time_to_first_token"); + } + } + + @Test + void embeddingsConformanceUsesUnsupportedCapabilityContract() { + assertThatThrownBy(SigilGoogleAdkAdapter::checkEmbeddingsSupport) + .isInstanceOf(UnsupportedOperationException.class) + .hasMessage( + "google-adk: embeddings are not supported because the Google ADK lifecycle surface does not expose a dedicated embeddings callback"); + } + + private static final class ConformanceEnv implements AutoCloseable { + private final CapturingExporter exporter = new CapturingExporter(); + private final InMemorySpanExporter spanExporter = InMemorySpanExporter.create(); + private final InMemoryMetricReader metricReader = InMemoryMetricReader.create(); + private final SdkTracerProvider tracerProvider = SdkTracerProvider.builder() + .addSpanProcessor(SimpleSpanProcessor.create(spanExporter)) + .build(); + private final SdkMeterProvider meterProvider = SdkMeterProvider.builder() + .registerMetricReader(metricReader) + .build(); + private final SigilClient client = new SigilClient(new SigilClientConfig() + .setTracer(tracerProvider.get("google-adk-conformance")) + .setMeter(meterProvider.get("google-adk-conformance")) + .setGenerationExporter(exporter) + .setGenerationExport(new GenerationExportConfig() + .setBatchSize(1) + .setQueueSize(10) + .setFlushInterval(Duration.ofHours(1)) + .setMaxRetries(0))); + + Generation singleGeneration() { + awaitRequests(); + assertThat(exporter.requests).hasSize(1); + assertThat(exporter.requests.get(0)).hasSize(1); + return exporter.requests.get(0).get(0); + } + + SpanData latestGenerationSpan() { + List spans = spanExporter.getFinishedSpanItems().stream() + .filter(span -> { + String operation = span.getAttributes().get(AttributeKey.stringKey("gen_ai.operation.name")); + return "generateText".equals(operation) || "streamText".equals(operation); + }) + .toList(); + assertThat(spans).isNotEmpty(); + return spans.get(spans.size() - 1); + } + + List metricNames() { + return metricReader.collectAllMetrics().stream() + .map(metric -> metric.getName()) + .distinct() + .sorted() + .toList(); + } + + private void awaitRequests() { + long deadline = System.nanoTime() + Duration.ofSeconds(5).toNanos(); + while (System.nanoTime() < deadline) { + if (!exporter.requests.isEmpty()) { + return; + } + try { + Thread.sleep(10L); + } catch (InterruptedException exception) { + Thread.currentThread().interrupt(); + throw new AssertionError("interrupted while waiting for export", exception); + } + } + throw new AssertionError("timed out waiting for generation export"); + } + + @Override + public void close() throws Exception { + client.shutdown(); + meterProvider.close(); + tracerProvider.close(); + } + } + + private static final class CapturingExporter implements GenerationExporter { + private final List> requests = new CopyOnWriteArrayList<>(); + + @Override + public ExportGenerationsResponse exportGenerations(ExportGenerationsRequest request) { + List batch = new ArrayList<>(); + for (Generation generation : request.getGenerations()) { + batch.add(generation.copy()); + } + requests.add(batch); + + List results = new ArrayList<>(); + for (Generation generation : batch) { + results.add(new ExportGenerationResult().setGenerationId(generation.getId()).setAccepted(true)); + } + return new ExportGenerationsResponse().setResults(results); + } + } +} diff --git a/java/providers/anthropic/src/test/java/com/grafana/sigil/sdk/providers/anthropic/AnthropicAdapterTest.java b/java/providers/anthropic/src/test/java/com/grafana/sigil/sdk/providers/anthropic/AnthropicConformanceTest.java similarity index 95% rename from java/providers/anthropic/src/test/java/com/grafana/sigil/sdk/providers/anthropic/AnthropicAdapterTest.java rename to java/providers/anthropic/src/test/java/com/grafana/sigil/sdk/providers/anthropic/AnthropicConformanceTest.java index 88f310a..7793121 100644 --- a/java/providers/anthropic/src/test/java/com/grafana/sigil/sdk/providers/anthropic/AnthropicAdapterTest.java +++ b/java/providers/anthropic/src/test/java/com/grafana/sigil/sdk/providers/anthropic/AnthropicConformanceTest.java @@ -25,7 +25,7 @@ import java.util.stream.Stream; import org.junit.jupiter.api.Test; -class AnthropicAdapterTest { +class AnthropicConformanceTest { @Test void syncAndStreamWrappersSetAnthropicProviderAndModes() throws Exception { CapturingExporter exporter = new CapturingExporter(); @@ -105,6 +105,13 @@ void providerErrorsPopulateCallError() { assertThat(exporter.generations.get(0).getCallError()).contains("anthropic failed"); } + @Test + void embeddingConformanceIsExplicitlyUnsupportedWithoutPublicSurface() { + assertThat(AnthropicAdapter.class).isNotNull(); + assertThatThrownBy(() -> Class.forName("com.grafana.sigil.sdk.providers.anthropic.AnthropicEmbeddings")) + .isInstanceOf(ClassNotFoundException.class); + } + @Test void mapperSetsThinkingFalseWhenDisabled() throws Exception { MessageCreateParams request = MessageCreateParams.builder() diff --git a/java/providers/gemini/src/test/java/com/grafana/sigil/sdk/providers/gemini/GeminiAdapterTest.java b/java/providers/gemini/src/test/java/com/grafana/sigil/sdk/providers/gemini/GeminiConformanceTest.java similarity index 99% rename from java/providers/gemini/src/test/java/com/grafana/sigil/sdk/providers/gemini/GeminiAdapterTest.java rename to java/providers/gemini/src/test/java/com/grafana/sigil/sdk/providers/gemini/GeminiConformanceTest.java index a40303d..28188e5 100644 --- a/java/providers/gemini/src/test/java/com/grafana/sigil/sdk/providers/gemini/GeminiAdapterTest.java +++ b/java/providers/gemini/src/test/java/com/grafana/sigil/sdk/providers/gemini/GeminiConformanceTest.java @@ -26,7 +26,7 @@ import java.util.concurrent.CopyOnWriteArrayList; import org.junit.jupiter.api.Test; -class GeminiAdapterTest { +class GeminiConformanceTest { @Test void syncAndStreamWrappersSetGeminiProviderAndModes() throws Exception { CapturingExporter exporter = new CapturingExporter(); diff --git a/java/providers/openai/src/test/java/com/grafana/sigil/sdk/providers/openai/OpenAiMappingAndRecorderTests.java b/java/providers/openai/src/test/java/com/grafana/sigil/sdk/providers/openai/OpenAiConformanceTest.java similarity index 99% rename from java/providers/openai/src/test/java/com/grafana/sigil/sdk/providers/openai/OpenAiMappingAndRecorderTests.java rename to java/providers/openai/src/test/java/com/grafana/sigil/sdk/providers/openai/OpenAiConformanceTest.java index d577cff..e8a6a3b 100644 --- a/java/providers/openai/src/test/java/com/grafana/sigil/sdk/providers/openai/OpenAiMappingAndRecorderTests.java +++ b/java/providers/openai/src/test/java/com/grafana/sigil/sdk/providers/openai/OpenAiConformanceTest.java @@ -34,7 +34,7 @@ import java.util.stream.Stream; import org.junit.jupiter.api.Test; -class OpenAiMappingAndRecorderTests { +class OpenAiConformanceTest { @Test void chatSyncWrapperSetsSyncModeAndRawArtifactsOffByDefault() throws Exception { CapturingExporter exporter = new CapturingExporter(); From 6a4136e31fcae85dbc8301b8b80550696397a28e Mon Sep 17 00:00:00 2001 From: Cyril Tovena Date: Thu, 12 Mar 2026 18:00:44 +0100 Subject: [PATCH 065/133] Add non-Go provider and framework conformance suites MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - add provider-wrapper conformance coverage for the shipped JS, Python, Java, and .NET OpenAI, Anthropic, and Gemini helper packages - add framework-adapter conformance coverage for the shipped JS, Python, and Java integrations, including lineage and explicit unsupported-embedding assertions - document and wire aggregate `mise` entry points for core/provider/framework conformance, and fix the Python 3.11 task runtime, Java Google ADK test deps, and .NET OpenAI response model lookup so the documented commands pass ## Testing - `mise run test:sdk:provider-conformance` - `mise run test:sdk:framework-conformance` - `mise run sdk:conformance` ## Manual QA Plan - Not applicable: SDK/test/docs change only. ## Linear - `GRA-17` --- > [!NOTE] > **Medium Risk** > Expands test harness and build tooling across multiple SDK languages and adds new conformance assertions, which may cause CI/task failures if any language environment or conformance expectations are slightly off. Production runtime code changes are minimal, limited to a .NET OpenAI model-name lookup tweak. > > **Overview** > Adds **provider-wrapper conformance suites** for the shipped OpenAI/Anthropic/Gemini helpers across TypeScript/JavaScript, Python, Java, and .NET, including new negative-path coverage for malformed/missing provider payloads and explicit “no embeddings surface” assertions where applicable. > > Adds **framework-adapter conformance coverage** (JS/Python/Java) emphasizing parent-span lineage (`trace_id`/`span_id`), streaming stitching + TTFT metrics, and explicit unsupported-embedding lifecycle contracts. > > Updates documentation/spec to reflect the three-layer conformance model (core/provider/framework) and wires new aggregate `mise` tasks (`test:sdk:{core,provider,framework}-conformance`, updated `test:sdk:conformance`). Also fixes test/runtime wiring: Python tasks standardized on `uv` + Python 3.11, Java Google ADK tests add OTel trace/metrics deps, JS adds `@opentelemetry/context-async-hooks`, and .NET OpenAI recorder now reads `provider.Model` directly. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 4ceb2ce210703ebf9b395884fc555e94aaa2e57e. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). --- .../Grafana.Sigil.OpenAI/OpenAIRecorder.cs | 4 +- .../AnthropicConformanceTests.cs | 79 ++++++ .../GeminiConformanceTests.cs | 85 ++++++ .../OpenAIConformanceTests.cs | 128 +++++++++ java/frameworks/google-adk/build.gradle.kts | 2 + .../googleadk/SigilGoogleAdkAdapterTest.java | 260 ++++++++++++++++++ .../anthropic/AnthropicConformanceTest.java | 49 ++++ .../gemini/GeminiConformanceTest.java | 47 ++++ .../openai/OpenAiConformanceTest.java | 52 ++++ js/package.json | 1 + js/test/frameworks.additional.test.mjs | 83 ++++++ js/test/frameworks.langchain.test.mjs | 80 +++++- js/test/frameworks.langgraph.test.mjs | 80 +++++- js/test/providers.test.mjs | 166 +++++++++++ .../tests/test_sigil_sdk_google_adk.py | 59 +++- .../langchain/tests/test_langchain_handler.py | 53 ++++ .../langgraph/tests/test_langgraph_handler.py | 53 ++++ .../tests/test_sigil_sdk_llamaindex.py | 59 +++- .../tests/test_sigil_sdk_openai_agents.py | 59 +++- .../tests/test_anthropic_provider.py | 46 ++++ .../gemini/tests/test_gemini_provider.py | 42 +++ .../openai/tests/test_openai_provider.py | 41 +++ 22 files changed, 1521 insertions(+), 7 deletions(-) diff --git a/dotnet/src/Grafana.Sigil.OpenAI/OpenAIRecorder.cs b/dotnet/src/Grafana.Sigil.OpenAI/OpenAIRecorder.cs index 61dcd19..e356788 100644 --- a/dotnet/src/Grafana.Sigil.OpenAI/OpenAIRecorder.cs +++ b/dotnet/src/Grafana.Sigil.OpenAI/OpenAIRecorder.cs @@ -21,7 +21,7 @@ public static async Task CompleteChatAsync( } var effective = options ?? new OpenAISigilOptions(); - var modelName = ResolveInitialModelName(effective, provider.GetType().GetProperty("Model")?.GetValue(provider) as string); + var modelName = ResolveInitialModelName(effective, provider.Model); return await CompleteChatAsync( client, @@ -125,7 +125,7 @@ public static async Task CompleteChatStreami } var effective = options ?? new OpenAISigilOptions(); - var modelName = ResolveInitialModelName(effective, provider.GetType().GetProperty("Model")?.GetValue(provider) as string); + var modelName = ResolveInitialModelName(effective, provider.Model); return await CompleteChatStreamingAsync( client, diff --git a/dotnet/tests/Grafana.Sigil.Anthropic.Tests/AnthropicConformanceTests.cs b/dotnet/tests/Grafana.Sigil.Anthropic.Tests/AnthropicConformanceTests.cs index f80e0e0..731651d 100644 --- a/dotnet/tests/Grafana.Sigil.Anthropic.Tests/AnthropicConformanceTests.cs +++ b/dotnet/tests/Grafana.Sigil.Anthropic.Tests/AnthropicConformanceTests.cs @@ -1,4 +1,5 @@ using System.Text.Json; +using System.Diagnostics; using System.Reflection; using Anthropic.Models.Messages; using Xunit; @@ -119,6 +120,62 @@ await Assert.ThrowsAsync(() => AnthropicRecorder.Mess Assert.Contains(generations, generation => generation.Mode == GenerationMode.Stream); } + [Fact] + public async Task Recorder_StreamMappingErrors_PreserveReturnedSummaries_AndMarkSpans() + { + var exporter = new CapturingExporter(); + var spans = new List(); + using var listener = NewGenerationListener(spans); + ActivitySource.AddActivityListener(listener); + + await using var client = new SigilClient(new SigilClientConfig + { + GenerationExporter = exporter, + GenerationExport = new GenerationExportConfig + { + BatchSize = 1, + QueueSize = 10, + FlushInterval = TimeSpan.FromHours(1), + }, + }); + + var summary = await AnthropicRecorder.MessageStreamAsync( + client, + CreateRequest(), + (_, _) => EmptyStreamEvents(), + new AnthropicSigilOptions + { + ModelName = "claude-sonnet-4-5", + } + ); + + Assert.Empty(summary.Events); + + await client.FlushAsync(); + await client.ShutdownAsync(); + + var generations = exporter.Requests.SelectMany(request => request.Generations).ToList(); + Assert.Single(generations); + Assert.Equal(GenerationMode.Stream, generations[0].Mode); + Assert.Equal(string.Empty, generations[0].CallError); + Assert.Single(spans, span => span.GetTagItem("error.type")?.ToString() == "mapping_error"); + } + + [Fact] + public void MapperRejectsMissingOrMalformedResponses() + { + Assert.Throws(() => AnthropicGenerationMapper.FromRequestResponse( + CreateRequest(), + response: null!, + new AnthropicSigilOptions() + )); + Assert.Throws(() => AnthropicGenerationMapper.FromStream( + CreateRequest(), + new AnthropicStreamSummary(), + new AnthropicSigilOptions() + )); + } + [Fact] public void EmbeddingConformance_IsExplicitlyUnsupportedWithoutPublicSurface() { @@ -206,6 +263,12 @@ private static long ReadMetadataLong(Generation generation, string key) }; } + private static async IAsyncEnumerable EmptyStreamEvents() + { + await Task.CompletedTask; + yield break; + } + private static AnthropicMessage CreateResponse() { var usage = new Usage @@ -496,4 +559,20 @@ public Task ShutdownAsync(CancellationToken cancellationToken) return Task.CompletedTask; } } + + private static ActivityListener NewGenerationListener(List spans) + { + return new ActivityListener + { + ShouldListenTo = source => source.Name == "github.com/grafana/sigil/sdks/dotnet", + Sample = static (ref ActivityCreationOptions _) => ActivitySamplingResult.AllDataAndRecorded, + ActivityStopped = activity => + { + if (activity.GetTagItem("gen_ai.operation.name")?.ToString() != "execute_tool") + { + spans.Add(activity); + } + }, + }; + } } diff --git a/dotnet/tests/Grafana.Sigil.Gemini.Tests/GeminiConformanceTests.cs b/dotnet/tests/Grafana.Sigil.Gemini.Tests/GeminiConformanceTests.cs index 5810270..644f2a2 100644 --- a/dotnet/tests/Grafana.Sigil.Gemini.Tests/GeminiConformanceTests.cs +++ b/dotnet/tests/Grafana.Sigil.Gemini.Tests/GeminiConformanceTests.cs @@ -1,4 +1,5 @@ using Google.GenAI.Types; +using System.Diagnostics; using System.Reflection; using Xunit; using GPart = Google.GenAI.Types.Part; @@ -186,6 +187,68 @@ await Assert.ThrowsAsync(() => GeminiRecorder.Generat Assert.Contains(generations, generation => generation.Mode == GenerationMode.Stream); } + [Fact] + public async Task Recorder_StreamMappingErrors_PreserveReturnedSummaries_AndMarkSpans() + { + var exporter = new CapturingExporter(); + var spans = new List(); + using var listener = NewGenerationListener(spans); + ActivitySource.AddActivityListener(listener); + + await using var client = new SigilClient(new SigilClientConfig + { + GenerationExporter = exporter, + GenerationExport = new GenerationExportConfig + { + BatchSize = 1, + QueueSize = 10, + FlushInterval = TimeSpan.FromHours(1), + }, + }); + + var summary = await GeminiRecorder.GenerateContentStreamAsync( + client, + DefaultModel, + CreateContents(), + (_, _, _, _) => EmptyStreamResponses(), + CreateConfig(), + new GeminiSigilOptions + { + ModelName = DefaultModel, + } + ); + + Assert.Empty(summary.Responses); + + await client.FlushAsync(); + await client.ShutdownAsync(); + + var generations = exporter.Requests.SelectMany(request => request.Generations).ToList(); + Assert.Single(generations); + Assert.Equal(GenerationMode.Stream, generations[0].Mode); + Assert.Equal(string.Empty, generations[0].CallError); + Assert.Single(spans, span => span.GetTagItem("error.type")?.ToString() == "mapping_error"); + } + + [Fact] + public void MapperRejectsMissingOrMalformedResponses() + { + Assert.Throws(() => GeminiGenerationMapper.FromRequestResponse( + DefaultModel, + CreateContents(), + CreateConfig(), + response: null!, + new GeminiSigilOptions() + )); + Assert.Throws(() => GeminiGenerationMapper.FromStream( + DefaultModel, + CreateContents(), + CreateConfig(), + new GeminiStreamSummary(), + new GeminiSigilOptions() + )); + } + [Fact] public void EmbeddingFromResponse_MapsInputCountUsageAndDimensions() { @@ -637,6 +700,12 @@ private static async IAsyncEnumerable StreamResponses() await Task.CompletedTask; } + private static async IAsyncEnumerable EmptyStreamResponses() + { + await Task.CompletedTask; + yield break; + } + private sealed class CapturingExporter : IGenerationExporter { public List Requests { get; } = new(); @@ -659,4 +728,20 @@ public Task ShutdownAsync(CancellationToken cancellationToken) return Task.CompletedTask; } } + + private static ActivityListener NewGenerationListener(List spans) + { + return new ActivityListener + { + ShouldListenTo = source => source.Name == "github.com/grafana/sigil/sdks/dotnet", + Sample = static (ref ActivityCreationOptions _) => ActivitySamplingResult.AllDataAndRecorded, + ActivityStopped = activity => + { + if (activity.GetTagItem("gen_ai.operation.name")?.ToString() != "execute_tool") + { + spans.Add(activity); + } + }, + }; + } } diff --git a/dotnet/tests/Grafana.Sigil.OpenAI.Tests/OpenAIConformanceTests.cs b/dotnet/tests/Grafana.Sigil.OpenAI.Tests/OpenAIConformanceTests.cs index dd0248d..1225fd2 100644 --- a/dotnet/tests/Grafana.Sigil.OpenAI.Tests/OpenAIConformanceTests.cs +++ b/dotnet/tests/Grafana.Sigil.OpenAI.Tests/OpenAIConformanceTests.cs @@ -1,4 +1,5 @@ using System.ClientModel.Primitives; +using System.Diagnostics; using System.Reflection; using OpenAI.Chat; using OpenAI.Embeddings; @@ -321,6 +322,48 @@ public void ResponsesFromStream_MapsStreamModeWhenOnlyEventsPresent() Assert.Contains(generation.Artifacts, artifact => artifact.Kind == ArtifactKind.ProviderEvent && artifact.Name == "openai.responses.stream_events"); } + [Fact] + public void MapperRejectsMissingOrMalformedResponses() + { + var chatMessages = new List + { + new UserChatMessage("hello"), + }; + var responseItems = new List + { + ResponseItem.CreateUserMessageItem("hello"), + }; + + Assert.Throws(() => OpenAIGenerationMapper.ChatCompletionsFromRequestResponse( + "gpt-5", + chatMessages, + requestOptions: null, + response: null!, + new OpenAISigilOptions() + )); + Assert.Throws(() => OpenAIGenerationMapper.ResponsesFromRequestResponse( + "gpt-5", + responseItems, + requestOptions: null, + response: null!, + new OpenAISigilOptions() + )); + Assert.Throws(() => OpenAIGenerationMapper.ChatCompletionsFromStream( + "gpt-5", + chatMessages, + requestOptions: null, + new OpenAIChatCompletionsStreamSummary(), + new OpenAISigilOptions() + )); + Assert.Throws(() => OpenAIGenerationMapper.ResponsesFromStream( + "gpt-5", + responseItems, + requestOptions: null, + new OpenAIResponsesStreamSummary(), + new OpenAISigilOptions() + )); + } + [Fact] public async Task Recorder_RecordsChatAndResponsesModesAndPropagatesProviderErrors() { @@ -407,6 +450,63 @@ await Assert.ThrowsAsync(() => OpenAIRecorder.CreateR Assert.True(generations.Count(generation => generation.Mode == GenerationMode.Stream) >= 2); } + [Fact] + public async Task Recorder_StreamMappingErrors_PreserveReturnedSummaries_AndMarkSpans() + { + var exporter = new CapturingExporter(); + var spans = new List(); + using var listener = NewGenerationListener(spans); + ActivitySource.AddActivityListener(listener); + + await using var client = new SigilClient(new SigilClientConfig + { + GenerationExporter = exporter, + GenerationExport = new GenerationExportConfig + { + BatchSize = 1, + QueueSize = 10, + FlushInterval = TimeSpan.FromHours(1), + }, + }); + + var chatSummary = await OpenAIRecorder.CompleteChatStreamingAsync( + client, + new List { new UserChatMessage("hello") }, + (_, _, _) => EmptyChatUpdates(), + requestOptions: null, + options: new OpenAISigilOptions + { + ModelName = "gpt-5", + } + ); + + var responsesSummary = await OpenAIRecorder.CreateResponseStreamingAsync( + client, + new List { ResponseItem.CreateUserMessageItem("hello") }, + (_, _, _) => EmptyResponsesUpdates(), + requestOptions: new CreateResponseOptions(), + options: new OpenAISigilOptions + { + ModelName = "gpt-5", + } + ); + + Assert.Empty(chatSummary.Updates); + Assert.Empty(responsesSummary.Events); + + await client.FlushAsync(); + await client.ShutdownAsync(); + + var generations = exporter.Requests.SelectMany(request => request.Generations).ToList(); + Assert.Equal(2, generations.Count); + Assert.All(generations, generation => + { + Assert.Equal(GenerationMode.Stream, generation.Mode); + Assert.Equal(string.Empty, generation.CallError); + }); + Assert.Equal(2, spans.Count(span => span.GetTagItem("error.type")?.ToString() == "mapping_error")); + } + [Fact] public void EmbeddingsFromRequestResponse_MapsInputCountUsageAndDimensions() { @@ -567,6 +667,34 @@ private static async IAsyncEnumerable StreamResponsesUp await Task.CompletedTask; } + private static async IAsyncEnumerable EmptyChatUpdates() + { + await Task.CompletedTask; + yield break; + } + + private static async IAsyncEnumerable EmptyResponsesUpdates() + { + await Task.CompletedTask; + yield break; + } + + private static ActivityListener NewGenerationListener(List spans) + { + return new ActivityListener + { + ShouldListenTo = source => source.Name == "github.com/grafana/sigil/sdks/dotnet", + Sample = static (ref ActivityCreationOptions _) => ActivitySamplingResult.AllDataAndRecorded, + ActivityStopped = activity => + { + if (activity.GetTagItem("gen_ai.operation.name")?.ToString() != "execute_tool") + { + spans.Add(activity); + } + }, + }; + } + private sealed class CapturingExporter : IGenerationExporter { public List Requests { get; } = new(); diff --git a/java/frameworks/google-adk/build.gradle.kts b/java/frameworks/google-adk/build.gradle.kts index 8e8daae..4804368 100644 --- a/java/frameworks/google-adk/build.gradle.kts +++ b/java/frameworks/google-adk/build.gradle.kts @@ -9,5 +9,7 @@ dependencies { testImplementation(libs.junit.jupiter) testRuntimeOnly("org.junit.platform:junit-platform-launcher") testImplementation(libs.assertj.core) + testImplementation(libs.otel.sdk.trace) + testImplementation(libs.otel.sdk.metrics) testImplementation(libs.otel.sdk.testing) } diff --git a/java/frameworks/google-adk/src/test/java/com/grafana/sigil/sdk/frameworks/googleadk/SigilGoogleAdkAdapterTest.java b/java/frameworks/google-adk/src/test/java/com/grafana/sigil/sdk/frameworks/googleadk/SigilGoogleAdkAdapterTest.java index 3a628ff..05e2107 100644 --- a/java/frameworks/google-adk/src/test/java/com/grafana/sigil/sdk/frameworks/googleadk/SigilGoogleAdkAdapterTest.java +++ b/java/frameworks/google-adk/src/test/java/com/grafana/sigil/sdk/frameworks/googleadk/SigilGoogleAdkAdapterTest.java @@ -2,6 +2,11 @@ import static org.assertj.core.api.Assertions.assertThat; +import com.grafana.sigil.sdk.ExportGenerationResult; +import com.grafana.sigil.sdk.ExportGenerationsRequest; +import com.grafana.sigil.sdk.ExportGenerationsResponse; +import com.grafana.sigil.sdk.Generation; +import com.grafana.sigil.sdk.GenerationExporter; import com.grafana.sigil.sdk.GenerationMode; import com.grafana.sigil.sdk.GenerationRecorder; import com.grafana.sigil.sdk.GenerationExportConfig; @@ -13,7 +18,21 @@ import com.grafana.sigil.sdk.ModelRef; import com.grafana.sigil.sdk.SigilClient; import com.grafana.sigil.sdk.SigilClientConfig; +import com.grafana.sigil.sdk.TokenUsage; import com.grafana.sigil.sdk.ToolExecutionStart; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.context.Scope; +import io.opentelemetry.sdk.metrics.SdkMeterProvider; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.testing.exporter.InMemoryMetricReader; +import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; +import java.lang.reflect.Method; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.concurrent.CountDownLatch; @@ -157,6 +176,184 @@ void adapterUsesExplicitProviderWhenConfigured() { } } + @Test + void syncRunExportsFrameworkPayloadTagsAndMetrics() { + try (FrameworkConformanceEnv env = new FrameworkConformanceEnv()) { + SigilGoogleAdkAdapter adapter = new SigilGoogleAdkAdapter(env.client, new SigilGoogleAdkAdapter.Options() + .setAgentName("adk-agent") + .setAgentVersion("1.0.0") + .setCaptureInputs(true) + .setCaptureOutputs(true) + .putExtraTag("team", "infra") + .putExtraMetadata("workspace", "sigil")); + + var parentSpan = env.tracerProvider.get("sigil-framework-test") + .spanBuilder("framework.request") + .setAttribute(AttributeKey.stringKey("sigil.framework.name"), "google-adk") + .setAttribute(AttributeKey.stringKey("sigil.framework.source"), "handler") + .setAttribute(AttributeKey.stringKey("sigil.framework.language"), "java") + .startSpan(); + try (Scope ignored = parentSpan.makeCurrent()) { + adapter.onRunStart(new SigilGoogleAdkAdapter.RunStartEvent() + .setRunId("run-sync") + .setSessionId("session-42") + .setThreadId("thread-9") + .setParentRunId("framework-parent-run") + .setComponentName("planner") + .setRunType("chat") + .setRetryAttempt(2) + .setEventId("event-42") + .setModelName("gpt-5") + .addTag("prod") + .addTag("framework") + .addPrompt("hello") + .putMetadata("phase", "plan")); + adapter.onRunEnd("run-sync", new SigilGoogleAdkAdapter.RunEndEvent() + .setResponseModel("gpt-5") + .setStopReason("stop") + .setUsage(new TokenUsage().setInputTokens(3).setOutputTokens(2).setTotalTokens(5)) + .addOutputMessage(new Message() + .setRole(MessageRole.ASSISTANT) + .setParts(List.of(MessagePart.text("hi"))))); + } finally { + parentSpan.end(); + } + + env.client.flush(); + + Generation generation = env.exporter.singleGeneration(); + SpanData generationSpan = env.latestGenerationSpan(); + + assertThat(generation.getMode()).isEqualTo(GenerationMode.SYNC); + assertThat(generation.getOperationName()).isEqualTo("generateText"); + assertThat(generation.getConversationId()).isEqualTo("session-42"); + assertThat(generation.getResponseModel()).isEqualTo("gpt-5"); + assertThat(generation.getTraceId()).isEqualTo(generationSpan.getTraceId()); + assertThat(generation.getSpanId()).isEqualTo(generationSpan.getSpanId()); + assertThat(generation.getTags()) + .containsEntry("sigil.framework.name", "google-adk") + .containsEntry("sigil.framework.source", "handler") + .containsEntry("sigil.framework.language", "java") + .containsEntry("team", "infra"); + assertThat(generation.getMetadata()) + .containsEntry("workspace", "sigil") + .containsEntry("phase", "plan") + .containsEntry(SigilGoogleAdkAdapter.META_RUN_ID, "run-sync") + .containsEntry(SigilGoogleAdkAdapter.META_RUN_TYPE, "chat") + .containsEntry(SigilGoogleAdkAdapter.META_THREAD_ID, "thread-9") + .containsEntry(SigilGoogleAdkAdapter.META_PARENT_RUN_ID, "framework-parent-run") + .containsEntry(SigilGoogleAdkAdapter.META_COMPONENT_NAME, "planner") + .containsEntry(SigilGoogleAdkAdapter.META_RETRY_ATTEMPT, 2) + .containsEntry(SigilGoogleAdkAdapter.META_EVENT_ID, "event-42") + .containsEntry(SigilGoogleAdkAdapter.META_TAGS, List.of("prod", "framework")); + assertThat(generation.getOutput()).hasSize(1); + assertThat(generation.getOutput().get(0).getParts()).hasSize(1); + assertThat(generation.getOutput().get(0).getParts().get(0).getText()).isEqualTo("hi"); + assertThat(generationSpan.getParentSpanId()).isEqualTo(parentSpan.getSpanContext().getSpanId()); + assertThat(env.metricNames()) + .contains("gen_ai.client.operation.duration") + .doesNotContain("gen_ai.client.time_to_first_token"); + } + } + + @Test + void streamRunExportsStitchedOutputAndTtftMetric() { + try (FrameworkConformanceEnv env = new FrameworkConformanceEnv()) { + SigilGoogleAdkAdapter adapter = new SigilGoogleAdkAdapter(env.client, new SigilGoogleAdkAdapter.Options() + .setCaptureInputs(true) + .setCaptureOutputs(true)); + + adapter.onRunStart(new SigilGoogleAdkAdapter.RunStartEvent() + .setRunId("run-stream-export") + .setSessionId("session-stream") + .setModelName("claude-sonnet-4-5") + .setStream(true) + .addPrompt("stream me")); + adapter.onRunToken("run-stream-export", "hello"); + adapter.onRunToken("run-stream-export", " world"); + adapter.onRunEnd("run-stream-export", new SigilGoogleAdkAdapter.RunEndEvent() + .setResponseModel("claude-sonnet-4-5")); + + env.client.flush(); + + Generation generation = env.exporter.singleGeneration(); + assertThat(generation.getMode()).isEqualTo(GenerationMode.STREAM); + assertThat(generation.getOperationName()).isEqualTo("streamText"); + assertThat(generation.getResponseModel()).isEqualTo("claude-sonnet-4-5"); + assertThat(generation.getOutput()).hasSize(1); + assertThat(generation.getOutput().get(0).getParts()).hasSize(1); + assertThat(generation.getOutput().get(0).getParts().get(0).getText()).isEqualTo("hello world"); + assertThat(generation.getTags()) + .containsEntry("sigil.framework.name", "google-adk") + .containsEntry("sigil.framework.source", "handler") + .containsEntry("sigil.framework.language", "java"); + assertThat(env.metricNames()) + .contains("gen_ai.client.operation.duration", "gen_ai.client.time_to_first_token"); + } + } + + @Test + void generationSpanTracksActiveParentSpanAndPreservesExportLineage() { + InMemorySpanExporter spanExporter = InMemorySpanExporter.create(); + SdkTracerProvider tracerProvider = SdkTracerProvider.builder() + .addSpanProcessor(SimpleSpanProcessor.create(spanExporter)) + .build(); + SigilClient client = new SigilClient( + new SigilClientConfig() + .setTracer(tracerProvider.get("sigil-framework-test")) + .setGenerationExport( + new GenerationExportConfig() + .setProtocol(GenerationExportProtocol.NONE))); + try { + SigilGoogleAdkAdapter adapter = new SigilGoogleAdkAdapter(client, new SigilGoogleAdkAdapter.Options() + .setCaptureInputs(true) + .setCaptureOutputs(true)); + + var parentSpan = tracerProvider.get("sigil-framework-test").spanBuilder("framework.request").startSpan(); + try (Scope ignored = parentSpan.makeCurrent()) { + adapter.onRunStart(new SigilGoogleAdkAdapter.RunStartEvent() + .setRunId("run-lineage") + .setSessionId("session-lineage-42") + .setParentRunId("framework-parent-run") + .setRunType("chat") + .setModelName("gpt-5") + .addPrompt("hello")); + adapter.onRunEnd("run-lineage", new SigilGoogleAdkAdapter.RunEndEvent() + .setResponseModel("gpt-5") + .setStopReason("stop")); + } finally { + parentSpan.end(); + } + + assertThat(client.debugSnapshot().getGenerations()).hasSize(1); + var generation = client.debugSnapshot().getGenerations().get(0); + var generationSpan = spanExporter.getFinishedSpanItems().stream() + .filter(span -> "generateText".equals(span.getAttributes().get(io.opentelemetry.api.common.AttributeKey.stringKey("gen_ai.operation.name")))) + .findFirst() + .orElseThrow(); + + assertThat(generationSpan.getParentSpanId()).isEqualTo(parentSpan.getSpanContext().getSpanId()); + assertThat(generationSpan.getTraceId()).isEqualTo(parentSpan.getSpanContext().getTraceId()); + assertThat(generation.getTraceId()).isEqualTo(generationSpan.getTraceId()); + assertThat(generation.getSpanId()).isEqualTo(generationSpan.getSpanId()); + } finally { + client.shutdown(); + tracerProvider.close(); + } + } + + @Test + void adapterExplicitlyHasNoEmbeddingLifecycle() { + List publicMethodNames = Arrays.stream(SigilGoogleAdkAdapter.class.getMethods()) + .map(Method::getName) + .toList(); + + assertThat(publicMethodNames) + .doesNotContain("onEmbeddingStart") + .doesNotContain("onEmbeddingEnd") + .doesNotContain("onEmbeddingError"); + } + @Test void onRunEndDropsOutputsWhenCaptureOutputsDisabled() { SigilClient client = newClient(); @@ -377,4 +574,67 @@ void createCallbacksProvidesOneTimeLifecycleWiring() { client.shutdown(); } } + + private static final class FrameworkConformanceEnv implements AutoCloseable { + private final CapturingExporter exporter = new CapturingExporter(); + private final InMemorySpanExporter spanExporter = InMemorySpanExporter.create(); + private final SdkTracerProvider tracerProvider = SdkTracerProvider.builder() + .addSpanProcessor(SimpleSpanProcessor.create(spanExporter)) + .build(); + private final InMemoryMetricReader metricReader = InMemoryMetricReader.create(); + private final SdkMeterProvider meterProvider = SdkMeterProvider.builder() + .registerMetricReader(metricReader) + .build(); + private final SigilClient client = new SigilClient(new SigilClientConfig() + .setTracer(tracerProvider.get("sigil-framework-test")) + .setMeter(meterProvider.get("sigil-framework-test")) + .setGenerationExporter(exporter) + .setGenerationExport(new GenerationExportConfig() + .setBatchSize(1) + .setQueueSize(10) + .setFlushInterval(Duration.ofHours(1)) + .setMaxRetries(0))); + + private List metricNames() { + return metricReader.collectAllMetrics().stream() + .map(MetricData::getName) + .toList(); + } + + private SpanData latestGenerationSpan() { + return spanExporter.getFinishedSpanItems().stream() + .filter(span -> { + String operation = span.getAttributes().get(AttributeKey.stringKey("gen_ai.operation.name")); + return "generateText".equals(operation) || "streamText".equals(operation); + }) + .reduce((first, second) -> second) + .orElseThrow(); + } + + @Override + public void close() { + client.shutdown(); + tracerProvider.close(); + meterProvider.close(); + } + } + + private static final class CapturingExporter implements GenerationExporter { + private final List generations = new ArrayList<>(); + + @Override + public ExportGenerationsResponse exportGenerations(ExportGenerationsRequest request) { + List results = new ArrayList<>(); + for (Generation generation : request.getGenerations()) { + generations.add(generation.copy()); + results.add(new ExportGenerationResult().setGenerationId(generation.getId()).setAccepted(true)); + } + return new ExportGenerationsResponse().setResults(results); + } + + private Generation singleGeneration() { + assertThat(generations).hasSize(1); + return generations.get(0); + } + } } diff --git a/java/providers/anthropic/src/test/java/com/grafana/sigil/sdk/providers/anthropic/AnthropicConformanceTest.java b/java/providers/anthropic/src/test/java/com/grafana/sigil/sdk/providers/anthropic/AnthropicConformanceTest.java index 7793121..ad00b2c 100644 --- a/java/providers/anthropic/src/test/java/com/grafana/sigil/sdk/providers/anthropic/AnthropicConformanceTest.java +++ b/java/providers/anthropic/src/test/java/com/grafana/sigil/sdk/providers/anthropic/AnthropicConformanceTest.java @@ -105,6 +105,49 @@ void providerErrorsPopulateCallError() { assertThat(exporter.generations.get(0).getCallError()).contains("anthropic failed"); } + @Test + void wrappersTolerateMissingProviderPayloadFields() throws Exception { + CapturingExporter exporter = new CapturingExporter(); + try (SigilClient client = new SigilClient(new SigilClientConfig() + .setTracer(GlobalOpenTelemetry.getTracer("test")) + .setGenerationExporter(exporter) + .setGenerationExport(new GenerationExportConfig().setBatchSize(1).setFlushInterval(Duration.ofMinutes(10)).setMaxRetries(0)))) { + + AnthropicAdapter.completion( + client, + request(), + _r -> ObjectMappers.jsonMapper().readValue( + """ + { + "id": "msg_malformed", + "content": [], + "model": "claude-sonnet-4", + "usage": { + "input_tokens": 0, + "output_tokens": 0 + } + } + """, + Message.class), + new AnthropicOptions()); + AnthropicAdapter.completionStream( + client, + request(), + _r -> new FakeStreamResponse<>(List.of()), + new AnthropicOptions()); + } + + assertThat(exporter.generations).hasSize(2); + assertThat(exporter.generations.get(0).getMode()).isEqualTo(GenerationMode.SYNC); + assertThat(exporter.generations.get(0).getResponseId()).isEqualTo("msg_malformed"); + assertThat(exporter.generations.get(0).getResponseModel()).isEqualTo("claude-sonnet-4"); + assertThat(exporter.generations.get(0).getOutput()).isEmpty(); + assertThat(exporter.generations.get(0).getStopReason()).isEmpty(); + assertThat(exporter.generations.get(1).getMode()).isEqualTo(GenerationMode.STREAM); + assertThat(exporter.generations.get(1).getResponseModel()).isEqualTo("claude-sonnet-4"); + assertThat(exporter.generations.get(1).getOutput()).isEmpty(); + } + @Test void embeddingConformanceIsExplicitlyUnsupportedWithoutPublicSurface() { assertThat(AnthropicAdapter.class).isNotNull(); @@ -125,6 +168,12 @@ void mapperSetsThinkingFalseWhenDisabled() throws Exception { assertThat(mapped.getThinkingEnabled()).isFalse(); } + @Test + void mapperRejectsMissingResponse() { + assertThatThrownBy(() -> AnthropicAdapter.fromRequestResponse(request(), null, new AnthropicOptions())) + .isInstanceOf(NullPointerException.class); + } + private static MessageCreateParams request() { return MessageCreateParams.builder() .model("claude-sonnet-4") diff --git a/java/providers/gemini/src/test/java/com/grafana/sigil/sdk/providers/gemini/GeminiConformanceTest.java b/java/providers/gemini/src/test/java/com/grafana/sigil/sdk/providers/gemini/GeminiConformanceTest.java index 28188e5..79ee046 100644 --- a/java/providers/gemini/src/test/java/com/grafana/sigil/sdk/providers/gemini/GeminiConformanceTest.java +++ b/java/providers/gemini/src/test/java/com/grafana/sigil/sdk/providers/gemini/GeminiConformanceTest.java @@ -105,6 +105,53 @@ void providerErrorsPopulateCallError() { assertThat(exporter.generations.get(0).getCallError()).contains("gemini failed"); } + @Test + void wrappersTolerateMissingProviderPayloadFields() throws Exception { + CapturingExporter exporter = new CapturingExporter(); + try (SigilClient client = new SigilClient(new SigilClientConfig() + .setTracer(GlobalOpenTelemetry.getTracer("test")) + .setGenerationExporter(exporter) + .setGenerationExport(new GenerationExportConfig().setBatchSize(1).setFlushInterval(Duration.ofMinutes(10)).setMaxRetries(0)))) { + + GeminiAdapter.completion( + client, + model(), + contents(), + config(), + (_m, _c, _cfg) -> GenerateContentResponse.fromJson( + """ + { + "responseId": "resp_malformed", + "modelVersion": "gemini-2.5-pro-001", + "candidates": [] + } + """), + new GeminiOptions()); + GeminiAdapter.completionStream( + client, + model(), + contents(), + config(), + (_m, _c, _cfg) -> List.of(GenerateContentResponse.fromJson( + """ + { + "modelVersion": "gemini-2.5-pro-001" + } + """)), + new GeminiOptions()); + } + + assertThat(exporter.generations).hasSize(2); + assertThat(exporter.generations.get(0).getMode()).isEqualTo(GenerationMode.SYNC); + assertThat(exporter.generations.get(0).getResponseId()).isEqualTo("resp_malformed"); + assertThat(exporter.generations.get(0).getResponseModel()).isEqualTo("gemini-2.5-pro-001"); + assertThat(exporter.generations.get(0).getOutput()).isEmpty(); + assertThat(exporter.generations.get(0).getStopReason()).isEmpty(); + assertThat(exporter.generations.get(1).getMode()).isEqualTo(GenerationMode.STREAM); + assertThat(exporter.generations.get(1).getResponseModel()).isEqualTo("gemini-2.5-pro-001"); + assertThat(exporter.generations.get(1).getOutput()).isEmpty(); + } + @Test void embeddingWrapperDoesNotEnqueueGenerations() throws Exception { CapturingExporter exporter = new CapturingExporter(); diff --git a/java/providers/openai/src/test/java/com/grafana/sigil/sdk/providers/openai/OpenAiConformanceTest.java b/java/providers/openai/src/test/java/com/grafana/sigil/sdk/providers/openai/OpenAiConformanceTest.java index e8a6a3b..cc34495 100644 --- a/java/providers/openai/src/test/java/com/grafana/sigil/sdk/providers/openai/OpenAiConformanceTest.java +++ b/java/providers/openai/src/test/java/com/grafana/sigil/sdk/providers/openai/OpenAiConformanceTest.java @@ -137,6 +137,58 @@ void providerErrorsPopulateCallErrorForChatAndResponses() throws Exception { } } + @Test + void wrappersTolerateMissingProviderPayloadFields() throws Exception { + try (SigilClient client = newClient(new CapturingExporter())) { + OpenAiChatCompletions.create( + client, + chatRequestFixture(), + _request -> json( + """ + { + "id": "chatcmpl_malformed", + "choices": [], + "created": 1, + "model": "gpt-5", + "object": "chat.completion" + } + """, + ChatCompletion.class), + new OpenAiOptions()); + Generation chatGeneration = singleDebugGeneration(client); + assertThat(chatGeneration.getMode()).isEqualTo(GenerationMode.SYNC); + assertThat(chatGeneration.getResponseId()).isEqualTo("chatcmpl_malformed"); + assertThat(chatGeneration.getResponseModel()).isEqualTo("gpt-5"); + assertThat(chatGeneration.getOutput()).isEmpty(); + assertThat(chatGeneration.getStopReason()).isEmpty(); + } + + try (SigilClient client = newClient(new CapturingExporter())) { + OpenAiResponses.createStreaming( + client, + responsesRequestFixture(), + _request -> new FakeStreamResponse<>(List.of( + json( + """ + { + "type": "response.output_text.delta", + "content_index": 0, + "delta": 42, + "item_id": "msg_1", + "output_index": 0, + "sequence_number": 1 + } + """, + ResponseStreamEvent.class))), + new OpenAiOptions()); + Generation streamGeneration = singleDebugGeneration(client); + assertThat(streamGeneration.getMode()).isEqualTo(GenerationMode.STREAM); + assertThat(streamGeneration.getOutput()).hasSize(1); + assertThat(streamGeneration.getOutput().get(0).getParts()).hasSize(1); + assertThat(streamGeneration.getOutput().get(0).getParts().get(0).getText()).isEqualTo("42"); + } + } + @Test void embeddingsWrapperDoesNotEnqueueGenerations() throws Exception { CapturingExporter exporter = new CapturingExporter(); diff --git a/js/package.json b/js/package.json index 15382e5..0a9b03b 100644 --- a/js/package.json +++ b/js/package.json @@ -72,6 +72,7 @@ "openai": "^6.27.0" }, "devDependencies": { + "@opentelemetry/context-async-hooks": "^2.5.1", "@types/node": "^24.11.0", "typescript": "^5.9.3" } diff --git a/js/test/frameworks.additional.test.mjs b/js/test/frameworks.additional.test.mjs index e84bccd..8c81fd2 100644 --- a/js/test/frameworks.additional.test.mjs +++ b/js/test/frameworks.additional.test.mjs @@ -1,5 +1,7 @@ import assert from 'node:assert/strict'; import test from 'node:test'; +import { context, trace } from '@opentelemetry/api'; +import { BasicTracerProvider, InMemorySpanExporter, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base'; import { defaultConfig, SigilClient } from '../.test-dist/index.js'; import { @@ -254,6 +256,87 @@ for (const framework of frameworks) { assert.deepEqual(generation.metadata.first, { nested: { ok: true } }); assert.deepEqual(generation.metadata.second, { nested: { ok: true } }); }); + + test(`${framework.name} generation span tracks active parent span and preserves export lineage`, async () => { + const spanExporter = new InMemorySpanExporter(); + const tracerProvider = new BasicTracerProvider({ + spanProcessors: [new SimpleSpanProcessor(spanExporter)], + }); + const baseTracer = tracerProvider.getTracer('sigil-framework-test'); + let parentContext; + const tracer = { + startSpan(name, options, contextArg) { + return baseTracer.startSpan(name, options, contextArg ?? parentContext); + }, + startActiveSpan(...args) { + return baseTracer.startActiveSpan(...args); + }, + }; + const defaults = defaultConfig(); + const exporter = new CapturingExporter(); + const client = new SigilClient({ + generationExport: { + ...defaults.generationExport, + batchSize: 10, + flushIntervalMs: 60_000, + }, + generationExporter: exporter, + tracer, + }); + + try { + const handler = new framework.handlerCtor(client); + const parentSpan = baseTracer.startSpan('framework.request'); + parentContext = trace.setSpan(context.active(), parentSpan); + await handler.handleChatModelStart( + { name: 'ChatModel' }, + [[{ type: 'human', content: 'hello' }]], + 'run-lineage', + 'parent-run-lineage', + { invocation_params: { model: 'gpt-5' } }, + ['prod'], + { + conversation_id: 'framework-conversation-lineage-42', + thread_id: 'framework-thread-lineage-42', + } + ); + await handler.handleLLMEnd( + { + generations: [[{ text: 'world' }]], + llm_output: { model_name: 'gpt-5', finish_reason: 'stop' }, + }, + 'run-lineage' + ); + parentSpan.end(); + + await client.flush(); + const generation = exporter.requests[0].generations[0]; + const generationSpan = spanExporter + .getFinishedSpans() + .find((span) => span.attributes['gen_ai.operation.name'] === 'generateText'); + + assert.ok(generationSpan); + assert.equal(generationSpan.parentSpanContext?.spanId, parentSpan.spanContext().spanId); + assert.equal(generationSpan.spanContext().traceId, parentSpan.spanContext().traceId); + assert.equal(generation.traceId, generationSpan.spanContext().traceId); + assert.equal(generation.spanId, generationSpan.spanContext().spanId); + } finally { + await client.shutdown(); + await tracerProvider.shutdown(); + } + }); + + test(`${framework.name} handler explicitly has no embedding lifecycle`, async () => { + const client = new SigilClient(defaultConfig()); + try { + const handler = new framework.handlerCtor(client); + assert.equal(typeof handler.handleEmbeddingStart, 'undefined'); + assert.equal(typeof handler.handleEmbeddingEnd, 'undefined'); + assert.equal(typeof handler.handleEmbeddingError, 'undefined'); + } finally { + await client.shutdown(); + } + }); } async function captureSingleGeneration(run) { diff --git a/js/test/frameworks.langchain.test.mjs b/js/test/frameworks.langchain.test.mjs index f4bf9e0..c081c29 100644 --- a/js/test/frameworks.langchain.test.mjs +++ b/js/test/frameworks.langchain.test.mjs @@ -1,6 +1,6 @@ import assert from 'node:assert/strict'; import test from 'node:test'; -import { SpanStatusCode } from '@opentelemetry/api'; +import { context, SpanStatusCode, trace } from '@opentelemetry/api'; import { BasicTracerProvider, InMemorySpanExporter, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base'; import { defaultConfig, SigilClient } from '../.test-dist/index.js'; import { SigilLangChainHandler } from '../.test-dist/frameworks/langchain/index.js'; @@ -143,6 +143,72 @@ test('langchain handler records first token timestamp once per run', async () => } }); +test('langchain generation span tracks active parent span and preserves export lineage', async () => { + const spanExporter = new InMemorySpanExporter(); + const tracerProvider = new BasicTracerProvider({ + spanProcessors: [new SimpleSpanProcessor(spanExporter)], + }); + const baseTracer = tracerProvider.getTracer('sigil-framework-test'); + let parentContext; + const tracer = { + startSpan(name, options, contextArg) { + return baseTracer.startSpan(name, options, contextArg ?? parentContext); + }, + startActiveSpan(...args) { + return baseTracer.startActiveSpan(...args); + }, + }; + const defaults = defaultConfig(); + const exporter = new CapturingExporter(); + const client = new SigilClient({ + generationExport: { + ...defaults.generationExport, + batchSize: 10, + flushIntervalMs: 60_000, + }, + generationExporter: exporter, + tracer, + }); + + try { + const handler = new SigilLangChainHandler(client); + const parentSpan = baseTracer.startSpan('framework.request'); + parentContext = trace.setSpan(context.active(), parentSpan); + await handler.handleChatModelStart( + { name: 'ChatOpenAI' }, + [[{ type: 'human', content: 'hello' }]], + 'run-lineage', + 'parent-run-lineage', + { invocation_params: { model: 'gpt-5' } }, + ['prod'], + { thread_id: 'chain-thread-lineage-42' } + ); + await handler.handleLLMEnd( + { + generations: [[{ text: 'world' }]], + llm_output: { model_name: 'gpt-5', finish_reason: 'stop' }, + }, + 'run-lineage' + ); + parentSpan.end(); + + await client.flush(); + const generation = exporter.requests[0].generations[0]; + const generationSpan = spanExporter + .getFinishedSpans() + .find((span) => span.attributes['gen_ai.operation.name'] === 'generateText'); + + assert.ok(generationSpan); + assert.equal(generationSpan.parentSpanContext?.spanId, parentSpan.spanContext().spanId); + assert.equal(generationSpan.spanContext().traceId, parentSpan.spanContext().traceId); + assert.equal(generation.traceId, generationSpan.spanContext().traceId); + assert.equal(generation.spanId, generationSpan.spanContext().spanId); + } finally { + await client.shutdown(); + await tracerProvider.shutdown(); + } +}); + test('langchain provider mapping covers openai anthopic gemini and fallback', async () => { const providers = []; @@ -177,6 +243,18 @@ test('langchain handler sets call_error on llm error', async () => { assert.equal(generation.tags['sigil.framework.name'], 'langchain'); }); +test('langchain handler explicitly has no embedding lifecycle', async () => { + const client = new SigilClient(defaultConfig()); + try { + const handler = new SigilLangChainHandler(client); + assert.equal(typeof handler.handleEmbeddingStart, 'undefined'); + assert.equal(typeof handler.handleEmbeddingEnd, 'undefined'); + assert.equal(typeof handler.handleEmbeddingError, 'undefined'); + } finally { + await client.shutdown(); + } +}); + test('langchain handler maps tool callbacks and emits chain/retriever spans', async () => { const spanExporter = new InMemorySpanExporter(); const tracerProvider = new BasicTracerProvider({ diff --git a/js/test/frameworks.langgraph.test.mjs b/js/test/frameworks.langgraph.test.mjs index 25c7807..d3e541f 100644 --- a/js/test/frameworks.langgraph.test.mjs +++ b/js/test/frameworks.langgraph.test.mjs @@ -1,6 +1,6 @@ import assert from 'node:assert/strict'; import test from 'node:test'; -import { SpanStatusCode } from '@opentelemetry/api'; +import { context, SpanStatusCode, trace } from '@opentelemetry/api'; import { BasicTracerProvider, InMemorySpanExporter, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base'; import { defaultConfig, SigilClient } from '../.test-dist/index.js'; import { SigilLangGraphHandler } from '../.test-dist/frameworks/langgraph/index.js'; @@ -97,6 +97,72 @@ test('langgraph handler records stream mode and token fallback output', async () assert.equal(generation.output[0].content, 'hello world'); }); +test('langgraph generation span tracks active parent span and preserves export lineage', async () => { + const spanExporter = new InMemorySpanExporter(); + const tracerProvider = new BasicTracerProvider({ + spanProcessors: [new SimpleSpanProcessor(spanExporter)], + }); + const baseTracer = tracerProvider.getTracer('sigil-framework-test'); + let parentContext; + const tracer = { + startSpan(name, options, contextArg) { + return baseTracer.startSpan(name, options, contextArg ?? parentContext); + }, + startActiveSpan(...args) { + return baseTracer.startActiveSpan(...args); + }, + }; + const defaults = defaultConfig(); + const exporter = new CapturingExporter(); + const client = new SigilClient({ + generationExport: { + ...defaults.generationExport, + batchSize: 10, + flushIntervalMs: 60_000, + }, + generationExporter: exporter, + tracer, + }); + + try { + const handler = new SigilLangGraphHandler(client); + const parentSpan = baseTracer.startSpan('framework.request'); + parentContext = trace.setSpan(context.active(), parentSpan); + await handler.handleChatModelStart( + { name: 'ChatOpenAI' }, + [[{ type: 'human', content: 'hello' }]], + 'run-lineage', + 'parent-run-lineage', + { invocation_params: { model: 'gpt-5' } }, + ['prod'], + { thread_id: 'graph-thread-lineage-42', langgraph_node: 'answer_node' } + ); + await handler.handleLLMEnd( + { + generations: [[{ text: 'world' }]], + llm_output: { model_name: 'gpt-5', finish_reason: 'stop' }, + }, + 'run-lineage' + ); + parentSpan.end(); + + await client.flush(); + const generation = exporter.requests[0].generations[0]; + const generationSpan = spanExporter + .getFinishedSpans() + .find((span) => span.attributes['gen_ai.operation.name'] === 'generateText'); + + assert.ok(generationSpan); + assert.equal(generationSpan.parentSpanContext?.spanId, parentSpan.spanContext().spanId); + assert.equal(generationSpan.spanContext().traceId, parentSpan.spanContext().traceId); + assert.equal(generation.traceId, generationSpan.spanContext().traceId); + assert.equal(generation.spanId, generationSpan.spanContext().spanId); + } finally { + await client.shutdown(); + await tracerProvider.shutdown(); + } +}); + test('langgraph provider mapping covers openai anthopic gemini and fallback', async () => { const providers = []; @@ -131,6 +197,18 @@ test('langgraph handler sets call_error on llm error', async () => { assert.equal(generation.tags['sigil.framework.name'], 'langgraph'); }); +test('langgraph handler explicitly has no embedding lifecycle', async () => { + const client = new SigilClient(defaultConfig()); + try { + const handler = new SigilLangGraphHandler(client); + assert.equal(typeof handler.handleEmbeddingStart, 'undefined'); + assert.equal(typeof handler.handleEmbeddingEnd, 'undefined'); + assert.equal(typeof handler.handleEmbeddingError, 'undefined'); + } finally { + await client.shutdown(); + } +}); + test('langgraph handler maps tool callbacks and emits chain/retriever spans', async () => { const spanExporter = new InMemorySpanExporter(); const tracerProvider = new BasicTracerProvider({ diff --git a/js/test/providers.test.mjs b/js/test/providers.test.mjs index 5d4de76..b77ccbc 100644 --- a/js/test/providers.test.mjs +++ b/js/test/providers.test.mjs @@ -549,6 +549,172 @@ test('embedding provider wrapper errors set provider_call_error span status', as } }); +test('provider mappers throw on missing provider responses and stream summaries', () => { + assert.throws( + () => openai.chat.completions.fromRequestResponse( + { + model: 'gpt-5', + messages: [{ role: 'user', content: 'hello' }], + }, + undefined + ), + /reading 'id'/ + ); + assert.throws( + () => openai.responses.fromRequestResponse( + { + model: 'gpt-5', + input: 'hello', + }, + undefined + ), + /reading 'id'/ + ); + assert.throws( + () => openai.chat.completions.fromStream( + { + model: 'gpt-5', + stream: true, + messages: [{ role: 'user', content: 'hello' }], + }, + undefined + ), + /reading 'outputText'/ + ); + assert.throws( + () => openai.responses.fromStream( + { + model: 'gpt-5', + stream: true, + input: 'hello', + }, + undefined + ), + /reading 'events'/ + ); + + assert.throws( + () => anthropic.messages.fromRequestResponse( + { + model: 'claude-sonnet-4-5', + max_tokens: 128, + messages: [{ role: 'user', content: [{ type: 'text', text: 'hello' }] }], + }, + undefined + ), + /reading 'content'/ + ); + assert.throws( + () => anthropic.messages.fromStream( + { + model: 'claude-sonnet-4-5', + max_tokens: 128, + messages: [{ role: 'user', content: [{ type: 'text', text: 'hello' }] }], + }, + undefined + ), + /reading 'events'/ + ); + + assert.throws( + () => gemini.models.fromRequestResponse( + 'gemini-2.5-pro', + [{ role: 'user', parts: [{ text: 'hello' }] }], + undefined, + undefined + ), + /reading 'candidates'/ + ); + assert.throws( + () => gemini.models.fromStream( + 'gemini-2.5-pro', + [{ role: 'user', parts: [{ text: 'hello' }] }], + undefined, + undefined + ), + /reading 'responses'/ + ); +}); + +test('provider wrappers surface mapper failures when provider payloads are missing', async () => { + for (const suite of [ + { + provider: 'openai', + error: /reading 'id'/, + run: async (client) => { + await openai.chat.completions.create( + client, + { + model: 'gpt-5', + messages: [{ role: 'user', content: 'hello' }], + }, + async () => undefined + ); + }, + }, + { + provider: 'openai', + error: /reading 'id'/, + run: async (client) => { + await openai.responses.create( + client, + { + model: 'gpt-5', + input: 'hello', + }, + async () => undefined + ); + }, + }, + { + provider: 'anthropic', + error: /reading 'content'/, + run: async (client) => { + await anthropic.messages.create( + client, + { + model: 'claude-sonnet-4-5', + max_tokens: 128, + messages: [{ role: 'user', content: [{ type: 'text', text: 'hello' }] }], + }, + async () => undefined + ); + }, + }, + { + provider: 'gemini', + error: /reading 'candidates'/, + run: async (client) => { + await gemini.models.generateContent( + client, + 'gemini-2.5-pro', + [{ role: 'user', parts: [{ text: 'hello' }] }], + undefined, + async () => undefined + ); + }, + }, + ]) { + const exporter = new CapturingExporter(); + const client = newClient(exporter); + try { + await assert.rejects(suite.run(client), suite.error); + await client.flush(); + const generation = firstGeneration(exporter); + assert.equal(generation.model.provider, suite.provider); + assert.match(generation.callError ?? '', suite.error); + assert.equal(generation.output, undefined); + } finally { + await client.shutdown(); + } + } +}); + +test('anthropic provider namespace explicitly has no embeddings surface', () => { + assert.ok(anthropic.messages); + assert.equal(anthropic.embeddings, undefined); +}); + test('provider wrappers propagate provider errors and persist callError', async () => { for (const suite of [ { diff --git a/python-frameworks/google-adk/tests/test_sigil_sdk_google_adk.py b/python-frameworks/google-adk/tests/test_sigil_sdk_google_adk.py index 460420e..9f91ba5 100644 --- a/python-frameworks/google-adk/tests/test_sigil_sdk_google_adk.py +++ b/python-frameworks/google-adk/tests/test_sigil_sdk_google_adk.py @@ -7,6 +7,9 @@ from uuid import UUID, uuid4 from google.adk.plugins import BasePlugin +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import SimpleSpanProcessor +from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter from sigil_sdk import Client, ClientConfig, GenerationExportConfig from sigil_sdk.models import ExportGenerationResult, ExportGenerationsResponse from sigil_sdk_google_adk import ( @@ -38,9 +41,10 @@ def shutdown(self) -> None: return -def _new_client(exporter: _CapturingExporter) -> Client: +def _new_client(exporter: _CapturingExporter, tracer=None) -> Client: return Client( ClientConfig( + tracer=tracer, generation_export=GenerationExportConfig(batch_size=10, flush_interval=timedelta(seconds=60)), generation_exporter=exporter, ) @@ -183,6 +187,47 @@ def test_sigil_sdk_google_adk_stream_mode_uses_chunks_when_output_missing() -> N client.shutdown() +def test_sigil_sdk_google_adk_generation_span_tracks_active_parent_span_and_export_lineage() -> None: + exporter = _CapturingExporter() + span_exporter = InMemorySpanExporter() + provider = TracerProvider() + provider.add_span_processor(SimpleSpanProcessor(span_exporter)) + tracer = provider.get_tracer("sigil-framework-test") + client = _new_client(exporter, tracer=tracer) + + try: + run_id = uuid4() + with tracer.start_as_current_span("framework.request"): + handler = SigilGoogleAdkHandler(client=client, provider_resolver="auto") + handler.on_chat_model_start( + {"name": "ChatModel"}, + [[{"type": "human", "content": "hello"}]], + run_id=run_id, + parent_run_id=uuid4(), + invocation_params={"model": "gpt-5"}, + metadata={"conversation_id": "framework-conversation-lineage-42", "thread_id": "framework-thread-lineage-42"}, + ) + handler.on_llm_end( + {"generations": [[{"text": "world"}]], "llm_output": {"model_name": "gpt-5", "finish_reason": "stop"}}, + run_id=run_id, + ) + + client.flush() + generation = exporter.requests[0].generations[0] + spans = span_exporter.get_finished_spans() + parent_span = next(span for span in spans if span.name == "framework.request") + generation_span = next(span for span in spans if span.attributes.get("gen_ai.operation.name") == "generateText") + + assert generation_span.parent is not None + assert generation_span.parent.span_id == parent_span.context.span_id + assert generation_span.context.trace_id == parent_span.context.trace_id + assert generation.trace_id == generation_span.context.trace_id.to_bytes(16, "big").hex() + assert generation.span_id == generation_span.context.span_id.to_bytes(8, "big").hex() + finally: + client.shutdown() + provider.shutdown() + + def test_sigil_sdk_google_adk_normalizes_extra_metadata() -> None: exporter = _CapturingExporter() client = _new_client(exporter) @@ -333,6 +378,18 @@ async def _run() -> None: client.shutdown() +def test_sigil_sdk_google_adk_handler_explicitly_has_no_embedding_lifecycle() -> None: + exporter = _CapturingExporter() + client = _new_client(exporter) + try: + handler = SigilGoogleAdkHandler(client=client) + assert not hasattr(handler, "on_embedding_start") + assert not hasattr(handler, "on_embedding_end") + assert not hasattr(handler, "on_embedding_error") + finally: + client.shutdown() + + def test_sigil_sdk_google_adk_callbacks_close_tool_runs_without_function_call_id() -> None: class _CapturingHandler: def __init__(self) -> None: diff --git a/python-frameworks/langchain/tests/test_langchain_handler.py b/python-frameworks/langchain/tests/test_langchain_handler.py index 04bae49..58570ea 100644 --- a/python-frameworks/langchain/tests/test_langchain_handler.py +++ b/python-frameworks/langchain/tests/test_langchain_handler.py @@ -175,6 +175,47 @@ def _tracking_set_first_token_at(self, first_token_at): client.shutdown() +def test_langchain_generation_span_tracks_active_parent_span_and_export_lineage() -> None: + exporter = _CapturingExporter() + span_exporter = InMemorySpanExporter() + provider = TracerProvider() + provider.add_span_processor(SimpleSpanProcessor(span_exporter)) + tracer = provider.get_tracer("sigil-framework-test") + client = _new_client(exporter, tracer=tracer) + + try: + run_id = uuid4() + with tracer.start_as_current_span("framework.request"): + handler = SigilLangChainHandler(client=client) + handler.on_chat_model_start( + {"name": "ChatOpenAI"}, + [[{"type": "human", "content": "hello"}]], + run_id=run_id, + parent_run_id=uuid4(), + invocation_params={"model": "gpt-5"}, + metadata={"thread_id": "chain-thread-lineage-42"}, + ) + handler.on_llm_end( + {"generations": [[{"text": "world"}]], "llm_output": {"model_name": "gpt-5", "finish_reason": "stop"}}, + run_id=run_id, + ) + + client.flush() + generation = exporter.requests[0].generations[0] + spans = span_exporter.get_finished_spans() + parent_span = next(span for span in spans if span.name == "framework.request") + generation_span = next(span for span in spans if span.attributes.get("gen_ai.operation.name") == "generateText") + + assert generation_span.parent is not None + assert generation_span.parent.span_id == parent_span.context.span_id + assert generation_span.context.trace_id == parent_span.context.trace_id + assert generation.trace_id == generation_span.context.trace_id.to_bytes(16, "big").hex() + assert generation.span_id == generation_span.context.span_id.to_bytes(8, "big").hex() + finally: + client.shutdown() + provider.shutdown() + + def test_langchain_provider_resolution_supports_known_models_and_fallback() -> None: exporter = _CapturingExporter() client = _new_client(exporter) @@ -333,6 +374,18 @@ def test_langchain_attach_helpers_preserve_existing_callbacks() -> None: client.shutdown() +def test_langchain_handler_explicitly_has_no_embedding_lifecycle() -> None: + exporter = _CapturingExporter() + client = _new_client(exporter) + try: + handler = SigilLangChainHandler(client=client) + assert not hasattr(handler, "on_embedding_start") + assert not hasattr(handler, "on_embedding_end") + assert not hasattr(handler, "on_embedding_error") + finally: + client.shutdown() + + def test_langchain_attach_helpers_do_not_duplicate_existing_sigil_handler() -> None: exporter = _CapturingExporter() client = _new_client(exporter) diff --git a/python-frameworks/langgraph/tests/test_langgraph_handler.py b/python-frameworks/langgraph/tests/test_langgraph_handler.py index ba8d793..fb1c703 100644 --- a/python-frameworks/langgraph/tests/test_langgraph_handler.py +++ b/python-frameworks/langgraph/tests/test_langgraph_handler.py @@ -142,6 +142,47 @@ def test_langgraph_stream_lifecycle_uses_stream_mode_and_chunk_fallback() -> Non client.shutdown() +def test_langgraph_generation_span_tracks_active_parent_span_and_export_lineage() -> None: + exporter = _CapturingExporter() + span_exporter = InMemorySpanExporter() + provider = TracerProvider() + provider.add_span_processor(SimpleSpanProcessor(span_exporter)) + tracer = provider.get_tracer("sigil-framework-test") + client = _new_client(exporter, tracer=tracer) + + try: + run_id = uuid4() + with tracer.start_as_current_span("framework.request"): + handler = SigilLangGraphHandler(client=client) + handler.on_chat_model_start( + {"name": "ChatOpenAI"}, + [[{"type": "human", "content": "hello"}]], + run_id=run_id, + parent_run_id=uuid4(), + invocation_params={"model": "gpt-5"}, + metadata={"thread_id": "graph-thread-lineage-42", "langgraph_node": "answer_node"}, + ) + handler.on_llm_end( + {"generations": [[{"text": "world"}]], "llm_output": {"model_name": "gpt-5", "finish_reason": "stop"}}, + run_id=run_id, + ) + + client.flush() + generation = exporter.requests[0].generations[0] + spans = span_exporter.get_finished_spans() + parent_span = next(span for span in spans if span.name == "framework.request") + generation_span = next(span for span in spans if span.attributes.get("gen_ai.operation.name") == "generateText") + + assert generation_span.parent is not None + assert generation_span.parent.span_id == parent_span.context.span_id + assert generation_span.context.trace_id == parent_span.context.trace_id + assert generation.trace_id == generation_span.context.trace_id.to_bytes(16, "big").hex() + assert generation.span_id == generation_span.context.span_id.to_bytes(8, "big").hex() + finally: + client.shutdown() + provider.shutdown() + + def test_langgraph_provider_resolution_supports_known_models_and_fallback() -> None: exporter = _CapturingExporter() client = _new_client(exporter) @@ -299,3 +340,15 @@ def test_langgraph_attach_helpers_preserve_existing_callbacks() -> None: assert isinstance(callbacks[1], SigilLangGraphHandler) finally: client.shutdown() + + +def test_langgraph_handler_explicitly_has_no_embedding_lifecycle() -> None: + exporter = _CapturingExporter() + client = _new_client(exporter) + try: + handler = SigilLangGraphHandler(client=client) + assert not hasattr(handler, "on_embedding_start") + assert not hasattr(handler, "on_embedding_end") + assert not hasattr(handler, "on_embedding_error") + finally: + client.shutdown() diff --git a/python-frameworks/llamaindex/tests/test_sigil_sdk_llamaindex.py b/python-frameworks/llamaindex/tests/test_sigil_sdk_llamaindex.py index 350fea8..6fccab0 100644 --- a/python-frameworks/llamaindex/tests/test_sigil_sdk_llamaindex.py +++ b/python-frameworks/llamaindex/tests/test_sigil_sdk_llamaindex.py @@ -7,6 +7,9 @@ from uuid import uuid4 from llama_index.core.callbacks.base_handler import BaseCallbackHandler +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import SimpleSpanProcessor +from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter from sigil_sdk import Client, ClientConfig, GenerationExportConfig from sigil_sdk.models import ExportGenerationResult, ExportGenerationsResponse from sigil_sdk_llamaindex import ( @@ -35,9 +38,10 @@ def shutdown(self) -> None: return -def _new_client(exporter: _CapturingExporter) -> Client: +def _new_client(exporter: _CapturingExporter, tracer=None) -> Client: return Client( ClientConfig( + tracer=tracer, generation_export=GenerationExportConfig(batch_size=10, flush_interval=timedelta(seconds=60)), generation_exporter=exporter, ) @@ -180,6 +184,47 @@ def test_sigil_sdk_llamaindex_stream_mode_uses_chunks_when_output_missing() -> N client.shutdown() +def test_sigil_sdk_llamaindex_generation_span_tracks_active_parent_span_and_export_lineage() -> None: + exporter = _CapturingExporter() + span_exporter = InMemorySpanExporter() + provider = TracerProvider() + provider.add_span_processor(SimpleSpanProcessor(span_exporter)) + tracer = provider.get_tracer("sigil-framework-test") + client = _new_client(exporter, tracer=tracer) + + try: + run_id = uuid4() + with tracer.start_as_current_span("framework.request"): + handler = SigilLlamaIndexHandler(client=client, provider_resolver="auto") + handler.on_chat_model_start( + {"name": "ChatModel"}, + [[{"type": "human", "content": "hello"}]], + run_id=run_id, + parent_run_id=uuid4(), + invocation_params={"model": "gpt-5"}, + metadata={"conversation_id": "framework-conversation-lineage-42", "thread_id": "framework-thread-lineage-42"}, + ) + handler.on_llm_end( + {"generations": [[{"text": "world"}]], "llm_output": {"model_name": "gpt-5", "finish_reason": "stop"}}, + run_id=run_id, + ) + + client.flush() + generation = exporter.requests[0].generations[0] + spans = span_exporter.get_finished_spans() + parent_span = next(span for span in spans if span.name == "framework.request") + generation_span = next(span for span in spans if span.attributes.get("gen_ai.operation.name") == "generateText") + + assert generation_span.parent is not None + assert generation_span.parent.span_id == parent_span.context.span_id + assert generation_span.context.trace_id == parent_span.context.trace_id + assert generation.trace_id == generation_span.context.trace_id.to_bytes(16, "big").hex() + assert generation.span_id == generation_span.context.span_id.to_bytes(8, "big").hex() + finally: + client.shutdown() + provider.shutdown() + + def test_sigil_sdk_llamaindex_normalizes_extra_metadata() -> None: exporter = _CapturingExporter() client = _new_client(exporter) @@ -306,3 +351,15 @@ def end_trace(self, *_args, **_kwargs) -> None: assert generation.stop_reason == "stop" finally: client.shutdown() + + +def test_sigil_sdk_llamaindex_handler_explicitly_has_no_embedding_lifecycle() -> None: + exporter = _CapturingExporter() + client = _new_client(exporter) + try: + handler = SigilLlamaIndexHandler(client=client) + assert not hasattr(handler, "on_embedding_start") + assert not hasattr(handler, "on_embedding_end") + assert not hasattr(handler, "on_embedding_error") + finally: + client.shutdown() diff --git a/python-frameworks/openai-agents/tests/test_sigil_sdk_openai_agents.py b/python-frameworks/openai-agents/tests/test_sigil_sdk_openai_agents.py index 54e285d..5754f41 100644 --- a/python-frameworks/openai-agents/tests/test_sigil_sdk_openai_agents.py +++ b/python-frameworks/openai-agents/tests/test_sigil_sdk_openai_agents.py @@ -7,6 +7,9 @@ from uuid import uuid4 import pytest +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import SimpleSpanProcessor +from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter from agents import RunHooks from sigil_sdk import Client, ClientConfig, GenerationExportConfig from sigil_sdk.models import ExportGenerationResult, ExportGenerationsResponse @@ -36,9 +39,10 @@ def shutdown(self) -> None: return -def _new_client(exporter: _CapturingExporter) -> Client: +def _new_client(exporter: _CapturingExporter, tracer=None) -> Client: return Client( ClientConfig( + tracer=tracer, generation_export=GenerationExportConfig(batch_size=10, flush_interval=timedelta(seconds=60)), generation_exporter=exporter, ) @@ -181,6 +185,47 @@ def test_sigil_sdk_openai_agents_stream_mode_uses_chunks_when_output_missing() - client.shutdown() +def test_sigil_sdk_openai_agents_generation_span_tracks_active_parent_span_and_export_lineage() -> None: + exporter = _CapturingExporter() + span_exporter = InMemorySpanExporter() + provider = TracerProvider() + provider.add_span_processor(SimpleSpanProcessor(span_exporter)) + tracer = provider.get_tracer("sigil-framework-test") + client = _new_client(exporter, tracer=tracer) + + try: + run_id = uuid4() + with tracer.start_as_current_span("framework.request"): + handler = SigilOpenAIAgentsHandler(client=client, provider_resolver="auto") + handler.on_chat_model_start( + {"name": "ChatModel"}, + [[{"type": "human", "content": "hello"}]], + run_id=run_id, + parent_run_id=uuid4(), + invocation_params={"model": "gpt-5"}, + metadata={"conversation_id": "framework-conversation-lineage-42", "thread_id": "framework-thread-lineage-42"}, + ) + handler.on_llm_end( + {"generations": [[{"text": "world"}]], "llm_output": {"model_name": "gpt-5", "finish_reason": "stop"}}, + run_id=run_id, + ) + + client.flush() + generation = exporter.requests[0].generations[0] + spans = span_exporter.get_finished_spans() + parent_span = next(span for span in spans if span.name == "framework.request") + generation_span = next(span for span in spans if span.attributes.get("gen_ai.operation.name") == "generateText") + + assert generation_span.parent is not None + assert generation_span.parent.span_id == parent_span.context.span_id + assert generation_span.context.trace_id == parent_span.context.trace_id + assert generation.trace_id == generation_span.context.trace_id.to_bytes(16, "big").hex() + assert generation.span_id == generation_span.context.span_id.to_bytes(8, "big").hex() + finally: + client.shutdown() + provider.shutdown() + + def test_sigil_sdk_openai_agents_normalizes_extra_metadata() -> None: exporter = _CapturingExporter() client = _new_client(exporter) @@ -299,3 +344,15 @@ async def _run() -> None: with_sigil_openai_agents_hooks({"hooks": [existing]}, client=client) finally: client.shutdown() + + +def test_sigil_sdk_openai_agents_handler_explicitly_has_no_embedding_lifecycle() -> None: + exporter = _CapturingExporter() + client = _new_client(exporter) + try: + handler = SigilOpenAIAgentsHandler(client=client) + assert not hasattr(handler, "on_embedding_start") + assert not hasattr(handler, "on_embedding_end") + assert not hasattr(handler, "on_embedding_error") + finally: + client.shutdown() diff --git a/python-providers/anthropic/tests/test_anthropic_provider.py b/python-providers/anthropic/tests/test_anthropic_provider.py index d3b5fb2..59477cf 100644 --- a/python-providers/anthropic/tests/test_anthropic_provider.py +++ b/python-providers/anthropic/tests/test_anthropic_provider.py @@ -8,6 +8,7 @@ from sigil_sdk import Client, ClientConfig, GenerationExportConfig from sigil_sdk.models import ExportGenerationResult, ExportGenerationsResponse +import sigil_sdk_anthropic from sigil_sdk_anthropic import AnthropicOptions, AnthropicStreamSummary, messages @@ -145,6 +146,45 @@ def test_anthropic_wrapper_propagates_provider_error_and_sets_call_error() -> No client.shutdown() +def test_anthropic_wrappers_tolerate_missing_provider_payload_fields() -> None: + exporter = _CapturingExporter() + client = _new_client(exporter) + try: + messages.create( + client, + _request(), + lambda _request: { + "id": "resp-malformed", + "model": "claude-sonnet-4-5-20260210", + "role": "assistant", + "content": [], + }, + ) + messages.stream( + client, + _request(), + lambda _request: AnthropicStreamSummary(events=[{"type": "content_block_delta", "delta": {"type": "text_delta"}}]), + ) + + client.flush() + generations = exporter.requests[0].generations + assert len(generations) == 2 + + sync_generation = generations[0] + assert sync_generation.mode.value == "SYNC" + assert sync_generation.response_id == "resp-malformed" + assert sync_generation.response_model == "claude-sonnet-4-5-20260210" + assert sync_generation.output == [] + assert sync_generation.stop_reason == "" + + stream_generation = generations[1] + assert stream_generation.mode.value == "STREAM" + assert stream_generation.response_model == "claude-sonnet-4-5" + assert stream_generation.output == [] + finally: + client.shutdown() + + def test_anthropic_mappers_use_strict_payloads_and_support_raw_artifacts() -> None: request = _request() response = _response() @@ -200,3 +240,9 @@ def test_anthropic_mapper_maps_thinking_disabled() -> None: mapped = messages.from_request_response(request, response) assert mapped.thinking_enabled is False + + +def test_anthropic_provider_explicitly_has_no_embeddings_surface() -> None: + assert "messages" in sigil_sdk_anthropic.__all__ + assert "embeddings" not in sigil_sdk_anthropic.__all__ + assert not hasattr(sigil_sdk_anthropic, "embeddings") diff --git a/python-providers/gemini/tests/test_gemini_provider.py b/python-providers/gemini/tests/test_gemini_provider.py index f878346..7113cf4 100644 --- a/python-providers/gemini/tests/test_gemini_provider.py +++ b/python-providers/gemini/tests/test_gemini_provider.py @@ -195,6 +195,48 @@ def test_gemini_wrapper_propagates_provider_error_and_sets_call_error() -> None: client.shutdown() +def test_gemini_wrappers_tolerate_missing_provider_payload_fields() -> None: + exporter = _CapturingExporter() + client = _new_client(exporter) + try: + models.generate_content( + client, + "gemini-2.5-pro", + _contents(), + _config(), + lambda _model, _contents, _config: { + "response_id": "resp-malformed", + "model_version": "gemini-2.5-pro-001", + "candidates": [], + }, + ) + models.generate_content_stream( + client, + "gemini-2.5-pro", + _contents(), + _config(), + lambda _model, _contents, _config: GeminiStreamSummary(responses=[{"model_version": "gemini-2.5-pro-001"}]), + ) + + client.flush() + generations = exporter.requests[0].generations + assert len(generations) == 2 + + sync_generation = generations[0] + assert sync_generation.mode.value == "SYNC" + assert sync_generation.response_id == "resp-malformed" + assert sync_generation.response_model == "gemini-2.5-pro-001" + assert sync_generation.output == [] + assert sync_generation.stop_reason == "" + + stream_generation = generations[1] + assert stream_generation.mode.value == "STREAM" + assert stream_generation.response_model == "gemini-2.5-pro-001" + assert stream_generation.output == [] + finally: + client.shutdown() + + def test_gemini_embeddings_wrapper_records_span_and_skips_generation_export() -> None: exporter = _CapturingExporter() span_exporter = InMemorySpanExporter() diff --git a/python-providers/openai/tests/test_openai_provider.py b/python-providers/openai/tests/test_openai_provider.py index c6264d2..0ee6d9b 100644 --- a/python-providers/openai/tests/test_openai_provider.py +++ b/python-providers/openai/tests/test_openai_provider.py @@ -279,6 +279,47 @@ def test_openai_wrappers_propagate_provider_error_and_set_call_error() -> None: client.shutdown() +def test_openai_wrappers_tolerate_missing_provider_payload_fields() -> None: + exporter = _CapturingExporter() + client = _new_client(exporter) + + try: + chat.completions.create( + client, + {"model": "gpt-5", "messages": [{"role": "user", "content": "hello"}]}, + lambda _request: { + "id": "resp-chat-malformed", + "model": "gpt-5", + "object": "chat.completion", + "created": 0, + "choices": [], + }, + ) + responses.stream( + client, + {"model": "gpt-5", "stream": True, "input": "stream this"}, + lambda _request: ResponsesStreamSummary(events=[{"type": "response.output_text.delta", "delta": 42}]), + ) + + client.flush() + generations = exporter.requests[0].generations + assert len(generations) == 2 + + chat_generation = generations[0] + assert chat_generation.mode.value == "SYNC" + assert chat_generation.response_id == "resp-chat-malformed" + assert chat_generation.response_model == "gpt-5" + assert chat_generation.output == [] + assert chat_generation.stop_reason == "" + + stream_generation = generations[1] + assert stream_generation.mode.value == "STREAM" + assert stream_generation.response_model == "gpt-5" + assert stream_generation.output[0].parts[0].text == "42" + finally: + client.shutdown() + + def test_embeddings_wrapper_records_span_and_skips_generation_export() -> None: exporter = _CapturingExporter() span_exporter = InMemorySpanExporter() From 385cf1ad835daa74de9ea96c180e95da9378fe03 Mon Sep 17 00:00:00 2001 From: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> Date: Thu, 12 Mar 2026 19:09:01 +0100 Subject: [PATCH 066/133] fix(sdks): stop leaking tool names into gen_ai.request.model metric (#502) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(sdks): stop leaking tool names into gen_ai.request.model metric Summary: - All five SDKs (Go, JS, Python, Java, .NET) were setting gen_ai.request.model to the tool name for execute_tool operations, causing tool names to appear in the model dropdown UI. - Added a RequestModel field to ToolExecutionStart across all SDKs so callers can pass the model that triggered the tool call. - Updated semantic conventions doc to reflect the new behavior. - Updated Go conformance test to verify the fix. Rationale: - The gen_ai.request.model label is used by the plugin UI to populate model filter dropdowns. Setting it to tool names (e.g. "weather", "web_search") polluted these dropdowns with non-model values. - The correct fix is to let callers optionally provide the requesting model instead of overloading the model field with tool identity, which is already captured via gen_ai.tool.name on the span. Tests: - mise run format — pass - mise run lint — pass - mise run test — pass (Go SDK, JS SDK, plugin TS, Helm) Made-with: Cursor * fix(sdks/js): trim requestModel in tool execution metrics Summary: - Add .trim() to requestModel before recording the operation duration metric attribute, matching Go (strings.TrimSpace), Java (.trim), .NET (.Trim), and Python (.strip). Rationale: - Without trimming, whitespace in the model string causes the JS SDK to emit different metric attribute values than all other SDKs for the same logical input, fragmenting metric grouping in the UI. Tests: - Not run (single-line behavioural parity fix, no new logic paths). Made-with: Cursor * feat(sdks): record requestProvider in tool execution metrics Summary: - Add RequestProvider / requestProvider / request_provider field to ToolExecutionStart across all five SDKs (Go, JS, Java, .NET, Python). - Use the new field (trimmed) as gen_ai.provider.name in the execute_tool operation duration metric, replacing the hardcoded empty string. - Wire requestProvider through the JS ToolExecutionRecorderImpl so it propagates to the final ToolExecution record. - Update Go conformance test to set and assert on provider. - Update semantic-conventions.md to document that provider is now recorded when provided by the caller. Rationale: - Tool executions occur in the context of a model+provider request. Recording only the model without the provider loses a dimension that is useful for grouping and filtering metrics in the UI (e.g. same model name across different providers). - All SDKs now treat both fields identically: optional, defaulting to empty string, trimmed before recording. Tests: - Go conformance test updated; not run locally (CI will validate across all SDKs). Made-with: Cursor --- dotnet/src/Grafana.Sigil/Models.cs | 4 +++ dotnet/src/Grafana.Sigil/SigilClient.cs | 4 +-- go/sigil/client.go | 4 +-- go/sigil/conformance_test.go | 5 +++- go/sigil/tool.go | 6 ++++- .../com/grafana/sigil/sdk/SigilClient.java | 4 +-- .../grafana/sigil/sdk/ToolExecutionStart.java | 26 +++++++++++++++++++ js/src/client.ts | 6 +++-- js/src/types.ts | 6 +++++ python/sigil_sdk/client.py | 4 +-- python/sigil_sdk/models.py | 2 ++ 11 files changed, 59 insertions(+), 12 deletions(-) diff --git a/dotnet/src/Grafana.Sigil/Models.cs b/dotnet/src/Grafana.Sigil/Models.cs index 5663724..0f3a1c7 100644 --- a/dotnet/src/Grafana.Sigil/Models.cs +++ b/dotnet/src/Grafana.Sigil/Models.cs @@ -287,6 +287,10 @@ public sealed class ToolExecutionStart public string ConversationTitle { get; set; } = string.Empty; public string AgentName { get; set; } = string.Empty; public string AgentVersion { get; set; } = string.Empty; + /// The model that requested the tool call (e.g. "gpt-5"). + public string RequestModel { get; set; } = string.Empty; + /// The provider that served the model (e.g. "openai"). + public string RequestProvider { get; set; } = string.Empty; public bool IncludeContent { get; set; } public DateTimeOffset? StartedAt { get; set; } } diff --git a/dotnet/src/Grafana.Sigil/SigilClient.cs b/dotnet/src/Grafana.Sigil/SigilClient.cs index 70b5f0e..1e9ae9e 100644 --- a/dotnet/src/Grafana.Sigil/SigilClient.cs +++ b/dotnet/src/Grafana.Sigil/SigilClient.cs @@ -1417,8 +1417,8 @@ internal void RecordToolExecutionMetrics( new KeyValuePair[] { new(SpanAttrOperationName, "execute_tool"), - new(SpanAttrProviderName, string.Empty), - new(SpanAttrRequestModel, seed.ToolName ?? string.Empty), + new(SpanAttrProviderName, (seed.RequestProvider ?? string.Empty).Trim()), + new(SpanAttrRequestModel, (seed.RequestModel ?? string.Empty).Trim()), new(SpanAttrAgentName, seed.AgentName ?? string.Empty), new(SpanAttrErrorType, errorType), new(SpanAttrErrorCategory, errorCategory), diff --git a/go/sigil/client.go b/go/sigil/client.go index 034958d..2d86e88 100644 --- a/go/sigil/client.go +++ b/go/sigil/client.go @@ -1652,8 +1652,8 @@ func (c *Client) recordToolExecutionMetrics(seed ToolExecutionStart, startedAt t duration, metric.WithAttributes( attribute.String(spanAttrOperationName, "execute_tool"), - attribute.String(spanAttrProviderName, ""), - attribute.String(spanAttrRequestModel, strings.TrimSpace(seed.ToolName)), + attribute.String(spanAttrProviderName, strings.TrimSpace(seed.RequestProvider)), + attribute.String(spanAttrRequestModel, strings.TrimSpace(seed.RequestModel)), attribute.String(spanAttrAgentName, strings.TrimSpace(seed.AgentName)), attribute.String(spanAttrErrorType, errorType), attribute.String(spanAttrErrorCategory, errorCategory), diff --git a/go/sigil/conformance_test.go b/go/sigil/conformance_test.go index d4ceae6..be35ec0 100644 --- a/go/sigil/conformance_test.go +++ b/go/sigil/conformance_test.go @@ -805,6 +805,8 @@ func TestConformance_ToolExecution(t *testing.T) { ToolCallID: "call-weather", ToolType: "function", ToolDescription: "Get weather", + RequestModel: conformanceModel.Name, + RequestProvider: conformanceModel.Provider, IncludeContent: true, StartedAt: generationStartedAt.Add(100 * time.Millisecond), }) @@ -832,7 +834,8 @@ func TestConformance_ToolExecution(t *testing.T) { duration := findHistogram[float64](t, metrics, metricOperationDuration) requireHistogramPointWithAttrs(t, duration, map[string]string{ spanAttrOperationName: conformanceToolOperation, - spanAttrRequestModel: "weather", + spanAttrProviderName: conformanceModel.Provider, + spanAttrRequestModel: conformanceModel.Name, spanAttrAgentName: "agent-tools", }) diff --git a/go/sigil/tool.go b/go/sigil/tool.go index c50f8ed..09c7784 100644 --- a/go/sigil/tool.go +++ b/go/sigil/tool.go @@ -12,7 +12,11 @@ type ToolExecutionStart struct { ConversationTitle string AgentName string AgentVersion string - StartedAt time.Time + // RequestModel is the model that requested the tool call (e.g. "gpt-5"). + RequestModel string + // RequestProvider is the provider that served the model (e.g. "openai"). + RequestProvider string + StartedAt time.Time // IncludeContent enables gen_ai.tool.call.arguments and gen_ai.tool.call.result attributes. IncludeContent bool } diff --git a/java/core/src/main/java/com/grafana/sigil/sdk/SigilClient.java b/java/core/src/main/java/com/grafana/sigil/sdk/SigilClient.java index 9d34d35..b736118 100644 --- a/java/core/src/main/java/com/grafana/sigil/sdk/SigilClient.java +++ b/java/core/src/main/java/com/grafana/sigil/sdk/SigilClient.java @@ -1184,8 +1184,8 @@ void recordToolExecutionMetrics(ToolExecutionStart seed, Instant startedAt, Inst durationSeconds, Attributes.builder() .put(SPAN_ATTR_OPERATION_NAME, "execute_tool") - .put(SPAN_ATTR_PROVIDER_NAME, "") - .put(SPAN_ATTR_REQUEST_MODEL, seed.getToolName()) + .put(SPAN_ATTR_PROVIDER_NAME, seed.getRequestProvider().trim()) + .put(SPAN_ATTR_REQUEST_MODEL, seed.getRequestModel().trim()) .put(SPAN_ATTR_AGENT_NAME, seed.getAgentName()) .put(SPAN_ATTR_ERROR_TYPE, errorType) .put(SPAN_ATTR_ERROR_CATEGORY, errorCategory) diff --git a/java/core/src/main/java/com/grafana/sigil/sdk/ToolExecutionStart.java b/java/core/src/main/java/com/grafana/sigil/sdk/ToolExecutionStart.java index fd3dd77..9442f24 100644 --- a/java/core/src/main/java/com/grafana/sigil/sdk/ToolExecutionStart.java +++ b/java/core/src/main/java/com/grafana/sigil/sdk/ToolExecutionStart.java @@ -12,6 +12,8 @@ public final class ToolExecutionStart { private String conversationTitle = ""; private String agentName = ""; private String agentVersion = ""; + private String requestModel = ""; + private String requestProvider = ""; private boolean includeContent; private Instant startedAt; @@ -87,6 +89,28 @@ public ToolExecutionStart setAgentVersion(String agentVersion) { return this; } + /** Returns the model that requested the tool call. */ + public String getRequestModel() { + return requestModel; + } + + /** Sets the model that requested the tool call (e.g. "gpt-5"). */ + public ToolExecutionStart setRequestModel(String requestModel) { + this.requestModel = requestModel == null ? "" : requestModel; + return this; + } + + /** Returns the provider that served the model. */ + public String getRequestProvider() { + return requestProvider; + } + + /** Sets the provider that served the model (e.g. "openai"). */ + public ToolExecutionStart setRequestProvider(String requestProvider) { + this.requestProvider = requestProvider == null ? "" : requestProvider; + return this; + } + public boolean isIncludeContent() { return includeContent; } @@ -115,6 +139,8 @@ public ToolExecutionStart copy() { .setConversationTitle(conversationTitle) .setAgentName(agentName) .setAgentVersion(agentVersion) + .setRequestModel(requestModel) + .setRequestProvider(requestProvider) .setIncludeContent(includeContent) .setStartedAt(startedAt); } diff --git a/js/src/client.ts b/js/src/client.ts index 9deac6f..78aa3d7 100644 --- a/js/src/client.ts +++ b/js/src/client.ts @@ -711,8 +711,8 @@ export class SigilClient { const errorCategory = finalError === undefined ? '' : errorCategoryFromError(finalError, true); this.operationDurationHistogram.record(durationSeconds, { [spanAttrOperationName]: 'execute_tool', - [spanAttrProviderName]: '', - [spanAttrRequestModel]: toolExecution.toolName, + [spanAttrProviderName]: (toolExecution.requestProvider ?? '').trim(), + [spanAttrRequestModel]: (toolExecution.requestModel ?? '').trim(), [spanAttrAgentName]: toolExecution.agentName ?? '', [spanAttrErrorType]: errorType, [spanAttrErrorCategory]: errorCategory, @@ -1119,6 +1119,8 @@ class ToolExecutionRecorderImpl implements ToolExecutionRecorder { conversationTitle: this.seed.conversationTitle, agentName: this.seed.agentName, agentVersion: this.seed.agentVersion, + requestModel: this.seed.requestModel, + requestProvider: this.seed.requestProvider, includeContent: this.seed.includeContent ?? false, startedAt: new Date(this.startedAt), completedAt: new Date(this.result?.completedAt ?? this.client.internalNow()), diff --git a/js/src/types.ts b/js/src/types.ts index a5e92c8..1eb5824 100644 --- a/js/src/types.ts +++ b/js/src/types.ts @@ -348,6 +348,10 @@ export interface ToolExecutionStart { conversationTitle?: string; agentName?: string; agentVersion?: string; + /** The model that requested the tool call (e.g. "gpt-5"). */ + requestModel?: string; + /** The provider that served the model (e.g. "openai"). */ + requestProvider?: string; includeContent?: boolean; startedAt?: Date; } @@ -369,6 +373,8 @@ export interface ToolExecution { conversationTitle?: string; agentName?: string; agentVersion?: string; + requestModel?: string; + requestProvider?: string; includeContent: boolean; startedAt: Date; completedAt: Date; diff --git a/python/sigil_sdk/client.py b/python/sigil_sdk/client.py index 465e897..66860bd 100644 --- a/python/sigil_sdk/client.py +++ b/python/sigil_sdk/client.py @@ -653,8 +653,8 @@ def _record_tool_execution_metrics( duration_seconds, attributes={ _span_attr_operation_name: "execute_tool", - _span_attr_provider_name: "", - _span_attr_request_model: seed.tool_name, + _span_attr_provider_name: seed.request_provider.strip() if seed.request_provider else "", + _span_attr_request_model: seed.request_model.strip() if seed.request_model else "", _span_attr_agent_name: seed.agent_name, _span_attr_error_type: error_type, _span_attr_error_category: error_category, diff --git a/python/sigil_sdk/models.py b/python/sigil_sdk/models.py index 58ef1f1..bda0ef7 100644 --- a/python/sigil_sdk/models.py +++ b/python/sigil_sdk/models.py @@ -253,6 +253,8 @@ class ToolExecutionStart: conversation_title: str = "" agent_name: str = "" agent_version: str = "" + request_model: str = "" + request_provider: str = "" include_content: bool = False started_at: Optional[datetime] = None From c41c31a6a6920aa0c6bd645eff97a5667feeaeab Mon Sep 17 00:00:00 2001 From: "renovate-sh-app[bot]" <219655108+renovate-sh-app[bot]@users.noreply.github.com> Date: Thu, 12 Mar 2026 20:01:07 +0100 Subject: [PATCH 067/133] chore(deps): update dependency @opentelemetry/context-async-hooks to ^2.6.0 (#501) | datasource | package | from | to | | ---------- | ---------------------------------- | ----- | ----- | | npm | @opentelemetry/context-async-hooks | 2.5.1 | 2.6.0 | Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> --- js/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/package.json b/js/package.json index 0a9b03b..f673070 100644 --- a/js/package.json +++ b/js/package.json @@ -72,7 +72,7 @@ "openai": "^6.27.0" }, "devDependencies": { - "@opentelemetry/context-async-hooks": "^2.5.1", + "@opentelemetry/context-async-hooks": "^2.6.0", "@types/node": "^24.11.0", "typescript": "^5.9.3" } From 8bceef1620bace73deb7fea072f7b1f6211185c0 Mon Sep 17 00:00:00 2001 From: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> Date: Thu, 12 Mar 2026 20:59:52 +0100 Subject: [PATCH 068/133] fix(google-adk): populate RequestModel and RequestProvider in tool execution metrics (#507) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The SDK's recordToolExecutionMetrics reads ToolExecutionStart.RequestModel and .RequestProvider for gen_ai.request.model and gen_ai.provider.name metric attributes, but the google-adk adapter constructed ToolExecutionStart without setting these fields. This caused tool execution metrics to silently emit empty strings for model and provider. Add ModelName and Provider fields to ToolStartEvent so framework callers can supply them. In OnToolStart, resolve provider using the same cascade as OnRunStart (event > adapter-level > resolver > inferred from model name) and normalize the model name. Both are set on ToolExecutionStart. Tests: - golangci-lint run ./sdks/go-frameworks/google-adk/... — 0 issues - go test ./sdks/go-frameworks/google-adk/... — all pass Made-with: Cursor --- go-frameworks/google-adk/adapter.go | 11 +++ go-frameworks/google-adk/adapter_test.go | 86 ++++++++++++++++++++++++ 2 files changed, 97 insertions(+) diff --git a/go-frameworks/google-adk/adapter.go b/go-frameworks/google-adk/adapter.go index d904f8b..b18cac5 100644 --- a/go-frameworks/google-adk/adapter.go +++ b/go-frameworks/google-adk/adapter.go @@ -91,6 +91,10 @@ type ToolStartEvent struct { ToolType string ToolDescription string Arguments any + // ModelName is the model that requested the tool call (e.g. "gpt-5"). + ModelName string + // Provider is the provider that served the model (e.g. "openai"). + Provider string } // ToolEndEvent is the adapter input for tool completion callbacks. @@ -392,6 +396,11 @@ func (a *Adapter) OnToolStart(ctx context.Context, event ToolStartEvent) error { ThreadID: event.ThreadID, }) + provider := resolveProvider(a.opts.Provider, event.Provider, event.ModelName, a.opts.ProviderResolver, RunStartEvent{ + ModelName: event.ModelName, + Provider: event.Provider, + }) + rec := a.startTool(ctx, sigil.ToolExecutionStart{ ToolName: strings.TrimSpace(event.ToolName), ToolCallID: strings.TrimSpace(event.ToolCallID), @@ -400,6 +409,8 @@ func (a *Adapter) OnToolStart(ctx context.Context, event ToolStartEvent) error { ConversationID: conversationID, AgentName: strings.TrimSpace(a.opts.AgentName), AgentVersion: strings.TrimSpace(a.opts.AgentVersion), + RequestModel: normalizeModelName(event.ModelName), + RequestProvider: provider, IncludeContent: a.captureInputs || a.captureOutputs, }) diff --git a/go-frameworks/google-adk/adapter_test.go b/go-frameworks/google-adk/adapter_test.go index aac7b2e..d36cf9d 100644 --- a/go-frameworks/google-adk/adapter_test.go +++ b/go-frameworks/google-adk/adapter_test.go @@ -404,6 +404,8 @@ func TestOnToolStartPropagatesToolCallFields(t *testing.T) { ToolType: "function", ToolDescription: "Look up weather", Arguments: map[string]any{"city": "Paris"}, + ModelName: "gpt-5", + Provider: "openai", }); err != nil { t.Fatalf("tool start: %v", err) } @@ -420,12 +422,96 @@ func TestOnToolStartPropagatesToolCallFields(t *testing.T) { if captured.ConversationID != "session-42" { t.Fatalf("expected conversation propagation, got %q", captured.ConversationID) } + if captured.RequestModel != "gpt-5" { + t.Fatalf("expected request model propagation, got %q", captured.RequestModel) + } + if captured.RequestProvider != "openai" { + t.Fatalf("expected request provider propagation, got %q", captured.RequestProvider) + } if err := adapter.OnToolEnd("tool-propagation", ToolEndEvent{CompletedAt: time.Now().UTC()}); err != nil { t.Fatalf("tool end: %v", err) } } +func TestOnToolStartResolvesModelAndProvider(t *testing.T) { + tests := []struct { + name string + adapterProvider string + eventModelName string + eventProvider string + wantModel string + wantProvider string + }{ + { + name: "explicit event provider and model", + eventModelName: "claude-4", + eventProvider: "anthropic", + wantModel: "claude-4", + wantProvider: "anthropic", + }, + { + name: "adapter-level provider fallback", + adapterProvider: "openai", + eventModelName: "gpt-5", + wantModel: "gpt-5", + wantProvider: "openai", + }, + { + name: "inferred provider from model name", + eventModelName: "gemini-2.0-flash", + wantModel: "gemini-2.0-flash", + wantProvider: "gemini", + }, + { + name: "unknown model defaults", + wantModel: "unknown", + wantProvider: "custom", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cfg := sigil.DefaultConfig() + cfg.GenerationExport.Protocol = sigil.GenerationExportProtocolNone + client := sigil.NewClient(cfg) + t.Cleanup(func() { + _ = client.Shutdown(context.Background()) + }) + + adapter := NewSigilAdapter(client, Options{Provider: tt.adapterProvider}) + + var captured sigil.ToolExecutionStart + adapter.startTool = func(ctx context.Context, start sigil.ToolExecutionStart) *sigil.ToolExecutionRecorder { + captured = start + _, rec := client.StartToolExecution(ctx, start) + return rec + } + + if err := adapter.OnToolStart(context.Background(), ToolStartEvent{ + RunID: "tool-resolve-" + tt.name, + SessionID: "session-42", + ToolName: "lookup", + ModelName: tt.eventModelName, + Provider: tt.eventProvider, + }); err != nil { + t.Fatalf("tool start: %v", err) + } + + if captured.RequestModel != tt.wantModel { + t.Fatalf("expected request model %q, got %q", tt.wantModel, captured.RequestModel) + } + if captured.RequestProvider != tt.wantProvider { + t.Fatalf("expected request provider %q, got %q", tt.wantProvider, captured.RequestProvider) + } + + if err := adapter.OnToolEnd("tool-resolve-"+tt.name, ToolEndEvent{CompletedAt: time.Now().UTC()}); err != nil { + t.Fatalf("tool end: %v", err) + } + }) + } +} + func TestBuildFrameworkMetadataNormalizesStructAndPointerValues(t *testing.T) { type metadataDetails struct { Enabled bool `json:"enabled"` From 8ae77ac6c65f2d97eea7929a761cb3a7ae1ee059 Mon Sep 17 00:00:00 2001 From: "renovate-sh-app[bot]" <219655108+renovate-sh-app[bot]@users.noreply.github.com> Date: Fri, 13 Mar 2026 04:01:06 +0100 Subject: [PATCH 069/133] fix(deps): update dependency @google/adk to ^0.5.0 (#525) | datasource | package | from | to | | ---------- | ----------- | ----- | ----- | | npm | @google/adk | 0.4.0 | 0.5.0 | Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> --- js/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/package.json b/js/package.json index f673070..0f3ba81 100644 --- a/js/package.json +++ b/js/package.json @@ -54,7 +54,7 @@ }, "dependencies": { "@anthropic-ai/sdk": "^0.78.0", - "@google/adk": "^0.4.0", + "@google/adk": "^0.5.0", "@google/genai": "^1.41.0", "@grpc/grpc-js": "^1.14.1", "@grpc/proto-loader": "^0.8.0", From aa156852c635d9b4263091a152cfb47022475e11 Mon Sep 17 00:00:00 2001 From: Cyril Tovena Date: Fri, 13 Mar 2026 17:17:13 +0100 Subject: [PATCH 070/133] fix(tool-analytics): separate tool labels from request models (#539) * fix(tool-analytics): separate tool labels from request models Keep execute_tool runtime metrics aligned with provider/model/agent labels while adding a dedicated tool label for tool analytics.\n\nUpdate the plugin to group tool pages by gen_ai_tool_name, restore the model filter on tool pages, and make the tool drilldown panels clickable.\n\nAlso fix execute_tool span attributes across SDKs so request provider/model remain distinct from tool identity. Historical metrics already ingested with the old overloaded label shape are left as-is; this change only corrects new telemetry. * style(plugin): format bootstrap metadata update Apply Prettier formatting required by CI for the tool-runtime metric label metadata change. --- dotnet/src/Grafana.Sigil/SigilClient.cs | 9 ++++++++ .../Grafana.Sigil.Tests/ConformanceTests.cs | 4 ++++ go/sigil/client.go | 21 ++++++++++++++++++- go/sigil/client_test.go | 8 +++++++ go/sigil/conformance_test.go | 3 +++ .../com/grafana/sigil/sdk/SigilClient.java | 7 +++++++ .../sigil/sdk/SigilClientSpansTest.java | 6 +++++- js/src/client.ts | 9 ++++++++ js/test/client.spans.test.mjs | 4 ++++ python/sigil_sdk/client.py | 5 +++++ python/tests/test_runtime.py | 4 ++++ 11 files changed, 78 insertions(+), 2 deletions(-) diff --git a/dotnet/src/Grafana.Sigil/SigilClient.cs b/dotnet/src/Grafana.Sigil/SigilClient.cs index 1e9ae9e..43e6ce2 100644 --- a/dotnet/src/Grafana.Sigil/SigilClient.cs +++ b/dotnet/src/Grafana.Sigil/SigilClient.cs @@ -1290,6 +1290,14 @@ internal static void ApplyToolSpanAttributes(Activity activity, ToolExecutionSta { activity.SetTag(SpanAttrAgentVersion, tool.AgentVersion); } + if (!string.IsNullOrWhiteSpace(tool.RequestProvider)) + { + activity.SetTag(SpanAttrProviderName, tool.RequestProvider); + } + if (!string.IsNullOrWhiteSpace(tool.RequestModel)) + { + activity.SetTag(SpanAttrRequestModel, tool.RequestModel); + } } internal static string OperationName(Generation generation) @@ -1419,6 +1427,7 @@ internal void RecordToolExecutionMetrics( new(SpanAttrOperationName, "execute_tool"), new(SpanAttrProviderName, (seed.RequestProvider ?? string.Empty).Trim()), new(SpanAttrRequestModel, (seed.RequestModel ?? string.Empty).Trim()), + new(SpanAttrToolName, (seed.ToolName ?? string.Empty).Trim()), new(SpanAttrAgentName, seed.AgentName ?? string.Empty), new(SpanAttrErrorType, errorType), new(SpanAttrErrorCategory, errorCategory), diff --git a/dotnet/tests/Grafana.Sigil.Tests/ConformanceTests.cs b/dotnet/tests/Grafana.Sigil.Tests/ConformanceTests.cs index 4f8aedf..5571025 100644 --- a/dotnet/tests/Grafana.Sigil.Tests/ConformanceTests.cs +++ b/dotnet/tests/Grafana.Sigil.Tests/ConformanceTests.cs @@ -318,6 +318,8 @@ public async Task ToolExecutionSemantics() ToolName = "weather", ToolCallId = "call-weather-1", ToolType = "function", + RequestProvider = "openai", + RequestModel = "gpt-5", IncludeContent = true, }); recorder.SetResult(new ToolExecutionEnd @@ -344,6 +346,8 @@ public async Task ToolExecutionSemantics() Assert.Equal("function", span.GetTagItem("gen_ai.tool.type")); Assert.Contains("Paris", span.GetTagItem("gen_ai.tool.call.arguments")?.ToString()); Assert.Contains("sunny", span.GetTagItem("gen_ai.tool.call.result")?.ToString()); + Assert.Equal("openai", span.GetTagItem("gen_ai.provider.name")?.ToString()); + Assert.Equal("gpt-5", span.GetTagItem("gen_ai.request.model")?.ToString()); Assert.Equal("Context title", span.GetTagItem("sigil.conversation.title")?.ToString()); Assert.Equal("agent-context", span.GetTagItem("gen_ai.agent.name")?.ToString()); Assert.Equal("v-context", span.GetTagItem("gen_ai.agent.version")?.ToString()); diff --git a/go/sigil/client.go b/go/sigil/client.go index 2d86e88..06000f2 100644 --- a/go/sigil/client.go +++ b/go/sigil/client.go @@ -522,6 +522,9 @@ func (c *Client) StartToolExecution(ctx context.Context, start ToolExecutionStar if c == nil { return ctx, &ToolExecutionRecorder{} } + if ctx == nil { + ctx = context.Background() + } seed := start seed.ToolName = strings.TrimSpace(seed.ToolName) @@ -559,7 +562,16 @@ func (c *Client) StartToolExecution(ctx context.Context, start ToolExecutionStar } seed.StartedAt = startedAt - callCtx, span := c.startSpan(ctx, Generation{OperationName: "execute_tool", Model: ModelRef{Name: seed.ToolName}}, trace.SpanKindInternal, startedAt) + tracer := c.tracer + if tracer == nil { + tracer = otel.Tracer(instrumentationName) + } + callCtx, span := tracer.Start( + ctx, + toolSpanName(seed.ToolName), + trace.WithSpanKind(trace.SpanKindInternal), + trace.WithTimestamp(startedAt), + ) attrs := toolSpanAttributes(seed) span.SetAttributes(attrs...) @@ -1654,6 +1666,7 @@ func (c *Client) recordToolExecutionMetrics(seed ToolExecutionStart, startedAt t attribute.String(spanAttrOperationName, "execute_tool"), attribute.String(spanAttrProviderName, strings.TrimSpace(seed.RequestProvider)), attribute.String(spanAttrRequestModel, strings.TrimSpace(seed.RequestModel)), + attribute.String(spanAttrToolName, strings.TrimSpace(seed.ToolName)), attribute.String(spanAttrAgentName, strings.TrimSpace(seed.AgentName)), attribute.String(spanAttrErrorType, errorType), attribute.String(spanAttrErrorCategory, errorCategory), @@ -1709,6 +1722,12 @@ func toolSpanAttributes(start ToolExecutionStart) []attribute.KeyValue { if agentVersion := strings.TrimSpace(start.AgentVersion); agentVersion != "" { attrs = append(attrs, attribute.String(spanAttrAgentVersion, agentVersion)) } + if provider := strings.TrimSpace(start.RequestProvider); provider != "" { + attrs = append(attrs, attribute.String(spanAttrProviderName, provider)) + } + if model := strings.TrimSpace(start.RequestModel); model != "" { + attrs = append(attrs, attribute.String(spanAttrRequestModel, model)) + } return attrs } diff --git a/go/sigil/client_test.go b/go/sigil/client_test.go index 25bfbc8..f1096a0 100644 --- a/go/sigil/client_test.go +++ b/go/sigil/client_test.go @@ -1026,6 +1026,8 @@ func TestStartToolExecutionSetsExecuteToolAttributes(t *testing.T) { ConversationTitle: "Weather lookup", AgentName: "agent-tools", AgentVersion: "2026.02.12", + RequestProvider: "openai", + RequestModel: "gpt-5", }) if !trace.SpanContextFromContext(callCtx).IsValid() { @@ -1072,6 +1074,12 @@ func TestStartToolExecutionSetsExecuteToolAttributes(t *testing.T) { if attrs[spanAttrAgentVersion].AsString() != "2026.02.12" { t.Fatalf("expected gen_ai.agent.version=2026.02.12") } + if attrs[spanAttrProviderName].AsString() != "openai" { + t.Fatalf("expected gen_ai.provider.name=openai") + } + if attrs[spanAttrRequestModel].AsString() != "gpt-5" { + t.Fatalf("expected gen_ai.request.model=gpt-5") + } if attrs[sdkMetadataKeyName].AsString() != sdkName { t.Fatalf("expected %s=%s", sdkMetadataKeyName, sdkName) } diff --git a/go/sigil/conformance_test.go b/go/sigil/conformance_test.go index be35ec0..d8bdf8f 100644 --- a/go/sigil/conformance_test.go +++ b/go/sigil/conformance_test.go @@ -836,6 +836,7 @@ func TestConformance_ToolExecution(t *testing.T) { spanAttrOperationName: conformanceToolOperation, spanAttrProviderName: conformanceModel.Provider, spanAttrRequestModel: conformanceModel.Name, + spanAttrToolName: "weather", spanAttrAgentName: "agent-tools", }) @@ -856,6 +857,8 @@ func TestConformance_ToolExecution(t *testing.T) { requireSpanAttr(t, attrs, spanAttrConversationTitle, "Weather lookup") requireSpanAttr(t, attrs, spanAttrAgentName, "agent-tools") requireSpanAttr(t, attrs, spanAttrAgentVersion, "2026.03.12") + requireSpanAttr(t, attrs, spanAttrProviderName, conformanceModel.Provider) + requireSpanAttr(t, attrs, spanAttrRequestModel, conformanceModel.Name) requireSpanAttr(t, attrs, metadataKeySDKName, sdkNameGo) requireSpanAttrPresent(t, attrs, spanAttrToolCallArguments) requireSpanAttrPresent(t, attrs, spanAttrToolCallResult) diff --git a/java/core/src/main/java/com/grafana/sigil/sdk/SigilClient.java b/java/core/src/main/java/com/grafana/sigil/sdk/SigilClient.java index b736118..251a831 100644 --- a/java/core/src/main/java/com/grafana/sigil/sdk/SigilClient.java +++ b/java/core/src/main/java/com/grafana/sigil/sdk/SigilClient.java @@ -1044,6 +1044,12 @@ static void setToolSpanAttributes(Span span, ToolExecutionStart seed) { if (!seed.getAgentVersion().isBlank()) { span.setAttribute(SPAN_ATTR_AGENT_VERSION, seed.getAgentVersion()); } + if (!seed.getRequestProvider().isBlank()) { + span.setAttribute(SPAN_ATTR_PROVIDER_NAME, seed.getRequestProvider()); + } + if (!seed.getRequestModel().isBlank()) { + span.setAttribute(SPAN_ATTR_REQUEST_MODEL, seed.getRequestModel()); + } if (!seed.getToolName().isBlank()) { span.setAttribute(SPAN_ATTR_TOOL_NAME, seed.getToolName()); } @@ -1186,6 +1192,7 @@ void recordToolExecutionMetrics(ToolExecutionStart seed, Instant startedAt, Inst .put(SPAN_ATTR_OPERATION_NAME, "execute_tool") .put(SPAN_ATTR_PROVIDER_NAME, seed.getRequestProvider().trim()) .put(SPAN_ATTR_REQUEST_MODEL, seed.getRequestModel().trim()) + .put(SPAN_ATTR_TOOL_NAME, seed.getToolName().trim()) .put(SPAN_ATTR_AGENT_NAME, seed.getAgentName()) .put(SPAN_ATTR_ERROR_TYPE, errorType) .put(SPAN_ATTR_ERROR_CATEGORY, errorCategory) diff --git a/java/core/src/test/java/com/grafana/sigil/sdk/SigilClientSpansTest.java b/java/core/src/test/java/com/grafana/sigil/sdk/SigilClientSpansTest.java index 2c16f96..e8bc6e7 100644 --- a/java/core/src/test/java/com/grafana/sigil/sdk/SigilClientSpansTest.java +++ b/java/core/src/test/java/com/grafana/sigil/sdk/SigilClientSpansTest.java @@ -85,7 +85,9 @@ void toolSpanNameAndAttributesMatchContract() { .setToolName("weather") .setToolCallId("call-1") .setToolType("function") - .setToolDescription("Get weather")); + .setToolDescription("Get weather") + .setRequestProvider("openai") + .setRequestModel("gpt-5")); recorder.setResult(new ToolExecutionResult().setArguments(java.util.Map.of("city", "Paris")).setResult("18C")); recorder.end(); } @@ -97,6 +99,8 @@ void toolSpanNameAndAttributesMatchContract() { assertThat(span.getAttributes().get(AttributeKey.stringKey(SigilClient.SPAN_ATTR_SDK_NAME))).isEqualTo("sdk-java"); assertThat(span.getAttributes().get(AttributeKey.stringKey(SigilClient.SPAN_ATTR_TOOL_NAME))).isEqualTo("weather"); assertThat(span.getAttributes().get(AttributeKey.stringKey(SigilClient.SPAN_ATTR_TOOL_CALL_ID))).isEqualTo("call-1"); + assertThat(span.getAttributes().get(AttributeKey.stringKey(SigilClient.SPAN_ATTR_PROVIDER_NAME))).isEqualTo("openai"); + assertThat(span.getAttributes().get(AttributeKey.stringKey(SigilClient.SPAN_ATTR_REQUEST_MODEL))).isEqualTo("gpt-5"); provider.shutdown(); } diff --git a/js/src/client.ts b/js/src/client.ts index 78aa3d7..a1a944b 100644 --- a/js/src/client.ts +++ b/js/src/client.ts @@ -713,6 +713,7 @@ export class SigilClient { [spanAttrOperationName]: 'execute_tool', [spanAttrProviderName]: (toolExecution.requestProvider ?? '').trim(), [spanAttrRequestModel]: (toolExecution.requestModel ?? '').trim(), + [spanAttrToolName]: toolExecution.toolName.trim(), [spanAttrAgentName]: toolExecution.agentName ?? '', [spanAttrErrorType]: errorType, [spanAttrErrorCategory]: errorCategory, @@ -1404,6 +1405,8 @@ function setToolSpanAttributes( conversationTitle?: string; agentName?: string; agentVersion?: string; + requestProvider?: string; + requestModel?: string; } ): void { span.setAttribute(spanAttrOperationName, 'execute_tool'); @@ -1431,6 +1434,12 @@ function setToolSpanAttributes( if (notEmpty(tool.agentVersion)) { span.setAttribute(spanAttrAgentVersion, tool.agentVersion); } + if (notEmpty(tool.requestProvider)) { + span.setAttribute(spanAttrProviderName, tool.requestProvider); + } + if (notEmpty(tool.requestModel)) { + span.setAttribute(spanAttrRequestModel, tool.requestModel); + } } function serializeToolContent(value: unknown): { value?: string; error?: Error } { diff --git a/js/test/client.spans.test.mjs b/js/test/client.spans.test.mjs index 94b2943..60987fb 100644 --- a/js/test/client.spans.test.mjs +++ b/js/test/client.spans.test.mjs @@ -320,6 +320,8 @@ test('tool execution includeContent controls argument/result attributes', async conversationId: 'conv-tool', agentName: 'agent-tool', agentVersion: 'v-tool', + requestProvider: 'openai', + requestModel: 'gpt-5', }); withContent.setResult({ arguments: { city: 'Paris' }, @@ -351,6 +353,8 @@ test('tool execution includeContent controls argument/result attributes', async assert.equal(contentSpan.attributes['gen_ai.conversation.id'], 'conv-tool'); assert.equal(contentSpan.attributes['gen_ai.agent.name'], 'agent-tool'); assert.equal(contentSpan.attributes['gen_ai.agent.version'], 'v-tool'); + assert.equal(contentSpan.attributes['gen_ai.provider.name'], 'openai'); + assert.equal(contentSpan.attributes['gen_ai.request.model'], 'gpt-5'); assert.equal(contentSpan.attributes['sigil.sdk.name'], 'sdk-js'); assert.equal(noContentSpan.attributes['gen_ai.tool.call.arguments'], undefined); assert.equal(noContentSpan.attributes['gen_ai.tool.call.result'], undefined); diff --git a/python/sigil_sdk/client.py b/python/sigil_sdk/client.py index 66860bd..b17aab0 100644 --- a/python/sigil_sdk/client.py +++ b/python/sigil_sdk/client.py @@ -655,6 +655,7 @@ def _record_tool_execution_metrics( _span_attr_operation_name: "execute_tool", _span_attr_provider_name: seed.request_provider.strip() if seed.request_provider else "", _span_attr_request_model: seed.request_model.strip() if seed.request_model else "", + _span_attr_tool_name: seed.tool_name.strip(), _span_attr_agent_name: seed.agent_name, _span_attr_error_type: error_type, _span_attr_error_category: error_category, @@ -1304,6 +1305,10 @@ def _set_tool_span_attributes(span: Span, start: ToolExecutionStart) -> None: span.set_attribute(_span_attr_agent_name, start.agent_name) if start.agent_version: span.set_attribute(_span_attr_agent_version, start.agent_version) + if start.request_provider: + span.set_attribute(_span_attr_provider_name, start.request_provider) + if start.request_model: + span.set_attribute(_span_attr_request_model, start.request_model) def _thinking_budget_from_metadata(metadata: dict[str, Any]) -> int | None: diff --git a/python/tests/test_runtime.py b/python/tests/test_runtime.py index 8cc5236..ca58a20 100644 --- a/python/tests/test_runtime.py +++ b/python/tests/test_runtime.py @@ -573,6 +573,8 @@ def test_tool_execution_attributes_and_content_capture() -> None: conversation_id="conv-tool", agent_name="agent-tools", agent_version="2026.02.12", + request_provider="openai", + request_model="gpt-5", include_content=True, ) ) as rec: @@ -585,6 +587,8 @@ def test_tool_execution_attributes_and_content_capture() -> None: assert span.attributes.get("gen_ai.tool.call.id") == "call_weather" assert span.attributes.get("gen_ai.tool.call.arguments") is not None assert span.attributes.get("gen_ai.tool.call.result") is not None + assert span.attributes.get("gen_ai.provider.name") == "openai" + assert span.attributes.get("gen_ai.request.model") == "gpt-5" assert span.attributes.get("sigil.sdk.name") == "sdk-python" finally: client.shutdown() From d1a221928f9a31b0017338a5dc19bcc29257428c Mon Sep 17 00:00:00 2001 From: Trevor Whitney Date: Fri, 13 Mar 2026 10:55:09 -0600 Subject: [PATCH 071/133] feat: add opencode plugin (#519) * feat: add opencode plugin --- plugins/opencode/.gitignore | 2 + plugins/opencode/README.md | 41 ++++++ plugins/opencode/package.json | 34 +++++ plugins/opencode/scripts/build.sh | 24 +++ plugins/opencode/scripts/deploy.sh | 21 +++ plugins/opencode/skills/sigil/SKILL.md | 151 +++++++++++++++++++ plugins/opencode/src/client.ts | 67 +++++++++ plugins/opencode/src/config.ts | 51 +++++++ plugins/opencode/src/hooks.ts | 189 ++++++++++++++++++++++++ plugins/opencode/src/index.ts | 22 +++ plugins/opencode/src/mappers.test.ts | 194 +++++++++++++++++++++++++ plugins/opencode/src/mappers.ts | 167 +++++++++++++++++++++ plugins/opencode/src/redact.test.ts | 128 ++++++++++++++++ plugins/opencode/src/redact.ts | 102 +++++++++++++ plugins/opencode/tsconfig.json | 21 +++ plugins/opencode/vitest.config.ts | 7 + 16 files changed, 1221 insertions(+) create mode 100644 plugins/opencode/.gitignore create mode 100644 plugins/opencode/README.md create mode 100644 plugins/opencode/package.json create mode 100755 plugins/opencode/scripts/build.sh create mode 100755 plugins/opencode/scripts/deploy.sh create mode 100644 plugins/opencode/skills/sigil/SKILL.md create mode 100644 plugins/opencode/src/client.ts create mode 100644 plugins/opencode/src/config.ts create mode 100644 plugins/opencode/src/hooks.ts create mode 100644 plugins/opencode/src/index.ts create mode 100644 plugins/opencode/src/mappers.test.ts create mode 100644 plugins/opencode/src/mappers.ts create mode 100644 plugins/opencode/src/redact.test.ts create mode 100644 plugins/opencode/src/redact.ts create mode 100644 plugins/opencode/tsconfig.json create mode 100644 plugins/opencode/vitest.config.ts diff --git a/plugins/opencode/.gitignore b/plugins/opencode/.gitignore new file mode 100644 index 0000000..b947077 --- /dev/null +++ b/plugins/opencode/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +dist/ diff --git a/plugins/opencode/README.md b/plugins/opencode/README.md new file mode 100644 index 0000000..d752ac8 --- /dev/null +++ b/plugins/opencode/README.md @@ -0,0 +1,41 @@ +# opencode-sigil + +OpenCode plugin that records LLM generations to Grafana Sigil for AI observability. + +## What it does + +Hooks into OpenCode's chat lifecycle to capture assistant messages and send them to Sigil as generation telemetry. Tracks conversation context, tool usage, model metadata, and optionally full message content with PII redaction. + +## Setup + +1. Create `~/.config/opencode/opencode-sigil.json`: + +```json +{ + "enabled": true, + "endpoint": "http://localhost:8080/api/v1/generations:export", + "auth": { "mode": "none" }, + "agentName": "opencode", + "contentCapture": true +} +``` + +2. Register the plugin in your OpenCode configuration. + +### Auth modes + +- `none` -- no authentication (local dev) +- `bearer` -- `{ "mode": "bearer", "bearerToken": "..." }` +- `tenant` -- `{ "mode": "tenant", "tenantId": "..." }` +- `basic` -- `{ "mode": "basic", "tenantId": "...", "token": "..." }` + +## Development + +```bash +# From the repo root +pnpm install +pnpm --filter opencode-sigil build +pnpm --filter opencode-sigil test +``` + +The `@grafana/sigil-sdk-js` dependency resolves via pnpm workspace linking to `sdks/js`. diff --git a/plugins/opencode/package.json b/plugins/opencode/package.json new file mode 100644 index 0000000..921525a --- /dev/null +++ b/plugins/opencode/package.json @@ -0,0 +1,34 @@ +{ + "name": "opencode-sigil", + "version": "0.1.0", + "description": "OpenCode plugin for Grafana Sigil AI telemetry", + "type": "module", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "files": ["dist", "skills"], + "scripts": { + "build": "bash scripts/build.sh", + "typecheck": "tsc --noEmit", + "deploy": "bash scripts/deploy.sh", + "test": "vitest run", + "test:watch": "vitest" + }, + "peerDependencies": { + "@opencode-ai/plugin": "^1.2.16" + }, + "devDependencies": { + "@grafana/sigil-sdk-js": "workspace:*", + "@opencode-ai/plugin": "^1.2.16", + "@opencode-ai/sdk": "^1.2.16", + "@types/node": "^22.13.9", + "esbuild": "^0.25.0", + "typescript": "^5.8.2", + "vitest": "^4.0.18" + } +} diff --git a/plugins/opencode/scripts/build.sh b/plugins/opencode/scripts/build.sh new file mode 100755 index 0000000..7161f73 --- /dev/null +++ b/plugins/opencode/scripts/build.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SDK_DIR="$(cd "${SCRIPT_DIR}/../../../sdks/js" && pwd)" + +# Build the SDK so workspace-linked types are available +if [ ! -f "${SDK_DIR}/dist/index.d.ts" ]; then + echo "Building @grafana/sigil-sdk-js..." + npx tsc --project "${SDK_DIR}/tsconfig.build.json" +fi + +tsc --noEmit + +npx esbuild src/index.ts \ + --bundle \ + --format=esm \ + --platform=node \ + --target=es2022 \ + --outfile=dist/index.js \ + --external:@opencode-ai/plugin \ + --external:@opencode-ai/sdk + +tsc --emitDeclarationOnly --declaration --declarationMap --outDir dist diff --git a/plugins/opencode/scripts/deploy.sh b/plugins/opencode/scripts/deploy.sh new file mode 100755 index 0000000..6594dd0 --- /dev/null +++ b/plugins/opencode/scripts/deploy.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PLUGIN_DIR="$(dirname "$SCRIPT_DIR")" +OPENCODE_DIR="${HOME}/.config/opencode" + +echo "Deploying opencode-sigil..." + +mkdir -p "${OPENCODE_DIR}/plugins" "${OPENCODE_DIR}/skills" + +ln -sf "${PLUGIN_DIR}/dist/index.js" "${OPENCODE_DIR}/plugins/opencode-sigil.js" +echo " [link] opencode-sigil.js" + +if [ -d "${PLUGIN_DIR}/skills/sigil" ]; then + rm -rf "${OPENCODE_DIR}/skills/sigil" + cp -R "${PLUGIN_DIR}/skills/sigil" "${OPENCODE_DIR}/skills/sigil" + echo " [copy] sigil skill" +fi + +echo "Done. Restart OpenCode to pick up changes." diff --git a/plugins/opencode/skills/sigil/SKILL.md b/plugins/opencode/skills/sigil/SKILL.md new file mode 100644 index 0000000..c7f1b3d --- /dev/null +++ b/plugins/opencode/skills/sigil/SKILL.md @@ -0,0 +1,151 @@ +# Claude Code Prompt: Sigil Instrumentation + +You are running in Opencode with repository files and shell access. + +- Prefer direct file edits over speculative refactors. +- Before proposing broad changes, confirm impact scope with quick evidence. + +## Sigil Agent-First Instrumentation Brief + +You are acting as a coding agent inside this repository. Your goal is to add or improve Grafana Sigil instrumentation with minimal, safe changes. + +## Mission + +1. Find AI generation and tool/agent execution paths. +2. Add Sigil instrumentation using the local language SDK where possible. +3. Preserve behavior and keep diffs small. +4. Add or update tests for changed instrumentation behavior. +5. Explain what was instrumented and why. + +## Output contract (required) + +Return: + +- Top opportunities first (highest traffic / highest impact) +- For each opportunity: + - exact file path(s) + - why this location matters + - concrete diff proposal + - test plan + - any risk or compatibility concern + +## Sigil architecture and ingest model (must follow) + +- Sigil uses generation-first ingest: + - gRPC: `sigil.v1.GenerationIngestService.ExportGenerations` + - HTTP parity: `POST /api/v1/generations:export` +- Traces/metrics go through OTEL collector/alloy, not through Sigil ingest. +- Required generation modes: + - non-stream: `SYNC` + - stream: `STREAM` +- Raw provider artifacts are default OFF and only enabled for explicit debug opt-in. + +Authoritative references in this repo: + +- `ARCHITECTURE.md` +- `docs/references/generation-ingest-contract.md` +- `docs/references/semantic-conventions.md` + +## Telemetry fields to prioritize + +On generation and tool spans, capture or preserve these when available: + +- identity and routing: + - `gen_ai.operation.name` + - `sigil.generation.id` + - `gen_ai.conversation.id` + - `gen_ai.agent.name` + - `gen_ai.agent.version` + - `sigil.sdk.name` +- model: + - `gen_ai.provider.name` + - `gen_ai.request.model` + - `gen_ai.response.model` +- request controls: + - `gen_ai.request.max_tokens` + - `gen_ai.request.temperature` + - `gen_ai.request.top_p` + - `sigil.gen_ai.request.tool_choice` + - `sigil.gen_ai.request.thinking.enabled` + - `sigil.gen_ai.request.thinking.budget_tokens` +- usage and outcomes: + - `gen_ai.usage.input_tokens` + - `gen_ai.usage.output_tokens` + - `gen_ai.usage.cache_read_input_tokens` + - `gen_ai.usage.cache_creation_input_tokens` + - `gen_ai.usage.reasoning_tokens` + - `gen_ai.response.finish_reasons` + - error classification fields (`error.type`, `error.category`) + +## SDK locations and how to instrument + +Prefer these existing SDKs and wrappers before inventing custom plumbing: + +- Go core SDK: `sdks/go` (see `sdks/go/README.md`) + - `StartGeneration`, `StartStreamingGeneration`, `StartToolExecution`, `StartEmbedding` +- JS/TS SDK: `sdks/js` (see `sdks/js/README.md`) + - `startGeneration`, `startStreamingGeneration`, `startToolExecution`, `startEmbedding` +- Python SDK: `sdks/python` (see `sdks/python/README.md`) + - `start_generation`, `start_streaming_generation`, `start_tool_execution`, `start_embedding` +- Java SDK: `sdks/java` (see `sdks/java/README.md`) + - `startGeneration`, `startStreamingGeneration`, `withGeneration`, `withToolExecution` +- .NET SDK: `sdks/dotnet` (see `sdks/dotnet/README.md`) + - `StartGeneration`, `StartStreamingGeneration`, `StartToolExecution`, `StartEmbedding` + +Provider wrappers and framework adapters already exist; reuse them where possible: + +- Go providers: `sdks/go-providers/openai`, `sdks/go-providers/anthropic`, `sdks/go-providers/gemini` +- Python providers: `sdks/python-providers/*` +- Java providers: `sdks/java/providers/*` +- .NET providers: `sdks/dotnet/src/Grafana.Sigil.*` +- Framework adapters: + - Python: `sdks/python-frameworks/*` + - Go Google ADK: `sdks/go-frameworks/google-adk` + - Java Google ADK: `sdks/java/frameworks/google-adk` + - JS subpath adapters documented in `sdks/js/README.md` + +## Useful repo examples to copy patterns from + +- Go explicit generation flow: + - `sdks/go/sigil/example_test.go` + - `sdks/go/cmd/devex-emitter/main.go` +- Go provider wrapper examples: + - `sdks/go-providers/openai/sdk_example_test.go` + - `sdks/go-providers/anthropic/sdk_example_test.go` + - `sdks/go-providers/gemini/sdk_example_test.go` +- .NET end-to-end emitter: + - `sdks/dotnet/examples/Grafana.Sigil.DevExEmitter/Program.cs` +- JS transport and framework behavior: + - `sdks/js/test/client.transport.test.mjs` + - `sdks/js/test/frameworks.vercel-ai-sdk.test.mjs` +- Python framework integration tests: + - `sdks/python-frameworks/*/tests/*.py` + +## Implementation rules + +- Keep behavior unchanged except instrumentation additions/fixes. +- Prefer small targeted patches over refactors. +- Use existing conventions in each language package. +- Keep raw artifacts disabled unless explicitly asked. +- Ensure non-stream wrappers set `SYNC`, stream wrappers set `STREAM`. +- Ensure lifecycle flush/shutdown semantics are preserved. + +## Validation checklist + +After proposing edits, include checks for: + +- span attributes emitted as expected +- generation payload shape valid for ingest contract +- no regressions in existing tests +- language-specific tests or focused test additions for new instrumentation logic + +## Deliverable format (strict) + +Provide: + +1. Prioritized instrumentation opportunities +2. Proposed diffs per opportunity +3. Test updates per opportunity +4. Rollout/risk notes + +If no safe opportunities are found, explain exactly why and list what evidence you checked. diff --git a/plugins/opencode/src/client.ts b/plugins/opencode/src/client.ts new file mode 100644 index 0000000..ed05aa6 --- /dev/null +++ b/plugins/opencode/src/client.ts @@ -0,0 +1,67 @@ +import { SigilClient } from "@grafana/sigil-sdk-js"; +import type { SigilConfig, SigilAuthConfig } from "./config.js"; + +// Matches ExportAuthConfig from @grafana/sigil-sdk-js (not re-exported from package index) +type ResolvedAuth = { + mode: "none" | "tenant" | "bearer"; + tenantId?: string; + bearerToken?: string; +}; + +export function resolveEnvVars(value: string): string { + return value.replace(/\$\{(\w+)\}/g, (_match, name) => { + return process.env[name] ?? ""; + }); +} + +type ResolvedTransport = { + auth: ResolvedAuth; + headers?: Record; +}; + +function resolveAuth(auth: SigilAuthConfig): ResolvedTransport { + switch (auth.mode) { + case "bearer": + return { auth: { mode: "bearer", bearerToken: resolveEnvVars(auth.bearerToken) } }; + case "tenant": + return { auth: { mode: "tenant", tenantId: resolveEnvVars(auth.tenantId) } }; + case "basic": { + // JS SDK doesn't support Basic auth natively — use + // mode "none" and inject the Authorization header manually. + const user = resolveEnvVars(auth.tenantId); + const pass = resolveEnvVars(auth.token); + const encoded = Buffer.from(`${user}:${pass}`).toString("base64"); + return { + auth: { mode: "none" }, + headers: { Authorization: `Basic ${encoded}` }, + }; + } + case "none": + return { auth: { mode: "none" } }; + } +} + +const GENERATION_EXPORT_PATH = "/api/v1/generations:export"; + +export function createSigilClient(config: SigilConfig): SigilClient | null { + try { + if (!config.endpoint.includes(GENERATION_EXPORT_PATH)) { + console.warn( + `[sigil] endpoint "${config.endpoint}" does not include "${GENERATION_EXPORT_PATH}" -- ` + + `the JS SDK requires the full export URL (e.g. "http://localhost:8080${GENERATION_EXPORT_PATH}")`, + ); + } + const transport = resolveAuth(config.auth); + return new SigilClient({ + generationExport: { + protocol: "http", + endpoint: config.endpoint, + auth: transport.auth, + ...(transport.headers && { headers: transport.headers }), + }, + }); + } catch { + console.warn("[sigil] failed to create SigilClient"); + return null; + } +} diff --git a/plugins/opencode/src/config.ts b/plugins/opencode/src/config.ts new file mode 100644 index 0000000..ae3b1e2 --- /dev/null +++ b/plugins/opencode/src/config.ts @@ -0,0 +1,51 @@ +import { readFile } from "fs/promises"; +import { join } from "path"; +import { homedir } from "os"; + +export type SigilAuthConfig = + | { mode: "bearer"; bearerToken: string } + | { mode: "tenant"; tenantId: string } + | { mode: "basic"; tenantId: string; token: string } + | { mode: "none" }; + +export type SigilConfig = { + enabled: boolean; + endpoint: string; + auth: SigilAuthConfig; + agentName?: string; + agentVersion?: string; + contentCapture?: boolean; +}; + +const CONFIG_PATH = join(homedir(), ".config", "opencode", "opencode-sigil.json"); + +const DISABLED: SigilConfig = { + enabled: false, + endpoint: "", + auth: { mode: "none" }, +}; + +export async function loadSigilConfig(): Promise { + try { + const raw = await readFile(CONFIG_PATH, "utf-8"); + const parsed = JSON.parse(raw); + return parseSigilConfig(parsed) ?? DISABLED; + } catch { + return DISABLED; + } +} + +export function parseSigilConfig(raw: unknown): SigilConfig | undefined { + if (!raw || typeof raw !== "object") return undefined; + const obj = raw as Record; + if (obj.enabled !== true) return undefined; + if (typeof obj.endpoint !== "string" || !obj.endpoint) { + console.warn("[sigil] enabled but endpoint is missing -- disabling"); + return undefined; + } + if (!obj.auth || typeof obj.auth !== "object") { + console.warn("[sigil] enabled but auth config is missing -- disabling"); + return undefined; + } + return raw as SigilConfig; +} diff --git a/plugins/opencode/src/hooks.ts b/plugins/opencode/src/hooks.ts new file mode 100644 index 0000000..9aecfc9 --- /dev/null +++ b/plugins/opencode/src/hooks.ts @@ -0,0 +1,189 @@ +import type { SigilClient } from "@grafana/sigil-sdk-js"; +import type { AssistantMessage, UserMessage, Part } from "@opencode-ai/sdk"; +import type { PluginInput } from "@opencode-ai/plugin"; +import type { SigilConfig } from "./config.js"; +import { createSigilClient } from "./client.js"; +import { Redactor } from "./redact.js"; +import { mapGeneration, mapError, mapToolDefinitions } from "./mappers.js"; + +type OpencodeClient = PluginInput["client"]; + +// Track recorded messages per session for dedup and cleanup +const recordedMessages = new Map>(); + +// Pending generation store: user-side data captured before assistant responds +type PendingGeneration = { + systemPrompt: string | undefined; + userParts: Part[]; + tools: Record | undefined; +}; +const pendingGenerations = new Map(); + +function buildAgentName(prefix: string | undefined, mode: string | undefined): string { + const base = prefix || "opencode"; + return mode ? `${base}:${mode}` : base; +} + +/** + * Called from the chat.message hook. Stores user-side data for later use + * when the assistant message completes. + */ +function handleChatMessage( + input: { sessionID: string }, + output: { message: UserMessage; parts: Part[] }, +): void { + pendingGenerations.set(input.sessionID, { + systemPrompt: output.message.system, + userParts: output.parts, + tools: output.message.tools, + }); +} + +async function handleEvent( + sigil: SigilClient, + config: SigilConfig, + client: OpencodeClient, + redactor: Redactor, + event: { type: string; properties: unknown }, +): Promise { + if (event.type !== "message.updated") return; + + const properties = event.properties as { info?: { role?: string } } | undefined; + const msg = properties?.info; + if (!msg || msg.role !== "assistant") return; + + const assistantMsg = msg as AssistantMessage; + + // Only record terminal messages + const isTerminal = assistantMsg.finish || assistantMsg.error || assistantMsg.time.completed; + if (!isTerminal) return; + + // Dedup + const sessionSet = recordedMessages.get(assistantMsg.sessionID) ?? new Set(); + if (sessionSet.has(assistantMsg.id)) return; + sessionSet.add(assistantMsg.id); + recordedMessages.set(assistantMsg.sessionID, sessionSet); + + // Look up pending generation (user-side data) + const pending = pendingGenerations.get(assistantMsg.sessionID); + + // Fetch assistant parts via REST + let assistantParts: Part[] = []; + try { + const response = await client.session.message({ + path: { id: assistantMsg.sessionID, messageID: assistantMsg.id }, + }); + assistantParts = response.data?.parts ?? []; + } catch { + // REST fetch failed — fall back to metadata-only + } + + const contentCapture = config.contentCapture ?? true; + + const seed = { + conversationId: assistantMsg.sessionID, + agentName: buildAgentName(config.agentName, assistantMsg.mode), + agentVersion: config.agentVersion, + model: { provider: assistantMsg.providerID, name: assistantMsg.modelID }, + startedAt: new Date(assistantMsg.time.created), + ...(contentCapture && { + systemPrompt: pending?.systemPrompt, + tools: mapToolDefinitions(pending?.tools), + }), + }; + + // When contentCapture is enabled, map full content with redaction; + // otherwise fall back to metadata-only result (no message content). + const result = contentCapture + ? mapGeneration(assistantMsg, pending?.userParts ?? [], assistantParts, redactor) + : mapGeneration(assistantMsg, [], [], redactor); + + try { + if (assistantMsg.error) { + await sigil.startGeneration(seed, async (recorder) => { + recorder.setResult(result); + recorder.setCallError(mapError(assistantMsg.error!)); + }); + } else { + await sigil.startGeneration(seed, async (recorder) => { + recorder.setResult(result); + }); + } + } catch { + // Sigil recording failure should never break the plugin + } + + // Clean up pending generation + pendingGenerations.delete(assistantMsg.sessionID); +} + +async function handleLifecycle( + sigil: SigilClient, + event: { type: string; properties: unknown }, +): Promise { + const type = event.type as string; + + if (type === "session.idle") { + try { + await sigil.flush(); + } catch { + // flush failure is non-fatal + } + } + + if (type === "session.deleted") { + const properties = event.properties as { info?: { id?: string } } | undefined; + const sessionId = properties?.info?.id; + if (sessionId) { + recordedMessages.delete(sessionId); + pendingGenerations.delete(sessionId); + } + } + + if (type === "global.disposed") { + try { + await sigil.shutdown(); + } catch { + // shutdown failure is non-fatal + } + } +} + +export type SigilHooks = { + event: (input: { event: { type: string; properties: unknown } }) => Promise; + chatMessage: ( + input: { sessionID: string }, + output: { message: UserMessage; parts: Part[] }, + ) => void; +}; + +export async function createSigilHooks( + config: SigilConfig, + client: OpencodeClient, +): Promise { + if (!config.enabled) return null; + + if (!config.endpoint) { + console.warn("[sigil] endpoint is required when enabled -- skipping Sigil initialization"); + return null; + } + + const sigil = createSigilClient(config); + if (!sigil) return null; + + const redactor = new Redactor(); + + process.on("beforeExit", () => { + sigil.shutdown().catch(() => {}); + }); + + return { + event: async (input) => { + await handleEvent(sigil, config, client, redactor, input.event); + await handleLifecycle(sigil, input.event); + }, + chatMessage: (input, output) => { + handleChatMessage(input, output); + }, + }; +} diff --git a/plugins/opencode/src/index.ts b/plugins/opencode/src/index.ts new file mode 100644 index 0000000..88e897b --- /dev/null +++ b/plugins/opencode/src/index.ts @@ -0,0 +1,22 @@ +import type { Plugin } from "@opencode-ai/plugin"; +import { loadSigilConfig } from "./config.js"; +import { createSigilHooks } from "./hooks.js"; + +export const SigilPlugin: Plugin = async ({ client }) => { + const config = await loadSigilConfig(); + if (!config.enabled) return {}; + + const hooks = await createSigilHooks(config, client); + if (!hooks) return {}; + + return { + "chat.message": async (input, output) => { + hooks.chatMessage(input, output); + }, + event: async ({ event }) => { + await hooks.event({ + event: event as { type: string; properties: unknown }, + }); + }, + }; +}; diff --git a/plugins/opencode/src/mappers.test.ts b/plugins/opencode/src/mappers.test.ts new file mode 100644 index 0000000..7ffb0e7 --- /dev/null +++ b/plugins/opencode/src/mappers.test.ts @@ -0,0 +1,194 @@ +import { describe, it, expect } from "vitest"; +import { mapGeneration, mapInputMessages, mapOutputMessages, mapToolDefinitions } from "./mappers.js"; +import { Redactor } from "./redact.js"; +import type { AssistantMessage, Part } from "@opencode-ai/sdk"; + +const redactor = new Redactor(); + +function makeAssistantMsg(overrides?: Partial): AssistantMessage { + return { + id: "msg-1", + sessionID: "sess-1", + role: "assistant", + parentID: "parent-1", + modelID: "claude-opus-4-20250514", + providerID: "anthropic", + mode: "code", + path: { cwd: "/tmp", root: "/tmp" }, + cost: 0.01, + tokens: { input: 100, output: 50, reasoning: 10, cache: { read: 5, write: 3 } }, + time: { created: Date.now(), completed: Date.now() + 1000 }, + finish: "end_turn", + ...overrides, + } as AssistantMessage; +} + +describe("mapInputMessages", () => { + it("maps TextParts to Sigil user messages", () => { + const parts = [ + { id: "p1", sessionID: "s1", messageID: "m1", type: "text" as const, text: "hello world" }, + ] as Part[]; + const result = mapInputMessages(parts); + expect(result).toHaveLength(1); + expect(result[0].role).toBe("user"); + expect(result[0].parts?.[0]).toEqual({ type: "text", text: "hello world" }); + }); + + it("skips non-text parts", () => { + const parts = [ + { id: "p1", sessionID: "s1", messageID: "m1", type: "file" as const, mime: "image/png", url: "..." }, + ] as Part[]; + expect(mapInputMessages(parts)).toHaveLength(0); + }); + + it("skips text parts with empty or whitespace-only text", () => { + const parts = [ + { id: "p1", sessionID: "s1", messageID: "m1", type: "text" as const, text: "" }, + { id: "p2", sessionID: "s1", messageID: "m1", type: "text" as const, text: " " }, + { id: "p3", sessionID: "s1", messageID: "m1", type: "text" as const, text: "\n\t" }, + { id: "p4", sessionID: "s1", messageID: "m1", type: "text" as const, text: "hello" }, + ] as Part[]; + const result = mapInputMessages(parts); + expect(result).toHaveLength(1); + expect(result[0].parts?.[0]).toEqual({ type: "text", text: "hello" }); + }); +}); + +describe("mapOutputMessages", () => { + it("maps TextParts with lightweight redaction", () => { + const parts = [ + { id: "p1", sessionID: "s1", messageID: "m1", type: "text" as const, text: "The result is 42" }, + ] as Part[]; + const result = mapOutputMessages(parts, redactor); + expect(result).toHaveLength(1); + expect(result[0].role).toBe("assistant"); + expect(result[0].parts?.[0]).toEqual({ type: "text", text: "The result is 42" }); + }); + + it("redacts secrets in tool output but not in assistant text (lightweight)", () => { + const secretToken = "glc_abcdefghijklmnopqrstuvwxyz1234"; + const textParts = [ + { id: "p1", sessionID: "s1", messageID: "m1", type: "text" as const, text: `Found token: ${secretToken}` }, + ] as Part[]; + const result = mapOutputMessages(textParts, redactor); + // Tier 1 patterns fire even in lightweight mode + expect(result[0].parts?.[0]).toHaveProperty("type", "text"); + const textContent = (result[0].parts?.[0] as any).text; + expect(textContent).not.toContain(secretToken); + expect(textContent).toContain("[REDACTED:"); + }); + + it("maps completed ToolParts to tool_call + tool_result with full redaction", () => { + const parts = [ + { + id: "p1", sessionID: "s1", messageID: "m1", type: "tool" as const, + callID: "call-1", tool: "bash", + state: { + status: "completed" as const, + input: { command: "echo test" }, + output: "test output", + title: "Run bash", + metadata: {}, + time: { start: 1000, end: 2000 }, + }, + }, + ] as Part[]; + const result = mapOutputMessages(parts, redactor); + expect(result).toHaveLength(2); + expect(result[0].role).toBe("assistant"); + expect(result[0].parts?.[0].type).toBe("tool_call"); + expect(result[1].role).toBe("tool"); + expect(result[1].parts?.[0].type).toBe("tool_result"); + }); + + it("skips text parts with empty or whitespace-only text", () => { + const parts = [ + { id: "p1", sessionID: "s1", messageID: "m1", type: "text" as const, text: "" }, + { id: "p2", sessionID: "s1", messageID: "m1", type: "text" as const, text: " " }, + { id: "p3", sessionID: "s1", messageID: "m1", type: "text" as const, text: "actual content" }, + ] as Part[]; + const result = mapOutputMessages(parts, redactor); + expect(result).toHaveLength(1); + expect(result[0].parts?.[0]).toEqual({ type: "text", text: "actual content" }); + }); + + it("skips reasoning parts with empty or whitespace-only text", () => { + const parts = [ + { id: "p1", sessionID: "s1", messageID: "m1", type: "reasoning" as const, text: "", time: { start: 1000 } }, + { id: "p2", sessionID: "s1", messageID: "m1", type: "reasoning" as const, text: " ", time: { start: 1000 } }, + { id: "p3", sessionID: "s1", messageID: "m1", type: "reasoning" as const, text: "thinking about it", time: { start: 1000 } }, + ] as Part[]; + const result = mapOutputMessages(parts, redactor); + expect(result).toHaveLength(1); + expect(result[0].parts?.[0]).toEqual({ type: "thinking", thinking: "thinking about it" }); + }); + + it("maps error ToolParts to tool_call + tool_result with is_error flag", () => { + const parts = [ + { + id: "p1", sessionID: "s1", messageID: "m1", type: "tool" as const, + callID: "call-1", tool: "bash", + state: { + status: "error" as const, + input: { command: "fail" }, + error: "command failed", + metadata: {}, + time: { start: 1000, end: 2000 }, + }, + }, + ] as Part[]; + const result = mapOutputMessages(parts, redactor); + expect(result).toHaveLength(2); + expect(result[0].role).toBe("assistant"); + expect(result[0].parts?.[0].type).toBe("tool_call"); + const toolCall = (result[0].parts?.[0] as any).toolCall; + expect(toolCall.id).toBe("call-1"); + expect(toolCall.name).toBe("bash"); + expect(result[1].role).toBe("tool"); + expect(result[1].parts?.[0].type).toBe("tool_result"); + const toolResult = (result[1].parts?.[0] as any).toolResult; + expect(toolResult.toolCallId).toBe("call-1"); + expect(toolResult.isError).toBe(true); + expect(toolResult.content).toBe("command failed"); + }); +}); + +describe("mapToolDefinitions", () => { + it("maps enabled tools to ToolDefinition array, excludes disabled", () => { + const tools = { bash: true, read: true, write: false }; + const result = mapToolDefinitions(tools); + expect(result).toHaveLength(2); + expect(result.map((t) => t.name)).toContain("bash"); + expect(result.map((t) => t.name)).toContain("read"); + expect(result.map((t) => t.name)).not.toContain("write"); + }); + + it("returns empty array for undefined", () => { + expect(mapToolDefinitions(undefined)).toEqual([]); + }); +}); + +describe("mapGeneration", () => { + it("maps usage tokens and cost from assistant message", () => { + const msg = makeAssistantMsg(); + const userParts = [ + { id: "p1", sessionID: "s1", messageID: "m1", type: "text" as const, text: "hello" }, + ] as Part[]; + const assistantParts = [ + { id: "p2", sessionID: "s1", messageID: "m2", type: "text" as const, text: "hi there" }, + ] as Part[]; + const result = mapGeneration(msg, userParts, assistantParts, redactor); + expect(result.input).toHaveLength(1); + expect(result.output).toHaveLength(1); + expect(result.usage?.inputTokens).toBe(100); + expect(result.metadata?.cost).toBe(0.01); + }); + + it("maps response model, stop reason, and completion timestamp from assistant message", () => { + const msg = makeAssistantMsg(); + const result = mapGeneration(msg, [], [], redactor); + expect(result.responseModel).toBe("claude-opus-4-20250514"); + expect(result.stopReason).toBe("end_turn"); + expect(result.completedAt).toBeInstanceOf(Date); + }); +}); diff --git a/plugins/opencode/src/mappers.ts b/plugins/opencode/src/mappers.ts new file mode 100644 index 0000000..3177655 --- /dev/null +++ b/plugins/opencode/src/mappers.ts @@ -0,0 +1,167 @@ +import type { AssistantMessage, Part } from "@opencode-ai/sdk"; +import type { + GenerationResult, + Message, + ToolDefinition, +} from "@grafana/sigil-sdk-js"; +import type { Redactor } from "./redact.js"; + +export type { GenerationResult }; + +/** + * Map user-side parts to Sigil input messages. No redaction applied — user text is the + * user's own data and Sigil needs it verbatim for prompt analysis. Tier 1 patterns in + * user text (e.g., pasted connection strings) are a known accepted gap; apply redaction + * here if this becomes a problem. + */ +export function mapInputMessages(parts: Part[]): Message[] { + const messages: Message[] = []; + for (const part of parts) { + if (part.type === "text" && part.text.trim().length > 0) { + messages.push({ + role: "user", + parts: [{ type: "text", text: part.text }], + }); + } + } + return messages; +} + +/** Map assistant-side parts to Sigil output messages with redaction. */ +export function mapOutputMessages(parts: Part[], redactor: Redactor): Message[] { + const messages: Message[] = []; + for (const part of parts) { + switch (part.type) { + case "text": { + const text = redactor.redactLightweight(part.text); + if (text.trim().length > 0) { + messages.push({ + role: "assistant", + parts: [{ type: "text", text }], + }); + } + break; + } + case "reasoning": { + const thinking = redactor.redactLightweight(part.text); + if (thinking.trim().length > 0) { + messages.push({ + role: "assistant", + parts: [{ type: "thinking", thinking }], + }); + } + break; + } + case "tool": { + const { state } = part; + if (state.status === "completed") { + messages.push({ + role: "assistant", + parts: [{ + type: "tool_call", + toolCall: { + id: part.callID, + name: part.tool, + inputJSON: redactor.redact(JSON.stringify(state.input ?? {})), + }, + }], + }); + messages.push({ + role: "tool", + parts: [{ + type: "tool_result", + toolResult: { + toolCallId: part.callID, + name: part.tool, + content: redactor.redact(state.output ?? ""), + }, + }], + }); + } else if (state.status === "error") { + messages.push({ + role: "assistant", + parts: [{ + type: "tool_call", + toolCall: { + id: part.callID, + name: part.tool, + inputJSON: redactor.redact(JSON.stringify(state.input ?? {})), + }, + }], + }); + messages.push({ + role: "tool", + parts: [{ + type: "tool_result", + toolResult: { + toolCallId: part.callID, + name: part.tool, + content: redactor.redact(state.error ?? "unknown error"), + isError: true, + }, + }], + }); + } + break; + } + } + } + return messages; +} + +/** Convert opencode tool name map to Sigil ToolDefinition array. Only includes enabled tools. */ +export function mapToolDefinitions( + tools: Record | undefined, +): ToolDefinition[] { + if (!tools) return []; + return Object.entries(tools) + .filter(([, enabled]) => enabled) + .map(([name]) => ({ name })); +} + +/** Map an AssistantMessage + parts to a Sigil GenerationResult with content. */ +export function mapGeneration( + msg: AssistantMessage, + userParts: Part[], + assistantParts: Part[], + redactor: Redactor, +): GenerationResult { + return { + input: mapInputMessages(userParts), + output: mapOutputMessages(assistantParts, redactor), + usage: { + inputTokens: msg.tokens.input, + outputTokens: msg.tokens.output, + reasoningTokens: msg.tokens.reasoning, + cacheReadInputTokens: msg.tokens.cache.read, + cacheCreationInputTokens: msg.tokens.cache.write, + }, + responseModel: msg.modelID, + stopReason: msg.finish, + completedAt: msg.time.completed ? new Date(msg.time.completed) : undefined, + metadata: { + cost: msg.cost, + }, + }; +} + +export function mapError( + error: NonNullable, +): Error { + switch (error.name) { + case "ProviderAuthError": + return new Error("provider_auth"); + case "APIError": + return new Error(`api_error: ${error.data.statusCode ?? "unknown"}`); + case "MessageOutputLengthError": + return new Error("output_length_exceeded"); + case "MessageAbortedError": + return new Error("aborted"); + case "UnknownError": + return new Error("unknown_error"); + default: { + const _exhaustive: never = error; + return new Error("unknown_error"); + } + } +} diff --git a/plugins/opencode/src/redact.test.ts b/plugins/opencode/src/redact.test.ts new file mode 100644 index 0000000..5aea6ee --- /dev/null +++ b/plugins/opencode/src/redact.test.ts @@ -0,0 +1,128 @@ +import { describe, it, expect } from "vitest"; +import { Redactor } from "./redact.js"; + +describe("Redactor", () => { + const redactor = new Redactor(); + + describe("redact (full — tier 1 + tier 2)", () => { + it("redacts Grafana Cloud tokens", () => { + const input = "token: glc_abcdefghijklmnopqrstuvwxyz1234"; + const result = redactor.redact(input); + expect(result).not.toContain("glc_abcdefghijklmnopqrstuvwxyz1234"); + expect(result).toContain("[REDACTED:"); + }); + + it("redacts Grafana service account tokens", () => { + const input = "glsa_abcdefghijklmnopqrstuvwxyz1234"; + const result = redactor.redact(input); + expect(result).not.toContain("glsa_"); + expect(result).toContain("[REDACTED:"); + }); + + it("redacts AWS access keys", () => { + const input = "aws_access_key_id = AKIAIOSFODNN7REALKEY"; + const result = redactor.redact(input); + expect(result).not.toContain("AKIAIOSFODNN7REALKEY"); + expect(result).toContain("[REDACTED:"); + }); + + it("redacts GitHub personal access tokens", () => { + const input = "ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghij"; + const result = redactor.redact(input); + expect(result).not.toContain("ghp_"); + expect(result).toContain("[REDACTED:"); + }); + + it("redacts PEM private keys", () => { + const input = `-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA0Z3VS5JJcds3xfn/ygWyF8PbnGy5AhEiS0C5 +-----END RSA PRIVATE KEY-----`; + const result = redactor.redact(input); + expect(result).not.toContain("MIIEpAIBAAKCAQ"); + expect(result).toContain("[REDACTED:"); + }); + + it("redacts connection strings with passwords", () => { + const input = "postgres://admin:s3cretP4ss@db.example.com:5432/mydb"; + const result = redactor.redact(input); + expect(result).not.toContain("s3cretP4ss"); + expect(result).toContain("[REDACTED:"); + }); + + it("redacts Anthropic API keys", () => { + const input = "sk-ant-api03-" + "a".repeat(93) + "AA"; + const result = redactor.redact(input); + expect(result).not.toContain("sk-ant-api03-"); + expect(result).toContain("[REDACTED:"); + }); + + it("redacts modern OpenAI project keys (sk-proj-)", () => { + const input = "sk-proj-" + "a".repeat(50); + const result = redactor.redact(input); + expect(result).not.toContain("sk-proj-"); + expect(result).toContain("[REDACTED:"); + }); + + it("redacts OpenAI service account keys (sk-svcacct-)", () => { + const input = "sk-svcacct-" + "b".repeat(50); + const result = redactor.redact(input); + expect(result).not.toContain("sk-svcacct-"); + expect(result).toContain("[REDACTED:"); + }); + + it("redacts env file secret values (tier 2)", () => { + const input = "DATABASE_PASSWORD=hunter2secret123"; + const result = redactor.redact(input); + expect(result).toContain("DATABASE_PASSWORD="); + expect(result).not.toContain("hunter2secret123"); + expect(result).toContain("[REDACTED:"); + }); + + it("redacts bearer tokens in headers", () => { + const input = 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U'; + const result = redactor.redact(input); + expect(result).toContain("[REDACTED:"); + }); + + it("does NOT redact normal text", () => { + const input = "The function returns a list of users from the database."; + expect(redactor.redact(input)).toBe(input); + }); + + it("does NOT redact UUIDs", () => { + const input = "session-id: 550e8400-e29b-41d4-a716-446655440000"; + expect(redactor.redact(input)).toBe(input); + }); + + it("handles empty string", () => { + expect(redactor.redact("")).toBe(""); + }); + + it("handles multiple secrets in one string", () => { + const input = "key=AKIAIOSFODNN7REALKEY token=glc_abcdefghijklmnopqrstuvwxyz1234"; + const result = redactor.redact(input); + expect(result).not.toContain("AKIAIOSFODNN7REALKEY"); + expect(result).not.toContain("glc_abcdefghijklmnopqrstuvwxyz1234"); + }); + }); + + describe("redactLightweight (tier 1 only)", () => { + it("redacts Grafana Cloud tokens", () => { + const input = "I found the token: glc_abcdefghijklmnopqrstuvwxyz1234"; + const result = redactor.redactLightweight(input); + expect(result).not.toContain("glc_abcdefghijklmnopqrstuvwxyz1234"); + expect(result).toContain("[REDACTED:"); + }); + + it("does NOT redact env file patterns (tier 2 only)", () => { + const input = "The file contains DATABASE_PASSWORD=hunter2secret123"; + const result = redactor.redactLightweight(input); + expect(result).toContain("hunter2secret123"); + }); + + it("does NOT redact normal text", () => { + const input = "The API key configuration is stored in the settings panel."; + expect(redactor.redactLightweight(input)).toBe(input); + }); + }); +}); diff --git a/plugins/opencode/src/redact.ts b/plugins/opencode/src/redact.ts new file mode 100644 index 0000000..76b8d94 --- /dev/null +++ b/plugins/opencode/src/redact.ts @@ -0,0 +1,102 @@ +/** + * Secret redaction engine for Sigil content capture. + * + * ~20 high-confidence patterns hand-curated from Gitleaks + * (https://github.com/gitleaks/gitleaks). Two tiers: + * - Tier 1: definite secret formats — used by both redact() and redactLightweight() + * - Tier 2: heuristic env patterns — used only by redact() + * + * Add more patterns when concrete unredacted secrets are observed. + */ + +interface SecretPattern { + id: string; + regex: RegExp; + tier: 1 | 2; +} + +// --- Tier 1: High-confidence patterns (definite secret formats) --- +const TIER1_PATTERNS: SecretPattern[] = [ + // Grafana + { id: "grafana-cloud-token", regex: /\bglc_[A-Za-z0-9_-]{20,}/g, tier: 1 }, + { id: "grafana-service-account-token", regex: /\bglsa_[A-Za-z0-9_-]{20,}/g, tier: 1 }, + // AWS + { id: "aws-access-token", regex: /\b(?:A3T[A-Z0-9]|AKIA|ASIA|ABIA|ACCA)[A-Z2-7]{16}\b/g, tier: 1 }, + // GitHub + { id: "github-pat", regex: /\bghp_[A-Za-z0-9_]{36,}/g, tier: 1 }, + { id: "github-oauth", regex: /\bgho_[A-Za-z0-9_]{36,}/g, tier: 1 }, + { id: "github-app-token", regex: /\bghs_[A-Za-z0-9_]{36,}/g, tier: 1 }, + { id: "github-fine-grained-pat", regex: /\bgithub_pat_[A-Za-z0-9_]{82}/g, tier: 1 }, + // Anthropic + { id: "anthropic-api-key", regex: /\bsk-ant-api03-[a-zA-Z0-9_-]{93}AA/g, tier: 1 }, + { id: "anthropic-admin-key", regex: /\bsk-ant-admin01-[a-zA-Z0-9_-]{93}AA/g, tier: 1 }, + // OpenAI (legacy format + modern sk-proj-/sk-svcacct- formats) + { id: "openai-api-key", regex: /\bsk-[a-zA-Z0-9]{20}T3BlbkFJ[a-zA-Z0-9]{20}/g, tier: 1 }, + { id: "openai-project-key", regex: /\bsk-proj-[a-zA-Z0-9_-]{40,}/g, tier: 1 }, + { id: "openai-svcacct-key", regex: /\bsk-svcacct-[a-zA-Z0-9_-]{40,}/g, tier: 1 }, + // GCP + { id: "gcp-api-key", regex: /\bAIza[A-Za-z0-9_-]{35}/g, tier: 1 }, + // PEM private keys + { id: "private-key", regex: /-----BEGIN[A-Z ]*PRIVATE KEY-----[\s\S]*?-----END[A-Z ]*PRIVATE KEY-----/g, tier: 1 }, + // Connection strings with embedded credentials + { id: "connection-string", regex: /(?:postgres|mysql|mongodb|redis|amqp):\/\/[^\s'"]+@[^\s'"]+/g, tier: 1 }, + // Bearer tokens in Authorization headers + { id: "bearer-token", regex: /[Bb]earer\s+[A-Za-z0-9_.\-~+/]{20,}={0,3}/g, tier: 1 }, + // Slack tokens + { id: "slack-token", regex: /\bxox[bporas]-[A-Za-z0-9-]{10,}/g, tier: 1 }, + // Stripe keys + { id: "stripe-key", regex: /\b[sr]k_(?:live|test)_[A-Za-z0-9]{20,}/g, tier: 1 }, + // SendGrid + { id: "sendgrid-api-key", regex: /\bSG\.[A-Za-z0-9_-]{22}\.[A-Za-z0-9_-]{43}/g, tier: 1 }, + // Twilio + { id: "twilio-api-key", regex: /\bSK[a-f0-9]{32}/g, tier: 1 }, + // npm tokens + { id: "npm-token", regex: /\bnpm_[A-Za-z0-9]{36}/g, tier: 1 }, + // PyPI tokens + { id: "pypi-token", regex: /\bpypi-[A-Za-z0-9_-]{50,}/g, tier: 1 }, +]; + +// --- Tier 2: Heuristic patterns (env file values) --- +const TIER2_PATTERNS: SecretPattern[] = [ + { + id: "env-secret-value", + regex: /(?<=(?:PASSWORD|SECRET|TOKEN|KEY|CREDENTIAL|API_KEY|PRIVATE_KEY|ACCESS_KEY)\s*[=:]\s*)\S+/gi, + tier: 2, + }, +]; + +/** + * Note: Pattern arrays are shared by reference across Redactor instances. + * This is safe because: (1) there's a single Redactor instance in production, + * (2) JS is single-threaded so .replace() completes synchronously, and + * (3) lastIndex is reset before each replace call. If this class is ever used + * in workers or multiple instances, clone regexes in the constructor. + */ +export class Redactor { + private tier1 = TIER1_PATTERNS; + private tier2 = TIER2_PATTERNS; + + /** Full redaction: tier 1 + tier 2. Use for tool call args and tool results. */ + redact(text: string): string { + let result = text; + for (const pattern of this.tier1) { + pattern.regex.lastIndex = 0; + result = result.replace(pattern.regex, `[REDACTED:${pattern.id}]`); + } + for (const pattern of this.tier2) { + pattern.regex.lastIndex = 0; + result = result.replace(pattern.regex, `[REDACTED:${pattern.id}]`); + } + return result; + } + + /** Lightweight redaction: tier 1 only. Use for assistant text and reasoning. */ + redactLightweight(text: string): string { + let result = text; + for (const pattern of this.tier1) { + pattern.regex.lastIndex = 0; + result = result.replace(pattern.regex, `[REDACTED:${pattern.id}]`); + } + return result; + } +} diff --git a/plugins/opencode/tsconfig.json b/plugins/opencode/tsconfig.json new file mode 100644 index 0000000..ce62ced --- /dev/null +++ b/plugins/opencode/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "bundler", + "lib": ["ES2022"], + "outDir": "dist", + "rootDir": "src", + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "isolatedModules": true + }, + "include": ["src"], + "exclude": ["node_modules", "dist"] +} diff --git a/plugins/opencode/vitest.config.ts b/plugins/opencode/vitest.config.ts new file mode 100644 index 0000000..ae847ff --- /dev/null +++ b/plugins/opencode/vitest.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + include: ["src/**/*.test.ts"], + }, +}); From fe64e5888124766189d76d4a55ccc533fc380cfe Mon Sep 17 00:00:00 2001 From: "renovate-sh-app[bot]" <219655108+renovate-sh-app[bot]@users.noreply.github.com> Date: Sat, 14 Mar 2026 10:16:35 +0100 Subject: [PATCH 072/133] chore(deps): update dotnet monorepo to 10.0.4 (#541) | datasource | package | from | to | | ---------- | ------------------------- | ------ | ------ | | nuget | System.Text.Json | 10.0.3 | 10.0.4 | | nuget | System.Threading.Channels | 10.0.3 | 10.0.4 | Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> --- dotnet/src/Grafana.Sigil/Grafana.Sigil.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dotnet/src/Grafana.Sigil/Grafana.Sigil.csproj b/dotnet/src/Grafana.Sigil/Grafana.Sigil.csproj index 0fe8af8..4e749c4 100644 --- a/dotnet/src/Grafana.Sigil/Grafana.Sigil.csproj +++ b/dotnet/src/Grafana.Sigil/Grafana.Sigil.csproj @@ -15,8 +15,8 @@ - - + +
From 2779763d685959a1ebd0ba4c84cf1cf3a1ca01bc Mon Sep 17 00:00:00 2001 From: "renovate-sh-app[bot]" <219655108+renovate-sh-app[bot]@users.noreply.github.com> Date: Sat, 14 Mar 2026 10:18:20 +0100 Subject: [PATCH 073/133] chore(deps): update dependency @types/node to v24 (#544) | datasource | package | from | to | | ---------- | ----------- | -------- | ------- | | npm | @types/node | 22.19.15 | 24.12.0 | Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: Cyril Tovena --- plugins/opencode/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/opencode/package.json b/plugins/opencode/package.json index 921525a..02b0dd7 100644 --- a/plugins/opencode/package.json +++ b/plugins/opencode/package.json @@ -26,7 +26,7 @@ "@grafana/sigil-sdk-js": "workspace:*", "@opencode-ai/plugin": "^1.2.16", "@opencode-ai/sdk": "^1.2.16", - "@types/node": "^22.13.9", + "@types/node": "^24.0.0", "esbuild": "^0.25.0", "typescript": "^5.8.2", "vitest": "^4.0.18" From b3880f076070c343ad16995f260e502faf2c2f32 Mon Sep 17 00:00:00 2001 From: "renovate-sh-app[bot]" <219655108+renovate-sh-app[bot]@users.noreply.github.com> Date: Sat, 14 Mar 2026 10:19:06 +0100 Subject: [PATCH 074/133] chore(deps): update dependency esbuild to ^0.27.0 (#542) | datasource | package | from | to | | ---------- | ------- | ------- | ------ | | npm | esbuild | 0.25.12 | 0.27.3 | Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: Cyril Tovena From 98943269d9ea0d452fbba78d2e61933df5ce920e Mon Sep 17 00:00:00 2001 From: Jack Gordley Date: Sun, 15 Mar 2026 09:48:36 -0400 Subject: [PATCH 075/133] fix(sdks): fall back to Anthropic-style keys in LangChain framework handler (#546) --- .../langchain/tests/test_langchain_handler.py | 50 +++++++++++++++++++ python/sigil_sdk/framework_handler.py | 8 +-- 2 files changed, 55 insertions(+), 3 deletions(-) diff --git a/python-frameworks/langchain/tests/test_langchain_handler.py b/python-frameworks/langchain/tests/test_langchain_handler.py index 58570ea..74dabce 100644 --- a/python-frameworks/langchain/tests/test_langchain_handler.py +++ b/python-frameworks/langchain/tests/test_langchain_handler.py @@ -115,6 +115,56 @@ def test_langchain_sync_lifecycle_sets_framework_tags_and_metadata() -> None: client.shutdown() +def test_langchain_sync_lifecycle_extracts_anthropic_style_usage_and_stop_reason() -> None: + """ChatAnthropic puts token usage under 'usage' (not 'token_usage') and + stop reason under 'stop_reason' (not 'finish_reason').""" + exporter = _CapturingExporter() + client = _new_client(exporter) + + try: + run_id = uuid4() + handler = SigilLangChainHandler( + client=client, + agent_name="agent-langchain", + agent_version="v1", + provider_resolver="auto", + ) + + handler.on_chat_model_start( + {"name": "ChatAnthropic"}, + [[{"type": "human", "content": "hello"}]], + run_id=run_id, + invocation_params={"model": "claude-haiku-4-5-20251001"}, + ) + handler.on_llm_end( + { + "generations": [[{"text": "world"}]], + "llm_output": { + "id": "msg_01ABC", + "model": "claude-haiku-4-5-20251001", + "model_name": "claude-haiku-4-5-20251001", + "stop_reason": "end_turn", + "usage": { + "input_tokens": 42, + "output_tokens": 17, + }, + }, + }, + run_id=run_id, + ) + + client.flush() + generation = exporter.requests[0].generations[0] + assert generation.model.provider == "anthropic" + assert generation.model.name == "claude-haiku-4-5-20251001" + assert generation.usage.input_tokens == 42 + assert generation.usage.output_tokens == 17 + assert generation.usage.total_tokens == 59 + assert generation.stop_reason == "end_turn" + finally: + client.shutdown() + + def test_langchain_stream_lifecycle_uses_stream_mode_and_chunk_fallback() -> None: exporter = _CapturingExporter() client = _new_client(exporter) diff --git a/python/sigil_sdk/framework_handler.py b/python/sigil_sdk/framework_handler.py index 5270b5a..c3d935e 100644 --- a/python/sigil_sdk/framework_handler.py +++ b/python/sigil_sdk/framework_handler.py @@ -304,9 +304,11 @@ def _on_llm_end(self, *, response: Any, run_id: UUID) -> None: return try: - usage = _map_usage(_read(_read(response, "llm_output"), "token_usage")) - response_model = _as_str(_read(_read(response, "llm_output"), "model_name")) - stop_reason = _as_str(_read(_read(response, "llm_output"), "finish_reason")) + llm_output = _read(response, "llm_output") + raw_usage = _read(llm_output, "token_usage") or _read(llm_output, "usage") + usage = _map_usage(raw_usage) + response_model = _as_str(_read(llm_output, "model_name")) + stop_reason = _as_str(_read(llm_output, "finish_reason") or _read(llm_output, "stop_reason")) output_messages: list[Message] = [] if run_state.capture_outputs: From 5eb6a97efbad040d7ca358b121998d573829c5fb Mon Sep 17 00:00:00 2001 From: "renovate-sh-app[bot]" <219655108+renovate-sh-app[bot]@users.noreply.github.com> Date: Mon, 16 Mar 2026 10:18:26 +0100 Subject: [PATCH 076/133] chore(deps): update dotnet monorepo to 10.0.5 (#564) | datasource | package | from | to | | ---------- | ------------------------- | ------ | ------ | | nuget | System.Text.Json | 10.0.4 | 10.0.5 | | nuget | System.Threading.Channels | 10.0.4 | 10.0.5 | Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> --- dotnet/src/Grafana.Sigil/Grafana.Sigil.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dotnet/src/Grafana.Sigil/Grafana.Sigil.csproj b/dotnet/src/Grafana.Sigil/Grafana.Sigil.csproj index 4e749c4..3b9287f 100644 --- a/dotnet/src/Grafana.Sigil/Grafana.Sigil.csproj +++ b/dotnet/src/Grafana.Sigil/Grafana.Sigil.csproj @@ -15,8 +15,8 @@ - - + + From 99a993c23ae09ec0798aa59d8c43738f3aa435cc Mon Sep 17 00:00:00 2001 From: "renovate-sh-app[bot]" <219655108+renovate-sh-app[bot]@users.noreply.github.com> Date: Mon, 16 Mar 2026 10:18:28 +0100 Subject: [PATCH 077/133] fix(deps): update dependency @openai/agents to ^0.7.0 (#560) | datasource | package | from | to | | ---------- | -------------- | ----- | ----- | | npm | @openai/agents | 0.6.0 | 0.7.0 | Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> --- js/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/package.json b/js/package.json index 0f3ba81..4edb49f 100644 --- a/js/package.json +++ b/js/package.json @@ -60,7 +60,7 @@ "@grpc/proto-loader": "^0.8.0", "@langchain/core": "^1.0.0", "@langchain/langgraph": "^1.2.0", - "@openai/agents": "^0.6.0", + "@openai/agents": "^0.7.0", "@opentelemetry/api": "^1.9.0", "@opentelemetry/exporter-metrics-otlp-grpc": "^0.213.0", "@opentelemetry/exporter-metrics-otlp-http": "^0.213.0", From 68f6b52e19ec5785f76e78287342840b8f42041e Mon Sep 17 00:00:00 2001 From: "renovate-sh-app[bot]" <219655108+renovate-sh-app[bot]@users.noreply.github.com> Date: Mon, 16 Mar 2026 10:18:43 +0100 Subject: [PATCH 078/133] fix(deps): update dependency com.anthropic:anthropic-java to v2.16.1 (#557) | datasource | package | from | to | | ---------- | ---------------------------- | ------ | ------ | | maven | com.anthropic:anthropic-java | 2.16.0 | 2.16.1 | Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> --- java/gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java/gradle/libs.versions.toml b/java/gradle/libs.versions.toml index 29528b1..7659284 100644 --- a/java/gradle/libs.versions.toml +++ b/java/gradle/libs.versions.toml @@ -42,7 +42,7 @@ mockwebserver = { module = "com.squareup.okhttp3:mockwebserver", version.ref = " javax-annotation = { module = "javax.annotation:javax.annotation-api", version.ref = "javaxAnnotation" } openai-java = { module = "com.openai:openai-java", version = "4.26.0" } -anthropic-java = { module = "com.anthropic:anthropic-java", version = "2.16.0" } +anthropic-java = { module = "com.anthropic:anthropic-java", version = "2.16.1" } google-genai = { module = "com.google.genai:google-genai", version = "1.42.0" } [plugins] From 2c9ed7dda76a64b8a043cf62330432ceec2c6910 Mon Sep 17 00:00:00 2001 From: "renovate-sh-app[bot]" <219655108+renovate-sh-app[bot]@users.noreply.github.com> Date: Mon, 16 Mar 2026 10:19:13 +0100 Subject: [PATCH 079/133] chore(deps): update dependency esbuild to ^0.27.0 (#555) | datasource | package | from | to | | ---------- | ------- | ------- | ------ | | npm | esbuild | 0.25.12 | 0.27.3 | Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> --- plugins/opencode/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/opencode/package.json b/plugins/opencode/package.json index 02b0dd7..79c3445 100644 --- a/plugins/opencode/package.json +++ b/plugins/opencode/package.json @@ -27,7 +27,7 @@ "@opencode-ai/plugin": "^1.2.16", "@opencode-ai/sdk": "^1.2.16", "@types/node": "^24.0.0", - "esbuild": "^0.25.0", + "esbuild": "^0.27.0", "typescript": "^5.8.2", "vitest": "^4.0.18" } From 6839018a30b77f9a9c24221bedef10c6413c2bc1 Mon Sep 17 00:00:00 2001 From: "renovate-sh-app[bot]" <219655108+renovate-sh-app[bot]@users.noreply.github.com> Date: Mon, 16 Mar 2026 09:21:28 +0000 Subject: [PATCH 080/133] chore(deps): update dependency @grafana/scenes to v7.1.6 (#563) | datasource | package | from | to | | ---------- | --------------- | ----- | ----- | | npm | @grafana/scenes | 7.1.4 | 7.1.6 | Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: Cyril Tovena From 3ed6b59a9f725cce73de39a90e2d0187cfdeea88 Mon Sep 17 00:00:00 2001 From: Cyril Tovena Date: Mon, 16 Mar 2026 12:27:15 +0100 Subject: [PATCH 081/133] Sdk basic auth standard (#540) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(sdks): add basic auth mode across all SDKs Add dedicated 'basic' auth mode for generation export in Python, JS, Java, and .NET SDKs, matching the existing Go SDK implementation. Basic auth is the standard credential format for Grafana Cloud: Authorization: Basic base64(instance_id:api_key) Each SDK's basic mode: - Requires basic_password (+ basic_user or tenant_id for username) - Injects Authorization: Basic - Optionally injects X-Scope-OrgID when tenant_id is set - Respects explicit header precedence (existing headers win) Updated all SDK READMEs with Grafana Cloud auth section and updated the generation ingest contract reference doc. Co-authored-by: Cyril Tovena * docs(sdks): clarify X-Scope-OrgID injection is for self-hosted multi-tenancy only The basic auth mode optionally injects X-Scope-OrgID when tenant_id is set, but this header is only relevant for self-hosted multi-tenant deployments. Grafana Cloud does not need it — the Basic auth credential carries all required identity. Updated all SDK READMEs and the generation ingest contract to make this distinction explicit. Co-authored-by: Cyril Tovena * fix(sdks): handle non-ASCII basic auth credentials and validate basic auth fields in none mode Bug 1: The JS SDK used btoa() for basic auth base64 encoding, which only handles Latin-1 characters and throws InvalidCharacterError for non-ASCII input. All other SDKs (Go, Python, Java, .NET) explicitly UTF-8 encode credentials before base64 encoding per RFC 7617. Fixed by using TextEncoder to UTF-8 encode the credential string before base64 encoding. Bug 2: The 'none' auth mode validation rejected stray tenantId/bearerToken to catch misconfiguration, but did not check the newly added basicUser and basicPassword fields. A developer who set mode: 'none' with basicPassword populated would silently get unauthenticated requests instead of a fail-fast error. Extended the none-mode check to include basicUser and basicPassword across all five SDKs (JS, Python, Go, Java, .NET). Regression tests added for both bugs in all affected SDKs. Applied via @cursor push command --------- Co-authored-by: Cursor Agent --- dotnet/src/Grafana.Sigil/Config.cs | 44 +++++- dotnet/src/Grafana.Sigil/README.md | 26 ++++ .../Grafana.Sigil.Tests/AuthConfigTests.cs | 88 ++++++++++++ go/README.md | 27 +++- go/sigil/exporter.go | 6 +- go/sigil/exporter_auth_test.go | 2 + java/README.md | 22 +++ .../com/grafana/sigil/sdk/AuthConfig.java | 29 +++- .../com/grafana/sigil/sdk/AuthHeaders.java | 31 +++- .../java/com/grafana/sigil/sdk/AuthMode.java | 3 +- .../sigil/sdk/SigilAuthConfigTest.java | 76 ++++++++++ js/README.md | 33 ++++- js/src/config.ts | 29 +++- js/src/types.ts | 6 +- js/test/client.auth.config.test.mjs | 132 ++++++++++++++++++ python/README.md | 36 ++++- python/sigil_sdk/config.py | 24 +++- python/tests/test_auth_config.py | 56 ++++++++ 18 files changed, 653 insertions(+), 17 deletions(-) diff --git a/dotnet/src/Grafana.Sigil/Config.cs b/dotnet/src/Grafana.Sigil/Config.cs index 073f263..1a021fb 100644 --- a/dotnet/src/Grafana.Sigil/Config.cs +++ b/dotnet/src/Grafana.Sigil/Config.cs @@ -11,7 +11,8 @@ public enum ExportAuthMode { None, Tenant, - Bearer + Bearer, + Basic } public sealed class AuthConfig @@ -19,6 +20,10 @@ public sealed class AuthConfig public ExportAuthMode Mode { get; set; } = ExportAuthMode.None; public string TenantId { get; set; } = string.Empty; public string BearerToken { get; set; } = string.Empty; + /// Username for basic auth. When empty, TenantId is used. + public string BasicUser { get; set; } = string.Empty; + /// Password/token for basic auth. + public string BasicPassword { get; set; } = string.Empty; } public sealed class GenerationExportConfig @@ -152,9 +157,11 @@ string label switch (auth.Mode) { case ExportAuthMode.None: - if (tenantId.Length > 0 || bearerToken.Length > 0) + var noneBasicUser = auth.BasicUser?.Trim() ?? string.Empty; + var noneBasicPassword = auth.BasicPassword?.Trim() ?? string.Empty; + if (tenantId.Length > 0 || bearerToken.Length > 0 || noneBasicUser.Length > 0 || noneBasicPassword.Length > 0) { - throw new ArgumentException($"{label} auth mode 'none' does not allow tenant_id or bearer_token"); + throw new ArgumentException($"{label} auth mode 'none' does not allow credentials"); } return resolved; case ExportAuthMode.Tenant: @@ -190,6 +197,37 @@ string label resolved[AuthorizationHeaderName] = FormatBearerTokenValue(bearerToken); } + return resolved; + case ExportAuthMode.Basic: + var basicPassword = auth.BasicPassword?.Trim() ?? string.Empty; + if (basicPassword.Length == 0) + { + throw new ArgumentException($"{label} auth mode 'basic' requires basic_password"); + } + + var basicUser = auth.BasicUser?.Trim() ?? string.Empty; + if (basicUser.Length == 0) + { + basicUser = tenantId; + } + + if (basicUser.Length == 0) + { + throw new ArgumentException($"{label} auth mode 'basic' requires basic_user or tenant_id"); + } + + if (!resolved.ContainsKey(AuthorizationHeaderName)) + { + var encoded = Convert.ToBase64String( + System.Text.Encoding.UTF8.GetBytes($"{basicUser}:{basicPassword}")); + resolved[AuthorizationHeaderName] = $"Basic {encoded}"; + } + + if (tenantId.Length > 0 && !resolved.ContainsKey(TenantHeaderName)) + { + resolved[TenantHeaderName] = tenantId; + } + return resolved; default: throw new ArgumentException($"unsupported {label} auth mode '{auth.Mode}'"); diff --git a/dotnet/src/Grafana.Sigil/README.md b/dotnet/src/Grafana.Sigil/README.md index 786e341..02d1ce5 100644 --- a/dotnet/src/Grafana.Sigil/README.md +++ b/dotnet/src/Grafana.Sigil/README.md @@ -260,9 +260,35 @@ Per export path, supported auth modes are: - `ExportAuthMode.None` - `ExportAuthMode.Tenant` (`X-Scope-OrgID`) - `ExportAuthMode.Bearer` (`Authorization: Bearer `) +- `ExportAuthMode.Basic` (requires `BasicPassword` + `BasicUser` or `TenantId`, injects `Authorization: Basic `; also injects `X-Scope-OrgID` when `TenantId` is set — for self-hosted multi-tenancy only, not needed for Grafana Cloud) Explicit transport headers take precedence over auth-derived headers (`Authorization`, `X-Scope-OrgID`, case-insensitive). +### Grafana Cloud auth (basic) + +For Grafana Cloud, use `Basic` auth mode. The username is your Grafana Cloud instance/tenant ID and the password is your Grafana Cloud API key: + +```csharp +Auth = new AuthConfig +{ + Mode = ExportAuthMode.Basic, + TenantId = Environment.GetEnvironmentVariable("GRAFANA_CLOUD_INSTANCE_ID") ?? "", + BasicPassword = Environment.GetEnvironmentVariable("GRAFANA_CLOUD_API_KEY") ?? "", +}, +``` + +If your deployment requires a distinct username, set `BasicUser` explicitly: + +```csharp +Auth = new AuthConfig +{ + Mode = ExportAuthMode.Basic, + TenantId = Environment.GetEnvironmentVariable("GRAFANA_CLOUD_INSTANCE_ID") ?? "", + BasicUser = Environment.GetEnvironmentVariable("GRAFANA_CLOUD_INSTANCE_ID") ?? "", + BasicPassword = Environment.GetEnvironmentVariable("GRAFANA_CLOUD_API_KEY") ?? "", +}, +``` + ## Lifecycle and performance guidance - Reuse one `SigilClient` for the process lifetime. diff --git a/dotnet/tests/Grafana.Sigil.Tests/AuthConfigTests.cs b/dotnet/tests/Grafana.Sigil.Tests/AuthConfigTests.cs index 7701fd2..41f6bdf 100644 --- a/dotnet/tests/Grafana.Sigil.Tests/AuthConfigTests.cs +++ b/dotnet/tests/Grafana.Sigil.Tests/AuthConfigTests.cs @@ -1,3 +1,4 @@ +using System.Text; using Xunit; namespace Grafana.Sigil.Tests; @@ -54,6 +55,37 @@ public sealed class AuthConfigTests }, "unsupported generation auth mode" }, + { + new AuthConfig + { + Mode = ExportAuthMode.None, + BasicUser = "user", + }, + "generation auth mode 'none' does not allow credentials" + }, + { + new AuthConfig + { + Mode = ExportAuthMode.None, + BasicPassword = "secret", + }, + "generation auth mode 'none' does not allow credentials" + }, + { + new AuthConfig + { + Mode = ExportAuthMode.Basic, + }, + "generation auth mode 'basic' requires basic_password" + }, + { + new AuthConfig + { + Mode = ExportAuthMode.Basic, + BasicPassword = "secret", + }, + "generation auth mode 'basic' requires basic_user or tenant_id" + }, }; [Theory] @@ -99,4 +131,60 @@ public async Task Constructor_PreservesExplicitGenerationAuthorizationHeader() Assert.Equal("Bearer override-token", config.GenerationExport.Headers["authorization"]); } + [Fact] + public async Task Constructor_AppliesBasicAuthWithTenantId() + { + var config = TestHelpers.TestConfig(new CapturingGenerationExporter()); + config.GenerationExport.Auth = new AuthConfig + { + Mode = ExportAuthMode.Basic, + TenantId = "42", + BasicPassword = "secret", + }; + + await using var client = new SigilClient(config); + + var expected = "Basic " + Convert.ToBase64String(Encoding.UTF8.GetBytes("42:secret")); + Assert.Equal(expected, config.GenerationExport.Headers["Authorization"]); + Assert.Equal("42", config.GenerationExport.Headers["X-Scope-OrgID"]); + } + + [Fact] + public async Task Constructor_AppliesBasicAuthWithExplicitUser() + { + var config = TestHelpers.TestConfig(new CapturingGenerationExporter()); + config.GenerationExport.Auth = new AuthConfig + { + Mode = ExportAuthMode.Basic, + TenantId = "42", + BasicUser = "probe-user", + BasicPassword = "secret", + }; + + await using var client = new SigilClient(config); + + var expected = "Basic " + Convert.ToBase64String(Encoding.UTF8.GetBytes("probe-user:secret")); + Assert.Equal(expected, config.GenerationExport.Headers["Authorization"]); + Assert.Equal("42", config.GenerationExport.Headers["X-Scope-OrgID"]); + } + + [Fact] + public async Task Constructor_BasicAuthExplicitHeaderWins() + { + var config = TestHelpers.TestConfig(new CapturingGenerationExporter()); + config.GenerationExport.Headers["Authorization"] = "Basic override"; + config.GenerationExport.Headers["X-Scope-OrgID"] = "override-tenant"; + config.GenerationExport.Auth = new AuthConfig + { + Mode = ExportAuthMode.Basic, + TenantId = "42", + BasicPassword = "secret", + }; + + await using var client = new SigilClient(config); + + Assert.Equal("Basic override", config.GenerationExport.Headers["Authorization"]); + Assert.Equal("override-tenant", config.GenerationExport.Headers["X-Scope-OrgID"]); + } + } diff --git a/go/README.md b/go/README.md index d5e64b5..b7c9459 100644 --- a/go/README.md +++ b/go/README.md @@ -133,6 +133,7 @@ Auth is configured for generation export. - `none` - `tenant` (requires `TenantID`, injects `X-Scope-OrgID`) - `bearer` (requires `BearerToken`, injects `Authorization: Bearer `) +- `basic` (requires `BasicPassword` + `BasicUser` or `TenantID`, injects `Authorization: Basic `; also injects `X-Scope-OrgID` when `TenantID` is set — for self-hosted multi-tenancy only, not needed for Grafana Cloud) Invalid combinations fail fast during `NewClient(...)`. @@ -145,6 +146,29 @@ cfg.GenerationExport.Auth = sigil.AuthConfig{ Explicit transport headers remain the highest-precedence escape hatch. If `Headers` already contains `Authorization` or `X-Scope-OrgID`, the SDK does not overwrite them. +### Grafana Cloud auth (basic) + +For Grafana Cloud, use `basic` auth mode. The username is your Grafana Cloud instance/tenant ID and the password is your Grafana Cloud API key: + +```go +cfg.GenerationExport.Auth = sigil.AuthConfig{ + Mode: sigil.ExportAuthModeBasic, + TenantID: os.Getenv("GRAFANA_CLOUD_INSTANCE_ID"), + BasicPassword: os.Getenv("GRAFANA_CLOUD_API_KEY"), +} +``` + +If your deployment requires a distinct username (different from the tenant ID), set `BasicUser` explicitly: + +```go +cfg.GenerationExport.Auth = sigil.AuthConfig{ + Mode: sigil.ExportAuthModeBasic, + TenantID: os.Getenv("GRAFANA_CLOUD_INSTANCE_ID"), + BasicUser: os.Getenv("GRAFANA_CLOUD_INSTANCE_ID"), + BasicPassword: os.Getenv("GRAFANA_CLOUD_API_KEY"), +} +``` + ## Env-secret wiring example The SDK does not auto-load env vars. Read env values in your app and assign config explicitly. @@ -161,7 +185,8 @@ if genToken != "" { Common topology: -- Generations direct to Sigil: generation `tenant` mode. +- Grafana Cloud: generation `basic` mode with instance ID and API key. +- Self-hosted direct to Sigil: generation `tenant` mode. - Traces/metrics via OTEL Collector/Alloy: configure exporters in your app OTEL SDK setup. - Enterprise proxy: generation `bearer` mode to proxy; proxy authenticates and forwards tenant header upstream. diff --git a/go/sigil/exporter.go b/go/sigil/exporter.go index 69cd17c..368610a 100644 --- a/go/sigil/exporter.go +++ b/go/sigil/exporter.go @@ -310,8 +310,10 @@ func resolveHeadersWithAuth(headers map[string]string, auth AuthConfig) (map[str switch mode { case ExportAuthModeNone: - if tenantID != "" || bearerToken != "" { - return nil, errors.New("auth mode none does not allow tenant_id or bearer_token") + basicUser := strings.TrimSpace(auth.BasicUser) + basicPassword := strings.TrimSpace(auth.BasicPassword) + if tenantID != "" || bearerToken != "" || basicUser != "" || basicPassword != "" { + return nil, errors.New("auth mode none does not allow credentials") } return cloneTags(headers), nil case ExportAuthModeTenant: diff --git a/go/sigil/exporter_auth_test.go b/go/sigil/exporter_auth_test.go index b7f5dc8..0c1575f 100644 --- a/go/sigil/exporter_auth_test.go +++ b/go/sigil/exporter_auth_test.go @@ -120,6 +120,8 @@ func TestResolveHeadersWithAuthRejectsInvalidConfig(t *testing.T) { {Mode: ExportAuthModeBearer}, {Mode: ExportAuthModeNone, TenantID: "tenant-a"}, {Mode: ExportAuthModeNone, BearerToken: "token"}, + {Mode: ExportAuthModeNone, BasicUser: "user"}, + {Mode: ExportAuthModeNone, BasicPassword: "secret"}, {Mode: ExportAuthModeTenant, TenantID: "tenant-a", BearerToken: "token"}, {Mode: ExportAuthModeBearer, TenantID: "tenant-a", BearerToken: "token"}, {Mode: ExportAuthMode("unknown"), TenantID: "tenant-a"}, diff --git a/java/README.md b/java/README.md index e2481c3..9ff2351 100644 --- a/java/README.md +++ b/java/README.md @@ -136,9 +136,31 @@ Auth is configured for generation export: - `NONE` - `TENANT` (injects `X-Scope-OrgID`) - `BEARER` (injects `Authorization: Bearer `) +- `BASIC` (requires `basicPassword` + `basicUser` or `tenantId`, injects `Authorization: Basic `; also injects `X-Scope-OrgID` when `tenantId` is set — for self-hosted multi-tenancy only, not needed for Grafana Cloud) Invalid combinations fail fast at client construction. If explicit headers already contain `Authorization` or `X-Scope-OrgID`, explicit headers win. +### Grafana Cloud auth (basic) + +For Grafana Cloud, use `BASIC` auth mode. The username is your Grafana Cloud instance/tenant ID and the password is your Grafana Cloud API key: + +```java +.setAuth(new AuthConfig() + .setMode(AuthMode.BASIC) + .setTenantId(System.getenv("GRAFANA_CLOUD_INSTANCE_ID")) + .setBasicPassword(System.getenv("GRAFANA_CLOUD_API_KEY"))) +``` + +If your deployment requires a distinct username, set `basicUser` explicitly: + +```java +.setAuth(new AuthConfig() + .setMode(AuthMode.BASIC) + .setTenantId(System.getenv("GRAFANA_CLOUD_INSTANCE_ID")) + .setBasicUser(System.getenv("GRAFANA_CLOUD_INSTANCE_ID")) + .setBasicPassword(System.getenv("GRAFANA_CLOUD_API_KEY"))) +``` + Generation export transport protocols: - `GenerationExportProtocol.HTTP` diff --git a/java/core/src/main/java/com/grafana/sigil/sdk/AuthConfig.java b/java/core/src/main/java/com/grafana/sigil/sdk/AuthConfig.java index d6d1cc4..b3d871d 100644 --- a/java/core/src/main/java/com/grafana/sigil/sdk/AuthConfig.java +++ b/java/core/src/main/java/com/grafana/sigil/sdk/AuthConfig.java @@ -5,6 +5,8 @@ public final class AuthConfig { private AuthMode mode = AuthMode.NONE; private String tenantId = ""; private String bearerToken = ""; + private String basicUser = ""; + private String basicPassword = ""; public AuthMode getMode() { return mode; @@ -33,7 +35,32 @@ public AuthConfig setBearerToken(String bearerToken) { return this; } + /** Username for basic auth. When empty, tenantId is used. */ + public String getBasicUser() { + return basicUser; + } + + public AuthConfig setBasicUser(String basicUser) { + this.basicUser = basicUser == null ? "" : basicUser; + return this; + } + + /** Password/token for basic auth. */ + public String getBasicPassword() { + return basicPassword; + } + + public AuthConfig setBasicPassword(String basicPassword) { + this.basicPassword = basicPassword == null ? "" : basicPassword; + return this; + } + public AuthConfig copy() { - return new AuthConfig().setMode(mode).setTenantId(tenantId).setBearerToken(bearerToken); + return new AuthConfig() + .setMode(mode) + .setTenantId(tenantId) + .setBearerToken(bearerToken) + .setBasicUser(basicUser) + .setBasicPassword(basicPassword); } } diff --git a/java/core/src/main/java/com/grafana/sigil/sdk/AuthHeaders.java b/java/core/src/main/java/com/grafana/sigil/sdk/AuthHeaders.java index 35498ad..be5ab11 100644 --- a/java/core/src/main/java/com/grafana/sigil/sdk/AuthHeaders.java +++ b/java/core/src/main/java/com/grafana/sigil/sdk/AuthHeaders.java @@ -1,5 +1,7 @@ package com.grafana.sigil.sdk; +import java.nio.charset.StandardCharsets; +import java.util.Base64; import java.util.LinkedHashMap; import java.util.Map; @@ -21,8 +23,10 @@ static Map resolve(Map headers, AuthConfig auth, String bearer = auth == null ? "" : auth.getBearerToken().trim(); if (mode == AuthMode.NONE) { - if (!tenantId.isEmpty() || !bearer.isEmpty()) { - throw new IllegalArgumentException(label + " auth mode 'none' does not allow tenantId or bearerToken"); + String basicUser = auth == null ? "" : auth.getBasicUser().trim(); + String basicPassword = auth == null ? "" : auth.getBasicPassword().trim(); + if (!tenantId.isEmpty() || !bearer.isEmpty() || !basicUser.isEmpty() || !basicPassword.isEmpty()) { + throw new IllegalArgumentException(label + " auth mode 'none' does not allow credentials"); } return out; } @@ -53,6 +57,29 @@ static Map resolve(Map headers, AuthConfig auth, return out; } + if (mode == AuthMode.BASIC) { + String password = auth == null ? "" : auth.getBasicPassword().trim(); + if (password.isEmpty()) { + throw new IllegalArgumentException(label + " auth mode 'basic' requires basicPassword"); + } + String user = auth == null ? "" : auth.getBasicUser().trim(); + if (user.isEmpty()) { + user = tenantId; + } + if (user.isEmpty()) { + throw new IllegalArgumentException(label + " auth mode 'basic' requires basicUser or tenantId"); + } + if (!hasHeader(out, AUTHORIZATION_HEADER)) { + String encoded = Base64.getEncoder().encodeToString( + (user + ":" + password).getBytes(StandardCharsets.UTF_8)); + out.put(AUTHORIZATION_HEADER, "Basic " + encoded); + } + if (!tenantId.isEmpty() && !hasHeader(out, TENANT_HEADER)) { + out.put(TENANT_HEADER, tenantId); + } + return out; + } + throw new IllegalArgumentException("unsupported " + label + " auth mode " + mode); } diff --git a/java/core/src/main/java/com/grafana/sigil/sdk/AuthMode.java b/java/core/src/main/java/com/grafana/sigil/sdk/AuthMode.java index f6f1447..ba41a47 100644 --- a/java/core/src/main/java/com/grafana/sigil/sdk/AuthMode.java +++ b/java/core/src/main/java/com/grafana/sigil/sdk/AuthMode.java @@ -3,5 +3,6 @@ public enum AuthMode { NONE, TENANT, - BEARER + BEARER, + BASIC } diff --git a/java/core/src/test/java/com/grafana/sigil/sdk/SigilAuthConfigTest.java b/java/core/src/test/java/com/grafana/sigil/sdk/SigilAuthConfigTest.java index 39b0aac..6c26a18 100644 --- a/java/core/src/test/java/com/grafana/sigil/sdk/SigilAuthConfigTest.java +++ b/java/core/src/test/java/com/grafana/sigil/sdk/SigilAuthConfigTest.java @@ -3,6 +3,9 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.LinkedHashMap; import java.util.Map; import org.junit.jupiter.api.Test; @@ -13,6 +16,14 @@ void validatesAuthModeShape() { .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("mode 'none'"); + assertThatThrownBy(() -> AuthHeaders.resolve(Map.of(), new AuthConfig().setMode(AuthMode.NONE).setBasicUser("user"), "trace")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("mode 'none'"); + + assertThatThrownBy(() -> AuthHeaders.resolve(Map.of(), new AuthConfig().setMode(AuthMode.NONE).setBasicPassword("secret"), "trace")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("mode 'none'"); + assertThatThrownBy(() -> AuthHeaders.resolve(Map.of(), new AuthConfig().setMode(AuthMode.TENANT), "generation export")) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("requires tenantId"); @@ -36,4 +47,69 @@ void explicitHeadersOverrideInjectedAuthHeaders() { "generation export"); assertThat(generation.get("x-scope-orgid")).isEqualTo("tenant-override"); } + + @Test + void basicAuthWithTenantId() { + Map headers = AuthHeaders.resolve( + Map.of(), + new AuthConfig().setMode(AuthMode.BASIC).setTenantId("42").setBasicPassword("secret"), + "generation export"); + String expected = "Basic " + Base64.getEncoder() + .encodeToString("42:secret".getBytes(StandardCharsets.UTF_8)); + assertThat(headers.get("Authorization")).isEqualTo(expected); + assertThat(headers.get("X-Scope-OrgID")).isEqualTo("42"); + } + + @Test + void basicAuthWithExplicitUser() { + Map headers = AuthHeaders.resolve( + Map.of(), + new AuthConfig().setMode(AuthMode.BASIC).setTenantId("42") + .setBasicUser("probe-user").setBasicPassword("secret"), + "generation export"); + String expected = "Basic " + Base64.getEncoder() + .encodeToString("probe-user:secret".getBytes(StandardCharsets.UTF_8)); + assertThat(headers.get("Authorization")).isEqualTo(expected); + assertThat(headers.get("X-Scope-OrgID")).isEqualTo("42"); + } + + @Test + void basicAuthExplicitHeaderWins() { + Map input = new LinkedHashMap<>(); + input.put("Authorization", "Basic override"); + input.put("X-Scope-OrgID", "override-tenant"); + Map headers = AuthHeaders.resolve( + input, + new AuthConfig().setMode(AuthMode.BASIC).setTenantId("42").setBasicPassword("secret"), + "generation export"); + assertThat(headers.get("Authorization")).isEqualTo("Basic override"); + assertThat(headers.get("X-Scope-OrgID")).isEqualTo("override-tenant"); + } + + @Test + void basicAuthRejectsInvalidConfig() { + assertThatThrownBy(() -> AuthHeaders.resolve(Map.of(), + new AuthConfig().setMode(AuthMode.BASIC), "generation export")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("requires basicPassword"); + + assertThatThrownBy(() -> AuthHeaders.resolve(Map.of(), + new AuthConfig().setMode(AuthMode.BASIC).setBasicPassword("secret"), "generation export")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("requires basicUser or tenantId"); + } + + @Test + void basicAuthCopy() { + AuthConfig original = new AuthConfig() + .setMode(AuthMode.BASIC) + .setTenantId("42") + .setBasicUser("user") + .setBasicPassword("pass"); + AuthConfig copy = original.copy(); + assertThat(copy.getMode()).isEqualTo(AuthMode.BASIC); + assertThat(copy.getTenantId()).isEqualTo("42"); + assertThat(copy.getBasicUser()).isEqualTo("user"); + assertThat(copy.getBasicPassword()).isEqualTo("pass"); + } } diff --git a/js/README.md b/js/README.md index f34f581..2c35f2d 100644 --- a/js/README.md +++ b/js/README.md @@ -271,6 +271,7 @@ Auth is configured for `generationExport`. - `mode: "none"` - `mode: "tenant"` (requires `tenantId`, injects `X-Scope-OrgID`) - `mode: "bearer"` (requires `bearerToken`, injects `Authorization: Bearer `) +- `mode: "basic"` (requires `basicPassword` + `basicUser` or `tenantId`, injects `Authorization: Basic `; also injects `X-Scope-OrgID` when `tenantId` is set — for self-hosted multi-tenancy only, not needed for Grafana Cloud) Invalid mode/field combinations throw during client config resolution. @@ -289,6 +290,35 @@ const client = new SigilClient({ }); ``` +### Grafana Cloud auth (basic) + +For Grafana Cloud, use `basic` auth mode. The username is your Grafana Cloud instance/tenant ID and the password is your Grafana Cloud API key: + +```ts +const client = new SigilClient({ + generationExport: { + protocol: "http", + endpoint: "https://.grafana.net/api/v1/generations:export", + auth: { + mode: "basic", + tenantId: process.env.GRAFANA_CLOUD_INSTANCE_ID, + basicPassword: process.env.GRAFANA_CLOUD_API_KEY, + }, + }, +}); +``` + +If your deployment requires a distinct username, set `basicUser` explicitly: + +```ts +auth: { + mode: "basic", + tenantId: process.env.GRAFANA_CLOUD_INSTANCE_ID, + basicUser: process.env.GRAFANA_CLOUD_INSTANCE_ID, + basicPassword: process.env.GRAFANA_CLOUD_API_KEY, +}, +``` + ## Env-secret wiring example The SDK does not auto-load env vars. Resolve env secrets in your app and map them into config. @@ -313,7 +343,8 @@ const client = new SigilClient({ Common topology: -- Generations direct to Sigil: generation `tenant` mode. +- Grafana Cloud: generation `basic` mode with instance ID and API key. +- Self-hosted direct to Sigil: generation `tenant` mode. - Traces/metrics via OTEL Collector/Alloy: configure exporters in your app OTEL SDK setup. - Enterprise proxy: generation `bearer` mode to proxy; proxy authenticates and forwards tenant header upstream. diff --git a/js/src/config.ts b/js/src/config.ts index 2ffa342..4484314 100644 --- a/js/src/config.ts +++ b/js/src/config.ts @@ -120,8 +120,10 @@ function resolveHeadersWithAuth( const out = headers ? { ...headers } : undefined; if (mode === 'none') { - if (tenantId.length > 0 || bearerToken.length > 0) { - throw new Error(`${label} auth mode "none" does not allow tenantId or bearerToken`); + const basicUser = auth.basicUser?.trim() ?? ''; + const basicPassword = auth.basicPassword?.trim() ?? ''; + if (tenantId.length > 0 || bearerToken.length > 0 || basicUser.length > 0 || basicPassword.length > 0) { + throw new Error(`${label} auth mode "none" does not allow credentials`); } return out; } @@ -158,6 +160,29 @@ function resolveHeadersWithAuth( }; } + if (mode === 'basic') { + const password = auth.basicPassword?.trim() ?? ''; + if (password.length === 0) { + throw new Error(`${label} auth mode "basic" requires basicPassword`); + } + let user = auth.basicUser?.trim() ?? ''; + if (user.length === 0) { + user = tenantId; + } + if (user.length === 0) { + throw new Error(`${label} auth mode "basic" requires basicUser or tenantId`); + } + const result: Record = { ...(out ?? {}) }; + if (!hasHeaderKey(result, authorizationHeaderName)) { + const encoded = new TextEncoder().encode(`${user}:${password}`); + result[authorizationHeaderName] = 'Basic ' + btoa(String.fromCharCode(...encoded)); + } + if (tenantId.length > 0 && !hasHeaderKey(result, tenantHeaderName)) { + result[tenantHeaderName] = tenantId; + } + return result; + } + throw new Error(`unsupported ${label} auth mode: ${auth.mode}`); } diff --git a/js/src/types.ts b/js/src/types.ts index 1eb5824..0e3210a 100644 --- a/js/src/types.ts +++ b/js/src/types.ts @@ -5,13 +5,17 @@ export type GenerationExportProtocol = 'grpc' | 'http' | 'none'; /** Generation execution mode. */ export type GenerationMode = 'SYNC' | 'STREAM'; /** Supported auth modes for transport exports. */ -export type ExportAuthMode = 'none' | 'tenant' | 'bearer'; +export type ExportAuthMode = 'none' | 'tenant' | 'bearer' | 'basic'; /** Per-export auth configuration. */ export interface ExportAuthConfig { mode: ExportAuthMode; tenantId?: string; bearerToken?: string; + /** Username for basic auth. When empty, tenantId is used. */ + basicUser?: string; + /** Password/token for basic auth. */ + basicPassword?: string; } /** Generation exporter runtime configuration. */ diff --git a/js/test/client.auth.config.test.mjs b/js/test/client.auth.config.test.mjs index c0984d3..852c01a 100644 --- a/js/test/client.auth.config.test.mjs +++ b/js/test/client.auth.config.test.mjs @@ -15,3 +15,135 @@ test('invalid generation auth config throws at client init', () => { /requires tenantId/ ); }); + +test('basic auth mode injects Authorization and X-Scope-OrgID headers', () => { + const client = new SigilClient({ + generationExport: { + auth: { + mode: 'basic', + tenantId: '42', + basicPassword: 'secret', + }, + }, + }); + + const expected = 'Basic ' + btoa('42:secret'); + assert.equal(client.config.generationExport.headers?.['Authorization'], expected); + assert.equal(client.config.generationExport.headers?.['X-Scope-OrgID'], '42'); + client.shutdown(); +}); + +test('basic auth mode uses basicUser over tenantId for credential', () => { + const client = new SigilClient({ + generationExport: { + auth: { + mode: 'basic', + tenantId: '42', + basicUser: 'probe-user', + basicPassword: 'secret', + }, + }, + }); + + const expected = 'Basic ' + btoa('probe-user:secret'); + assert.equal(client.config.generationExport.headers?.['Authorization'], expected); + assert.equal(client.config.generationExport.headers?.['X-Scope-OrgID'], '42'); + client.shutdown(); +}); + +test('basic auth mode requires basicPassword', () => { + assert.throws( + () => + new SigilClient({ + generationExport: { + auth: { + mode: 'basic', + tenantId: '42', + }, + }, + }), + /requires basicPassword/ + ); +}); + +test('basic auth mode requires basicUser or tenantId', () => { + assert.throws( + () => + new SigilClient({ + generationExport: { + auth: { + mode: 'basic', + basicPassword: 'secret', + }, + }, + }), + /requires basicUser or tenantId/ + ); +}); + +test('none auth mode rejects basicUser', () => { + assert.throws( + () => + new SigilClient({ + generationExport: { + auth: { + mode: 'none', + basicUser: 'user', + }, + }, + }), + /does not allow credentials/ + ); +}); + +test('none auth mode rejects basicPassword', () => { + assert.throws( + () => + new SigilClient({ + generationExport: { + auth: { + mode: 'none', + basicPassword: 'secret', + }, + }, + }), + /does not allow credentials/ + ); +}); + +test('basic auth handles non-ASCII credentials via UTF-8 encoding', () => { + const client = new SigilClient({ + generationExport: { + auth: { + mode: 'basic', + basicUser: 'ユーザー', + basicPassword: 'パスワード', + }, + }, + }); + + const encoded = Buffer.from('ユーザー:パスワード', 'utf-8').toString('base64'); + const expected = 'Basic ' + encoded; + assert.equal(client.config.generationExport.headers?.['Authorization'], expected); + client.shutdown(); +}); + +test('basic auth explicit headers win over auth-derived headers', () => { + const client = new SigilClient({ + generationExport: { + headers: { + Authorization: 'Basic override', + 'X-Scope-OrgID': 'override-tenant', + }, + auth: { + mode: 'basic', + tenantId: '42', + basicPassword: 'secret', + }, + }, + }); + + assert.equal(client.config.generationExport.headers?.['Authorization'], 'Basic override'); + assert.equal(client.config.generationExport.headers?.['X-Scope-OrgID'], 'override-tenant'); + client.shutdown(); +}); diff --git a/python/README.md b/python/README.md index 76c6c8c..3caa9ae 100644 --- a/python/README.md +++ b/python/README.md @@ -298,6 +298,7 @@ Auth is resolved for `generation_export`. - `mode="none"` - `mode="tenant"` (requires `tenant_id`, injects `X-Scope-OrgID`) - `mode="bearer"` (requires `bearer_token`, injects `Authorization: Bearer `) +- `mode="basic"` (requires `basic_password` + `basic_user` or `tenant_id`, injects `Authorization: Basic `; also injects `X-Scope-OrgID` when `tenant_id` is set — for self-hosted multi-tenancy only, not needed for Grafana Cloud) Invalid mode/field combinations fail fast in `resolve_config(...)`. @@ -316,6 +317,38 @@ cfg = ClientConfig( ) ``` +### Grafana Cloud auth (basic) + +For Grafana Cloud, use `basic` auth mode. The username is your Grafana Cloud instance/tenant ID and the password is your Grafana Cloud API key: + +```python +import os +from sigil_sdk import AuthConfig, ClientConfig, GenerationExportConfig + +cfg = ClientConfig( + generation_export=GenerationExportConfig( + protocol="http", + endpoint="https://.grafana.net/api/v1/generations:export", + auth=AuthConfig( + mode="basic", + tenant_id=os.environ["GRAFANA_CLOUD_INSTANCE_ID"], + basic_password=os.environ["GRAFANA_CLOUD_API_KEY"], + ), + ), +) +``` + +If your deployment requires a distinct username, set `basic_user` explicitly: + +```python +auth=AuthConfig( + mode="basic", + tenant_id=os.environ["GRAFANA_CLOUD_INSTANCE_ID"], + basic_user=os.environ["GRAFANA_CLOUD_INSTANCE_ID"], + basic_password=os.environ["GRAFANA_CLOUD_API_KEY"], +) +``` + ## Env-secret wiring example The SDK does not auto-load env vars. Resolve env values in your application and pass them into config explicitly. @@ -333,7 +366,8 @@ if gen_token: Common topology: -- Generations direct to Sigil: generation `tenant` mode. +- Grafana Cloud: generation `basic` mode with instance ID and API key. +- Self-hosted direct to Sigil: generation `tenant` mode. - Traces/metrics via OTEL Collector/Alloy: configure exporters in your app OTEL SDK setup. - Enterprise proxy: generation `bearer` mode to proxy; proxy authenticates and forwards tenant header upstream. diff --git a/python/sigil_sdk/config.py b/python/sigil_sdk/config.py index fc1dd66..d60395a 100644 --- a/python/sigil_sdk/config.py +++ b/python/sigil_sdk/config.py @@ -2,6 +2,7 @@ from __future__ import annotations +import base64 from dataclasses import dataclass, field from datetime import datetime, timedelta import logging @@ -25,6 +26,8 @@ class AuthConfig: mode: str = "none" tenant_id: str = "" bearer_token: str = "" + basic_user: str = "" + basic_password: str = "" @dataclass(slots=True) @@ -139,8 +142,10 @@ def _resolve_export_headers(headers: dict[str, str], auth: AuthConfig, label: st out = dict(headers) if mode == "none": - if tenant_id or bearer_token: - raise ValueError(f"{label} auth mode 'none' does not allow tenant_id or bearer_token") + basic_user = auth.basic_user.strip() + basic_password = auth.basic_password.strip() + if tenant_id or bearer_token or basic_user or basic_password: + raise ValueError(f"{label} auth mode 'none' does not allow credentials") return out if mode == "tenant": if not tenant_id: @@ -158,6 +163,21 @@ def _resolve_export_headers(headers: dict[str, str], auth: AuthConfig, label: st if not _has_header(out, AUTHORIZATION_HEADER): out[AUTHORIZATION_HEADER] = _format_bearer_token(bearer_token) return out + if mode == "basic": + password = auth.basic_password.strip() + if not password: + raise ValueError(f"{label} auth mode 'basic' requires basic_password") + user = auth.basic_user.strip() + if not user: + user = tenant_id + if not user: + raise ValueError(f"{label} auth mode 'basic' requires basic_user or tenant_id") + if not _has_header(out, AUTHORIZATION_HEADER): + creds = base64.b64encode(f"{user}:{password}".encode()).decode() + out[AUTHORIZATION_HEADER] = f"Basic {creds}" + if tenant_id and not _has_header(out, TENANT_HEADER): + out[TENANT_HEADER] = tenant_id + return out raise ValueError(f"unsupported {label} auth mode {auth.mode!r}") diff --git a/python/tests/test_auth_config.py b/python/tests/test_auth_config.py index 537b77d..2a4c439 100644 --- a/python/tests/test_auth_config.py +++ b/python/tests/test_auth_config.py @@ -2,6 +2,8 @@ from __future__ import annotations +import base64 + import pytest from sigil_sdk import AuthConfig, ClientConfig, GenerationExportConfig @@ -33,6 +35,56 @@ def test_resolve_config_keeps_explicit_headers() -> None: assert cfg.generation_export.headers["x-scope-orgid"] == "override-tenant" +def test_resolve_config_basic_auth_with_tenant_id() -> None: + cfg = resolve_config( + ClientConfig( + generation_export=GenerationExportConfig( + auth=AuthConfig(mode="basic", tenant_id="42", basic_password="secret"), + ), + ) + ) + + expected = "Basic " + base64.b64encode(b"42:secret").decode() + assert cfg.generation_export.headers["Authorization"] == expected + assert cfg.generation_export.headers["X-Scope-OrgID"] == "42" + + +def test_resolve_config_basic_auth_with_explicit_user() -> None: + cfg = resolve_config( + ClientConfig( + generation_export=GenerationExportConfig( + auth=AuthConfig( + mode="basic", + tenant_id="42", + basic_user="probe-user", + basic_password="secret", + ), + ), + ) + ) + + expected = "Basic " + base64.b64encode(b"probe-user:secret").decode() + assert cfg.generation_export.headers["Authorization"] == expected + assert cfg.generation_export.headers["X-Scope-OrgID"] == "42" + + +def test_resolve_config_basic_auth_explicit_header_wins() -> None: + cfg = resolve_config( + ClientConfig( + generation_export=GenerationExportConfig( + headers={ + "Authorization": "Basic override", + "X-Scope-OrgID": "override-tenant", + }, + auth=AuthConfig(mode="basic", tenant_id="42", basic_password="secret"), + ), + ) + ) + + assert cfg.generation_export.headers["Authorization"] == "Basic override" + assert cfg.generation_export.headers["X-Scope-OrgID"] == "override-tenant" + + @pytest.mark.parametrize( "auth", [ @@ -40,9 +92,13 @@ def test_resolve_config_keeps_explicit_headers() -> None: AuthConfig(mode="bearer"), AuthConfig(mode="none", tenant_id="tenant-a"), AuthConfig(mode="none", bearer_token="token"), + AuthConfig(mode="none", basic_user="user"), + AuthConfig(mode="none", basic_password="secret"), AuthConfig(mode="tenant", tenant_id="tenant-a", bearer_token="token"), AuthConfig(mode="bearer", tenant_id="tenant-a", bearer_token="token"), AuthConfig(mode="unknown", tenant_id="tenant-a"), + AuthConfig(mode="basic"), + AuthConfig(mode="basic", basic_password="secret"), ], ) def test_resolve_config_rejects_invalid_auth_combinations(auth: AuthConfig) -> None: From 885801490a8910c22da90d40c361c1d5c9956adc Mon Sep 17 00:00:00 2001 From: "renovate-sh-app[bot]" <219655108+renovate-sh-app[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 10:16:19 +0100 Subject: [PATCH 082/133] fix(deps): update module google.golang.org/genai to v1.50.0 (#575) | datasource | package | from | to | | ---------- | ----------------------- | ------- | ------- | | go | google.golang.org/genai | v1.49.0 | v1.50.0 | Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> --- go-providers/gemini/go.mod | 2 +- go-providers/gemini/go.sum | 4 ++-- go/cmd/devex-emitter/go.mod | 2 +- go/cmd/devex-emitter/go.sum | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go-providers/gemini/go.mod b/go-providers/gemini/go.mod index fbf6bc9..2d07ed2 100644 --- a/go-providers/gemini/go.mod +++ b/go-providers/gemini/go.mod @@ -4,7 +4,7 @@ go 1.25.6 require ( github.com/grafana/sigil/sdks/go v0.0.0 - google.golang.org/genai v1.49.0 + google.golang.org/genai v1.50.0 ) require ( diff --git a/go-providers/gemini/go.sum b/go-providers/gemini/go.sum index ebf704a..2e44301 100644 --- a/go-providers/gemini/go.sum +++ b/go-providers/gemini/go.sum @@ -61,8 +61,8 @@ golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/genai v1.49.0 h1:Se+QJaH2GYK1aaR1o5S38mlU2GD5FnVvP76nfkV7LH0= -google.golang.org/genai v1.49.0/go.mod h1:A3kkl0nyBjyFlNjgxIwKq70julKbIxpSxqKO5gw/gmk= +google.golang.org/genai v1.50.0 h1:yHKV/vjoeN9PJ3iF0ur4cBZco4N3Kl7j09rMq7XSoWk= +google.golang.org/genai v1.50.0/go.mod h1:A3kkl0nyBjyFlNjgxIwKq70julKbIxpSxqKO5gw/gmk= google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU= diff --git a/go/cmd/devex-emitter/go.mod b/go/cmd/devex-emitter/go.mod index f0388f0..bae9db0 100644 --- a/go/cmd/devex-emitter/go.mod +++ b/go/cmd/devex-emitter/go.mod @@ -16,7 +16,7 @@ require ( go.opentelemetry.io/otel/sdk v1.42.0 go.opentelemetry.io/otel/sdk/metric v1.42.0 go.opentelemetry.io/otel/trace v1.42.0 - google.golang.org/genai v1.49.0 + google.golang.org/genai v1.50.0 ) require ( diff --git a/go/cmd/devex-emitter/go.sum b/go/cmd/devex-emitter/go.sum index c07a809..92b7950 100644 --- a/go/cmd/devex-emitter/go.sum +++ b/go/cmd/devex-emitter/go.sum @@ -89,8 +89,8 @@ golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/genai v1.49.0 h1:Se+QJaH2GYK1aaR1o5S38mlU2GD5FnVvP76nfkV7LH0= -google.golang.org/genai v1.49.0/go.mod h1:A3kkl0nyBjyFlNjgxIwKq70julKbIxpSxqKO5gw/gmk= +google.golang.org/genai v1.50.0 h1:yHKV/vjoeN9PJ3iF0ur4cBZco4N3Kl7j09rMq7XSoWk= +google.golang.org/genai v1.50.0/go.mod h1:A3kkl0nyBjyFlNjgxIwKq70julKbIxpSxqKO5gw/gmk= google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 h1:JLQynH/LBHfCTSbDWl+py8C+Rg/k1OVH3xfcaiANuF0= google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:kSJwQxqmFXeo79zOmbrALdflXQeAYcUbgS7PbpMknCY= google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac= From 5853c64dfd0b247b7763153e47bf938bc164b378 Mon Sep 17 00:00:00 2001 From: "renovate-sh-app[bot]" <219655108+renovate-sh-app[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 10:16:23 +0100 Subject: [PATCH 083/133] fix(deps): update dependency com.google.genai:google-genai to v1.43.0 (#574) | datasource | package | from | to | | ---------- | ----------------------------- | ------ | ------ | | maven | com.google.genai:google-genai | 1.42.0 | 1.43.0 | Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> --- java/gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java/gradle/libs.versions.toml b/java/gradle/libs.versions.toml index 7659284..5b7f050 100644 --- a/java/gradle/libs.versions.toml +++ b/java/gradle/libs.versions.toml @@ -43,7 +43,7 @@ javax-annotation = { module = "javax.annotation:javax.annotation-api", version.r openai-java = { module = "com.openai:openai-java", version = "4.26.0" } anthropic-java = { module = "com.anthropic:anthropic-java", version = "2.16.1" } -google-genai = { module = "com.google.genai:google-genai", version = "1.42.0" } +google-genai = { module = "com.google.genai:google-genai", version = "1.43.0" } [plugins] protobuf = { id = "com.google.protobuf", version.ref = "protobufPlugin" } From 14d3a6dc0f18b4c6daef061b6890276eb30426fb Mon Sep 17 00:00:00 2001 From: "renovate-sh-app[bot]" <219655108+renovate-sh-app[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 10:16:27 +0100 Subject: [PATCH 084/133] chore(deps): update dependency google.genai to 1.4.0 (#573) | datasource | package | from | to | | ---------- | ------------ | ----- | ----- | | nuget | Google.GenAI | 1.3.0 | 1.4.0 | Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> --- dotnet/src/Grafana.Sigil.Gemini/Grafana.Sigil.Gemini.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/src/Grafana.Sigil.Gemini/Grafana.Sigil.Gemini.csproj b/dotnet/src/Grafana.Sigil.Gemini/Grafana.Sigil.Gemini.csproj index 2311485..b0e3d59 100644 --- a/dotnet/src/Grafana.Sigil.Gemini/Grafana.Sigil.Gemini.csproj +++ b/dotnet/src/Grafana.Sigil.Gemini/Grafana.Sigil.Gemini.csproj @@ -11,7 +11,7 @@ - + From 9f4f26653d1c5aae87290a8573252b58b55c06b9 Mon Sep 17 00:00:00 2001 From: "renovate-sh-app[bot]" <219655108+renovate-sh-app[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 10:16:39 +0100 Subject: [PATCH 085/133] chore(deps): update dependency esbuild to ^0.27.3 (#570) | datasource | package | from | to | | ---------- | ------- | ------ | ------ | | npm | esbuild | 0.27.3 | 0.27.4 | Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> --- plugins/opencode/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/opencode/package.json b/plugins/opencode/package.json index 79c3445..185e9bb 100644 --- a/plugins/opencode/package.json +++ b/plugins/opencode/package.json @@ -27,7 +27,7 @@ "@opencode-ai/plugin": "^1.2.16", "@opencode-ai/sdk": "^1.2.16", "@types/node": "^24.0.0", - "esbuild": "^0.27.0", + "esbuild": "^0.27.3", "typescript": "^5.8.2", "vitest": "^4.0.18" } From f17743a641bdaace89668d82ffc9b8878c1ccde3 Mon Sep 17 00:00:00 2001 From: "renovate-sh-app[bot]" <219655108+renovate-sh-app[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 10:16:42 +0100 Subject: [PATCH 086/133] chore(deps): update dependency @opencode-ai/sdk to ^1.2.24 (#569) | datasource | package | from | to | | ---------- | ---------------- | ------ | ------ | | npm | @opencode-ai/sdk | 1.2.24 | 1.2.25 | Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> --- plugins/opencode/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/opencode/package.json b/plugins/opencode/package.json index 185e9bb..f036bea 100644 --- a/plugins/opencode/package.json +++ b/plugins/opencode/package.json @@ -25,7 +25,7 @@ "devDependencies": { "@grafana/sigil-sdk-js": "workspace:*", "@opencode-ai/plugin": "^1.2.16", - "@opencode-ai/sdk": "^1.2.16", + "@opencode-ai/sdk": "^1.2.24", "@types/node": "^24.0.0", "esbuild": "^0.27.3", "typescript": "^5.8.2", From 18aa4aabc90a544e818ae20e9c055b0f34ba7c9a Mon Sep 17 00:00:00 2001 From: "renovate-sh-app[bot]" <219655108+renovate-sh-app[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 10:17:58 +0100 Subject: [PATCH 087/133] chore(deps): update dependency @opencode-ai/plugin to ^1.2.24 (#568) | datasource | package | from | to | | ---------- | ------------------- | ------ | ------ | | npm | @opencode-ai/plugin | 1.2.24 | 1.2.25 | Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: Cyril Tovena --- plugins/opencode/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/opencode/package.json b/plugins/opencode/package.json index f036bea..039bc38 100644 --- a/plugins/opencode/package.json +++ b/plugins/opencode/package.json @@ -24,7 +24,7 @@ }, "devDependencies": { "@grafana/sigil-sdk-js": "workspace:*", - "@opencode-ai/plugin": "^1.2.16", + "@opencode-ai/plugin": "^1.2.24", "@opencode-ai/sdk": "^1.2.24", "@types/node": "^24.0.0", "esbuild": "^0.27.3", From 0c96df5f465721f27bc2a759d54542bc43f36242 Mon Sep 17 00:00:00 2001 From: Jack Gordley Date: Thu, 19 Mar 2026 08:19:05 -0400 Subject: [PATCH 088/133] fix(langchain): extract ToolMessage content before passing to base SDK (#596) --- .../langchain/sigil_sdk_langchain/handler.py | 21 +++++++++- .../langchain/tests/test_langchain_handler.py | 40 +++++++++++++++++++ 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/python-frameworks/langchain/sigil_sdk_langchain/handler.py b/python-frameworks/langchain/sigil_sdk_langchain/handler.py index d7f0eba..8bbd867 100644 --- a/python-frameworks/langchain/sigil_sdk_langchain/handler.py +++ b/python-frameworks/langchain/sigil_sdk_langchain/handler.py @@ -18,6 +18,23 @@ class AsyncCallbackHandler: # type: ignore[no-redef] """Fallback async base class when langchain-core is unavailable.""" +def _extract_tool_output(output: Any) -> Any: + """Extract serializable content from LangChain tool output. + + LangChain's on_tool_end receives the raw output which may be a + ToolMessage or other BaseMessage subclass that isn't directly JSON + serializable. Extract the .content string when available. + """ + if output is None: + return output + if isinstance(output, str): + return output + # Handle LangChain BaseMessage subclasses (ToolMessage, AIMessage, etc.) + if hasattr(output, "content"): + return output.content + return output + + _framework_name = "langchain" _framework_source = "handler" _framework_language = "python" @@ -149,7 +166,7 @@ def on_tool_start( ) def on_tool_end(self, output: Any, *, run_id: UUID, **_kwargs: Any) -> None: - self._on_tool_end(output=output, run_id=run_id) + self._on_tool_end(output=_extract_tool_output(output), run_id=run_id) def on_tool_error(self, error: BaseException, *, run_id: UUID, **_kwargs: Any) -> None: self._on_tool_error(error=error, run_id=run_id) @@ -284,7 +301,7 @@ async def on_tool_start( ) async def on_tool_end(self, output: Any, *, run_id: UUID, **_kwargs: Any) -> None: - self._on_tool_end(output=output, run_id=run_id) + self._on_tool_end(output=_extract_tool_output(output), run_id=run_id) async def on_tool_error(self, error: BaseException, *, run_id: UUID, **_kwargs: Any) -> None: self._on_tool_error(error=error, run_id=run_id) diff --git a/python-frameworks/langchain/tests/test_langchain_handler.py b/python-frameworks/langchain/tests/test_langchain_handler.py index 74dabce..5af3b89 100644 --- a/python-frameworks/langchain/tests/test_langchain_handler.py +++ b/python-frameworks/langchain/tests/test_langchain_handler.py @@ -18,6 +18,7 @@ create_sigil_langchain_handler, with_sigil_langchain_callbacks, ) +from sigil_sdk_langchain.handler import _extract_tool_output class _CapturingExporter: @@ -335,6 +336,45 @@ async def _run() -> None: client.shutdown() +def test_extract_tool_output_unwraps_message_content_and_preserves_plain_values() -> None: + class _FakeToolMessage: + def __init__(self, content): + self.content = content + + payload = {"temp_c": 18} + + assert _extract_tool_output(_FakeToolMessage("tool result text")) == "tool result text" + assert _extract_tool_output("plain string") == "plain string" + assert _extract_tool_output(None) is None + assert _extract_tool_output(payload) is payload + + +def test_langchain_tool_end_extracts_message_content_before_recording() -> None: + exporter = _CapturingExporter() + client = _new_client(exporter) + + class _FakeToolMessage: + def __init__(self, content): + self.content = content + + try: + handler = SigilLangChainHandler(client=client) + captured: dict[str, object] = {} + + def _capture_tool_end(*, output, run_id) -> None: + captured["output"] = output + captured["run_id"] = run_id + + handler._on_tool_end = _capture_tool_end # type: ignore[method-assign] + + run_id = uuid4() + handler.on_tool_end(_FakeToolMessage("tool result text"), run_id=run_id) + + assert captured == {"output": "tool result text", "run_id": run_id} + finally: + client.shutdown() + + def test_langchain_tool_chain_and_retriever_callbacks_emit_spans() -> None: exporter = _CapturingExporter() span_exporter = InMemorySpanExporter() From 5d84817c6242ef20e2d76875228d46853a679e14 Mon Sep 17 00:00:00 2001 From: "renovate-sh-app[bot]" <219655108+renovate-sh-app[bot]@users.noreply.github.com> Date: Fri, 20 Mar 2026 10:17:32 +0100 Subject: [PATCH 089/133] fix(deps): update module google.golang.org/grpc to v1.79.3 [security] (#595) | datasource | package | from | to | | ---------- | ---------------------- | ------- | ------- | | go | google.golang.org/grpc | v1.79.2 | v1.79.3 | Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> --- go-frameworks/google-adk/go.mod | 2 +- go-frameworks/google-adk/go.sum | 4 ++-- go-providers/anthropic/go.mod | 2 +- go-providers/anthropic/go.sum | 4 ++-- go-providers/gemini/go.mod | 2 +- go-providers/gemini/go.sum | 4 ++-- go-providers/openai/go.mod | 2 +- go-providers/openai/go.sum | 4 ++-- go/cmd/devex-emitter/go.mod | 2 +- go/cmd/devex-emitter/go.sum | 4 ++-- go/go.mod | 2 +- go/go.sum | 4 ++-- 12 files changed, 18 insertions(+), 18 deletions(-) diff --git a/go-frameworks/google-adk/go.mod b/go-frameworks/google-adk/go.mod index c1bd584..4ca2490 100644 --- a/go-frameworks/google-adk/go.mod +++ b/go-frameworks/google-adk/go.mod @@ -21,7 +21,7 @@ require ( golang.org/x/sys v0.41.0 // indirect golang.org/x/text v0.34.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect - google.golang.org/grpc v1.79.2 // indirect + google.golang.org/grpc v1.79.3 // indirect google.golang.org/protobuf v1.36.11 // indirect ) diff --git a/go-frameworks/google-adk/go.sum b/go-frameworks/google-adk/go.sum index d23fe57..377d78e 100644 --- a/go-frameworks/google-adk/go.sum +++ b/go-frameworks/google-adk/go.sum @@ -41,8 +41,8 @@ gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= -google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU= -google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= +google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/go-providers/anthropic/go.mod b/go-providers/anthropic/go.mod index d6f4e08..2c17d2f 100644 --- a/go-providers/anthropic/go.mod +++ b/go-providers/anthropic/go.mod @@ -27,7 +27,7 @@ require ( golang.org/x/sys v0.41.0 // indirect golang.org/x/text v0.34.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect - google.golang.org/grpc v1.79.2 // indirect + google.golang.org/grpc v1.79.3 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/go-providers/anthropic/go.sum b/go-providers/anthropic/go.sum index 3ceadba..ba8e433 100644 --- a/go-providers/anthropic/go.sum +++ b/go-providers/anthropic/go.sum @@ -57,8 +57,8 @@ gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= -google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU= -google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= +google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/go-providers/gemini/go.mod b/go-providers/gemini/go.mod index 2d07ed2..eb0e79b 100644 --- a/go-providers/gemini/go.mod +++ b/go-providers/gemini/go.mod @@ -33,7 +33,7 @@ require ( golang.org/x/sys v0.41.0 // indirect golang.org/x/text v0.34.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect - google.golang.org/grpc v1.79.2 // indirect + google.golang.org/grpc v1.79.3 // indirect google.golang.org/protobuf v1.36.11 // indirect ) diff --git a/go-providers/gemini/go.sum b/go-providers/gemini/go.sum index 2e44301..75289e6 100644 --- a/go-providers/gemini/go.sum +++ b/go-providers/gemini/go.sum @@ -65,8 +65,8 @@ google.golang.org/genai v1.50.0 h1:yHKV/vjoeN9PJ3iF0ur4cBZco4N3Kl7j09rMq7XSoWk= google.golang.org/genai v1.50.0/go.mod h1:A3kkl0nyBjyFlNjgxIwKq70julKbIxpSxqKO5gw/gmk= google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= -google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU= -google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= +google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/go-providers/openai/go.mod b/go-providers/openai/go.mod index 0f88f02..0e438e6 100644 --- a/go-providers/openai/go.mod +++ b/go-providers/openai/go.mod @@ -26,7 +26,7 @@ require ( golang.org/x/sys v0.41.0 // indirect golang.org/x/text v0.34.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect - google.golang.org/grpc v1.79.2 // indirect + google.golang.org/grpc v1.79.3 // indirect google.golang.org/protobuf v1.36.11 // indirect ) diff --git a/go-providers/openai/go.sum b/go-providers/openai/go.sum index 67f85ad..9261267 100644 --- a/go-providers/openai/go.sum +++ b/go-providers/openai/go.sum @@ -53,8 +53,8 @@ gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= -google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU= -google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= +google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/go/cmd/devex-emitter/go.mod b/go/cmd/devex-emitter/go.mod index bae9db0..2e38748 100644 --- a/go/cmd/devex-emitter/go.mod +++ b/go/cmd/devex-emitter/go.mod @@ -50,7 +50,7 @@ require ( golang.org/x/text v0.34.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect - google.golang.org/grpc v1.79.2 // indirect + google.golang.org/grpc v1.79.3 // indirect google.golang.org/protobuf v1.36.11 // indirect ) diff --git a/go/cmd/devex-emitter/go.sum b/go/cmd/devex-emitter/go.sum index 92b7950..11594ef 100644 --- a/go/cmd/devex-emitter/go.sum +++ b/go/cmd/devex-emitter/go.sum @@ -95,8 +95,8 @@ google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 h1: google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:kSJwQxqmFXeo79zOmbrALdflXQeAYcUbgS7PbpMknCY= google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac= google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= -google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU= -google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= +google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= diff --git a/go/go.mod b/go/go.mod index 6154ef7..431bec9 100644 --- a/go/go.mod +++ b/go/go.mod @@ -8,7 +8,7 @@ require ( go.opentelemetry.io/otel/sdk v1.42.0 go.opentelemetry.io/otel/sdk/metric v1.42.0 go.opentelemetry.io/otel/trace v1.42.0 - google.golang.org/grpc v1.79.2 + google.golang.org/grpc v1.79.3 google.golang.org/protobuf v1.36.11 ) diff --git a/go/go.sum b/go/go.sum index d23fe57..377d78e 100644 --- a/go/go.sum +++ b/go/go.sum @@ -41,8 +41,8 @@ gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= -google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU= -google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= +google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= From 68fad0993863e3d2ea90e991517cca3045f5ee55 Mon Sep 17 00:00:00 2001 From: "renovate-sh-app[bot]" <219655108+renovate-sh-app[bot]@users.noreply.github.com> Date: Fri, 20 Mar 2026 10:17:48 +0100 Subject: [PATCH 090/133] fix(deps): update module github.com/openai/openai-go/v3 to v3.28.0 (#584) | datasource | package | from | to | | ---------- | ------------------------------ | ------- | ------- | | go | github.com/openai/openai-go/v3 | v3.26.0 | v3.28.0 | Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> --- go-providers/openai/go.mod | 2 +- go-providers/openai/go.sum | 4 ++-- go/cmd/devex-emitter/go.mod | 2 +- go/cmd/devex-emitter/go.sum | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go-providers/openai/go.mod b/go-providers/openai/go.mod index 0e438e6..cc0b484 100644 --- a/go-providers/openai/go.mod +++ b/go-providers/openai/go.mod @@ -4,7 +4,7 @@ go 1.25.6 require ( github.com/grafana/sigil/sdks/go v0.0.0 - github.com/openai/openai-go/v3 v3.26.0 + github.com/openai/openai-go/v3 v3.28.0 ) require ( diff --git a/go-providers/openai/go.sum b/go-providers/openai/go.sum index 9261267..45eeb92 100644 --- a/go-providers/openai/go.sum +++ b/go-providers/openai/go.sum @@ -13,8 +13,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/openai/openai-go/v3 v3.26.0 h1:bRt6H/ozMNt/dDkN4gobnLqaEGrRGBzmbVs0xxJEnQE= -github.com/openai/openai-go/v3 v3.26.0/go.mod h1:cdufnVK14cWcT9qA1rRtrXx4FTRsgbDPW7Ia7SS5cZo= +github.com/openai/openai-go/v3 v3.28.0 h1:2+FfrCVMdGXSQrBv1tLWtokm+BU7+3hJ/8rAHPQ63KM= +github.com/openai/openai-go/v3 v3.28.0/go.mod h1:cdufnVK14cWcT9qA1rRtrXx4FTRsgbDPW7Ia7SS5cZo= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= diff --git a/go/cmd/devex-emitter/go.mod b/go/cmd/devex-emitter/go.mod index 2e38748..811338e 100644 --- a/go/cmd/devex-emitter/go.mod +++ b/go/cmd/devex-emitter/go.mod @@ -8,7 +8,7 @@ require ( github.com/grafana/sigil/sdks/go-providers/anthropic v0.0.0 github.com/grafana/sigil/sdks/go-providers/gemini v0.0.0 github.com/grafana/sigil/sdks/go-providers/openai v0.0.0 - github.com/openai/openai-go/v3 v3.26.0 + github.com/openai/openai-go/v3 v3.28.0 go.opentelemetry.io/otel v1.42.0 go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.42.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0 diff --git a/go/cmd/devex-emitter/go.sum b/go/cmd/devex-emitter/go.sum index 11594ef..778acab 100644 --- a/go/cmd/devex-emitter/go.sum +++ b/go/cmd/devex-emitter/go.sum @@ -37,8 +37,8 @@ github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aN github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= -github.com/openai/openai-go/v3 v3.26.0 h1:bRt6H/ozMNt/dDkN4gobnLqaEGrRGBzmbVs0xxJEnQE= -github.com/openai/openai-go/v3 v3.26.0/go.mod h1:cdufnVK14cWcT9qA1rRtrXx4FTRsgbDPW7Ia7SS5cZo= +github.com/openai/openai-go/v3 v3.28.0 h1:2+FfrCVMdGXSQrBv1tLWtokm+BU7+3hJ/8rAHPQ63KM= +github.com/openai/openai-go/v3 v3.28.0/go.mod h1:cdufnVK14cWcT9qA1rRtrXx4FTRsgbDPW7Ia7SS5cZo= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= From 8469fd611bc6a047f42a08857de5c45c0dab4709 Mon Sep 17 00:00:00 2001 From: "renovate-sh-app[bot]" <219655108+renovate-sh-app[bot]@users.noreply.github.com> Date: Fri, 20 Mar 2026 10:17:53 +0100 Subject: [PATCH 091/133] fix(deps): update dependency com.openai:openai-java to v4.28.0 (#583) | datasource | package | from | to | | ---------- | ---------------------- | ------ | ------ | | maven | com.openai:openai-java | 4.26.0 | 4.28.0 | Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> --- java/gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java/gradle/libs.versions.toml b/java/gradle/libs.versions.toml index 5b7f050..817cd5f 100644 --- a/java/gradle/libs.versions.toml +++ b/java/gradle/libs.versions.toml @@ -41,7 +41,7 @@ junit-jupiter = { module = "org.junit.jupiter:junit-jupiter" } mockwebserver = { module = "com.squareup.okhttp3:mockwebserver", version.ref = "mockwebserver" } javax-annotation = { module = "javax.annotation:javax.annotation-api", version.ref = "javaxAnnotation" } -openai-java = { module = "com.openai:openai-java", version = "4.26.0" } +openai-java = { module = "com.openai:openai-java", version = "4.28.0" } anthropic-java = { module = "com.anthropic:anthropic-java", version = "2.16.1" } google-genai = { module = "com.google.genai:google-genai", version = "1.43.0" } From b2678835d1e35d1e1e7e78598fdbe4f5c2712d1a Mon Sep 17 00:00:00 2001 From: "grafana-plugins-platform-bot[bot]" <144369747+grafana-plugins-platform-bot[bot]@users.noreply.github.com> Date: Fri, 20 Mar 2026 13:51:46 +0000 Subject: [PATCH 092/133] chore(sdk-python): bump version to 0.1.1 --- python-frameworks/google-adk/pyproject.toml | 4 ++-- python-frameworks/langchain/pyproject.toml | 4 ++-- python-frameworks/langgraph/pyproject.toml | 4 ++-- python-frameworks/llamaindex/pyproject.toml | 4 ++-- python-frameworks/openai-agents/pyproject.toml | 4 ++-- python-providers/anthropic/pyproject.toml | 4 ++-- python-providers/gemini/pyproject.toml | 4 ++-- python-providers/openai/pyproject.toml | 4 ++-- python/pyproject.toml | 2 +- 9 files changed, 17 insertions(+), 17 deletions(-) diff --git a/python-frameworks/google-adk/pyproject.toml b/python-frameworks/google-adk/pyproject.toml index 6dcf6f2..2e66316 100644 --- a/python-frameworks/google-adk/pyproject.toml +++ b/python-frameworks/google-adk/pyproject.toml @@ -1,12 +1,12 @@ [project] name = "sigil-sdk-google-adk" -version = "0.1.0" +version = "0.1.1" description = "Google ADK callback handlers for Sigil Python SDK" readme = "README.md" license = { file = "LICENSE" } requires-python = ">=3.10" dependencies = [ - "sigil-sdk>=0.1.0", + "sigil-sdk>=0.1.1", "google-adk>=1.0.0", ] diff --git a/python-frameworks/langchain/pyproject.toml b/python-frameworks/langchain/pyproject.toml index 110f282..07b113f 100644 --- a/python-frameworks/langchain/pyproject.toml +++ b/python-frameworks/langchain/pyproject.toml @@ -1,12 +1,12 @@ [project] name = "sigil-sdk-langchain" -version = "0.1.0" +version = "0.1.1" description = "LangChain callback handlers for Sigil Python SDK" readme = "README.md" license = { file = "LICENSE" } requires-python = ">=3.10" dependencies = [ - "sigil-sdk>=0.1.0", + "sigil-sdk>=0.1.1", "langchain-core>=0.3.0", ] diff --git a/python-frameworks/langgraph/pyproject.toml b/python-frameworks/langgraph/pyproject.toml index a47c530..f012019 100644 --- a/python-frameworks/langgraph/pyproject.toml +++ b/python-frameworks/langgraph/pyproject.toml @@ -1,12 +1,12 @@ [project] name = "sigil-sdk-langgraph" -version = "0.1.0" +version = "0.1.1" description = "LangGraph callback handlers for Sigil Python SDK" readme = "README.md" license = { file = "LICENSE" } requires-python = ">=3.10" dependencies = [ - "sigil-sdk>=0.1.0", + "sigil-sdk>=0.1.1", "langchain-core>=0.3.0", "langgraph>=0.2.0", ] diff --git a/python-frameworks/llamaindex/pyproject.toml b/python-frameworks/llamaindex/pyproject.toml index ed38814..67f63b5 100644 --- a/python-frameworks/llamaindex/pyproject.toml +++ b/python-frameworks/llamaindex/pyproject.toml @@ -1,12 +1,12 @@ [project] name = "sigil-sdk-llamaindex" -version = "0.1.0" +version = "0.1.1" description = "LlamaIndex callback handlers for Sigil Python SDK" readme = "README.md" license = { file = "LICENSE" } requires-python = ">=3.10" dependencies = [ - "sigil-sdk>=0.1.0", + "sigil-sdk>=0.1.1", "llama-index>=0.14.0", ] diff --git a/python-frameworks/openai-agents/pyproject.toml b/python-frameworks/openai-agents/pyproject.toml index c322874..e4e13a9 100644 --- a/python-frameworks/openai-agents/pyproject.toml +++ b/python-frameworks/openai-agents/pyproject.toml @@ -1,12 +1,12 @@ [project] name = "sigil-sdk-openai-agents" -version = "0.1.0" +version = "0.1.1" description = "OpenAI Agents callback handlers for Sigil Python SDK" readme = "README.md" license = { file = "LICENSE" } requires-python = ">=3.10" dependencies = [ - "sigil-sdk>=0.1.0", + "sigil-sdk>=0.1.1", "openai-agents>=0.9.0", ] diff --git a/python-providers/anthropic/pyproject.toml b/python-providers/anthropic/pyproject.toml index a5e1997..a1f7cb5 100644 --- a/python-providers/anthropic/pyproject.toml +++ b/python-providers/anthropic/pyproject.toml @@ -1,12 +1,12 @@ [project] name = "sigil-sdk-anthropic" -version = "0.1.0" +version = "0.1.1" description = "Anthropic helper wrappers for Sigil Python SDK" readme = "README.md" license = { file = "LICENSE" } requires-python = ">=3.10" dependencies = [ - "sigil-sdk>=0.1.0", + "sigil-sdk>=0.1.1", "anthropic>=0.79.0,<1", ] diff --git a/python-providers/gemini/pyproject.toml b/python-providers/gemini/pyproject.toml index b5f21b7..75eb35f 100644 --- a/python-providers/gemini/pyproject.toml +++ b/python-providers/gemini/pyproject.toml @@ -1,12 +1,12 @@ [project] name = "sigil-sdk-gemini" -version = "0.1.0" +version = "0.1.1" description = "Gemini helper wrappers for Sigil Python SDK" readme = "README.md" license = { file = "LICENSE" } requires-python = ">=3.10" dependencies = [ - "sigil-sdk>=0.1.0", + "sigil-sdk>=0.1.1", "google-genai>=1.63.0,<2", ] diff --git a/python-providers/openai/pyproject.toml b/python-providers/openai/pyproject.toml index ec6756f..acb930e 100644 --- a/python-providers/openai/pyproject.toml +++ b/python-providers/openai/pyproject.toml @@ -1,12 +1,12 @@ [project] name = "sigil-sdk-openai" -version = "0.1.0" +version = "0.1.1" description = "OpenAI helper wrappers for Sigil Python SDK" readme = "README.md" license = { file = "LICENSE" } requires-python = ">=3.10" dependencies = [ - "sigil-sdk>=0.1.0", + "sigil-sdk>=0.1.1", "openai>=2.20.0,<3", ] diff --git a/python/pyproject.toml b/python/pyproject.toml index efb4b30..90294cf 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "sigil-sdk" -version = "0.1.0" +version = "0.1.1" description = "Grafana Sigil Python SDK" readme = "README.md" license = { file = "LICENSE" } From 1da108ee2ec03396e4aeae606ebe8d30b34c6975 Mon Sep 17 00:00:00 2001 From: "renovate-sh-app[bot]" <219655108+renovate-sh-app[bot]@users.noreply.github.com> Date: Sat, 21 Mar 2026 10:16:01 +0100 Subject: [PATCH 093/133] fix(deps): update grpc-java monorepo to v1.80.0 (#612) | datasource | package | from | to | | ---------- | ---------------------------- | ------ | ------ | | maven | io.grpc:protoc-gen-grpc-java | 1.79.0 | 1.80.0 | | maven | io.grpc:grpc-testing | 1.79.0 | 1.80.0 | | maven | io.grpc:grpc-services | 1.79.0 | 1.80.0 | | maven | io.grpc:grpc-stub | 1.79.0 | 1.80.0 | | maven | io.grpc:grpc-protobuf | 1.79.0 | 1.80.0 | | maven | io.grpc:grpc-netty-shaded | 1.79.0 | 1.80.0 | Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> --- java/gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java/gradle/libs.versions.toml b/java/gradle/libs.versions.toml index 817cd5f..76d62cb 100644 --- a/java/gradle/libs.versions.toml +++ b/java/gradle/libs.versions.toml @@ -7,7 +7,7 @@ junit = "6.0.3" otel = "1.60.1" protobuf = "4.34.0" protobufPlugin = "0.9.6" -grpc = "1.79.0" +grpc = "1.80.0" mockwebserver = "5.3.2" javaxAnnotation = "1.3.2" From e3c8ba5dd51923d1f8668bbe4724723f1f0d7839 Mon Sep 17 00:00:00 2001 From: "renovate-sh-app[bot]" <219655108+renovate-sh-app[bot]@users.noreply.github.com> Date: Sat, 21 Mar 2026 10:16:04 +0100 Subject: [PATCH 094/133] fix(deps): update dependency com.anthropic:anthropic-java to v2.17.0 (#611) | datasource | package | from | to | | ---------- | ---------------------------- | ------ | ------ | | maven | com.anthropic:anthropic-java | 2.16.1 | 2.17.0 | Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> --- java/gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java/gradle/libs.versions.toml b/java/gradle/libs.versions.toml index 76d62cb..f392b43 100644 --- a/java/gradle/libs.versions.toml +++ b/java/gradle/libs.versions.toml @@ -42,7 +42,7 @@ mockwebserver = { module = "com.squareup.okhttp3:mockwebserver", version.ref = " javax-annotation = { module = "javax.annotation:javax.annotation-api", version.ref = "javaxAnnotation" } openai-java = { module = "com.openai:openai-java", version = "4.28.0" } -anthropic-java = { module = "com.anthropic:anthropic-java", version = "2.16.1" } +anthropic-java = { module = "com.anthropic:anthropic-java", version = "2.17.0" } google-genai = { module = "com.google.genai:google-genai", version = "1.43.0" } [plugins] From 16a4ee65add9c79384a2753554d092dcc6c4de39 Mon Sep 17 00:00:00 2001 From: "renovate-sh-app[bot]" <219655108+renovate-sh-app[bot]@users.noreply.github.com> Date: Sat, 21 Mar 2026 10:16:08 +0100 Subject: [PATCH 095/133] fix(deps): update dependency @anthropic-ai/sdk to ^0.79.0 (#610) | datasource | package | from | to | | ---------- | ----------------- | ------ | ------ | | npm | @anthropic-ai/sdk | 0.78.0 | 0.79.0 | Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> --- js/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/package.json b/js/package.json index 4edb49f..bce2216 100644 --- a/js/package.json +++ b/js/package.json @@ -53,7 +53,7 @@ "test:ci": "pnpm run test" }, "dependencies": { - "@anthropic-ai/sdk": "^0.78.0", + "@anthropic-ai/sdk": "^0.79.0", "@google/adk": "^0.5.0", "@google/genai": "^1.41.0", "@grpc/grpc-js": "^1.14.1", From 05835107f1c401404429080b9459f69228839732 Mon Sep 17 00:00:00 2001 From: "renovate-sh-app[bot]" <219655108+renovate-sh-app[bot]@users.noreply.github.com> Date: Sat, 21 Mar 2026 10:16:11 +0100 Subject: [PATCH 096/133] chore(deps): update dependency anthropic to 12.9.0 (#609) | datasource | package | from | to | | ---------- | --------- | ------ | ------ | | nuget | Anthropic | 12.8.0 | 12.9.0 | Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> --- .../src/Grafana.Sigil.Anthropic/Grafana.Sigil.Anthropic.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/src/Grafana.Sigil.Anthropic/Grafana.Sigil.Anthropic.csproj b/dotnet/src/Grafana.Sigil.Anthropic/Grafana.Sigil.Anthropic.csproj index c1c9717..e99da1a 100644 --- a/dotnet/src/Grafana.Sigil.Anthropic/Grafana.Sigil.Anthropic.csproj +++ b/dotnet/src/Grafana.Sigil.Anthropic/Grafana.Sigil.Anthropic.csproj @@ -11,7 +11,7 @@ - + From e90b143b8b5cc0b0e8074167d5a4f3e9e317d968 Mon Sep 17 00:00:00 2001 From: "renovate-sh-app[bot]" <219655108+renovate-sh-app[bot]@users.noreply.github.com> Date: Sat, 21 Mar 2026 10:16:22 +0100 Subject: [PATCH 097/133] chore(deps): update dependency coverlet.collector to 8.0.1 (#605) | datasource | package | from | to | | ---------- | ------------------ | ----- | ----- | | nuget | coverlet.collector | 8.0.0 | 8.0.1 | Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> --- .../Grafana.Sigil.Anthropic.Tests.csproj | 2 +- .../Grafana.Sigil.Gemini.Tests.csproj | 2 +- .../Grafana.Sigil.OpenAI.Tests.csproj | 2 +- dotnet/tests/Grafana.Sigil.Tests/Grafana.Sigil.Tests.csproj | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dotnet/tests/Grafana.Sigil.Anthropic.Tests/Grafana.Sigil.Anthropic.Tests.csproj b/dotnet/tests/Grafana.Sigil.Anthropic.Tests/Grafana.Sigil.Anthropic.Tests.csproj index 62a5d95..cf3d66c 100644 --- a/dotnet/tests/Grafana.Sigil.Anthropic.Tests/Grafana.Sigil.Anthropic.Tests.csproj +++ b/dotnet/tests/Grafana.Sigil.Anthropic.Tests/Grafana.Sigil.Anthropic.Tests.csproj @@ -8,7 +8,7 @@ - + diff --git a/dotnet/tests/Grafana.Sigil.Gemini.Tests/Grafana.Sigil.Gemini.Tests.csproj b/dotnet/tests/Grafana.Sigil.Gemini.Tests/Grafana.Sigil.Gemini.Tests.csproj index 1304ae0..3427a2c 100644 --- a/dotnet/tests/Grafana.Sigil.Gemini.Tests/Grafana.Sigil.Gemini.Tests.csproj +++ b/dotnet/tests/Grafana.Sigil.Gemini.Tests/Grafana.Sigil.Gemini.Tests.csproj @@ -8,7 +8,7 @@ - + diff --git a/dotnet/tests/Grafana.Sigil.OpenAI.Tests/Grafana.Sigil.OpenAI.Tests.csproj b/dotnet/tests/Grafana.Sigil.OpenAI.Tests/Grafana.Sigil.OpenAI.Tests.csproj index b4dc888..b7a9ce9 100644 --- a/dotnet/tests/Grafana.Sigil.OpenAI.Tests/Grafana.Sigil.OpenAI.Tests.csproj +++ b/dotnet/tests/Grafana.Sigil.OpenAI.Tests/Grafana.Sigil.OpenAI.Tests.csproj @@ -9,7 +9,7 @@ - + diff --git a/dotnet/tests/Grafana.Sigil.Tests/Grafana.Sigil.Tests.csproj b/dotnet/tests/Grafana.Sigil.Tests/Grafana.Sigil.Tests.csproj index d22f1da..6dbbeb3 100644 --- a/dotnet/tests/Grafana.Sigil.Tests/Grafana.Sigil.Tests.csproj +++ b/dotnet/tests/Grafana.Sigil.Tests/Grafana.Sigil.Tests.csproj @@ -8,7 +8,7 @@ - + From c5f0fee2e2b02a23ef14e9d5d77efa9c6e7c7c6b Mon Sep 17 00:00:00 2001 From: "renovate-sh-app[bot]" <219655108+renovate-sh-app[bot]@users.noreply.github.com> Date: Sun, 22 Mar 2026 10:25:09 +0100 Subject: [PATCH 098/133] chore(deps): update dependency google.genai to 1.5.0 (#621) * chore(deps): update dependency google.genai to 1.5.0 | datasource | package | from | to | | ---------- | ------------ | ----- | ----- | | nuget | Google.GenAI | 1.4.0 | 1.5.0 | Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> * chore: format plugin changelog --------- Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: Cyril Tovena --- dotnet/src/Grafana.Sigil.Gemini/Grafana.Sigil.Gemini.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/src/Grafana.Sigil.Gemini/Grafana.Sigil.Gemini.csproj b/dotnet/src/Grafana.Sigil.Gemini/Grafana.Sigil.Gemini.csproj index b0e3d59..e283af4 100644 --- a/dotnet/src/Grafana.Sigil.Gemini/Grafana.Sigil.Gemini.csproj +++ b/dotnet/src/Grafana.Sigil.Gemini/Grafana.Sigil.Gemini.csproj @@ -11,7 +11,7 @@ - + From e904ebe1576c30fc25247df749c0d1fe683b1400 Mon Sep 17 00:00:00 2001 From: "renovate-sh-app[bot]" <219655108+renovate-sh-app[bot]@users.noreply.github.com> Date: Sun, 22 Mar 2026 10:25:16 +0100 Subject: [PATCH 099/133] fix(deps): update dependency com.google.genai:google-genai to v1.44.0 (#624) * fix(deps): update dependency com.google.genai:google-genai to v1.44.0 | datasource | package | from | to | | ---------- | ----------------------------- | ------ | ------ | | maven | com.google.genai:google-genai | 1.43.0 | 1.44.0 | Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> * chore: format plugin changelog --------- Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: Cyril Tovena --- java/gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java/gradle/libs.versions.toml b/java/gradle/libs.versions.toml index f392b43..65e7e4d 100644 --- a/java/gradle/libs.versions.toml +++ b/java/gradle/libs.versions.toml @@ -43,7 +43,7 @@ javax-annotation = { module = "javax.annotation:javax.annotation-api", version.r openai-java = { module = "com.openai:openai-java", version = "4.28.0" } anthropic-java = { module = "com.anthropic:anthropic-java", version = "2.17.0" } -google-genai = { module = "com.google.genai:google-genai", version = "1.43.0" } +google-genai = { module = "com.google.genai:google-genai", version = "1.44.0" } [plugins] protobuf = { id = "com.google.protobuf", version.ref = "protobufPlugin" } From 4455e21b205696b6b3a62dee6f33784626853479 Mon Sep 17 00:00:00 2001 From: "renovate-sh-app[bot]" <219655108+renovate-sh-app[bot]@users.noreply.github.com> Date: Sun, 22 Mar 2026 10:25:20 +0100 Subject: [PATCH 100/133] fix(deps): update dependency com.openai:openai-java to v4.29.0 (#625) * fix(deps): update dependency com.openai:openai-java to v4.29.0 | datasource | package | from | to | | ---------- | ---------------------- | ------ | ------ | | maven | com.openai:openai-java | 4.28.0 | 4.29.0 | Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> * chore: format plugin changelog --------- Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: Cyril Tovena --- java/gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java/gradle/libs.versions.toml b/java/gradle/libs.versions.toml index 65e7e4d..4cc9dd2 100644 --- a/java/gradle/libs.versions.toml +++ b/java/gradle/libs.versions.toml @@ -41,7 +41,7 @@ junit-jupiter = { module = "org.junit.jupiter:junit-jupiter" } mockwebserver = { module = "com.squareup.okhttp3:mockwebserver", version.ref = "mockwebserver" } javax-annotation = { module = "javax.annotation:javax.annotation-api", version.ref = "javaxAnnotation" } -openai-java = { module = "com.openai:openai-java", version = "4.28.0" } +openai-java = { module = "com.openai:openai-java", version = "4.29.0" } anthropic-java = { module = "com.anthropic:anthropic-java", version = "2.17.0" } google-genai = { module = "com.google.genai:google-genai", version = "1.44.0" } From 5b726c0daaf4489fdebbd39fd82a7d7f6b149086 Mon Sep 17 00:00:00 2001 From: "renovate-sh-app[bot]" <219655108+renovate-sh-app[bot]@users.noreply.github.com> Date: Sun, 22 Mar 2026 10:25:24 +0100 Subject: [PATCH 101/133] fix(deps): update module github.com/anthropics/anthropic-sdk-go to v1.27.1 (#626) * fix(deps): update module github.com/anthropics/anthropic-sdk-go to v1.27.1 | datasource | package | from | to | | ---------- | -------------------------------------- | ------- | ------- | | go | github.com/anthropics/anthropic-sdk-go | v1.26.0 | v1.27.1 | Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> * chore: format plugin changelog * fix: adapt anthropic thinking config for v1.27 --------- Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: Cyril Tovena --- go-providers/anthropic/go.mod | 2 +- go-providers/anthropic/go.sum | 4 ++-- go/cmd/devex-emitter/go.mod | 2 +- go/cmd/devex-emitter/go.sum | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go-providers/anthropic/go.mod b/go-providers/anthropic/go.mod index 2c17d2f..68c58a5 100644 --- a/go-providers/anthropic/go.mod +++ b/go-providers/anthropic/go.mod @@ -3,7 +3,7 @@ module github.com/grafana/sigil/sdks/go-providers/anthropic go 1.25.6 require ( - github.com/anthropics/anthropic-sdk-go v1.26.0 + github.com/anthropics/anthropic-sdk-go v1.27.1 github.com/grafana/sigil/sdks/go v0.0.0 ) diff --git a/go-providers/anthropic/go.sum b/go-providers/anthropic/go.sum index ba8e433..01dcbd0 100644 --- a/go-providers/anthropic/go.sum +++ b/go-providers/anthropic/go.sum @@ -1,5 +1,5 @@ -github.com/anthropics/anthropic-sdk-go v1.26.0 h1:oUTzFaUpAevfuELAP1sjL6CQJ9HHAfT7CoSYSac11PY= -github.com/anthropics/anthropic-sdk-go v1.26.0/go.mod h1:qUKmaW+uuPB64iy1l+4kOSvaLqPXnHTTBKH6RVZ7q5Q= +github.com/anthropics/anthropic-sdk-go v1.27.1 h1:7DgMZ2Ng3C2mPzJGHA30NXQTZolcF07mHd0tGaLwfzk= +github.com/anthropics/anthropic-sdk-go v1.27.1/go.mod h1:qUKmaW+uuPB64iy1l+4kOSvaLqPXnHTTBKH6RVZ7q5Q= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= diff --git a/go/cmd/devex-emitter/go.mod b/go/cmd/devex-emitter/go.mod index 811338e..42627af 100644 --- a/go/cmd/devex-emitter/go.mod +++ b/go/cmd/devex-emitter/go.mod @@ -3,7 +3,7 @@ module github.com/grafana/sigil/sdks/go/cmd/devex-emitter go 1.25.6 require ( - github.com/anthropics/anthropic-sdk-go v1.26.0 + github.com/anthropics/anthropic-sdk-go v1.27.1 github.com/grafana/sigil/sdks/go v0.0.0 github.com/grafana/sigil/sdks/go-providers/anthropic v0.0.0 github.com/grafana/sigil/sdks/go-providers/gemini v0.0.0 diff --git a/go/cmd/devex-emitter/go.sum b/go/cmd/devex-emitter/go.sum index 778acab..1ec04b4 100644 --- a/go/cmd/devex-emitter/go.sum +++ b/go/cmd/devex-emitter/go.sum @@ -4,8 +4,8 @@ cloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM= cloud.google.com/go/auth v0.18.2/go.mod h1:xD+oY7gcahcu7G2SG2DsBerfFxgPAJz17zz2joOFF3M= cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= -github.com/anthropics/anthropic-sdk-go v1.26.0 h1:oUTzFaUpAevfuELAP1sjL6CQJ9HHAfT7CoSYSac11PY= -github.com/anthropics/anthropic-sdk-go v1.26.0/go.mod h1:qUKmaW+uuPB64iy1l+4kOSvaLqPXnHTTBKH6RVZ7q5Q= +github.com/anthropics/anthropic-sdk-go v1.27.1 h1:7DgMZ2Ng3C2mPzJGHA30NXQTZolcF07mHd0tGaLwfzk= +github.com/anthropics/anthropic-sdk-go v1.27.1/go.mod h1:qUKmaW+uuPB64iy1l+4kOSvaLqPXnHTTBKH6RVZ7q5Q= github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= From 8c33928c7a3d2879ce161825213073bd66973e59 Mon Sep 17 00:00:00 2001 From: "renovate-sh-app[bot]" <219655108+renovate-sh-app[bot]@users.noreply.github.com> Date: Mon, 23 Mar 2026 10:16:33 +0100 Subject: [PATCH 102/133] chore(deps): update gradle to v9.4.1 (#629) | datasource | package | from | to | | -------------- | ------- | ----- | ----- | | gradle-version | gradle | 9.4.0 | 9.4.1 | Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> --- java/gradle/wrapper/gradle-wrapper.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java/gradle/wrapper/gradle-wrapper.properties b/java/gradle/wrapper/gradle-wrapper.properties index dbc3ce4..c61a118 100644 --- a/java/gradle/wrapper/gradle-wrapper.properties +++ b/java/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.0-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME From 72446cf1db36c1c3e7d79a247e483f3b66df4192 Mon Sep 17 00:00:00 2001 From: "renovate-sh-app[bot]" <219655108+renovate-sh-app[bot]@users.noreply.github.com> Date: Mon, 23 Mar 2026 10:16:37 +0100 Subject: [PATCH 103/133] fix(deps): update dependency @anthropic-ai/sdk to ^0.80.0 (#630) | datasource | package | from | to | | ---------- | ----------------- | ------ | ------ | | npm | @anthropic-ai/sdk | 0.79.0 | 0.80.0 | Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> --- js/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/package.json b/js/package.json index bce2216..1f0c020 100644 --- a/js/package.json +++ b/js/package.json @@ -53,7 +53,7 @@ "test:ci": "pnpm run test" }, "dependencies": { - "@anthropic-ai/sdk": "^0.79.0", + "@anthropic-ai/sdk": "^0.80.0", "@google/adk": "^0.5.0", "@google/genai": "^1.41.0", "@grpc/grpc-js": "^1.14.1", From 799b6800fcf0792ee4ee13af5a26dc5b3e1507d5 Mon Sep 17 00:00:00 2001 From: "renovate-sh-app[bot]" <219655108+renovate-sh-app[bot]@users.noreply.github.com> Date: Mon, 23 Mar 2026 10:16:41 +0100 Subject: [PATCH 104/133] fix(deps): update dependency com.anthropic:anthropic-java to v2.18.0 (#631) | datasource | package | from | to | | ---------- | ---------------------------- | ------ | ------ | | maven | com.anthropic:anthropic-java | 2.17.0 | 2.18.0 | Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> --- java/gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java/gradle/libs.versions.toml b/java/gradle/libs.versions.toml index 4cc9dd2..ab6ef8d 100644 --- a/java/gradle/libs.versions.toml +++ b/java/gradle/libs.versions.toml @@ -42,7 +42,7 @@ mockwebserver = { module = "com.squareup.okhttp3:mockwebserver", version.ref = " javax-annotation = { module = "javax.annotation:javax.annotation-api", version.ref = "javaxAnnotation" } openai-java = { module = "com.openai:openai-java", version = "4.29.0" } -anthropic-java = { module = "com.anthropic:anthropic-java", version = "2.17.0" } +anthropic-java = { module = "com.anthropic:anthropic-java", version = "2.18.0" } google-genai = { module = "com.google.genai:google-genai", version = "1.44.0" } [plugins] From 3a18ae1cb8ce3c464cd20b004e4fe35db29f3242 Mon Sep 17 00:00:00 2001 From: "renovate-sh-app[bot]" <219655108+renovate-sh-app[bot]@users.noreply.github.com> Date: Mon, 23 Mar 2026 10:16:45 +0100 Subject: [PATCH 105/133] fix(deps): update module github.com/grafana/sigil/sdks/go to v0.1.1 (#632) | datasource | package | from | to | | ---------- | -------------------------------- | ------ | ------ | | go | github.com/grafana/sigil/sdks/go | v0.0.0 | v0.1.1 | Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> --- go-frameworks/google-adk/go.mod | 2 +- go-providers/anthropic/go.mod | 2 +- go-providers/gemini/go.mod | 2 +- go-providers/openai/go.mod | 2 +- go/cmd/devex-emitter/go.mod | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/go-frameworks/google-adk/go.mod b/go-frameworks/google-adk/go.mod index 4ca2490..2ca0c93 100644 --- a/go-frameworks/google-adk/go.mod +++ b/go-frameworks/google-adk/go.mod @@ -3,7 +3,7 @@ module github.com/grafana/sigil/sdks/go-frameworks/google-adk go 1.25.6 require ( - github.com/grafana/sigil/sdks/go v0.0.0 + github.com/grafana/sigil/sdks/go v0.1.1 go.opentelemetry.io/otel v1.42.0 go.opentelemetry.io/otel/sdk v1.42.0 go.opentelemetry.io/otel/sdk/metric v1.42.0 diff --git a/go-providers/anthropic/go.mod b/go-providers/anthropic/go.mod index 68c58a5..f1bc121 100644 --- a/go-providers/anthropic/go.mod +++ b/go-providers/anthropic/go.mod @@ -4,7 +4,7 @@ go 1.25.6 require ( github.com/anthropics/anthropic-sdk-go v1.27.1 - github.com/grafana/sigil/sdks/go v0.0.0 + github.com/grafana/sigil/sdks/go v0.1.1 ) require ( diff --git a/go-providers/gemini/go.mod b/go-providers/gemini/go.mod index eb0e79b..b7f7523 100644 --- a/go-providers/gemini/go.mod +++ b/go-providers/gemini/go.mod @@ -3,7 +3,7 @@ module github.com/grafana/sigil/sdks/go-providers/gemini go 1.25.6 require ( - github.com/grafana/sigil/sdks/go v0.0.0 + github.com/grafana/sigil/sdks/go v0.1.1 google.golang.org/genai v1.50.0 ) diff --git a/go-providers/openai/go.mod b/go-providers/openai/go.mod index cc0b484..3930bd6 100644 --- a/go-providers/openai/go.mod +++ b/go-providers/openai/go.mod @@ -3,7 +3,7 @@ module github.com/grafana/sigil/sdks/go-providers/openai go 1.25.6 require ( - github.com/grafana/sigil/sdks/go v0.0.0 + github.com/grafana/sigil/sdks/go v0.1.1 github.com/openai/openai-go/v3 v3.28.0 ) diff --git a/go/cmd/devex-emitter/go.mod b/go/cmd/devex-emitter/go.mod index 42627af..3f1a120 100644 --- a/go/cmd/devex-emitter/go.mod +++ b/go/cmd/devex-emitter/go.mod @@ -4,7 +4,7 @@ go 1.25.6 require ( github.com/anthropics/anthropic-sdk-go v1.27.1 - github.com/grafana/sigil/sdks/go v0.0.0 + github.com/grafana/sigil/sdks/go v0.1.1 github.com/grafana/sigil/sdks/go-providers/anthropic v0.0.0 github.com/grafana/sigil/sdks/go-providers/gemini v0.0.0 github.com/grafana/sigil/sdks/go-providers/openai v0.0.0 From a8ccf65facb4179bf2c6366c51254c0ec4158613 Mon Sep 17 00:00:00 2001 From: "renovate-sh-app[bot]" <219655108+renovate-sh-app[bot]@users.noreply.github.com> Date: Mon, 23 Mar 2026 10:16:51 +0100 Subject: [PATCH 106/133] fix(deps): update module github.com/grafana/sigil/sdks/go-providers/gemini to v0.1.1 (#634) | datasource | package | from | to | | ---------- | ------------------------------------------------- | ------ | ------ | | go | github.com/grafana/sigil/sdks/go-providers/gemini | v0.0.0 | v0.1.1 | Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> --- go/cmd/devex-emitter/go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go/cmd/devex-emitter/go.mod b/go/cmd/devex-emitter/go.mod index 3f1a120..9dff98a 100644 --- a/go/cmd/devex-emitter/go.mod +++ b/go/cmd/devex-emitter/go.mod @@ -6,7 +6,7 @@ require ( github.com/anthropics/anthropic-sdk-go v1.27.1 github.com/grafana/sigil/sdks/go v0.1.1 github.com/grafana/sigil/sdks/go-providers/anthropic v0.0.0 - github.com/grafana/sigil/sdks/go-providers/gemini v0.0.0 + github.com/grafana/sigil/sdks/go-providers/gemini v0.1.1 github.com/grafana/sigil/sdks/go-providers/openai v0.0.0 github.com/openai/openai-go/v3 v3.28.0 go.opentelemetry.io/otel v1.42.0 From 6ce181e1ee10d28d16d27a538aff660433788666 Mon Sep 17 00:00:00 2001 From: "renovate-sh-app[bot]" <219655108+renovate-sh-app[bot]@users.noreply.github.com> Date: Mon, 23 Mar 2026 10:19:14 +0100 Subject: [PATCH 107/133] fix(deps): update module github.com/grafana/sigil/sdks/go-providers/anthropic to v0.1.1 (#633) | datasource | package | from | to | | ---------- | ---------------------------------------------------- | ------ | ------ | | go | github.com/grafana/sigil/sdks/go-providers/anthropic | v0.0.0 | v0.1.1 | Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: Cyril Tovena --- go/cmd/devex-emitter/go.mod | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/go/cmd/devex-emitter/go.mod b/go/cmd/devex-emitter/go.mod index 9dff98a..d92d9ae 100644 --- a/go/cmd/devex-emitter/go.mod +++ b/go/cmd/devex-emitter/go.mod @@ -4,9 +4,9 @@ go 1.25.6 require ( github.com/anthropics/anthropic-sdk-go v1.27.1 - github.com/grafana/sigil/sdks/go v0.1.1 - github.com/grafana/sigil/sdks/go-providers/anthropic v0.0.0 - github.com/grafana/sigil/sdks/go-providers/gemini v0.1.1 + github.com/grafana/sigil/sdks/go v0.0.0 + github.com/grafana/sigil/sdks/go-providers/anthropic v0.1.1 + github.com/grafana/sigil/sdks/go-providers/gemini v0.0.0 github.com/grafana/sigil/sdks/go-providers/openai v0.0.0 github.com/openai/openai-go/v3 v3.28.0 go.opentelemetry.io/otel v1.42.0 From 395b52160fcc89d08fbef589726b8217b5e02f15 Mon Sep 17 00:00:00 2001 From: "renovate-sh-app[bot]" <219655108+renovate-sh-app[bot]@users.noreply.github.com> Date: Mon, 23 Mar 2026 10:19:21 +0100 Subject: [PATCH 108/133] fix(deps): update module github.com/openai/openai-go/v3 to v3.29.0 (#637) | datasource | package | from | to | | ---------- | ------------------------------ | ------- | ------- | | go | github.com/openai/openai-go/v3 | v3.28.0 | v3.29.0 | Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: Cyril Tovena --- go-providers/openai/go.mod | 4 ++-- go-providers/openai/go.sum | 4 ++-- go/cmd/devex-emitter/go.mod | 2 +- go/cmd/devex-emitter/go.sum | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/go-providers/openai/go.mod b/go-providers/openai/go.mod index 3930bd6..1fc8b0d 100644 --- a/go-providers/openai/go.mod +++ b/go-providers/openai/go.mod @@ -3,8 +3,8 @@ module github.com/grafana/sigil/sdks/go-providers/openai go 1.25.6 require ( - github.com/grafana/sigil/sdks/go v0.1.1 - github.com/openai/openai-go/v3 v3.28.0 + github.com/grafana/sigil/sdks/go v0.0.0 + github.com/openai/openai-go/v3 v3.29.0 ) require ( diff --git a/go-providers/openai/go.sum b/go-providers/openai/go.sum index 45eeb92..2f3f140 100644 --- a/go-providers/openai/go.sum +++ b/go-providers/openai/go.sum @@ -13,8 +13,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/openai/openai-go/v3 v3.28.0 h1:2+FfrCVMdGXSQrBv1tLWtokm+BU7+3hJ/8rAHPQ63KM= -github.com/openai/openai-go/v3 v3.28.0/go.mod h1:cdufnVK14cWcT9qA1rRtrXx4FTRsgbDPW7Ia7SS5cZo= +github.com/openai/openai-go/v3 v3.29.0 h1:dZNJ0w7DxwpgppzKQjSKfLebW27KrtGqgSy4ipJS0U8= +github.com/openai/openai-go/v3 v3.29.0/go.mod h1:cdufnVK14cWcT9qA1rRtrXx4FTRsgbDPW7Ia7SS5cZo= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= diff --git a/go/cmd/devex-emitter/go.mod b/go/cmd/devex-emitter/go.mod index d92d9ae..88e77e1 100644 --- a/go/cmd/devex-emitter/go.mod +++ b/go/cmd/devex-emitter/go.mod @@ -8,7 +8,7 @@ require ( github.com/grafana/sigil/sdks/go-providers/anthropic v0.1.1 github.com/grafana/sigil/sdks/go-providers/gemini v0.0.0 github.com/grafana/sigil/sdks/go-providers/openai v0.0.0 - github.com/openai/openai-go/v3 v3.28.0 + github.com/openai/openai-go/v3 v3.29.0 go.opentelemetry.io/otel v1.42.0 go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.42.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0 diff --git a/go/cmd/devex-emitter/go.sum b/go/cmd/devex-emitter/go.sum index 1ec04b4..db074aa 100644 --- a/go/cmd/devex-emitter/go.sum +++ b/go/cmd/devex-emitter/go.sum @@ -37,8 +37,8 @@ github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aN github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= -github.com/openai/openai-go/v3 v3.28.0 h1:2+FfrCVMdGXSQrBv1tLWtokm+BU7+3hJ/8rAHPQ63KM= -github.com/openai/openai-go/v3 v3.28.0/go.mod h1:cdufnVK14cWcT9qA1rRtrXx4FTRsgbDPW7Ia7SS5cZo= +github.com/openai/openai-go/v3 v3.29.0 h1:dZNJ0w7DxwpgppzKQjSKfLebW27KrtGqgSy4ipJS0U8= +github.com/openai/openai-go/v3 v3.29.0/go.mod h1:cdufnVK14cWcT9qA1rRtrXx4FTRsgbDPW7Ia7SS5cZo= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= From 010802d439190fbdde9415922f36509014ce43b7 Mon Sep 17 00:00:00 2001 From: "renovate-sh-app[bot]" <219655108+renovate-sh-app[bot]@users.noreply.github.com> Date: Mon, 23 Mar 2026 10:19:48 +0100 Subject: [PATCH 109/133] fix(deps): update module github.com/grafana/sigil/sdks/go-providers/openai to v0.1.1 (#635) | datasource | package | from | to | | ---------- | ------------------------------------------------- | ------ | ------ | | go | github.com/grafana/sigil/sdks/go-providers/openai | v0.0.0 | v0.1.1 | Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: Cyril Tovena --- go/cmd/devex-emitter/go.mod | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/go/cmd/devex-emitter/go.mod b/go/cmd/devex-emitter/go.mod index 88e77e1..a7576d8 100644 --- a/go/cmd/devex-emitter/go.mod +++ b/go/cmd/devex-emitter/go.mod @@ -5,10 +5,10 @@ go 1.25.6 require ( github.com/anthropics/anthropic-sdk-go v1.27.1 github.com/grafana/sigil/sdks/go v0.0.0 - github.com/grafana/sigil/sdks/go-providers/anthropic v0.1.1 + github.com/grafana/sigil/sdks/go-providers/anthropic v0.0.0 github.com/grafana/sigil/sdks/go-providers/gemini v0.0.0 - github.com/grafana/sigil/sdks/go-providers/openai v0.0.0 - github.com/openai/openai-go/v3 v3.29.0 + github.com/grafana/sigil/sdks/go-providers/openai v0.1.1 + github.com/openai/openai-go/v3 v3.28.0 go.opentelemetry.io/otel v1.42.0 go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.42.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0 From 8c127e9d99c7dfc4c891cbd88e65a350bf285159 Mon Sep 17 00:00:00 2001 From: "renovate-sh-app[bot]" <219655108+renovate-sh-app[bot]@users.noreply.github.com> Date: Tue, 24 Mar 2026 10:16:30 +0100 Subject: [PATCH 110/133] fix(deps): update module google.golang.org/genai to v1.51.0 (#648) | datasource | package | from | to | | ---------- | ----------------------- | ------- | ------- | | go | google.golang.org/genai | v1.50.0 | v1.51.0 | Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> --- go-providers/gemini/go.mod | 2 +- go-providers/gemini/go.sum | 4 ++-- go/cmd/devex-emitter/go.mod | 6 +++--- go/cmd/devex-emitter/go.sum | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/go-providers/gemini/go.mod b/go-providers/gemini/go.mod index b7f7523..aa5480b 100644 --- a/go-providers/gemini/go.mod +++ b/go-providers/gemini/go.mod @@ -4,7 +4,7 @@ go 1.25.6 require ( github.com/grafana/sigil/sdks/go v0.1.1 - google.golang.org/genai v1.50.0 + google.golang.org/genai v1.51.0 ) require ( diff --git a/go-providers/gemini/go.sum b/go-providers/gemini/go.sum index 75289e6..2ca3732 100644 --- a/go-providers/gemini/go.sum +++ b/go-providers/gemini/go.sum @@ -61,8 +61,8 @@ golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/genai v1.50.0 h1:yHKV/vjoeN9PJ3iF0ur4cBZco4N3Kl7j09rMq7XSoWk= -google.golang.org/genai v1.50.0/go.mod h1:A3kkl0nyBjyFlNjgxIwKq70julKbIxpSxqKO5gw/gmk= +google.golang.org/genai v1.51.0 h1:IZGuUqgfx40INv3hLFGCbOSGp0qFqm7LVmDghzNIYqg= +google.golang.org/genai v1.51.0/go.mod h1:A3kkl0nyBjyFlNjgxIwKq70julKbIxpSxqKO5gw/gmk= google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= diff --git a/go/cmd/devex-emitter/go.mod b/go/cmd/devex-emitter/go.mod index a7576d8..8b2e9a3 100644 --- a/go/cmd/devex-emitter/go.mod +++ b/go/cmd/devex-emitter/go.mod @@ -4,11 +4,11 @@ go 1.25.6 require ( github.com/anthropics/anthropic-sdk-go v1.27.1 - github.com/grafana/sigil/sdks/go v0.0.0 + github.com/grafana/sigil/sdks/go v0.1.1 github.com/grafana/sigil/sdks/go-providers/anthropic v0.0.0 github.com/grafana/sigil/sdks/go-providers/gemini v0.0.0 github.com/grafana/sigil/sdks/go-providers/openai v0.1.1 - github.com/openai/openai-go/v3 v3.28.0 + github.com/openai/openai-go/v3 v3.29.0 go.opentelemetry.io/otel v1.42.0 go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.42.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0 @@ -16,7 +16,7 @@ require ( go.opentelemetry.io/otel/sdk v1.42.0 go.opentelemetry.io/otel/sdk/metric v1.42.0 go.opentelemetry.io/otel/trace v1.42.0 - google.golang.org/genai v1.50.0 + google.golang.org/genai v1.51.0 ) require ( diff --git a/go/cmd/devex-emitter/go.sum b/go/cmd/devex-emitter/go.sum index db074aa..38b7536 100644 --- a/go/cmd/devex-emitter/go.sum +++ b/go/cmd/devex-emitter/go.sum @@ -89,8 +89,8 @@ golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/genai v1.50.0 h1:yHKV/vjoeN9PJ3iF0ur4cBZco4N3Kl7j09rMq7XSoWk= -google.golang.org/genai v1.50.0/go.mod h1:A3kkl0nyBjyFlNjgxIwKq70julKbIxpSxqKO5gw/gmk= +google.golang.org/genai v1.51.0 h1:IZGuUqgfx40INv3hLFGCbOSGp0qFqm7LVmDghzNIYqg= +google.golang.org/genai v1.51.0/go.mod h1:A3kkl0nyBjyFlNjgxIwKq70julKbIxpSxqKO5gw/gmk= google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 h1:JLQynH/LBHfCTSbDWl+py8C+Rg/k1OVH3xfcaiANuF0= google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:kSJwQxqmFXeo79zOmbrALdflXQeAYcUbgS7PbpMknCY= google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac= From 091c4f1a8fa33632174626d9612d9f0478eaef28 Mon Sep 17 00:00:00 2001 From: "renovate-sh-app[bot]" <219655108+renovate-sh-app[bot]@users.noreply.github.com> Date: Tue, 24 Mar 2026 10:16:35 +0100 Subject: [PATCH 111/133] fix(deps): update module github.com/openai/openai-go/v3 to v3.29.0 (#647) | datasource | package | from | to | | ---------- | ------------------------------ | ------- | ------- | | go | github.com/openai/openai-go/v3 | v3.28.0 | v3.29.0 | Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> From 94e9602149d5e984ba2fb83bb4d0a8ee3415d81f Mon Sep 17 00:00:00 2001 From: "renovate-sh-app[bot]" <219655108+renovate-sh-app[bot]@users.noreply.github.com> Date: Tue, 24 Mar 2026 10:16:40 +0100 Subject: [PATCH 112/133] fix(deps): update module github.com/grafana/sigil/sdks/go-providers/gemini to v0.1.1 (#646) | datasource | package | from | to | | ---------- | ------------------------------------------------- | ------ | ------ | | go | github.com/grafana/sigil/sdks/go-providers/gemini | v0.0.0 | v0.1.1 | Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> --- go/cmd/devex-emitter/go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go/cmd/devex-emitter/go.mod b/go/cmd/devex-emitter/go.mod index 8b2e9a3..bdb7d33 100644 --- a/go/cmd/devex-emitter/go.mod +++ b/go/cmd/devex-emitter/go.mod @@ -6,7 +6,7 @@ require ( github.com/anthropics/anthropic-sdk-go v1.27.1 github.com/grafana/sigil/sdks/go v0.1.1 github.com/grafana/sigil/sdks/go-providers/anthropic v0.0.0 - github.com/grafana/sigil/sdks/go-providers/gemini v0.0.0 + github.com/grafana/sigil/sdks/go-providers/gemini v0.1.1 github.com/grafana/sigil/sdks/go-providers/openai v0.1.1 github.com/openai/openai-go/v3 v3.29.0 go.opentelemetry.io/otel v1.42.0 From 5ff50c7dfd15479fe016b0379230758820a6ebb6 Mon Sep 17 00:00:00 2001 From: "renovate-sh-app[bot]" <219655108+renovate-sh-app[bot]@users.noreply.github.com> Date: Tue, 24 Mar 2026 10:16:45 +0100 Subject: [PATCH 113/133] fix(deps): update module github.com/grafana/sigil/sdks/go to v0.1.1 (#644) | datasource | package | from | to | | ---------- | -------------------------------- | ------ | ------ | | go | github.com/grafana/sigil/sdks/go | v0.0.0 | v0.1.1 | Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> --- go-providers/openai/go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go-providers/openai/go.mod b/go-providers/openai/go.mod index 1fc8b0d..2093493 100644 --- a/go-providers/openai/go.mod +++ b/go-providers/openai/go.mod @@ -3,7 +3,7 @@ module github.com/grafana/sigil/sdks/go-providers/openai go 1.25.6 require ( - github.com/grafana/sigil/sdks/go v0.0.0 + github.com/grafana/sigil/sdks/go v0.1.1 github.com/openai/openai-go/v3 v3.29.0 ) From eb58b446e5c5ff4eec7e977ff174506f56862474 Mon Sep 17 00:00:00 2001 From: "renovate-sh-app[bot]" <219655108+renovate-sh-app[bot]@users.noreply.github.com> Date: Tue, 24 Mar 2026 10:17:56 +0100 Subject: [PATCH 114/133] fix(deps): update module github.com/grafana/sigil/sdks/go-providers/anthropic to v0.1.1 (#645) | datasource | package | from | to | | ---------- | ---------------------------------------------------- | ------ | ------ | | go | github.com/grafana/sigil/sdks/go-providers/anthropic | v0.0.0 | v0.1.1 | Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: Cyril Tovena --- go/cmd/devex-emitter/go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go/cmd/devex-emitter/go.mod b/go/cmd/devex-emitter/go.mod index bdb7d33..f9a682a 100644 --- a/go/cmd/devex-emitter/go.mod +++ b/go/cmd/devex-emitter/go.mod @@ -5,7 +5,7 @@ go 1.25.6 require ( github.com/anthropics/anthropic-sdk-go v1.27.1 github.com/grafana/sigil/sdks/go v0.1.1 - github.com/grafana/sigil/sdks/go-providers/anthropic v0.0.0 + github.com/grafana/sigil/sdks/go-providers/anthropic v0.1.1 github.com/grafana/sigil/sdks/go-providers/gemini v0.1.1 github.com/grafana/sigil/sdks/go-providers/openai v0.1.1 github.com/openai/openai-go/v3 v3.29.0 From 88bee8cd027f19edfa5b8c3cddee12f52a933ad3 Mon Sep 17 00:00:00 2001 From: Sven Grossmann Date: Tue, 24 Mar 2026 12:56:33 +0100 Subject: [PATCH 115/133] fix(sdk-python): lowercase gRPC metadata keys to avoid illegal header errors (#614) --- python/sigil_sdk/exporters/grpc.py | 2 +- python/tests/test_transport.py | 44 ++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/python/sigil_sdk/exporters/grpc.py b/python/sigil_sdk/exporters/grpc.py index 6eefbe3..d8cea91 100644 --- a/python/sigil_sdk/exporters/grpc.py +++ b/python/sigil_sdk/exporters/grpc.py @@ -17,7 +17,7 @@ class GRPCGenerationExporter: def __init__(self, endpoint: str, headers: dict[str, str] | None = None, insecure: bool = False) -> None: host, implicit_insecure = _parse_endpoint(endpoint) - self._headers = list((headers or {}).items()) + self._headers = [(k.lower(), v) for k, v in (headers or {}).items()] self._channel = grpc.insecure_channel(host) if (insecure or implicit_insecure) else grpc.secure_channel(host, grpc.ssl_channel_credentials()) self._stub = sigil_pb2_grpc.GenerationIngestServiceStub(self._channel) diff --git a/python/tests/test_transport.py b/python/tests/test_transport.py index d1857dd..5307f7c 100644 --- a/python/tests/test_transport.py +++ b/python/tests/test_transport.py @@ -265,6 +265,50 @@ def test_sdk_generation_auth_bearer_over_grpc_with_header_override() -> None: grpc_server.stop(grace=0) +def test_grpc_metadata_keys_are_lowercased() -> None: + """Mixed-case header keys must be lowercased for gRPC metadata (grpcio rejects uppercase).""" + servicer = _CapturingGenerationServicer() + grpc_server = grpc.server(thread_pool=__import__("concurrent.futures").futures.ThreadPoolExecutor(max_workers=2)) + sigil_pb2_grpc.add_GenerationIngestServiceServicer_to_server(servicer, grpc_server) + + sock = socket.socket() + sock.bind(("127.0.0.1", 0)) + port = sock.getsockname()[1] + sock.close() + + grpc_server.add_insecure_port(f"127.0.0.1:{port}") + grpc_server.start() + + client = _new_client( + GenerationExportConfig( + protocol="grpc", + endpoint=f"127.0.0.1:{port}", + insecure=True, + auth=AuthConfig(mode="tenant", tenant_id="12345"), + batch_size=1, + flush_interval=timedelta(seconds=1), + max_retries=1, + initial_backoff=timedelta(milliseconds=1), + max_backoff=timedelta(milliseconds=10), + ) + ) + + try: + start, result = _payload_fixture() + rec = client.start_generation(start) + rec.set_result(result) + rec.end() + assert rec.err() is None + client.shutdown() + + assert len(servicer.metadata) == 1 + meta = servicer.metadata[0] + assert meta.get("x-scope-orgid") == "12345" + assert not any(k != k.lower() for k in meta) + finally: + grpc_server.stop(grace=0) + + def _assert_generation_json_payload(generation: dict[str, Any]) -> None: assert generation["id"] == "gen-fixture-1" assert generation["conversation_id"] == "conv-fixture-1" From ffb49c00213e0ab4dcbb4bf20e5afff38f73d1b2 Mon Sep 17 00:00:00 2001 From: "grafana-plugins-platform-bot[bot]" <144369747+grafana-plugins-platform-bot[bot]@users.noreply.github.com> Date: Tue, 24 Mar 2026 12:48:18 +0000 Subject: [PATCH 116/133] chore(sdk-python): bump version to 0.1.2 --- python-frameworks/google-adk/pyproject.toml | 4 ++-- python-frameworks/langchain/pyproject.toml | 4 ++-- python-frameworks/langgraph/pyproject.toml | 4 ++-- python-frameworks/llamaindex/pyproject.toml | 4 ++-- python-frameworks/openai-agents/pyproject.toml | 4 ++-- python-providers/anthropic/pyproject.toml | 4 ++-- python-providers/gemini/pyproject.toml | 4 ++-- python-providers/openai/pyproject.toml | 4 ++-- python/pyproject.toml | 2 +- 9 files changed, 17 insertions(+), 17 deletions(-) diff --git a/python-frameworks/google-adk/pyproject.toml b/python-frameworks/google-adk/pyproject.toml index 2e66316..ba7a5a4 100644 --- a/python-frameworks/google-adk/pyproject.toml +++ b/python-frameworks/google-adk/pyproject.toml @@ -1,12 +1,12 @@ [project] name = "sigil-sdk-google-adk" -version = "0.1.1" +version = "0.1.2" description = "Google ADK callback handlers for Sigil Python SDK" readme = "README.md" license = { file = "LICENSE" } requires-python = ">=3.10" dependencies = [ - "sigil-sdk>=0.1.1", + "sigil-sdk>=0.1.2", "google-adk>=1.0.0", ] diff --git a/python-frameworks/langchain/pyproject.toml b/python-frameworks/langchain/pyproject.toml index 07b113f..be11da9 100644 --- a/python-frameworks/langchain/pyproject.toml +++ b/python-frameworks/langchain/pyproject.toml @@ -1,12 +1,12 @@ [project] name = "sigil-sdk-langchain" -version = "0.1.1" +version = "0.1.2" description = "LangChain callback handlers for Sigil Python SDK" readme = "README.md" license = { file = "LICENSE" } requires-python = ">=3.10" dependencies = [ - "sigil-sdk>=0.1.1", + "sigil-sdk>=0.1.2", "langchain-core>=0.3.0", ] diff --git a/python-frameworks/langgraph/pyproject.toml b/python-frameworks/langgraph/pyproject.toml index f012019..45e0b25 100644 --- a/python-frameworks/langgraph/pyproject.toml +++ b/python-frameworks/langgraph/pyproject.toml @@ -1,12 +1,12 @@ [project] name = "sigil-sdk-langgraph" -version = "0.1.1" +version = "0.1.2" description = "LangGraph callback handlers for Sigil Python SDK" readme = "README.md" license = { file = "LICENSE" } requires-python = ">=3.10" dependencies = [ - "sigil-sdk>=0.1.1", + "sigil-sdk>=0.1.2", "langchain-core>=0.3.0", "langgraph>=0.2.0", ] diff --git a/python-frameworks/llamaindex/pyproject.toml b/python-frameworks/llamaindex/pyproject.toml index 67f63b5..066bd64 100644 --- a/python-frameworks/llamaindex/pyproject.toml +++ b/python-frameworks/llamaindex/pyproject.toml @@ -1,12 +1,12 @@ [project] name = "sigil-sdk-llamaindex" -version = "0.1.1" +version = "0.1.2" description = "LlamaIndex callback handlers for Sigil Python SDK" readme = "README.md" license = { file = "LICENSE" } requires-python = ">=3.10" dependencies = [ - "sigil-sdk>=0.1.1", + "sigil-sdk>=0.1.2", "llama-index>=0.14.0", ] diff --git a/python-frameworks/openai-agents/pyproject.toml b/python-frameworks/openai-agents/pyproject.toml index e4e13a9..51f6163 100644 --- a/python-frameworks/openai-agents/pyproject.toml +++ b/python-frameworks/openai-agents/pyproject.toml @@ -1,12 +1,12 @@ [project] name = "sigil-sdk-openai-agents" -version = "0.1.1" +version = "0.1.2" description = "OpenAI Agents callback handlers for Sigil Python SDK" readme = "README.md" license = { file = "LICENSE" } requires-python = ">=3.10" dependencies = [ - "sigil-sdk>=0.1.1", + "sigil-sdk>=0.1.2", "openai-agents>=0.9.0", ] diff --git a/python-providers/anthropic/pyproject.toml b/python-providers/anthropic/pyproject.toml index a1f7cb5..088470e 100644 --- a/python-providers/anthropic/pyproject.toml +++ b/python-providers/anthropic/pyproject.toml @@ -1,12 +1,12 @@ [project] name = "sigil-sdk-anthropic" -version = "0.1.1" +version = "0.1.2" description = "Anthropic helper wrappers for Sigil Python SDK" readme = "README.md" license = { file = "LICENSE" } requires-python = ">=3.10" dependencies = [ - "sigil-sdk>=0.1.1", + "sigil-sdk>=0.1.2", "anthropic>=0.79.0,<1", ] diff --git a/python-providers/gemini/pyproject.toml b/python-providers/gemini/pyproject.toml index 75eb35f..ab65cf0 100644 --- a/python-providers/gemini/pyproject.toml +++ b/python-providers/gemini/pyproject.toml @@ -1,12 +1,12 @@ [project] name = "sigil-sdk-gemini" -version = "0.1.1" +version = "0.1.2" description = "Gemini helper wrappers for Sigil Python SDK" readme = "README.md" license = { file = "LICENSE" } requires-python = ">=3.10" dependencies = [ - "sigil-sdk>=0.1.1", + "sigil-sdk>=0.1.2", "google-genai>=1.63.0,<2", ] diff --git a/python-providers/openai/pyproject.toml b/python-providers/openai/pyproject.toml index acb930e..7009cc6 100644 --- a/python-providers/openai/pyproject.toml +++ b/python-providers/openai/pyproject.toml @@ -1,12 +1,12 @@ [project] name = "sigil-sdk-openai" -version = "0.1.1" +version = "0.1.2" description = "OpenAI helper wrappers for Sigil Python SDK" readme = "README.md" license = { file = "LICENSE" } requires-python = ">=3.10" dependencies = [ - "sigil-sdk>=0.1.1", + "sigil-sdk>=0.1.2", "openai>=2.20.0,<3", ] diff --git a/python/pyproject.toml b/python/pyproject.toml index 90294cf..fc5144f 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "sigil-sdk" -version = "0.1.1" +version = "0.1.2" description = "Grafana Sigil Python SDK" readme = "README.md" license = { file = "LICENSE" } From 3c6df1f57cbda67ab314690c80cae8659319185a Mon Sep 17 00:00:00 2001 From: "renovate-sh-app[bot]" <219655108+renovate-sh-app[bot]@users.noreply.github.com> Date: Wed, 25 Mar 2026 09:19:00 +0000 Subject: [PATCH 117/133] fix(deps): update protobuf monorepo (#661) | datasource | package | from | to | | ---------- | -------------------------------------- | ------ | ------ | | nuget | Google.Protobuf | 3.34.0 | 3.34.1 | | maven | com.google.protobuf:protoc | 4.34.0 | 4.34.1 | | maven | com.google.protobuf:protobuf-java-util | 4.34.0 | 4.34.1 | | maven | com.google.protobuf:protobuf-java | 4.34.0 | 4.34.1 | Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> --- dotnet/src/Grafana.Sigil/Grafana.Sigil.csproj | 2 +- java/gradle/libs.versions.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dotnet/src/Grafana.Sigil/Grafana.Sigil.csproj b/dotnet/src/Grafana.Sigil/Grafana.Sigil.csproj index 3b9287f..2ecda35 100644 --- a/dotnet/src/Grafana.Sigil/Grafana.Sigil.csproj +++ b/dotnet/src/Grafana.Sigil/Grafana.Sigil.csproj @@ -10,7 +10,7 @@ - + diff --git a/java/gradle/libs.versions.toml b/java/gradle/libs.versions.toml index ab6ef8d..bc9e261 100644 --- a/java/gradle/libs.versions.toml +++ b/java/gradle/libs.versions.toml @@ -5,7 +5,7 @@ jacksonAnnotations = "2.21" jmh = "0.7.3" junit = "6.0.3" otel = "1.60.1" -protobuf = "4.34.0" +protobuf = "4.34.1" protobufPlugin = "0.9.6" grpc = "1.80.0" mockwebserver = "5.3.2" From 1afdb92e5a5bdb4f2dc1e3d9ac61ea5086d94349 Mon Sep 17 00:00:00 2001 From: "renovate-sh-app[bot]" <219655108+renovate-sh-app[bot]@users.noreply.github.com> Date: Wed, 25 Mar 2026 09:19:17 +0000 Subject: [PATCH 118/133] fix(deps): update module github.com/grafana/sigil/sdks/go-providers/gemini to v0.1.2 (#657) | datasource | package | from | to | | ---------- | ------------------------------------------------- | ------ | ------ | | go | github.com/grafana/sigil/sdks/go-providers/gemini | v0.1.1 | v0.1.2 | Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> --- go/cmd/devex-emitter/go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go/cmd/devex-emitter/go.mod b/go/cmd/devex-emitter/go.mod index f9a682a..68a48c1 100644 --- a/go/cmd/devex-emitter/go.mod +++ b/go/cmd/devex-emitter/go.mod @@ -6,7 +6,7 @@ require ( github.com/anthropics/anthropic-sdk-go v1.27.1 github.com/grafana/sigil/sdks/go v0.1.1 github.com/grafana/sigil/sdks/go-providers/anthropic v0.1.1 - github.com/grafana/sigil/sdks/go-providers/gemini v0.1.1 + github.com/grafana/sigil/sdks/go-providers/gemini v0.1.2 github.com/grafana/sigil/sdks/go-providers/openai v0.1.1 github.com/openai/openai-go/v3 v3.29.0 go.opentelemetry.io/otel v1.42.0 From 2ba078007b94bb6c6be3cec4a142425a954450a1 Mon Sep 17 00:00:00 2001 From: "renovate-sh-app[bot]" <219655108+renovate-sh-app[bot]@users.noreply.github.com> Date: Thu, 26 Mar 2026 10:16:52 +0100 Subject: [PATCH 119/133] chore(deps): update dependency @opencode-ai/plugin to ^1.3.0 (#673) | datasource | package | from | to | | ---------- | ------------------- | ------ | ----- | | npm | @opencode-ai/plugin | 1.2.24 | 1.3.0 | Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> --- plugins/opencode/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/opencode/package.json b/plugins/opencode/package.json index 039bc38..8539ae9 100644 --- a/plugins/opencode/package.json +++ b/plugins/opencode/package.json @@ -24,7 +24,7 @@ }, "devDependencies": { "@grafana/sigil-sdk-js": "workspace:*", - "@opencode-ai/plugin": "^1.2.24", + "@opencode-ai/plugin": "^1.3.0", "@opencode-ai/sdk": "^1.2.24", "@types/node": "^24.0.0", "esbuild": "^0.27.3", From 7c4bb3dad62787cefabb348a9eb8922d5890a184 Mon Sep 17 00:00:00 2001 From: "renovate-sh-app[bot]" <219655108+renovate-sh-app[bot]@users.noreply.github.com> Date: Thu, 26 Mar 2026 10:17:22 +0100 Subject: [PATCH 120/133] fix(deps): update module github.com/grafana/sigil/sdks/go-providers/openai to v0.1.2 (#659) | datasource | package | from | to | | ---------- | ------------------------------------------------- | ------ | ------ | | go | github.com/grafana/sigil/sdks/go-providers/openai | v0.1.1 | v0.1.2 | Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> --- go/cmd/devex-emitter/go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go/cmd/devex-emitter/go.mod b/go/cmd/devex-emitter/go.mod index 68a48c1..c0b5027 100644 --- a/go/cmd/devex-emitter/go.mod +++ b/go/cmd/devex-emitter/go.mod @@ -7,7 +7,7 @@ require ( github.com/grafana/sigil/sdks/go v0.1.1 github.com/grafana/sigil/sdks/go-providers/anthropic v0.1.1 github.com/grafana/sigil/sdks/go-providers/gemini v0.1.2 - github.com/grafana/sigil/sdks/go-providers/openai v0.1.1 + github.com/grafana/sigil/sdks/go-providers/openai v0.1.2 github.com/openai/openai-go/v3 v3.29.0 go.opentelemetry.io/otel v1.42.0 go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.42.0 From a70ae6935719fd2336c0b336c9e8726f8c1883c6 Mon Sep 17 00:00:00 2001 From: "renovate-sh-app[bot]" <219655108+renovate-sh-app[bot]@users.noreply.github.com> Date: Thu, 26 Mar 2026 10:17:28 +0100 Subject: [PATCH 121/133] fix(deps): update module github.com/grafana/sigil/sdks/go-providers/anthropic to v0.1.2 (#656) | datasource | package | from | to | | ---------- | ---------------------------------------------------- | ------ | ------ | | go | github.com/grafana/sigil/sdks/go-providers/anthropic | v0.1.1 | v0.1.2 | Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> --- go/cmd/devex-emitter/go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go/cmd/devex-emitter/go.mod b/go/cmd/devex-emitter/go.mod index c0b5027..12bbd80 100644 --- a/go/cmd/devex-emitter/go.mod +++ b/go/cmd/devex-emitter/go.mod @@ -5,7 +5,7 @@ go 1.25.6 require ( github.com/anthropics/anthropic-sdk-go v1.27.1 github.com/grafana/sigil/sdks/go v0.1.1 - github.com/grafana/sigil/sdks/go-providers/anthropic v0.1.1 + github.com/grafana/sigil/sdks/go-providers/anthropic v0.1.2 github.com/grafana/sigil/sdks/go-providers/gemini v0.1.2 github.com/grafana/sigil/sdks/go-providers/openai v0.1.2 github.com/openai/openai-go/v3 v3.29.0 From dbeae96fb586c2f5bba347307e508b209dbf1cd8 Mon Sep 17 00:00:00 2001 From: "renovate-sh-app[bot]" <219655108+renovate-sh-app[bot]@users.noreply.github.com> Date: Thu, 26 Mar 2026 10:17:37 +0100 Subject: [PATCH 122/133] fix(deps): update jackson monorepo to v2.21.2 (#654) | datasource | package | from | to | | ---------- | ------------------------------------------------------ | ------ | ------ | | maven | com.fasterxml.jackson.datatype:jackson-datatype-jsr310 | 2.21.1 | 2.21.2 | | maven | com.fasterxml.jackson.core:jackson-databind | 2.21.1 | 2.21.2 | | maven | com.fasterxml.jackson.core:jackson-core | 2.21.1 | 2.21.2 | Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> --- java/gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java/gradle/libs.versions.toml b/java/gradle/libs.versions.toml index bc9e261..c82a48b 100644 --- a/java/gradle/libs.versions.toml +++ b/java/gradle/libs.versions.toml @@ -1,6 +1,6 @@ [versions] assertj = "3.27.7" -jackson = "2.21.1" +jackson = "2.21.2" jacksonAnnotations = "2.21" jmh = "0.7.3" junit = "6.0.3" From 269bb885e57f2117c570fd834c0dcf8335a3902d Mon Sep 17 00:00:00 2001 From: "renovate-sh-app[bot]" <219655108+renovate-sh-app[bot]@users.noreply.github.com> Date: Thu, 26 Mar 2026 10:18:52 +0100 Subject: [PATCH 123/133] fix(deps): update module github.com/grafana/sigil/sdks/go to v0.1.2 (#655) | datasource | package | from | to | | ---------- | -------------------------------- | ------ | ------ | | go | github.com/grafana/sigil/sdks/go | v0.1.1 | v0.1.2 | Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: Cyril Tovena --- go-frameworks/google-adk/go.mod | 2 +- go-providers/anthropic/go.mod | 2 +- go-providers/gemini/go.mod | 2 +- go-providers/openai/go.mod | 2 +- go/cmd/devex-emitter/go.mod | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/go-frameworks/google-adk/go.mod b/go-frameworks/google-adk/go.mod index 2ca0c93..756df64 100644 --- a/go-frameworks/google-adk/go.mod +++ b/go-frameworks/google-adk/go.mod @@ -3,7 +3,7 @@ module github.com/grafana/sigil/sdks/go-frameworks/google-adk go 1.25.6 require ( - github.com/grafana/sigil/sdks/go v0.1.1 + github.com/grafana/sigil/sdks/go v0.1.2 go.opentelemetry.io/otel v1.42.0 go.opentelemetry.io/otel/sdk v1.42.0 go.opentelemetry.io/otel/sdk/metric v1.42.0 diff --git a/go-providers/anthropic/go.mod b/go-providers/anthropic/go.mod index f1bc121..53747fa 100644 --- a/go-providers/anthropic/go.mod +++ b/go-providers/anthropic/go.mod @@ -4,7 +4,7 @@ go 1.25.6 require ( github.com/anthropics/anthropic-sdk-go v1.27.1 - github.com/grafana/sigil/sdks/go v0.1.1 + github.com/grafana/sigil/sdks/go v0.1.2 ) require ( diff --git a/go-providers/gemini/go.mod b/go-providers/gemini/go.mod index aa5480b..53ad539 100644 --- a/go-providers/gemini/go.mod +++ b/go-providers/gemini/go.mod @@ -3,7 +3,7 @@ module github.com/grafana/sigil/sdks/go-providers/gemini go 1.25.6 require ( - github.com/grafana/sigil/sdks/go v0.1.1 + github.com/grafana/sigil/sdks/go v0.1.2 google.golang.org/genai v1.51.0 ) diff --git a/go-providers/openai/go.mod b/go-providers/openai/go.mod index 2093493..d123cf8 100644 --- a/go-providers/openai/go.mod +++ b/go-providers/openai/go.mod @@ -3,7 +3,7 @@ module github.com/grafana/sigil/sdks/go-providers/openai go 1.25.6 require ( - github.com/grafana/sigil/sdks/go v0.1.1 + github.com/grafana/sigil/sdks/go v0.1.2 github.com/openai/openai-go/v3 v3.29.0 ) diff --git a/go/cmd/devex-emitter/go.mod b/go/cmd/devex-emitter/go.mod index 12bbd80..00185aa 100644 --- a/go/cmd/devex-emitter/go.mod +++ b/go/cmd/devex-emitter/go.mod @@ -4,7 +4,7 @@ go 1.25.6 require ( github.com/anthropics/anthropic-sdk-go v1.27.1 - github.com/grafana/sigil/sdks/go v0.1.1 + github.com/grafana/sigil/sdks/go v0.1.2 github.com/grafana/sigil/sdks/go-providers/anthropic v0.1.2 github.com/grafana/sigil/sdks/go-providers/gemini v0.1.2 github.com/grafana/sigil/sdks/go-providers/openai v0.1.2 From 643b3771fd8708f4bf7626887e5221df50ad3339 Mon Sep 17 00:00:00 2001 From: "renovate-sh-app[bot]" <219655108+renovate-sh-app[bot]@users.noreply.github.com> Date: Sat, 28 Mar 2026 10:16:53 +0100 Subject: [PATCH 124/133] chore(deps): update dependency @opencode-ai/sdk to ^1.3.2 (#678) | datasource | package | from | to | | ---------- | ---------------- | ------ | ----- | | npm | @opencode-ai/sdk | 1.2.24 | 1.3.2 | Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> --- plugins/opencode/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/opencode/package.json b/plugins/opencode/package.json index 8539ae9..0027eb0 100644 --- a/plugins/opencode/package.json +++ b/plugins/opencode/package.json @@ -25,7 +25,7 @@ "devDependencies": { "@grafana/sigil-sdk-js": "workspace:*", "@opencode-ai/plugin": "^1.3.0", - "@opencode-ai/sdk": "^1.2.24", + "@opencode-ai/sdk": "^1.3.2", "@types/node": "^24.0.0", "esbuild": "^0.27.3", "typescript": "^5.8.2", From edfde76b070df07e8306de518bb0b29ea9997d1b Mon Sep 17 00:00:00 2001 From: "renovate-sh-app[bot]" <219655108+renovate-sh-app[bot]@users.noreply.github.com> Date: Sat, 28 Mar 2026 10:17:00 +0100 Subject: [PATCH 125/133] fix(deps): update dependency com.openai:openai-java to v4.29.1 (#684) | datasource | package | from | to | | ---------- | ---------------------- | ------ | ------ | | maven | com.openai:openai-java | 4.29.0 | 4.29.1 | Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> --- java/gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java/gradle/libs.versions.toml b/java/gradle/libs.versions.toml index c82a48b..e8349a1 100644 --- a/java/gradle/libs.versions.toml +++ b/java/gradle/libs.versions.toml @@ -41,7 +41,7 @@ junit-jupiter = { module = "org.junit.jupiter:junit-jupiter" } mockwebserver = { module = "com.squareup.okhttp3:mockwebserver", version.ref = "mockwebserver" } javax-annotation = { module = "javax.annotation:javax.annotation-api", version.ref = "javaxAnnotation" } -openai-java = { module = "com.openai:openai-java", version = "4.29.0" } +openai-java = { module = "com.openai:openai-java", version = "4.29.1" } anthropic-java = { module = "com.anthropic:anthropic-java", version = "2.18.0" } google-genai = { module = "com.google.genai:google-genai", version = "1.44.0" } From ffb175f0018e4a23deee4bbaa9ea20a2aa208d80 Mon Sep 17 00:00:00 2001 From: "renovate-sh-app[bot]" <219655108+renovate-sh-app[bot]@users.noreply.github.com> Date: Sat, 28 Mar 2026 10:17:07 +0100 Subject: [PATCH 126/133] chore(deps): update dependency vitest to ^4.1.0 (#682) | datasource | package | from | to | | ---------- | ------- | ----- | ----- | | npm | vitest | 4.1.0 | 4.1.1 | Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> --- plugins/opencode/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/opencode/package.json b/plugins/opencode/package.json index 0027eb0..e51f233 100644 --- a/plugins/opencode/package.json +++ b/plugins/opencode/package.json @@ -29,6 +29,6 @@ "@types/node": "^24.0.0", "esbuild": "^0.27.3", "typescript": "^5.8.2", - "vitest": "^4.0.18" + "vitest": "^4.1.0" } } From 664f90d076098e120b070dd6a33518858bcff073 Mon Sep 17 00:00:00 2001 From: "renovate-sh-app[bot]" <219655108+renovate-sh-app[bot]@users.noreply.github.com> Date: Sat, 28 Mar 2026 10:17:14 +0100 Subject: [PATCH 127/133] fix(deps): update dependency @openai/agents to ^0.8.0 (#679) | datasource | package | from | to | | ---------- | -------------- | ----- | ----- | | npm | @openai/agents | 0.7.2 | 0.8.0 | Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> --- js/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/package.json b/js/package.json index 1f0c020..435b0db 100644 --- a/js/package.json +++ b/js/package.json @@ -60,7 +60,7 @@ "@grpc/proto-loader": "^0.8.0", "@langchain/core": "^1.0.0", "@langchain/langgraph": "^1.2.0", - "@openai/agents": "^0.7.0", + "@openai/agents": "^0.8.0", "@opentelemetry/api": "^1.9.0", "@opentelemetry/exporter-metrics-otlp-grpc": "^0.213.0", "@opentelemetry/exporter-metrics-otlp-http": "^0.213.0", From 98a583e663add94228502ae16b32d857fb46c334 Mon Sep 17 00:00:00 2001 From: "renovate-sh-app[bot]" <219655108+renovate-sh-app[bot]@users.noreply.github.com> Date: Sat, 28 Mar 2026 10:18:38 +0100 Subject: [PATCH 128/133] chore(deps): update dependency typescript to v6 (#683) | datasource | package | from | to | | ---------- | ---------- | ----- | ----- | | npm | typescript | 5.9.3 | 6.0.2 | Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: Cyril Tovena --- js/package.json | 2 +- plugins/opencode/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/js/package.json b/js/package.json index 435b0db..718b263 100644 --- a/js/package.json +++ b/js/package.json @@ -74,6 +74,6 @@ "devDependencies": { "@opentelemetry/context-async-hooks": "^2.6.0", "@types/node": "^24.11.0", - "typescript": "^5.9.3" + "typescript": "^6.0.0" } } diff --git a/plugins/opencode/package.json b/plugins/opencode/package.json index e51f233..386564e 100644 --- a/plugins/opencode/package.json +++ b/plugins/opencode/package.json @@ -28,7 +28,7 @@ "@opencode-ai/sdk": "^1.3.2", "@types/node": "^24.0.0", "esbuild": "^0.27.3", - "typescript": "^5.8.2", +"typescript": "^6.0.0", "vitest": "^4.1.0" } } From 6008623e8121d1910ed939308ecbe8c4b5434d0b Mon Sep 17 00:00:00 2001 From: Alexander Akhmetov Date: Wed, 1 Apr 2026 10:36:59 +0200 Subject: [PATCH 129/133] Move SDKs to a separate repo --- .github/CODEOWNERS | 1 + .github/workflows/ci.yml | 122 ++ .github/workflows/dotnet-publish.yml | 53 + .github/workflows/go-sdk-tag.yml | 65 + .github/workflows/java-publish.yml | 52 + .github/workflows/js-sdk-publish.yml | 87 ++ .github/workflows/python-sdks-publish.yml | 195 +++ LICENSE | 661 ++++++++++ README.md | 39 + dotnet/src/Grafana.Sigil/Grafana.Sigil.csproj | 4 +- go-frameworks/google-adk/README.md | 2 +- go-frameworks/google-adk/adapter.go | 2 +- go-frameworks/google-adk/adapter_test.go | 2 +- .../conformance/conformance_test.go | 4 +- go-frameworks/google-adk/conformance_test.go | 4 +- go-frameworks/google-adk/go.mod | 6 +- go-providers/anthropic/conformance_test.go | 4 +- go-providers/anthropic/go.mod | 6 +- go-providers/anthropic/mapper.go | 2 +- go-providers/anthropic/mapper_test.go | 2 +- go-providers/anthropic/record.go | 2 +- go-providers/anthropic/record_test.go | 2 +- go-providers/anthropic/sdk_example_test.go | 2 +- go-providers/anthropic/stream_mapper.go | 2 +- go-providers/gemini/conformance_test.go | 4 +- go-providers/gemini/go.mod | 6 +- go-providers/gemini/mapper.go | 2 +- go-providers/gemini/mapper_test.go | 2 +- go-providers/gemini/record.go | 2 +- go-providers/gemini/record_test.go | 2 +- go-providers/gemini/sdk_example_test.go | 2 +- go-providers/gemini/stream_mapper.go | 2 +- go-providers/openai/conformance_test.go | 4 +- go-providers/openai/go.mod | 6 +- go-providers/openai/mapper.go | 2 +- go-providers/openai/mapper_test.go | 2 +- go-providers/openai/record.go | 2 +- go-providers/openai/record_test.go | 2 +- go-providers/openai/responses_mapper.go | 2 +- go-providers/openai/sdk_example_test.go | 2 +- go-providers/openai/stream_mapper.go | 2 +- go.work | 9 + go.work.sum | 1 + go/README.md | 18 +- go/cmd/devex-emitter/go.mod | 63 - go/cmd/devex-emitter/go.sum | 105 -- go/cmd/devex-emitter/main.go | 1109 ----------------- go/cmd/devex-emitter/main_test.go | 230 ---- go/cmd/devex-emitter/telemetry_test.go | 68 - go/cmd/devex-emitter/ttft_test.go | 99 -- go/cmd/sigil-probe/main.go | 2 +- go/go.mod | 2 +- go/sigil/client.go | 2 +- go/sigil/client_test.go | 2 +- go/sigil/conformance_helpers_test.go | 4 +- go/sigil/conformance_test.go | 4 +- go/sigil/example_test.go | 2 +- go/sigil/exporter.go | 2 +- go/sigil/exporter_transport_test.go | 2 +- go/sigil/proto_mapping.go | 2 +- go/sigil/sigiltest/env.go | 4 +- go/sigil/sigiltest/record.go | 2 +- java/core/build.gradle.kts | 2 +- mise.toml | 428 +++++++ package.json | 4 + pnpm-workspace.yaml | 3 + python/scripts/generate_proto.sh | 8 +- 67 files changed, 1795 insertions(+), 1749 deletions(-) create mode 100644 .github/CODEOWNERS create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/dotnet-publish.yml create mode 100644 .github/workflows/go-sdk-tag.yml create mode 100644 .github/workflows/java-publish.yml create mode 100644 .github/workflows/js-sdk-publish.yml create mode 100644 .github/workflows/python-sdks-publish.yml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 go.work create mode 100644 go.work.sum delete mode 100644 go/cmd/devex-emitter/go.mod delete mode 100644 go/cmd/devex-emitter/go.sum delete mode 100644 go/cmd/devex-emitter/main.go delete mode 100644 go/cmd/devex-emitter/main_test.go delete mode 100644 go/cmd/devex-emitter/telemetry_test.go delete mode 100644 go/cmd/devex-emitter/ttft_test.go create mode 100644 mise.toml create mode 100644 package.json create mode 100644 pnpm-workspace.yaml diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..cefa5e4 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @grafana/sigil diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..b671fea --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,122 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +permissions: + contents: read + +jobs: + go: + name: Go SDK + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0 + with: + go-version-file: go/go.mod + + - name: golangci-lint + uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v8.0.0 + with: + args: --timeout=5m + working-directory: go + + - name: Test core + run: cd go && GOWORK=off go test ./... + + - name: Test anthropic provider + run: cd go-providers/anthropic && GOWORK=off go test ./... + + - name: Test openai provider + run: cd go-providers/openai && GOWORK=off go test ./... + + - name: Test gemini provider + run: cd go-providers/gemini && GOWORK=off go test ./... + + - name: Test google-adk framework + run: cd go-frameworks/google-adk && GOWORK=off go test ./... + + typescript: + name: TypeScript SDK + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0 + + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version-file: js/package.json + cache: pnpm + + - run: pnpm install + + - name: Typecheck + run: pnpm --filter @grafana/sigil-sdk-js run typecheck + + - name: Test + run: pnpm --filter @grafana/sigil-sdk-js run test:ci + + python: + name: Python SDK + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + suite: + - { name: core, cmd: "uv run --with '.[dev]' --directory python pytest tests" } + - { name: openai, cmd: "uv run --with './python[dev]' --with './python-providers/openai[dev]' pytest python-providers/openai/tests" } + - { name: anthropic, cmd: "uv run --with './python[dev]' --with './python-providers/anthropic[dev]' pytest python-providers/anthropic/tests" } + - { name: gemini, cmd: "uv run --with './python[dev]' --with './python-providers/gemini[dev]' pytest python-providers/gemini/tests" } + - { name: langchain, cmd: "uv run --with './python[dev]' --with './python-frameworks/langchain[dev]' pytest python-frameworks/langchain/tests" } + - { name: langgraph, cmd: "uv run --with './python[dev]' --with './python-frameworks/langgraph[dev]' pytest python-frameworks/langgraph/tests" } + - { name: openai-agents, cmd: "uv run --with './python[dev]' --with './python-frameworks/openai-agents[dev]' pytest python-frameworks/openai-agents/tests" } + - { name: llamaindex, cmd: "uv run --with './python[dev]' --with './python-frameworks/llamaindex[dev]' pytest python-frameworks/llamaindex/tests" } + - { name: google-adk, cmd: "uv run --with './python[dev]' --with './python-frameworks/google-adk[dev]' pytest python-frameworks/google-adk/tests" } + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: '3.11' + + - name: Install uv + uses: astral-sh/setup-uv@6b9c6063abd6010835644d4c2e1bef4cf5cd0fca # v6.0.1 + + - name: Test ${{ matrix.suite.name }} + run: ${{ matrix.suite.cmd }} + + dotnet: + name: .NET SDK + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1 + with: + dotnet-version: '8.0.x' + + - name: Format check + run: dotnet format dotnet/Sigil.DotNet.sln --verify-no-changes + + - name: Test + run: dotnet test dotnet/Sigil.DotNet.sln -c Release + + java: + name: Java SDK + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 + with: + distribution: temurin + java-version: '21' + + - name: Test + working-directory: java + run: ./gradlew --no-daemon test diff --git a/.github/workflows/dotnet-publish.yml b/.github/workflows/dotnet-publish.yml new file mode 100644 index 0000000..9a6cf71 --- /dev/null +++ b/.github/workflows/dotnet-publish.yml @@ -0,0 +1,53 @@ +name: Publish .NET SDK to NuGet + +on: + workflow_dispatch: + inputs: + version: + description: 'Package version (e.g. 0.2.0)' + required: true + type: string + +permissions: + contents: read + +jobs: + publish: + if: github.repository == 'grafana/sigil-sdk' && github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + + steps: + - name: Get secrets from Vault + id: get-secrets + uses: grafana/shared-workflows/actions/get-vault-secrets@f1614b210386ac420af6807a997ac7f6d96e477a # get-vault-secrets/v1.3.1 + env: + VAULT_INSTANCE: ops + with: + vault_instance: ${{ env.VAULT_INSTANCE }} + common_secrets: | + NUGET_API_KEY=sigil-nuget:api-key + export_env: false + + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1 + with: + dotnet-version: '8.0.x' + + - name: Pack + run: | + dotnet pack dotnet/Sigil.DotNet.sln -c Release \ + -p:PackageVersion=${{ inputs.version }} \ + -o nupkgs/ + + - name: Publish + run: | + for pkg in nupkgs/*.nupkg; do + dotnet nuget push "$pkg" \ + --api-key "${{ fromJSON(steps.get-secrets.outputs.secrets).NUGET_API_KEY }}" \ + --source https://api.nuget.org/v3/index.json \ + --skip-duplicate + done diff --git a/.github/workflows/go-sdk-tag.yml b/.github/workflows/go-sdk-tag.yml new file mode 100644 index 0000000..27c97d9 --- /dev/null +++ b/.github/workflows/go-sdk-tag.yml @@ -0,0 +1,65 @@ +name: Tag Go SDK modules + +on: + workflow_dispatch: + inputs: + version: + description: 'Version to tag (e.g. 0.2.0)' + required: true + type: string + +permissions: + contents: write + +jobs: + tag: + if: github.repository == 'grafana/sigil-sdk' && github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + + steps: + - name: Get secrets from Vault + id: get-secrets + uses: grafana/shared-workflows/actions/get-vault-secrets@f1614b210386ac420af6807a997ac7f6d96e477a # get-vault-secrets/v1.3.1 + env: + VAULT_INSTANCE: ops + with: + vault_instance: ${{ env.VAULT_INSTANCE }} + common_secrets: | + GITHUB_APP_ID=plugins-platform-bot-app:app-id + GITHUB_APP_PRIVATE_KEY=plugins-platform-bot-app:private-key + export_env: false + + - name: Generate GitHub token + id: generate-github-token + uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0 + with: + app-id: ${{ fromJSON(steps.get-secrets.outputs.secrets).GITHUB_APP_ID }} + private-key: ${{ fromJSON(steps.get-secrets.outputs.secrets).GITHUB_APP_PRIVATE_KEY }} + owner: ${{ github.repository_owner }} + + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + token: ${{ steps.generate-github-token.outputs.token }} + persist-credentials: true + + - name: Setup Git + run: | + git config user.name 'grafana-plugins-platform-bot[bot]' + git config user.email '144369747+grafana-plugins-platform-bot[bot]@users.noreply.github.com' + + - name: Tag all Go modules + run: | + VERSION="v${{ inputs.version }}" + MODULES=( + go + go-providers/anthropic + go-providers/openai + go-providers/gemini + go-frameworks/google-adk + ) + for mod in "${MODULES[@]}"; do + TAG="${mod}/${VERSION}" + echo "Tagging ${TAG}" + git tag -a "${TAG}" -m "${mod} ${VERSION}" + done + git push origin --tags diff --git a/.github/workflows/java-publish.yml b/.github/workflows/java-publish.yml new file mode 100644 index 0000000..86a368a --- /dev/null +++ b/.github/workflows/java-publish.yml @@ -0,0 +1,52 @@ +name: Publish Java SDK to Maven Central + +on: + workflow_dispatch: + inputs: + version: + description: 'Package version (e.g. 0.2.0)' + required: true + type: string + +permissions: + contents: read + +jobs: + publish: + if: github.repository == 'grafana/sigil-sdk' && github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + + steps: + - name: Get secrets from Vault + id: get-secrets + uses: grafana/shared-workflows/actions/get-vault-secrets@f1614b210386ac420af6807a997ac7f6d96e477a # get-vault-secrets/v1.3.1 + env: + VAULT_INSTANCE: ops + with: + vault_instance: ${{ env.VAULT_INSTANCE }} + common_secrets: | + MAVEN_USERNAME=sigil-maven:username + MAVEN_PASSWORD=sigil-maven:password + GPG_PRIVATE_KEY=sigil-maven:gpg-private-key + GPG_PASSPHRASE=sigil-maven:gpg-passphrase + export_env: false + + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 + with: + distribution: temurin + java-version: '21' + + - name: Publish + working-directory: java + run: | + ./gradlew --no-daemon publish \ + -Pversion=${{ inputs.version }} \ + -PossrhUsername="${{ fromJSON(steps.get-secrets.outputs.secrets).MAVEN_USERNAME }}" \ + -PossrhPassword="${{ fromJSON(steps.get-secrets.outputs.secrets).MAVEN_PASSWORD }}" \ + -Psigning.key="${{ fromJSON(steps.get-secrets.outputs.secrets).GPG_PRIVATE_KEY }}" \ + -Psigning.password="${{ fromJSON(steps.get-secrets.outputs.secrets).GPG_PASSPHRASE }}" diff --git a/.github/workflows/js-sdk-publish.yml b/.github/workflows/js-sdk-publish.yml new file mode 100644 index 0000000..96fc10b --- /dev/null +++ b/.github/workflows/js-sdk-publish.yml @@ -0,0 +1,87 @@ +name: Publish JS SDK to npm + +on: + workflow_dispatch: + inputs: + version: + description: 'Semver bump type (major / minor / patch)' + required: true + type: choice + options: + - patch + - minor + - major + +permissions: + contents: read + +jobs: + publish: + if: github.repository == 'grafana/sigil-sdk' && github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + + steps: + - name: Get secrets from Vault + id: get-secrets + uses: grafana/shared-workflows/actions/get-vault-secrets@f1614b210386ac420af6807a997ac7f6d96e477a # get-vault-secrets/v1.3.1 + env: + VAULT_INSTANCE: ops + with: + vault_instance: ${{ env.VAULT_INSTANCE }} + common_secrets: | + GITHUB_APP_ID=plugins-platform-bot-app:app-id + GITHUB_APP_PRIVATE_KEY=plugins-platform-bot-app:private-key + NPM_TOKEN=sigil-npm:token + export_env: false + + - name: Generate GitHub token + id: generate-github-token + uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0 + with: + app-id: ${{ fromJSON(steps.get-secrets.outputs.secrets).GITHUB_APP_ID }} + private-key: ${{ fromJSON(steps.get-secrets.outputs.secrets).GITHUB_APP_PRIVATE_KEY }} + owner: ${{ github.repository_owner }} + + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + token: ${{ steps.generate-github-token.outputs.token }} + persist-credentials: true + + - name: Setup Git + run: | + git config user.name 'grafana-plugins-platform-bot[bot]' + git config user.email '144369747+grafana-plugins-platform-bot[bot]@users.noreply.github.com' + + - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0 + + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version-file: js/package.json + cache: pnpm + registry-url: https://registry.npmjs.org + + - run: pnpm install + + - name: Bump version + working-directory: js + run: npm version ${{ inputs.version }} --no-git-tag-version + + - name: Build + run: pnpm --filter @grafana/sigil-sdk-js run build + + - name: Publish + working-directory: js + run: pnpm publish --no-git-checks --access public + env: + NODE_AUTH_TOKEN: ${{ fromJSON(steps.get-secrets.outputs.secrets).NPM_TOKEN }} + + - name: Commit and tag + run: | + VERSION=$(node -p "require('./js/package.json').version") + git add js/package.json + git commit -m "chore(sdk-js): bump version to ${VERSION}" + git tag -a "sdk-js/v${VERSION}" -m "JS SDK ${VERSION}" + git push origin HEAD --tags diff --git a/.github/workflows/python-sdks-publish.yml b/.github/workflows/python-sdks-publish.yml new file mode 100644 index 0000000..e25f87a --- /dev/null +++ b/.github/workflows/python-sdks-publish.yml @@ -0,0 +1,195 @@ +name: Publish Python SDKs to PyPI + +on: + workflow_dispatch: + inputs: + version: + description: 'Semver bump type (major / minor / patch)' + required: true + type: choice + options: + - patch + - minor + - major + +permissions: + contents: read + +jobs: + build: + if: github.repository == 'grafana/sigil-sdk' && github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + outputs: + new-version: ${{ steps.bump.outputs.new-version }} + + steps: + - name: Get secrets from Vault + id: get-secrets + uses: grafana/shared-workflows/actions/get-vault-secrets@f1614b210386ac420af6807a997ac7f6d96e477a # get-vault-secrets/v1.3.1 + env: + VAULT_INSTANCE: ops + with: + vault_instance: ${{ env.VAULT_INSTANCE }} + common_secrets: | + GITHUB_APP_ID=plugins-platform-bot-app:app-id + GITHUB_APP_PRIVATE_KEY=plugins-platform-bot-app:private-key + export_env: false + + - name: Generate GitHub token + id: generate-github-token + uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0 + with: + app-id: ${{ fromJSON(steps.get-secrets.outputs.secrets).GITHUB_APP_ID }} + private-key: ${{ fromJSON(steps.get-secrets.outputs.secrets).GITHUB_APP_PRIVATE_KEY }} + owner: ${{ github.repository_owner }} + + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + token: ${{ steps.generate-github-token.outputs.token }} + persist-credentials: true + + - name: Setup Git + run: | + git config user.name 'grafana-plugins-platform-bot[bot]' + git config user.email '144369747+grafana-plugins-platform-bot[bot]@users.noreply.github.com' + + - name: Set up Python + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: '3.11' + + - name: Install build tools + run: pip install build + + - name: Compute and apply version bump + id: bump + shell: bash + run: | + CURRENT=$(grep '^version = ' python/pyproject.toml | head -1 | sed 's/version = "\(.*\)"/\1/') + IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT" + + case "${{ inputs.version }}" in + major) MAJOR=$((MAJOR + 1)); MINOR=0; PATCH=0 ;; + minor) MINOR=$((MINOR + 1)); PATCH=0 ;; + patch) PATCH=$((PATCH + 1)) ;; + esac + + NEW="${MAJOR}.${MINOR}.${PATCH}" + echo "new-version=${NEW}" >> "$GITHUB_OUTPUT" + echo "Bumping Python SDK versions: ${CURRENT} -> ${NEW}" + + PACKAGE_DIRS=( + python + python-providers/openai + python-providers/anthropic + python-providers/gemini + python-frameworks/langchain + python-frameworks/langgraph + python-frameworks/openai-agents + python-frameworks/llamaindex + python-frameworks/google-adk + ) + + for dir in "${PACKAGE_DIRS[@]}"; do + file="${dir}/pyproject.toml" + sed -i "s/^version = \".*\"/version = \"${NEW}\"/" "$file" + if [[ "$dir" != "python" ]]; then + sed -i "s/\"sigil-sdk>=.*\"/\"sigil-sdk>=${NEW}\"/" "$file" + fi + echo " updated ${file}" + done + + - name: Build all packages + shell: bash + run: | + PACKAGE_DIRS=( + python + python-providers/openai + python-providers/anthropic + python-providers/gemini + python-frameworks/langchain + python-frameworks/langgraph + python-frameworks/openai-agents + python-frameworks/llamaindex + python-frameworks/google-adk + ) + + for dir in "${PACKAGE_DIRS[@]}"; do + pkg_name=$(grep '^name = ' "${dir}/pyproject.toml" | head -1 | sed 's/name = "\(.*\)"/\1/') + echo "Building ${pkg_name} from ${dir}..." + python -m build "$dir" --outdir "dist/${pkg_name}" + done + + - name: Commit and push + shell: bash + run: | + git add \ + python/pyproject.toml \ + python-providers/*/pyproject.toml \ + python-frameworks/*/pyproject.toml + git commit -m "chore(sdk-python): bump version to ${NEW_VERSION}" + git tag -a "sdk-python/v${NEW_VERSION}" -m "Python SDK ${NEW_VERSION}" + git push origin HEAD --tags + env: + NEW_VERSION: ${{ steps.bump.outputs.new-version }} + + - name: Upload built distributions + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + with: + name: python-sdk-dists + path: dist/ + retention-days: 5 + + publish-core: + needs: build + runs-on: ubuntu-latest + environment: pypi + permissions: + id-token: write + + steps: + - name: Download distributions + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: python-sdk-dists + path: dist/ + + - name: Publish sigil-sdk to PyPI + uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 + with: + packages-dir: dist/sigil-sdk/ + + publish-dependents: + needs: [build, publish-core] + runs-on: ubuntu-latest + environment: pypi + permissions: + id-token: write + strategy: + fail-fast: false + matrix: + package: + - sigil-sdk-openai + - sigil-sdk-anthropic + - sigil-sdk-gemini + - sigil-sdk-langchain + - sigil-sdk-langgraph + - sigil-sdk-openai-agents + - sigil-sdk-llamaindex + - sigil-sdk-google-adk + + steps: + - name: Download distributions + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: python-sdk-dists + path: dist/ + + - name: Publish ${{ matrix.package }} to PyPI + uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 + with: + packages-dir: dist/${{ matrix.package }}/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..be3f7b2 --- /dev/null +++ b/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/README.md b/README.md new file mode 100644 index 0000000..0926858 --- /dev/null +++ b/README.md @@ -0,0 +1,39 @@ +# Grafana Sigil SDK + +Client SDKs for [Grafana Sigil](https://github.com/grafana/sigil) — AI observability for LLM applications. + +## SDKs + +| Language | Package | Path | +|----------|---------|------| +| Go | `github.com/grafana/sigil-sdk/go` | [`go/`](go/) | +| Python | `sigil-sdk` | [`python/`](python/) | +| TypeScript/JavaScript | `@grafana/sigil-sdk-js` | [`js/`](js/) | +| .NET/C# | `Grafana.Sigil` | [`dotnet/`](dotnet/) | +| Java | `com.grafana.sigil` | [`java/`](java/) | + +## Provider Adapters + +| Language | Providers | Path | +|----------|-----------|------| +| Go | Anthropic, OpenAI, Gemini | [`go-providers/`](go-providers/) | +| Python | Anthropic, OpenAI, Gemini | [`python-providers/`](python-providers/) | + +## Framework Integrations + +| Language | Frameworks | Path | +|----------|------------|------| +| Go | Google ADK | [`go-frameworks/`](go-frameworks/) | +| Python | LangChain, LangGraph, OpenAI Agents, LlamaIndex, Google ADK | [`python-frameworks/`](python-frameworks/) | + +## Plugins + +- [OpenCode](plugins/opencode/) — Sigil integration for OpenCode + +## Proto + +Vendored protobuf definitions used by SDKs live in [`proto/`](proto/). + +## License + +[GNU AGPL v3](LICENSE) diff --git a/dotnet/src/Grafana.Sigil/Grafana.Sigil.csproj b/dotnet/src/Grafana.Sigil/Grafana.Sigil.csproj index 2ecda35..6fe4211 100644 --- a/dotnet/src/Grafana.Sigil/Grafana.Sigil.csproj +++ b/dotnet/src/Grafana.Sigil/Grafana.Sigil.csproj @@ -20,8 +20,8 @@ - diff --git a/go-frameworks/google-adk/README.md b/go-frameworks/google-adk/README.md index 9f34ac1..5beb8a0 100644 --- a/go-frameworks/google-adk/README.md +++ b/go-frameworks/google-adk/README.md @@ -27,7 +27,7 @@ if err := googleadk.CheckEmbeddingsSupport(); err != nil { ## Install ```bash -go get github.com/grafana/sigil/sdks/go-frameworks/google-adk +go get github.com/grafana/sigil-sdk/go-frameworks/google-adk ``` ## Quickstart diff --git a/go-frameworks/google-adk/adapter.go b/go-frameworks/google-adk/adapter.go index b18cac5..eec5109 100644 --- a/go-frameworks/google-adk/adapter.go +++ b/go-frameworks/google-adk/adapter.go @@ -9,7 +9,7 @@ import ( "sync" "time" - "github.com/grafana/sigil/sdks/go/sigil" + "github.com/grafana/sigil-sdk/go/sigil" ) const ( diff --git a/go-frameworks/google-adk/adapter_test.go b/go-frameworks/google-adk/adapter_test.go index d36cf9d..9a887a8 100644 --- a/go-frameworks/google-adk/adapter_test.go +++ b/go-frameworks/google-adk/adapter_test.go @@ -13,7 +13,7 @@ import ( "testing" "time" - "github.com/grafana/sigil/sdks/go/sigil" + "github.com/grafana/sigil-sdk/go/sigil" ) func boolPtr(v bool) *bool { diff --git a/go-frameworks/google-adk/conformance/conformance_test.go b/go-frameworks/google-adk/conformance/conformance_test.go index 30d8e1d..02388d7 100644 --- a/go-frameworks/google-adk/conformance/conformance_test.go +++ b/go-frameworks/google-adk/conformance/conformance_test.go @@ -10,8 +10,8 @@ import ( "testing" "time" - googleadk "github.com/grafana/sigil/sdks/go-frameworks/google-adk" - "github.com/grafana/sigil/sdks/go/sigil" + googleadk "github.com/grafana/sigil-sdk/go-frameworks/google-adk" + "github.com/grafana/sigil-sdk/go/sigil" "go.opentelemetry.io/otel/attribute" sdktrace "go.opentelemetry.io/otel/sdk/trace" "go.opentelemetry.io/otel/sdk/trace/tracetest" diff --git a/go-frameworks/google-adk/conformance_test.go b/go-frameworks/google-adk/conformance_test.go index 0616e76..70addf8 100644 --- a/go-frameworks/google-adk/conformance_test.go +++ b/go-frameworks/google-adk/conformance_test.go @@ -11,8 +11,8 @@ import ( "testing" "time" - googleadk "github.com/grafana/sigil/sdks/go-frameworks/google-adk" - sigil "github.com/grafana/sigil/sdks/go/sigil" + googleadk "github.com/grafana/sigil-sdk/go-frameworks/google-adk" + sigil "github.com/grafana/sigil-sdk/go/sigil" "go.opentelemetry.io/otel/attribute" sdkmetric "go.opentelemetry.io/otel/sdk/metric" "go.opentelemetry.io/otel/sdk/metric/metricdata" diff --git a/go-frameworks/google-adk/go.mod b/go-frameworks/google-adk/go.mod index 756df64..7d21ead 100644 --- a/go-frameworks/google-adk/go.mod +++ b/go-frameworks/google-adk/go.mod @@ -1,9 +1,9 @@ -module github.com/grafana/sigil/sdks/go-frameworks/google-adk +module github.com/grafana/sigil-sdk/go-frameworks/google-adk go 1.25.6 require ( - github.com/grafana/sigil/sdks/go v0.1.2 + github.com/grafana/sigil-sdk/go v0.1.2 go.opentelemetry.io/otel v1.42.0 go.opentelemetry.io/otel/sdk v1.42.0 go.opentelemetry.io/otel/sdk/metric v1.42.0 @@ -25,4 +25,4 @@ require ( google.golang.org/protobuf v1.36.11 // indirect ) -replace github.com/grafana/sigil/sdks/go => ../../go +replace github.com/grafana/sigil-sdk/go => ../../go diff --git a/go-providers/anthropic/conformance_test.go b/go-providers/anthropic/conformance_test.go index 4d772ab..b369023 100644 --- a/go-providers/anthropic/conformance_test.go +++ b/go-providers/anthropic/conformance_test.go @@ -10,8 +10,8 @@ import ( asdk "github.com/anthropics/anthropic-sdk-go" - sigil "github.com/grafana/sigil/sdks/go/sigil" - "github.com/grafana/sigil/sdks/go/sigil/sigiltest" + sigil "github.com/grafana/sigil-sdk/go/sigil" + "github.com/grafana/sigil-sdk/go/sigil/sigiltest" ) const anthropicSpanErrorCategory = "error.category" diff --git a/go-providers/anthropic/go.mod b/go-providers/anthropic/go.mod index 53747fa..69419ac 100644 --- a/go-providers/anthropic/go.mod +++ b/go-providers/anthropic/go.mod @@ -1,10 +1,10 @@ -module github.com/grafana/sigil/sdks/go-providers/anthropic +module github.com/grafana/sigil-sdk/go-providers/anthropic go 1.25.6 require ( github.com/anthropics/anthropic-sdk-go v1.27.1 - github.com/grafana/sigil/sdks/go v0.1.2 + github.com/grafana/sigil-sdk/go v0.1.2 ) require ( @@ -32,4 +32,4 @@ require ( gopkg.in/yaml.v2 v2.4.0 // indirect ) -replace github.com/grafana/sigil/sdks/go => ../../go +replace github.com/grafana/sigil-sdk/go => ../../go diff --git a/go-providers/anthropic/mapper.go b/go-providers/anthropic/mapper.go index 2936288..48fe0c2 100644 --- a/go-providers/anthropic/mapper.go +++ b/go-providers/anthropic/mapper.go @@ -7,7 +7,7 @@ import ( "strings" asdk "github.com/anthropics/anthropic-sdk-go" - "github.com/grafana/sigil/sdks/go/sigil" + "github.com/grafana/sigil-sdk/go/sigil" ) const thinkingBudgetMetadataKey = "sigil.gen_ai.request.thinking.budget_tokens" diff --git a/go-providers/anthropic/mapper_test.go b/go-providers/anthropic/mapper_test.go index 7c28cf5..5399c04 100644 --- a/go-providers/anthropic/mapper_test.go +++ b/go-providers/anthropic/mapper_test.go @@ -6,7 +6,7 @@ import ( asdk "github.com/anthropics/anthropic-sdk-go" "github.com/anthropics/anthropic-sdk-go/packages/param" - "github.com/grafana/sigil/sdks/go/sigil" + "github.com/grafana/sigil-sdk/go/sigil" ) func TestFromRequestResponse(t *testing.T) { diff --git a/go-providers/anthropic/record.go b/go-providers/anthropic/record.go index 382fbc4..78e21b5 100644 --- a/go-providers/anthropic/record.go +++ b/go-providers/anthropic/record.go @@ -6,7 +6,7 @@ import ( asdk "github.com/anthropics/anthropic-sdk-go" - "github.com/grafana/sigil/sdks/go/sigil" + "github.com/grafana/sigil-sdk/go/sigil" ) // Message calls the Anthropic messages API and records the generation. diff --git a/go-providers/anthropic/record_test.go b/go-providers/anthropic/record_test.go index febfd23..5b28e65 100644 --- a/go-providers/anthropic/record_test.go +++ b/go-providers/anthropic/record_test.go @@ -7,7 +7,7 @@ import ( asdk "github.com/anthropics/anthropic-sdk-go" - "github.com/grafana/sigil/sdks/go/sigil" + "github.com/grafana/sigil-sdk/go/sigil" ) func TestConformance_MessageErrorMapping(t *testing.T) { diff --git a/go-providers/anthropic/sdk_example_test.go b/go-providers/anthropic/sdk_example_test.go index 6672247..111765c 100644 --- a/go-providers/anthropic/sdk_example_test.go +++ b/go-providers/anthropic/sdk_example_test.go @@ -6,7 +6,7 @@ import ( asdk "github.com/anthropics/anthropic-sdk-go" asdkoption "github.com/anthropics/anthropic-sdk-go/option" - "github.com/grafana/sigil/sdks/go/sigil" + "github.com/grafana/sigil-sdk/go/sigil" ) // Example_withSigilWrapper shows the one-liner wrapper approach. diff --git a/go-providers/anthropic/stream_mapper.go b/go-providers/anthropic/stream_mapper.go index 3e447c0..3090b1f 100644 --- a/go-providers/anthropic/stream_mapper.go +++ b/go-providers/anthropic/stream_mapper.go @@ -7,7 +7,7 @@ import ( "time" asdk "github.com/anthropics/anthropic-sdk-go" - "github.com/grafana/sigil/sdks/go/sigil" + "github.com/grafana/sigil-sdk/go/sigil" ) // StreamSummary captures Anthropic stream events and an optional final message. diff --git a/go-providers/gemini/conformance_test.go b/go-providers/gemini/conformance_test.go index 636e2e0..43d0c69 100644 --- a/go-providers/gemini/conformance_test.go +++ b/go-providers/gemini/conformance_test.go @@ -8,8 +8,8 @@ import ( "google.golang.org/genai" - sigil "github.com/grafana/sigil/sdks/go/sigil" - "github.com/grafana/sigil/sdks/go/sigil/sigiltest" + sigil "github.com/grafana/sigil-sdk/go/sigil" + "github.com/grafana/sigil-sdk/go/sigil/sigiltest" ) const ( diff --git a/go-providers/gemini/go.mod b/go-providers/gemini/go.mod index 53ad539..afc8fb9 100644 --- a/go-providers/gemini/go.mod +++ b/go-providers/gemini/go.mod @@ -1,9 +1,9 @@ -module github.com/grafana/sigil/sdks/go-providers/gemini +module github.com/grafana/sigil-sdk/go-providers/gemini go 1.25.6 require ( - github.com/grafana/sigil/sdks/go v0.1.2 + github.com/grafana/sigil-sdk/go v0.1.2 google.golang.org/genai v1.51.0 ) @@ -37,4 +37,4 @@ require ( google.golang.org/protobuf v1.36.11 // indirect ) -replace github.com/grafana/sigil/sdks/go => ../../go +replace github.com/grafana/sigil-sdk/go => ../../go diff --git a/go-providers/gemini/mapper.go b/go-providers/gemini/mapper.go index 2263c78..9480def 100644 --- a/go-providers/gemini/mapper.go +++ b/go-providers/gemini/mapper.go @@ -7,7 +7,7 @@ import ( "google.golang.org/genai" - "github.com/grafana/sigil/sdks/go/sigil" + "github.com/grafana/sigil-sdk/go/sigil" ) const thinkingBudgetMetadataKey = "sigil.gen_ai.request.thinking.budget_tokens" diff --git a/go-providers/gemini/mapper_test.go b/go-providers/gemini/mapper_test.go index f67d481..9f27675 100644 --- a/go-providers/gemini/mapper_test.go +++ b/go-providers/gemini/mapper_test.go @@ -6,7 +6,7 @@ import ( "google.golang.org/genai" - "github.com/grafana/sigil/sdks/go/sigil" + "github.com/grafana/sigil-sdk/go/sigil" ) func TestFromRequestResponse(t *testing.T) { diff --git a/go-providers/gemini/record.go b/go-providers/gemini/record.go index 73b2329..317948f 100644 --- a/go-providers/gemini/record.go +++ b/go-providers/gemini/record.go @@ -6,7 +6,7 @@ import ( "google.golang.org/genai" - "github.com/grafana/sigil/sdks/go/sigil" + "github.com/grafana/sigil-sdk/go/sigil" ) // GenerateContent calls the Gemini generate-content API and records the generation. diff --git a/go-providers/gemini/record_test.go b/go-providers/gemini/record_test.go index 8d2b41d..4d3619e 100644 --- a/go-providers/gemini/record_test.go +++ b/go-providers/gemini/record_test.go @@ -8,7 +8,7 @@ import ( "google.golang.org/genai" - "github.com/grafana/sigil/sdks/go/sigil" + "github.com/grafana/sigil-sdk/go/sigil" ) func TestEmbedContentReturnsRecorderValidationErrorAfterEnd(t *testing.T) { diff --git a/go-providers/gemini/sdk_example_test.go b/go-providers/gemini/sdk_example_test.go index df52284..0ad8a17 100644 --- a/go-providers/gemini/sdk_example_test.go +++ b/go-providers/gemini/sdk_example_test.go @@ -4,7 +4,7 @@ import ( "context" "os" - "github.com/grafana/sigil/sdks/go/sigil" + "github.com/grafana/sigil-sdk/go/sigil" "google.golang.org/genai" ) diff --git a/go-providers/gemini/stream_mapper.go b/go-providers/gemini/stream_mapper.go index 6f3e43f..56b4431 100644 --- a/go-providers/gemini/stream_mapper.go +++ b/go-providers/gemini/stream_mapper.go @@ -7,7 +7,7 @@ import ( "google.golang.org/genai" - "github.com/grafana/sigil/sdks/go/sigil" + "github.com/grafana/sigil-sdk/go/sigil" ) // StreamSummary captures Gemini streamed responses. diff --git a/go-providers/openai/conformance_test.go b/go-providers/openai/conformance_test.go index 20f33fb..ca4eb40 100644 --- a/go-providers/openai/conformance_test.go +++ b/go-providers/openai/conformance_test.go @@ -12,8 +12,8 @@ import ( oresponses "github.com/openai/openai-go/v3/responses" "github.com/openai/openai-go/v3/shared" - sigil "github.com/grafana/sigil/sdks/go/sigil" - "github.com/grafana/sigil/sdks/go/sigil/sigiltest" + sigil "github.com/grafana/sigil-sdk/go/sigil" + "github.com/grafana/sigil-sdk/go/sigil/sigiltest" ) const ( diff --git a/go-providers/openai/go.mod b/go-providers/openai/go.mod index d123cf8..3217beb 100644 --- a/go-providers/openai/go.mod +++ b/go-providers/openai/go.mod @@ -1,9 +1,9 @@ -module github.com/grafana/sigil/sdks/go-providers/openai +module github.com/grafana/sigil-sdk/go-providers/openai go 1.25.6 require ( - github.com/grafana/sigil/sdks/go v0.1.2 + github.com/grafana/sigil-sdk/go v0.1.2 github.com/openai/openai-go/v3 v3.29.0 ) @@ -30,4 +30,4 @@ require ( google.golang.org/protobuf v1.36.11 // indirect ) -replace github.com/grafana/sigil/sdks/go => ../../go +replace github.com/grafana/sigil-sdk/go => ../../go diff --git a/go-providers/openai/mapper.go b/go-providers/openai/mapper.go index ede8a01..8217d0c 100644 --- a/go-providers/openai/mapper.go +++ b/go-providers/openai/mapper.go @@ -9,7 +9,7 @@ import ( osdk "github.com/openai/openai-go/v3" "github.com/openai/openai-go/v3/shared" - "github.com/grafana/sigil/sdks/go/sigil" + "github.com/grafana/sigil-sdk/go/sigil" ) const thinkingBudgetMetadataKey = "sigil.gen_ai.request.thinking.budget_tokens" diff --git a/go-providers/openai/mapper_test.go b/go-providers/openai/mapper_test.go index 81dfeca..2ebe7ab 100644 --- a/go-providers/openai/mapper_test.go +++ b/go-providers/openai/mapper_test.go @@ -8,7 +8,7 @@ import ( oresponses "github.com/openai/openai-go/v3/responses" "github.com/openai/openai-go/v3/shared" - "github.com/grafana/sigil/sdks/go/sigil" + "github.com/grafana/sigil-sdk/go/sigil" ) func TestFromRequestResponse(t *testing.T) { diff --git a/go-providers/openai/record.go b/go-providers/openai/record.go index c99ae49..f5fb43a 100644 --- a/go-providers/openai/record.go +++ b/go-providers/openai/record.go @@ -7,7 +7,7 @@ import ( osdk "github.com/openai/openai-go/v3" oresponses "github.com/openai/openai-go/v3/responses" - "github.com/grafana/sigil/sdks/go/sigil" + "github.com/grafana/sigil-sdk/go/sigil" ) // ChatCompletionsNew calls the OpenAI chat-completions API and records the generation. diff --git a/go-providers/openai/record_test.go b/go-providers/openai/record_test.go index e748caf..f7c21c8 100644 --- a/go-providers/openai/record_test.go +++ b/go-providers/openai/record_test.go @@ -11,7 +11,7 @@ import ( oresponses "github.com/openai/openai-go/v3/responses" "github.com/openai/openai-go/v3/shared" - "github.com/grafana/sigil/sdks/go/sigil" + "github.com/grafana/sigil-sdk/go/sigil" ) func TestEmbeddingsNewReturnsRecorderValidationErrorAfterEnd(t *testing.T) { diff --git a/go-providers/openai/responses_mapper.go b/go-providers/openai/responses_mapper.go index 2b5087d..bd59563 100644 --- a/go-providers/openai/responses_mapper.go +++ b/go-providers/openai/responses_mapper.go @@ -10,7 +10,7 @@ import ( "github.com/openai/openai-go/v3/responses" - "github.com/grafana/sigil/sdks/go/sigil" + "github.com/grafana/sigil-sdk/go/sigil" ) // ResponsesStreamSummary captures Responses API stream events and an optional final response. diff --git a/go-providers/openai/sdk_example_test.go b/go-providers/openai/sdk_example_test.go index 2eaf995..185b521 100644 --- a/go-providers/openai/sdk_example_test.go +++ b/go-providers/openai/sdk_example_test.go @@ -4,7 +4,7 @@ import ( "context" "os" - "github.com/grafana/sigil/sdks/go/sigil" + "github.com/grafana/sigil-sdk/go/sigil" osdk "github.com/openai/openai-go/v3" osdkoption "github.com/openai/openai-go/v3/option" "github.com/openai/openai-go/v3/shared" diff --git a/go-providers/openai/stream_mapper.go b/go-providers/openai/stream_mapper.go index 21bc80a..6bec6bd 100644 --- a/go-providers/openai/stream_mapper.go +++ b/go-providers/openai/stream_mapper.go @@ -7,7 +7,7 @@ import ( osdk "github.com/openai/openai-go/v3" - "github.com/grafana/sigil/sdks/go/sigil" + "github.com/grafana/sigil-sdk/go/sigil" ) // ChatCompletionsStreamSummary captures chat-completions stream chunks and an optional final response. diff --git a/go.work b/go.work new file mode 100644 index 0000000..107e75b --- /dev/null +++ b/go.work @@ -0,0 +1,9 @@ +go 1.25.6 + +use ( + ./go + ./go-frameworks/google-adk + ./go-providers/anthropic + ./go-providers/gemini + ./go-providers/openai +) diff --git a/go.work.sum b/go.work.sum new file mode 100644 index 0000000..2a13fa5 --- /dev/null +++ b/go.work.sum @@ -0,0 +1 @@ +google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 h1:VQZ/yAbAtjkHgH80teYd2em3xtIkkHd7ZhqfH2N9CsM= diff --git a/go/README.md b/go/README.md index b7c9459..f76600f 100644 --- a/go/README.md +++ b/go/README.md @@ -6,13 +6,13 @@ The Go SDK is the current production-ready baseline for normalized generation re Cross-language parity tracks are available for: -- Python: `sdks/python` -- TypeScript/JavaScript: `sdks/js` -- .NET/C#: `sdks/dotnet` +- Python: `python/` +- TypeScript/JavaScript: `js/` +- .NET/C#: `dotnet/` Framework modules: -- Google ADK helper: `../go-frameworks/google-adk/README.md` +- Google ADK helper: [`go-frameworks/google-adk`](../go-frameworks/google-adk/README.md) ## Core model @@ -232,9 +232,9 @@ The SDK emits four OTel histograms automatically through your configured OTel me The Go SDK ships a local no-Docker conformance harness for the current cross-SDK baseline. -- Shared spec: `../../docs/references/sdk-conformance-spec.md` +- Shared spec: `docs/references/sdk-conformance-spec.md` (in the sigil repo) - Default local command: `mise run sdk:conformance` -- Direct Go command: `cd sdks/go && GOWORK=off go test ./sigil -run '^TestConformance' -count=1` +- Direct Go command: `cd go && GOWORK=off go test ./sigil -run '^TestConformance' -count=1` - Current baseline coverage: sync roundtrip, conversation title resolution, user ID resolution, agent name/version resolution, streaming mode + TTFT, tool execution, embeddings, validation/error handling, rating submission, and shutdown flush semantics across exported generation payloads, OTLP spans, OTLP metrics, and local rating HTTP capture ## Explicit flow example @@ -332,9 +332,9 @@ Provider modules are documented wrapper-first for ergonomics and include explici Current Go provider helpers: -- `sdks/go-providers/openai` (OpenAI Chat Completions + Responses wrappers and mappers) -- `sdks/go-providers/anthropic` (Anthropic Messages wrappers and mappers; embeddings currently unsupported by the upstream SDK/API surface) -- `sdks/go-providers/gemini` +- `go-providers/openai` (OpenAI Chat Completions + Responses wrappers and mappers) +- `go-providers/anthropic` (Anthropic Messages wrappers and mappers; embeddings currently unsupported by the upstream SDK/API surface) +- `go-providers/gemini` ## Raw artifact policy diff --git a/go/cmd/devex-emitter/go.mod b/go/cmd/devex-emitter/go.mod deleted file mode 100644 index 00185aa..0000000 --- a/go/cmd/devex-emitter/go.mod +++ /dev/null @@ -1,63 +0,0 @@ -module github.com/grafana/sigil/sdks/go/cmd/devex-emitter - -go 1.25.6 - -require ( - github.com/anthropics/anthropic-sdk-go v1.27.1 - github.com/grafana/sigil/sdks/go v0.1.2 - github.com/grafana/sigil/sdks/go-providers/anthropic v0.1.2 - github.com/grafana/sigil/sdks/go-providers/gemini v0.1.2 - github.com/grafana/sigil/sdks/go-providers/openai v0.1.2 - github.com/openai/openai-go/v3 v3.29.0 - go.opentelemetry.io/otel v1.42.0 - go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.42.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0 - go.opentelemetry.io/otel/metric v1.42.0 - go.opentelemetry.io/otel/sdk v1.42.0 - go.opentelemetry.io/otel/sdk/metric v1.42.0 - go.opentelemetry.io/otel/trace v1.42.0 - google.golang.org/genai v1.51.0 -) - -require ( - cloud.google.com/go v0.123.0 // indirect - cloud.google.com/go/auth v0.18.2 // indirect - cloud.google.com/go/compute/metadata v0.9.0 // indirect - github.com/cenkalti/backoff/v5 v5.0.3 // indirect - github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/go-logr/logr v1.4.3 // indirect - github.com/go-logr/stdr v1.2.2 // indirect - github.com/google/go-cmp v0.7.0 // indirect - github.com/google/s2a-go v0.1.9 // indirect - github.com/google/uuid v1.6.0 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.3.11 // indirect - github.com/googleapis/gax-go/v2 v2.17.0 // indirect - github.com/gorilla/websocket v1.5.3 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect - github.com/tidwall/gjson v1.18.0 // indirect - github.com/tidwall/match v1.1.1 // indirect - github.com/tidwall/pretty v1.2.1 // indirect - github.com/tidwall/sjson v1.2.5 // indirect - go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 // indirect - go.opentelemetry.io/proto/otlp v1.9.0 // indirect - golang.org/x/crypto v0.48.0 // indirect - golang.org/x/net v0.51.0 // indirect - golang.org/x/sync v0.19.0 // indirect - golang.org/x/sys v0.41.0 // indirect - golang.org/x/text v0.34.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect - google.golang.org/grpc v1.79.3 // indirect - google.golang.org/protobuf v1.36.11 // indirect -) - -replace github.com/grafana/sigil/sdks/go => ../.. - -replace github.com/grafana/sigil/sdks/go-providers/anthropic => ../../../go-providers/anthropic - -replace github.com/grafana/sigil/sdks/go-providers/gemini => ../../../go-providers/gemini - -replace github.com/grafana/sigil/sdks/go-providers/openai => ../../../go-providers/openai diff --git a/go/cmd/devex-emitter/go.sum b/go/cmd/devex-emitter/go.sum deleted file mode 100644 index 38b7536..0000000 --- a/go/cmd/devex-emitter/go.sum +++ /dev/null @@ -1,105 +0,0 @@ -cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE= -cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU= -cloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM= -cloud.google.com/go/auth v0.18.2/go.mod h1:xD+oY7gcahcu7G2SG2DsBerfFxgPAJz17zz2joOFF3M= -cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= -cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= -github.com/anthropics/anthropic-sdk-go v1.27.1 h1:7DgMZ2Ng3C2mPzJGHA30NXQTZolcF07mHd0tGaLwfzk= -github.com/anthropics/anthropic-sdk-go v1.27.1/go.mod h1:qUKmaW+uuPB64iy1l+4kOSvaLqPXnHTTBKH6RVZ7q5Q= -github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= -github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= -github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= -github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI= -github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= -github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= -github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= -github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= -github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= -github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= -github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= -github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= -github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.3.11 h1:vAe81Msw+8tKUxi2Dqh/NZMz7475yUvmRIkXr4oN2ao= -github.com/googleapis/enterprise-certificate-proxy v0.3.11/go.mod h1:RFV7MUdlb7AgEq2v7FmMCfeSMCllAzWxFgRdusoGks8= -github.com/googleapis/gax-go/v2 v2.17.0 h1:RksgfBpxqff0EZkDWYuz9q/uWsTVz+kf43LsZ1J6SMc= -github.com/googleapis/gax-go/v2 v2.17.0/go.mod h1:mzaqghpQp4JDh3HvADwrat+6M3MOIDp5YKHhb9PAgDY= -github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= -github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= -github.com/openai/openai-go/v3 v3.29.0 h1:dZNJ0w7DxwpgppzKQjSKfLebW27KrtGqgSy4ipJS0U8= -github.com/openai/openai-go/v3 v3.29.0/go.mod h1:cdufnVK14cWcT9qA1rRtrXx4FTRsgbDPW7Ia7SS5cZo= -github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= -github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= -github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= -github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= -github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= -github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= -github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= -github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= -github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= -github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= -github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= -go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= -go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0= -go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho= -go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.42.0 h1:MdKucPl/HbzckWWEisiNqMPhRrAOQX8r4jTuGr636gk= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.42.0/go.mod h1:RolT8tWtfHcjajEH5wFIZ4Dgh5jpPdFXYV9pTAk/qjc= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 h1:THuZiwpQZuHPul65w4WcwEnkX2QIuMT+UFoOrygtoJw= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0/go.mod h1:J2pvYM5NGHofZ2/Ru6zw/TNWnEQp5crgyDeSrYpXkAw= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0 h1:zWWrB1U6nqhS/k6zYB74CjRpuiitRtLLi68VcgmOEto= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0/go.mod h1:2qXPNBX1OVRC0IwOnfo1ljoid+RD0QK3443EaqVlsOU= -go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4= -go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI= -go.opentelemetry.io/otel/sdk v1.42.0 h1:LyC8+jqk6UJwdrI/8VydAq/hvkFKNHZVIWuslJXYsDo= -go.opentelemetry.io/otel/sdk v1.42.0/go.mod h1:rGHCAxd9DAph0joO4W6OPwxjNTYWghRWmkHuGbayMts= -go.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA= -go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc= -go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY= -go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc= -go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= -go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= -go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= -go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= -golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= -golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= -golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= -golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= -golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= -golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= -golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= -golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= -gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= -gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/genai v1.51.0 h1:IZGuUqgfx40INv3hLFGCbOSGp0qFqm7LVmDghzNIYqg= -google.golang.org/genai v1.51.0/go.mod h1:A3kkl0nyBjyFlNjgxIwKq70julKbIxpSxqKO5gw/gmk= -google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 h1:JLQynH/LBHfCTSbDWl+py8C+Rg/k1OVH3xfcaiANuF0= -google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:kSJwQxqmFXeo79zOmbrALdflXQeAYcUbgS7PbpMknCY= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= -google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= -google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= -google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= -google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/go/cmd/devex-emitter/main.go b/go/cmd/devex-emitter/main.go deleted file mode 100644 index 361ef12..0000000 --- a/go/cmd/devex-emitter/main.go +++ /dev/null @@ -1,1109 +0,0 @@ -package main - -import ( - "context" - "errors" - "fmt" - "log" - "math/rand" - "os" - "strconv" - "strings" - "time" - - asdk "github.com/anthropics/anthropic-sdk-go" - goanthropic "github.com/grafana/sigil/sdks/go-providers/anthropic" - gogemini "github.com/grafana/sigil/sdks/go-providers/gemini" - goopenai "github.com/grafana/sigil/sdks/go-providers/openai" - "github.com/grafana/sigil/sdks/go/sigil" - osdk "github.com/openai/openai-go/v3" - "github.com/openai/openai-go/v3/packages/param" - oresponses "github.com/openai/openai-go/v3/responses" - "github.com/openai/openai-go/v3/shared" - "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/codes" - "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc" - "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" - sdkmetric "go.opentelemetry.io/otel/sdk/metric" - "go.opentelemetry.io/otel/sdk/resource" - sdktrace "go.opentelemetry.io/otel/sdk/trace" - oteltrace "go.opentelemetry.io/otel/trace" - "google.golang.org/genai" -) - -const ( - languageName = "go" - traceServiceName = "sigil-sdk-traffic-go" - traceServiceEnv = "sigil-devex" - traceShutdownGrace = 5 * time.Second - metricFlushInterval = 2 * time.Second - minSyntheticSpans = 6 - maxSyntheticSpans = 12 - minTraceLookback = 2 * time.Second - maxTraceLookback = 4 * time.Second -) - -type runtimeConfig struct { - interval time.Duration - streamPercent int - conversations int - rotateTurns int - maxCycles int - customProvider string - genGRPC string - traceGRPC string -} - -type source string - -const ( - sourceOpenAI source = "openai" - sourceAnthropic source = "anthropic" - sourceGemini source = "gemini" - sourceCustom source = "mistral" -) - -type threadState struct { - conversationID string - turn int -} - -type tagEnvelope struct { - agentPersona string - conversationTitle string - tags map[string]string - metadata map[string]any -} - -func main() { - cfg := loadConfig() - randSeed := rand.New(rand.NewSource(time.Now().UnixNano())) - telemetryShutdown, err := configureTelemetry(context.Background(), cfg) - if err != nil { - log.Fatalf("[go-emitter] telemetry setup failed: %v", err) - } - defer func() { - shutdownCtx, cancel := context.WithTimeout(context.Background(), traceShutdownGrace) - defer cancel() - if err := telemetryShutdown(shutdownCtx); err != nil { - log.Printf("[go-emitter] telemetry shutdown error: %v", err) - } - }() - - clientCfg := sigil.DefaultConfig() - clientCfg.GenerationExport.Protocol = sigil.GenerationExportProtocolGRPC - clientCfg.GenerationExport.Endpoint = cfg.genGRPC - clientCfg.GenerationExport.Auth = sigil.AuthConfig{Mode: sigil.ExportAuthModeNone} - - client := sigil.NewClient(clientCfg) - defer func() { - if err := client.Shutdown(context.Background()); err != nil { - log.Printf("[go-emitter] shutdown error: %v", err) - } - }() - - sources := []source{sourceOpenAI, sourceAnthropic, sourceGemini, sourceCustom} - threads := make(map[source][]threadState, len(sources)) - nextSlot := make(map[source]int, len(sources)) - for _, src := range sources { - threads[src] = make([]threadState, cfg.conversations) - } - - log.Printf( - "[go-emitter] started interval=%s stream_percent=%d conversations=%d rotate_turns=%d custom_provider=%s trace_grpc=%s", - cfg.interval, - cfg.streamPercent, - cfg.conversations, - cfg.rotateTurns, - cfg.customProvider, - cfg.traceGRPC, - ) - cycles := 0 - - for { - for _, src := range sources { - slot := nextSlot[src] % cfg.conversations - nextSlot[src]++ - - thread := &threads[src][slot] - ensureThread(thread, cfg.rotateTurns, src, slot) - mode := chooseMode(randSeed.Intn(100), cfg.streamPercent) - - if err := emitForSource(client, cfg, randSeed, src, slot, thread, mode); err != nil { - log.Fatalf("[go-emitter] emit failed source=%s slot=%d turn=%d: %v", src, slot, thread.turn, err) - } - thread.turn++ - } - - cycles++ - if cfg.maxCycles > 0 && cycles >= cfg.maxCycles { - return - } - - jitterMs := randSeed.Intn(401) - 200 - sleep := cfg.interval + time.Duration(jitterMs)*time.Millisecond - if sleep < 200*time.Millisecond { - sleep = 200 * time.Millisecond - } - time.Sleep(sleep) - } -} - -func emitForSource(client *sigil.Client, cfg runtimeConfig, randSeed *rand.Rand, src source, slot int, thread *threadState, mode sigil.GenerationMode) error { - envelope := buildTagEnvelope(src, mode, thread.turn, slot) - agentName := fmt.Sprintf("devex-%s-%s-%s", languageName, src, envelope.agentPersona) - agentVersion := "devex-1" - - ctx := context.Background() - tracer := otel.Tracer("sigil.devex.synthetic") - traceEnd := time.Now() - traceLookback := minTraceLookback - if randSeed != nil { - traceLookback += time.Duration(randSeed.Int63n(int64(maxTraceLookback-minTraceLookback) + 1)) - } - traceStart := traceEnd.Add(-traceLookback) - ctx, conversationSpan := tracer.Start( - ctx, - fmt.Sprintf("conversation.%s.turn", src), - oteltrace.WithTimestamp(traceStart), - oteltrace.WithAttributes( - attribute.String("sigil.synthetic.trace_type", "llm_conversation"), - attribute.String("sigil.devex.provider", string(src)), - attribute.String("sigil.devex.mode", string(mode)), - attribute.String("sigil.devex.conversation_id", thread.conversationID), - attribute.Int("sigil.devex.turn", thread.turn), - attribute.Int("sigil.devex.slot", slot), - attribute.String("sigil.devex.scenario", envelope.tags["sigil.devex.scenario"]), - ), - ) - defer conversationSpan.End() - syntheticCount := emitSyntheticLifecycleSpans(ctx, randSeed, traceStart, traceEnd) - conversationSpan.SetAttributes(attribute.Int("sigil.synthetic.span_count", syntheticCount)) - - switch src { - case sourceOpenAI: - if mode == sigil.GenerationModeStream { - if openAIUsesResponses(thread.turn) { - return emitOpenAIResponsesStream(ctx, client, thread.conversationID, envelope.conversationTitle, agentName, agentVersion, envelope.tags, envelope.metadata, thread.turn) - } - return emitOpenAIChatCompletionsStream(ctx, client, thread.conversationID, envelope.conversationTitle, agentName, agentVersion, envelope.tags, envelope.metadata, thread.turn) - } - if openAIUsesResponses(thread.turn) { - return emitOpenAIResponsesSync(ctx, client, thread.conversationID, envelope.conversationTitle, agentName, agentVersion, envelope.tags, envelope.metadata, thread.turn) - } - return emitOpenAIChatCompletionsSync(ctx, client, thread.conversationID, envelope.conversationTitle, agentName, agentVersion, envelope.tags, envelope.metadata, thread.turn) - case sourceAnthropic: - if mode == sigil.GenerationModeStream { - return emitAnthropicStream(ctx, client, thread.conversationID, envelope.conversationTitle, agentName, agentVersion, envelope.tags, envelope.metadata, thread.turn) - } - return emitAnthropicSync(ctx, client, thread.conversationID, envelope.conversationTitle, agentName, agentVersion, envelope.tags, envelope.metadata, thread.turn) - case sourceGemini: - if mode == sigil.GenerationModeStream { - return emitGeminiStream(ctx, client, thread.conversationID, envelope.conversationTitle, agentName, agentVersion, envelope.tags, envelope.metadata, thread.turn) - } - return emitGeminiSync(ctx, client, thread.conversationID, envelope.conversationTitle, agentName, agentVersion, envelope.tags, envelope.metadata, thread.turn) - case sourceCustom: - provider := cfg.customProvider - if provider == "" { - provider = string(sourceCustom) - } - if mode == sigil.GenerationModeStream { - return emitCustomStream(ctx, client, provider, thread.conversationID, envelope.conversationTitle, agentName, agentVersion, envelope.tags, envelope.metadata, thread.turn, randSeed) - } - return emitCustomSync(ctx, client, provider, thread.conversationID, envelope.conversationTitle, agentName, agentVersion, envelope.tags, envelope.metadata, thread.turn, randSeed) - default: - return fmt.Errorf("unknown source %q", src) - } -} - -func emitSyntheticLifecycleSpans(ctx context.Context, randSeed *rand.Rand, traceStart, traceEnd time.Time) int { - if randSeed == nil { - randSeed = rand.New(rand.NewSource(time.Now().UnixNano())) - } - if !traceEnd.After(traceStart) { - traceEnd = traceStart.Add(1 * time.Second) - } - operations := []struct { - name string - category string - component string - }{ - {name: "auth.validate_session", category: "auth", component: "auth-service"}, - {name: "auth.refresh_token", category: "auth", component: "auth-service"}, - {name: "db.load_conversation_context", category: "database", component: "postgres"}, - {name: "db.store_generation_metadata", category: "database", component: "postgres"}, - {name: "cache.redis_get", category: "cache", component: "redis"}, - {name: "cache.redis_set", category: "cache", component: "redis"}, - {name: "retrieval.vector_search", category: "retrieval", component: "vector-db"}, - {name: "retrieval.rerank_documents", category: "retrieval", component: "reranker"}, - {name: "tools.web_search.call", category: "tool_call", component: "tool-runner"}, - {name: "tools.sql_query.call", category: "tool_call", component: "tool-runner"}, - {name: "tools.code_interpreter.call", category: "tool_call", component: "tool-runner"}, - {name: "policy.safety_screen", category: "guardrail", component: "safety-service"}, - {name: "prompt.assemble_context", category: "prompting", component: "prompt-builder"}, - {name: "llm.request", category: "model", component: "provider-gateway"}, - {name: "llm.first_token_wait", category: "model", component: "provider-gateway"}, - {name: "output.stream_chunks", category: "streaming", component: "stream-router"}, - {name: "external.crm_lookup", category: "external_service", component: "crm-api"}, - {name: "external.calendar_lookup", category: "external_service", component: "calendar-api"}, - {name: "external.slack_post", category: "external_service", component: "slack-api"}, - {name: "observability.emit_metrics", category: "telemetry", component: "metrics-pipeline"}, - } - - spanCount := minSyntheticSpans + randSeed.Intn(maxSyntheticSpans-minSyntheticSpans+1) - tracer := otel.Tracer("sigil.devex.synthetic") - - for i := 0; i < spanCount; i++ { - op := operations[randSeed.Intn(len(operations))] - duration := syntheticDuration(op.category, randSeed) - windowStart := traceStart.Add(duration) - if !traceEnd.After(windowStart) { - windowStart = traceStart - duration = traceEnd.Sub(traceStart) / 2 - } - randomOffset := time.Duration(randSeed.Int63n(int64(traceEnd.Sub(windowStart)) + 1)) - endTime := windowStart.Add(randomOffset) - startTime := endTime.Add(-duration) - - _, span := tracer.Start( - ctx, - op.name, - oteltrace.WithTimestamp(startTime), - oteltrace.WithAttributes( - attribute.String("sigil.synthetic.category", op.category), - attribute.String("sigil.synthetic.component", op.component), - attribute.Int("sigil.synthetic.step_index", i), - attribute.Int64("sigil.synthetic.simulated_duration_ms", duration.Milliseconds()), - ), - ) - - if op.category == "database" { - span.SetAttributes( - attribute.String("db.system", "postgresql"), - attribute.String("db.operation", []string{"SELECT", "INSERT", "UPDATE"}[randSeed.Intn(3)]), - ) - } - if op.category == "tool_call" { - toolNames := []string{"web_search", "sql_query", "code_interpreter", "ticket_lookup"} - span.SetAttributes(attribute.String("gen_ai.tool.name", toolNames[randSeed.Intn(len(toolNames))])) - } - if op.category == "external_service" { - host := []string{"crm.internal", "calendar.internal", "slack.com"}[randSeed.Intn(3)] - span.SetAttributes(attribute.String("server.address", host)) - } - if op.category == "model" { - span.SetAttributes( - attribute.String("gen_ai.operation.name", []string{"generateText", "streamText"}[randSeed.Intn(2)]), - attribute.String("gen_ai.request.model", []string{"gpt-5", "claude-sonnet-4-5", "gemini-2.5-pro"}[randSeed.Intn(3)]), - ) - } - - // Keep failures sparse but present so UI/testing can exercise error states. - if randSeed.Intn(100) < 12 { - errorType := []string{"timeout", "rate_limit", "upstream_503", "validation_error"}[randSeed.Intn(4)] - span.SetStatus(codes.Error, errorType) - span.SetAttributes( - attribute.String("error.type", errorType), - attribute.Bool("error", true), - ) - } - - span.End(oteltrace.WithTimestamp(endTime)) - } - - return spanCount -} - -func syntheticDuration(category string, randSeed *rand.Rand) time.Duration { - switch category { - case "auth": - return time.Duration(8+randSeed.Intn(24)) * time.Millisecond - case "database": - return time.Duration(18+randSeed.Intn(120)) * time.Millisecond - case "cache": - return time.Duration(2+randSeed.Intn(10)) * time.Millisecond - case "retrieval": - return time.Duration(25+randSeed.Intn(150)) * time.Millisecond - case "tool_call": - return time.Duration(45+randSeed.Intn(260)) * time.Millisecond - case "guardrail": - return time.Duration(15+randSeed.Intn(70)) * time.Millisecond - case "prompting": - return time.Duration(10+randSeed.Intn(40)) * time.Millisecond - case "model": - return time.Duration(90+randSeed.Intn(520)) * time.Millisecond - case "streaming": - return time.Duration(25+randSeed.Intn(130)) * time.Millisecond - case "external_service": - return time.Duration(40+randSeed.Intn(220)) * time.Millisecond - default: - return time.Duration(10+randSeed.Intn(100)) * time.Millisecond - } -} - -func emitOpenAIChatCompletionsSync( - ctx context.Context, - client *sigil.Client, - conversationID string, - conversationTitle string, - agentName string, - agentVersion string, - tags map[string]string, - metadata map[string]any, - turn int, -) error { - req := osdk.ChatCompletionNewParams{ - Model: shared.ChatModel("gpt-5"), - Messages: []osdk.ChatCompletionMessageParamUnion{ - osdk.SystemMessage("You are a concise planner that always returns action bullets."), - osdk.UserMessage(fmt.Sprintf("Plan run %d for shipping issue triage.", turn)), - }, - } - resp := &osdk.ChatCompletion{ - ID: fmt.Sprintf("go-openai-sync-%d", turn), - Model: "gpt-5", - Choices: []osdk.ChatCompletionChoice{ - { - FinishReason: "stop", - Message: osdk.ChatCompletionMessage{ - Content: "1. Pull recent incidents\n2. Group by owner\n3. Draft next action", - }, - }, - }, - Usage: osdk.CompletionUsage{ - PromptTokens: int64(80 + turn%15), - CompletionTokens: int64(28 + turn%9), - TotalTokens: int64(108 + turn%24), - }, - } - - mapped, err := goopenai.ChatCompletionsFromRequestResponse(req, resp, - goopenai.WithConversationID(conversationID), - goopenai.WithConversationTitle(conversationTitle), - goopenai.WithAgentName(agentName), - goopenai.WithAgentVersion(agentVersion), - goopenai.WithTags(tags), - goopenai.WithMetadata(metadata), - ) - if err != nil { - return err - } - - _, rec := client.StartGeneration(ctx, sigil.GenerationStart{Model: sigil.ModelRef{Provider: "openai", Name: "gpt-5"}}) - rec.SetResult(mapped, nil) - rec.End() - return rec.Err() -} - -func emitOpenAIChatCompletionsStream( - ctx context.Context, - client *sigil.Client, - conversationID string, - conversationTitle string, - agentName string, - agentVersion string, - tags map[string]string, - metadata map[string]any, - turn int, -) error { - req := osdk.ChatCompletionNewParams{ - Model: shared.ChatModel("gpt-5"), - Messages: []osdk.ChatCompletionMessageParamUnion{ - osdk.UserMessage(fmt.Sprintf("Stream an execution status update for ticket %d.", turn)), - }, - } - summary := goopenai.ChatCompletionsStreamSummary{ - Chunks: []osdk.ChatCompletionChunk{ - { - ID: fmt.Sprintf("go-openai-stream-%d", turn), - Model: "gpt-5", - Choices: []osdk.ChatCompletionChunkChoice{ - { - Delta: osdk.ChatCompletionChunkChoiceDelta{Content: "Starting rollout checks..."}, - }, - { - Delta: osdk.ChatCompletionChunkChoiceDelta{Content: " completed."}, - FinishReason: "stop", - }, - }, - Usage: osdk.CompletionUsage{ - PromptTokens: 42, - CompletionTokens: 14, - TotalTokens: 56, - }, - }, - }, - } - - mapped, err := goopenai.ChatCompletionsFromStream(req, summary, - goopenai.WithConversationID(conversationID), - goopenai.WithConversationTitle(conversationTitle), - goopenai.WithAgentName(agentName), - goopenai.WithAgentVersion(agentVersion), - goopenai.WithTags(tags), - goopenai.WithMetadata(metadata), - ) - if err != nil { - return err - } - - _, rec := client.StartStreamingGeneration(ctx, sigil.GenerationStart{Model: sigil.ModelRef{Provider: "openai", Name: "gpt-5"}}) - rec.SetFirstTokenAt(time.Now().UTC()) - rec.SetResult(mapped, nil) - rec.End() - return rec.Err() -} - -func emitOpenAIResponsesSync( - ctx context.Context, - client *sigil.Client, - conversationID string, - conversationTitle string, - agentName string, - agentVersion string, - tags map[string]string, - metadata map[string]any, - turn int, -) error { - req := oresponses.ResponseNewParams{ - Model: shared.ResponsesModel("gpt-5"), - Instructions: param.NewOpt("You are a concise planner that always returns action bullets."), - Input: oresponses.ResponseNewParamsInputUnion{OfString: param.NewOpt(fmt.Sprintf("Plan run %d for shipping issue triage.", turn))}, - MaxOutputTokens: param.NewOpt(int64(256)), - } - resp := &oresponses.Response{ - ID: fmt.Sprintf("go-openai-responses-sync-%d", turn), - Model: shared.ResponsesModel("gpt-5"), - Status: oresponses.ResponseStatusCompleted, - Output: []oresponses.ResponseOutputItemUnion{ - { - Type: "message", - Content: []oresponses.ResponseOutputMessageContentUnion{ - {Type: "output_text", Text: "1. Pull recent incidents\n2. Group by owner\n3. Draft next action"}, - }, - }, - }, - Usage: oresponses.ResponseUsage{ - InputTokens: int64(80 + turn%15), - OutputTokens: int64(28 + turn%9), - TotalTokens: int64(108 + turn%24), - }, - } - - mapped, err := goopenai.ResponsesFromRequestResponse(req, resp, - goopenai.WithConversationID(conversationID), - goopenai.WithConversationTitle(conversationTitle), - goopenai.WithAgentName(agentName), - goopenai.WithAgentVersion(agentVersion), - goopenai.WithTags(tags), - goopenai.WithMetadata(metadata), - ) - if err != nil { - return err - } - - _, rec := client.StartGeneration(ctx, sigil.GenerationStart{Model: sigil.ModelRef{Provider: "openai", Name: "gpt-5"}}) - rec.SetResult(mapped, nil) - rec.End() - return rec.Err() -} - -func emitOpenAIResponsesStream( - ctx context.Context, - client *sigil.Client, - conversationID string, - conversationTitle string, - agentName string, - agentVersion string, - tags map[string]string, - metadata map[string]any, - turn int, -) error { - req := oresponses.ResponseNewParams{ - Model: shared.ResponsesModel("gpt-5"), - Input: oresponses.ResponseNewParamsInputUnion{ - OfString: param.NewOpt(fmt.Sprintf("Stream an execution status update for ticket %d.", turn)), - }, - MaxOutputTokens: param.NewOpt(int64(128)), - } - - summary := goopenai.ResponsesStreamSummary{ - Events: []oresponses.ResponseStreamEventUnion{ - { - Type: "response.output_text.delta", - Delta: "Starting rollout checks...", - }, - { - Type: "response.output_text.delta", - Delta: " completed.", - }, - { - Type: "response.completed", - Response: oresponses.Response{ - ID: fmt.Sprintf("go-openai-responses-stream-%d", turn), - Model: shared.ResponsesModel("gpt-5"), - Status: oresponses.ResponseStatusCompleted, - Usage: oresponses.ResponseUsage{ - InputTokens: 42, - OutputTokens: 14, - TotalTokens: 56, - }, - }, - }, - }, - } - - mapped, err := goopenai.ResponsesFromStream(req, summary, - goopenai.WithConversationID(conversationID), - goopenai.WithConversationTitle(conversationTitle), - goopenai.WithAgentName(agentName), - goopenai.WithAgentVersion(agentVersion), - goopenai.WithTags(tags), - goopenai.WithMetadata(metadata), - ) - if err != nil { - return err - } - - _, rec := client.StartStreamingGeneration(ctx, sigil.GenerationStart{Model: sigil.ModelRef{Provider: "openai", Name: "gpt-5"}}) - rec.SetFirstTokenAt(time.Now().UTC()) - rec.SetResult(mapped, nil) - rec.End() - return rec.Err() -} - -func emitAnthropicSync( - ctx context.Context, - client *sigil.Client, - conversationID string, - conversationTitle string, - agentName string, - agentVersion string, - tags map[string]string, - metadata map[string]any, - turn int, -) error { - req := asdk.BetaMessageNewParams{ - Model: asdk.Model("claude-sonnet-4-5"), - System: []asdk.BetaTextBlockParam{{ - Text: "Think in short phases and include rationale.", - }}, - Messages: []asdk.BetaMessageParam{{ - Role: asdk.BetaMessageParamRoleUser, - Content: []asdk.BetaContentBlockParamUnion{ - asdk.NewBetaTextBlock(fmt.Sprintf("Summarize weekly reliability drift (%d).", turn)), - }, - }}, - } - resp := &asdk.BetaMessage{ - ID: fmt.Sprintf("go-anthropic-sync-%d", turn), - Model: asdk.Model("claude-sonnet-4-5"), - Content: []asdk.BetaContentBlockUnion{ - {Type: "thinking", Thinking: "identify top two drift vectors"}, - {Type: "text", Text: "Drift rose in retries and latency on EU shards."}, - }, - StopReason: asdk.BetaStopReasonEndTurn, - Usage: asdk.BetaUsage{ - InputTokens: 75, - OutputTokens: 31, - }, - } - - mapped, err := goanthropic.FromRequestResponse(req, resp, - goanthropic.WithConversationID(conversationID), - goanthropic.WithConversationTitle(conversationTitle), - goanthropic.WithAgentName(agentName), - goanthropic.WithAgentVersion(agentVersion), - goanthropic.WithTags(tags), - goanthropic.WithMetadata(metadata), - ) - if err != nil { - return err - } - - _, rec := client.StartGeneration(ctx, sigil.GenerationStart{Model: sigil.ModelRef{Provider: "anthropic", Name: "claude-sonnet-4-5"}}) - rec.SetResult(mapped, nil) - rec.End() - return rec.Err() -} - -func emitAnthropicStream( - ctx context.Context, - client *sigil.Client, - conversationID string, - conversationTitle string, - agentName string, - agentVersion string, - tags map[string]string, - metadata map[string]any, - turn int, -) error { - req := asdk.BetaMessageNewParams{ - Model: asdk.Model("claude-sonnet-4-5"), - Messages: []asdk.BetaMessageParam{{ - Role: asdk.BetaMessageParamRoleUser, - Content: []asdk.BetaContentBlockParamUnion{ - asdk.NewBetaTextBlock(fmt.Sprintf("Stream a live mitigation status for deployment %d.", turn)), - }, - }}, - } - - summary := goanthropic.StreamSummary{ - Events: []asdk.BetaRawMessageStreamEventUnion{ - { - Type: "message_start", - Message: asdk.BetaMessage{ - ID: fmt.Sprintf("go-anthropic-stream-%d", turn), - Model: asdk.Model("claude-sonnet-4-5"), - }, - }, - { - Type: "content_block_start", - ContentBlock: asdk.BetaRawContentBlockStartEventContentBlockUnion{ - Type: "text", - Text: "Mitigation running on canary set.", - }, - }, - { - Type: "message_delta", - Delta: asdk.BetaRawMessageStreamEventUnionDelta{ - StopReason: asdk.BetaStopReasonEndTurn, - }, - }, - }, - } - - mapped, err := goanthropic.FromStream(req, summary, - goanthropic.WithConversationID(conversationID), - goanthropic.WithConversationTitle(conversationTitle), - goanthropic.WithAgentName(agentName), - goanthropic.WithAgentVersion(agentVersion), - goanthropic.WithTags(tags), - goanthropic.WithMetadata(metadata), - ) - if err != nil { - return err - } - - _, rec := client.StartStreamingGeneration(ctx, sigil.GenerationStart{Model: sigil.ModelRef{Provider: "anthropic", Name: "claude-sonnet-4-5"}}) - rec.SetFirstTokenAt(time.Now().UTC()) - rec.SetResult(mapped, nil) - rec.End() - return rec.Err() -} - -func emitGeminiSync( - ctx context.Context, - client *sigil.Client, - conversationID string, - conversationTitle string, - agentName string, - agentVersion string, - tags map[string]string, - metadata map[string]any, - turn int, -) error { - model := "gemini-2.5-pro" - contents := []*genai.Content{ - genai.NewContentFromText(fmt.Sprintf("Draft a short launch note for sprint %d.", turn), genai.RoleUser), - } - var requestConfig *genai.GenerateContentConfig - resp := &genai.GenerateContentResponse{ - ResponseID: fmt.Sprintf("go-gemini-sync-%d", turn), - ModelVersion: "gemini-2.5-pro-001", - Candidates: []*genai.Candidate{ - { - FinishReason: genai.FinishReasonStop, - Content: genai.NewContentFromText("Launch note: rollout green, no regressions observed.", genai.RoleModel), - }, - }, - UsageMetadata: &genai.GenerateContentResponseUsageMetadata{ - PromptTokenCount: 54, - CandidatesTokenCount: 18, - TotalTokenCount: 72, - }, - } - - mapped, err := gogemini.FromRequestResponse(model, contents, requestConfig, resp, - gogemini.WithConversationID(conversationID), - gogemini.WithConversationTitle(conversationTitle), - gogemini.WithAgentName(agentName), - gogemini.WithAgentVersion(agentVersion), - gogemini.WithTags(tags), - gogemini.WithMetadata(metadata), - ) - if err != nil { - return err - } - - _, rec := client.StartGeneration(ctx, sigil.GenerationStart{Model: sigil.ModelRef{Provider: "gemini", Name: "gemini-2.5-pro"}}) - rec.SetResult(mapped, nil) - rec.End() - return rec.Err() -} - -func emitGeminiStream( - ctx context.Context, - client *sigil.Client, - conversationID string, - conversationTitle string, - agentName string, - agentVersion string, - tags map[string]string, - metadata map[string]any, - turn int, -) error { - model := "gemini-2.5-pro" - contents := []*genai.Content{ - genai.NewContentFromText(fmt.Sprintf("Stream a migration checklist status for wave %d.", turn), genai.RoleUser), - } - var requestConfig *genai.GenerateContentConfig - summary := gogemini.StreamSummary{ - Responses: []*genai.GenerateContentResponse{ - { - ResponseID: fmt.Sprintf("go-gemini-stream-%d", turn), - ModelVersion: "gemini-2.5-pro-001", - Candidates: []*genai.Candidate{ - { - Content: genai.NewContentFromText("Checklist in progress...", genai.RoleModel), - FinishReason: genai.FinishReasonUnspecified, - }, - }, - }, - { - ResponseID: fmt.Sprintf("go-gemini-stream-%d", turn), - ModelVersion: "gemini-2.5-pro-001", - Candidates: []*genai.Candidate{ - { - Content: genai.NewContentFromText("Checklist complete. All gates passed.", genai.RoleModel), - FinishReason: genai.FinishReasonStop, - }, - }, - UsageMetadata: &genai.GenerateContentResponseUsageMetadata{ - PromptTokenCount: 44, - CandidatesTokenCount: 16, - TotalTokenCount: 60, - }, - }, - }, - } - - mapped, err := gogemini.FromStream(model, contents, requestConfig, summary, - gogemini.WithConversationID(conversationID), - gogemini.WithConversationTitle(conversationTitle), - gogemini.WithAgentName(agentName), - gogemini.WithAgentVersion(agentVersion), - gogemini.WithTags(tags), - gogemini.WithMetadata(metadata), - ) - if err != nil { - return err - } - - _, rec := client.StartStreamingGeneration(ctx, sigil.GenerationStart{Model: sigil.ModelRef{Provider: "gemini", Name: "gemini-2.5-pro"}}) - rec.SetFirstTokenAt(time.Now().UTC()) - rec.SetResult(mapped, nil) - rec.End() - return rec.Err() -} - -func emitCustomSync( - ctx context.Context, - client *sigil.Client, - provider string, - conversationID string, - conversationTitle string, - agentName string, - agentVersion string, - tags map[string]string, - metadata map[string]any, - turn int, - randSeed *rand.Rand, -) error { - _, rec := client.StartGeneration(ctx, sigil.GenerationStart{ - ConversationID: conversationID, - ConversationTitle: conversationTitle, - AgentName: agentName, - AgentVersion: agentVersion, - Model: sigil.ModelRef{ - Provider: provider, - Name: "mistral-large-devex", - }, - Tags: tags, - Metadata: metadata, - }) - result := sigil.Generation{ - Input: []sigil.Message{ - sigil.UserTextMessage(fmt.Sprintf("Generate custom provider narrative for checkpoint %d.", turn)), - }, - Output: []sigil.Message{ - sigil.AssistantTextMessage("Custom provider: checkpoint healthy, drift below threshold."), - }, - Usage: sigil.TokenUsage{ - InputTokens: int64(30 + randSeed.Intn(10)), - OutputTokens: int64(16 + randSeed.Intn(6)), - }, - StopReason: "stop", - } - rec.SetResult(result, nil) - rec.End() - return rec.Err() -} - -func emitCustomStream( - ctx context.Context, - client *sigil.Client, - provider string, - conversationID string, - conversationTitle string, - agentName string, - agentVersion string, - tags map[string]string, - metadata map[string]any, - turn int, - randSeed *rand.Rand, -) error { - _, rec := client.StartStreamingGeneration(ctx, sigil.GenerationStart{ - ConversationID: conversationID, - ConversationTitle: conversationTitle, - AgentName: agentName, - AgentVersion: agentVersion, - Model: sigil.ModelRef{ - Provider: provider, - Name: "mistral-large-devex", - }, - Tags: tags, - Metadata: metadata, - }) - rec.SetFirstTokenAt(time.Now().UTC()) - - result := sigil.Generation{ - Input: []sigil.Message{ - sigil.UserTextMessage(fmt.Sprintf("Stream a custom remediation summary for slot %d turn %d.", metadata["conversation_slot"], turn)), - }, - Output: []sigil.Message{ - { - Role: sigil.RoleAssistant, - Parts: []sigil.Part{ - sigil.ThinkingPart("composing synthetic stream segments"), - sigil.TextPart("Segment A complete. Segment B complete."), - }, - }, - }, - Usage: sigil.TokenUsage{ - InputTokens: int64(26 + randSeed.Intn(12)), - OutputTokens: int64(18 + randSeed.Intn(7)), - }, - StopReason: "end_turn", - } - rec.SetResult(result, nil) - rec.End() - return rec.Err() -} - -func sourceTagFor(src source) string { - if src == sourceCustom { - return "core_custom" - } - return "provider_wrapper" -} - -func ensureThread(thread *threadState, rotateTurns int, src source, slot int) { - if thread.conversationID == "" || thread.turn >= rotateTurns { - thread.conversationID = newConversationID(languageName, string(src), slot) - thread.turn = 0 - } -} - -func chooseMode(roll int, streamPercent int) sigil.GenerationMode { - if roll < streamPercent { - return sigil.GenerationModeStream - } - return sigil.GenerationModeSync -} - -func buildTagEnvelope(src source, mode sigil.GenerationMode, turn int, slot int) tagEnvelope { - agentPersona := personaForTurn(turn) - return tagEnvelope{ - agentPersona: agentPersona, - conversationTitle: conversationTitleFor(src, slot), - tags: map[string]string{ - "sigil.devex.language": languageName, - "sigil.devex.provider": string(src), - "sigil.devex.source": sourceTagFor(src), - "sigil.devex.scenario": scenarioFor(src, turn), - "sigil.devex.mode": string(mode), - }, - metadata: map[string]any{ - "turn_index": turn, - "conversation_slot": slot, - "agent_persona": agentPersona, - "emitter": "sdk-traffic", - "provider_shape": providerShapeFor(src, turn), - }, - } -} - -func conversationTitleFor(src source, slot int) string { - return fmt.Sprintf("Devex %s %s %d", strings.ToUpper(languageName), sourceDisplayName(src), slot+1) -} - -func sourceDisplayName(src source) string { - switch src { - case sourceOpenAI: - return "OpenAI" - case sourceAnthropic: - return "Anthropic" - case sourceGemini: - return "Gemini" - default: - return "Mistral" - } -} - -func scenarioFor(src source, turn int) string { - switch src { - case sourceOpenAI: - if turn%2 == 0 { - return "planning_brief" - } - return "status_stream" - case sourceAnthropic: - if turn%2 == 0 { - return "reasoning_digest" - } - return "delta_stream" - case sourceGemini: - if turn%2 == 0 { - return "launch_note" - } - return "checklist_stream" - default: - if turn%2 == 0 { - return "custom_sync" - } - return "custom_stream" - } -} - -func providerShapeFor(src source, turn int) string { - switch src { - case sourceOpenAI: - if openAIUsesResponses(turn) { - return "openai_responses" - } - return "openai_chat_completions" - case sourceAnthropic: - return "messages" - case sourceGemini: - return "generate_content" - default: - return "core_generation" - } -} - -func openAIUsesResponses(turn int) bool { - return turn%2 != 0 -} - -func personaForTurn(turn int) string { - personas := []string{"planner", "retriever", "executor"} - return personas[turn%len(personas)] -} - -func newConversationID(language, provider string, slot int) string { - return fmt.Sprintf("devex-%s-%s-%d-%d", language, provider, slot, time.Now().UnixMilli()) -} - -func loadConfig() runtimeConfig { - return runtimeConfig{ - interval: time.Duration(intFromEnv("SIGIL_TRAFFIC_INTERVAL_MS", 2000)) * time.Millisecond, - streamPercent: intFromEnv("SIGIL_TRAFFIC_STREAM_PERCENT", 30), - conversations: intFromEnv("SIGIL_TRAFFIC_CONVERSATIONS", 3), - rotateTurns: intFromEnv("SIGIL_TRAFFIC_ROTATE_TURNS", 24), - maxCycles: intFromEnv("SIGIL_TRAFFIC_MAX_CYCLES", 0), - customProvider: strings.TrimSpace(stringFromEnv("SIGIL_TRAFFIC_CUSTOM_PROVIDER", "mistral")), - genGRPC: stringFromEnv("SIGIL_TRAFFIC_GEN_GRPC_ENDPOINT", "sigil:4317"), - traceGRPC: stringFromEnv("SIGIL_TRAFFIC_TRACE_GRPC_ENDPOINT", "alloy:4317"), - } -} - -func configureTelemetry(ctx context.Context, cfg runtimeConfig) (func(context.Context) error, error) { - telemetryEndpoint := strings.TrimSpace(cfg.traceGRPC) - if telemetryEndpoint == "" { - return func(context.Context) error { return nil }, nil - } - - traceExporter, err := otlptracegrpc.New( - ctx, - otlptracegrpc.WithEndpoint(telemetryEndpoint), - otlptracegrpc.WithInsecure(), - ) - if err != nil { - return nil, fmt.Errorf("init otlp trace exporter: %w", err) - } - - metricExporter, err := otlpmetricgrpc.New( - ctx, - otlpmetricgrpc.WithEndpoint(telemetryEndpoint), - otlpmetricgrpc.WithInsecure(), - ) - if err != nil { - return nil, fmt.Errorf("init otlp metric exporter: %w", err) - } - - metricReader := sdkmetric.NewPeriodicReader(metricExporter, sdkmetric.WithInterval(metricFlushInterval)) - tracerProvider, meterProvider := installTelemetryProviders(traceExporter, metricReader) - return func(ctx context.Context) error { - metricErr := meterProvider.Shutdown(ctx) - traceErr := tracerProvider.Shutdown(ctx) - return errors.Join(metricErr, traceErr) - }, nil -} - -func installTelemetryProviders( - traceExporter sdktrace.SpanExporter, - metricReader sdkmetric.Reader, -) (*sdktrace.TracerProvider, *sdkmetric.MeterProvider) { - res := resource.NewSchemaless( - attribute.String("service.name", traceServiceName), - attribute.String("service.namespace", traceServiceEnv), - attribute.String("sigil.devex.language", languageName), - ) - - tracerProvider := sdktrace.NewTracerProvider( - sdktrace.WithSampler(sdktrace.ParentBased(sdktrace.AlwaysSample())), - sdktrace.WithBatcher(traceExporter), - sdktrace.WithResource(res), - ) - meterProvider := sdkmetric.NewMeterProvider( - sdkmetric.WithReader(metricReader), - sdkmetric.WithResource(res), - ) - - otel.SetTracerProvider(tracerProvider) - otel.SetMeterProvider(meterProvider) - - return tracerProvider, meterProvider -} - -func intFromEnv(key string, defaultValue int) int { - value := strings.TrimSpace(os.Getenv(key)) - if value == "" { - return defaultValue - } - parsed, err := strconv.Atoi(value) - if err != nil || parsed <= 0 { - return defaultValue - } - return parsed -} - -func stringFromEnv(key, defaultValue string) string { - value := strings.TrimSpace(os.Getenv(key)) - if value == "" { - return defaultValue - } - return value -} diff --git a/go/cmd/devex-emitter/main_test.go b/go/cmd/devex-emitter/main_test.go deleted file mode 100644 index fec1993..0000000 --- a/go/cmd/devex-emitter/main_test.go +++ /dev/null @@ -1,230 +0,0 @@ -package main - -import ( - "context" - "math/rand" - "testing" - "time" - - "github.com/grafana/sigil/sdks/go/sigil" - "go.opentelemetry.io/otel" - sdkmetric "go.opentelemetry.io/otel/sdk/metric" - "go.opentelemetry.io/otel/sdk/trace/tracetest" -) - -func TestBuildTagEnvelopeIncludesRequiredContractFields(t *testing.T) { - envelope := buildTagEnvelope(sourceOpenAI, sigil.GenerationModeSync, 2, 1) - - if envelope.tags["sigil.devex.language"] != languageName { - t.Fatalf("expected language tag %q, got %q", languageName, envelope.tags["sigil.devex.language"]) - } - if envelope.tags["sigil.devex.provider"] != "openai" { - t.Fatalf("expected provider tag openai, got %q", envelope.tags["sigil.devex.provider"]) - } - if envelope.tags["sigil.devex.source"] != "provider_wrapper" { - t.Fatalf("expected source tag provider_wrapper, got %q", envelope.tags["sigil.devex.source"]) - } - if envelope.tags["sigil.devex.mode"] != "SYNC" { - t.Fatalf("expected mode tag SYNC, got %q", envelope.tags["sigil.devex.mode"]) - } - - if envelope.metadata["turn_index"] != 2 { - t.Fatalf("expected turn_index metadata 2, got %#v", envelope.metadata["turn_index"]) - } - if envelope.metadata["conversation_slot"] != 1 { - t.Fatalf("expected conversation_slot metadata 1, got %#v", envelope.metadata["conversation_slot"]) - } - if envelope.metadata["emitter"] != "sdk-traffic" { - t.Fatalf("expected emitter metadata sdk-traffic, got %#v", envelope.metadata["emitter"]) - } - if envelope.metadata["provider_shape"] != "openai_chat_completions" { - t.Fatalf("expected provider_shape openai_chat_completions, got %#v", envelope.metadata["provider_shape"]) - } - if envelope.agentPersona == "" { - t.Fatalf("expected non-empty agent persona") - } - if envelope.conversationTitle != "Devex GO OpenAI 2" { - t.Fatalf("expected conversation title %q, got %q", "Devex GO OpenAI 2", envelope.conversationTitle) - } -} - -func TestConversationTitleForUsesStableProviderAndSlot(t *testing.T) { - if got := conversationTitleFor(sourceGemini, 0); got != "Devex GO Gemini 1" { - t.Fatalf("expected Gemini title, got %q", got) - } - if got := conversationTitleFor(sourceCustom, 2); got != "Devex GO Mistral 3" { - t.Fatalf("expected Mistral title, got %q", got) - } -} - -func TestBuildTagEnvelopeOpenAIAlternatesProviderShape(t *testing.T) { - chatEnvelope := buildTagEnvelope(sourceOpenAI, sigil.GenerationModeSync, 0, 0) - if chatEnvelope.metadata["provider_shape"] != "openai_chat_completions" { - t.Fatalf("expected openai_chat_completions, got %#v", chatEnvelope.metadata["provider_shape"]) - } - - responsesEnvelope := buildTagEnvelope(sourceOpenAI, sigil.GenerationModeSync, 1, 0) - if responsesEnvelope.metadata["provider_shape"] != "openai_responses" { - t.Fatalf("expected openai_responses, got %#v", responsesEnvelope.metadata["provider_shape"]) - } -} - -func TestSourceTagForCustomProvider(t *testing.T) { - if got := sourceTagFor(sourceCustom); got != "core_custom" { - t.Fatalf("expected core_custom, got %q", got) - } - if got := sourceTagFor(sourceGemini); got != "provider_wrapper" { - t.Fatalf("expected provider_wrapper, got %q", got) - } -} - -func TestChooseModeUsesThreshold(t *testing.T) { - if got := chooseMode(10, 30); got != sigil.GenerationModeStream { - t.Fatalf("expected STREAM, got %s", got) - } - if got := chooseMode(30, 30); got != sigil.GenerationModeSync { - t.Fatalf("expected SYNC, got %s", got) - } -} - -func TestEnsureThreadRotatesConversationAtThreshold(t *testing.T) { - thread := &threadState{} - ensureThread(thread, 3, sourceOpenAI, 0) - if thread.turn != 0 { - t.Fatalf("expected initial turn 0, got %d", thread.turn) - } - if thread.conversationID == "" { - t.Fatalf("expected conversation id to be set") - } - - firstID := thread.conversationID - thread.turn = 3 - // newConversationID uses Unix millis, so ensure the timestamp can advance. - time.Sleep(2 * time.Millisecond) - ensureThread(thread, 3, sourceOpenAI, 0) - if thread.turn != 0 { - t.Fatalf("expected rotated turn 0, got %d", thread.turn) - } - if thread.conversationID == firstID { - t.Fatalf("expected rotated conversation id to change") - } -} - -func TestLoadConfigReadsTraceGRPCEndpoint(t *testing.T) { - t.Setenv("SIGIL_TRAFFIC_TRACE_GRPC_ENDPOINT", "collector:14317") - - cfg := loadConfig() - - if cfg.traceGRPC != "collector:14317" { - t.Fatalf("expected trace GRPC endpoint collector:14317, got %q", cfg.traceGRPC) - } -} - -func TestInstallTelemetryProvidersExportsSpans(t *testing.T) { - previousProvider := otel.GetTracerProvider() - previousMeterProvider := otel.GetMeterProvider() - exporter := tracetest.NewInMemoryExporter() - tracerProvider, meterProvider := installTelemetryProviders(exporter, sdkmetric.NewManualReader()) - t.Cleanup(func() { - otel.SetTracerProvider(previousProvider) - otel.SetMeterProvider(previousMeterProvider) - _ = tracerProvider.Shutdown(context.Background()) - _ = meterProvider.Shutdown(context.Background()) - }) - - _, span := otel.Tracer("devex-emitter-test").Start(context.Background(), "synthetic-span") - span.End() - if err := tracerProvider.ForceFlush(context.Background()); err != nil { - t.Fatalf("force flush: %v", err) - } - - spans := exporter.GetSpans() - if len(spans) != 1 { - t.Fatalf("expected 1 exported span, got %d", len(spans)) - } - - serviceName := "" - for _, kv := range spans[0].Resource.Attributes() { - if string(kv.Key) == "service.name" { - serviceName = kv.Value.AsString() - break - } - } - if serviceName == "" { - t.Fatalf("expected service.name resource attribute") - } - if serviceName != traceServiceName { - t.Fatalf("expected service.name %q, got %q", traceServiceName, serviceName) - } -} - -func TestEmitSyntheticLifecycleSpansProducesTraceRichSpanCount(t *testing.T) { - previousProvider := otel.GetTracerProvider() - previousMeterProvider := otel.GetMeterProvider() - exporter := tracetest.NewInMemoryExporter() - tracerProvider, meterProvider := installTelemetryProviders(exporter, sdkmetric.NewManualReader()) - t.Cleanup(func() { - otel.SetTracerProvider(previousProvider) - otel.SetMeterProvider(previousMeterProvider) - _ = tracerProvider.Shutdown(context.Background()) - _ = meterProvider.Shutdown(context.Background()) - }) - - ctx, root := otel.Tracer("devex-emitter-test").Start(context.Background(), "root") - traceEnd := time.Now() - traceStart := traceEnd.Add(-3 * time.Second) - syntheticCount := emitSyntheticLifecycleSpans(ctx, rand.New(rand.NewSource(42)), traceStart, traceEnd) - root.End() - if err := tracerProvider.ForceFlush(context.Background()); err != nil { - t.Fatalf("force flush: %v", err) - } - - if syntheticCount < minSyntheticSpans || syntheticCount > maxSyntheticSpans { - t.Fatalf("expected synthetic count in [%d,%d], got %d", minSyntheticSpans, maxSyntheticSpans, syntheticCount) - } - - spans := exporter.GetSpans() - if len(spans) != syntheticCount+1 { - t.Fatalf("expected root + synthetic spans (%d), got %d", syntheticCount+1, len(spans)) - } - - syntheticSeen := 0 - for _, span := range spans { - if span.Name == "root" { - continue - } - syntheticSeen++ - foundCategory := false - var simulatedDurationMs int64 = -1 - for _, kv := range span.Attributes { - if string(kv.Key) == "sigil.synthetic.category" && kv.Value.AsString() != "" { - foundCategory = true - } - if string(kv.Key) == "sigil.synthetic.simulated_duration_ms" { - simulatedDurationMs = kv.Value.AsInt64() - } - } - if !span.EndTime.After(span.StartTime) { - t.Fatalf("expected synthetic span %q to have end time after start time", span.Name) - } - if !foundCategory { - t.Fatalf("expected synthetic span %q to include sigil.synthetic.category attribute", span.Name) - } - if simulatedDurationMs <= 0 { - t.Fatalf("expected synthetic span %q to include positive simulated duration attribute", span.Name) - } - actualDurationMs := span.EndTime.Sub(span.StartTime).Milliseconds() - if actualDurationMs != simulatedDurationMs { - t.Fatalf("expected synthetic span %q duration to match simulated duration: actual=%dms simulated=%dms", span.Name, actualDurationMs, simulatedDurationMs) - } - if span.StartTime.Before(traceStart) { - t.Fatalf("expected synthetic span %q start time (%s) to be inside trace window start (%s)", span.Name, span.StartTime, traceStart) - } - if span.EndTime.After(traceEnd) { - t.Fatalf("expected synthetic span %q end time (%s) to be inside trace window end (%s)", span.Name, span.EndTime, traceEnd) - } - } - if syntheticSeen != syntheticCount { - t.Fatalf("expected %d synthetic spans, saw %d", syntheticCount, syntheticSeen) - } -} diff --git a/go/cmd/devex-emitter/telemetry_test.go b/go/cmd/devex-emitter/telemetry_test.go deleted file mode 100644 index d7941b0..0000000 --- a/go/cmd/devex-emitter/telemetry_test.go +++ /dev/null @@ -1,68 +0,0 @@ -package main - -import ( - "context" - "testing" - - "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/metric" - sdkmetric "go.opentelemetry.io/otel/sdk/metric" - "go.opentelemetry.io/otel/sdk/metric/metricdata" - sdktrace "go.opentelemetry.io/otel/sdk/trace" - "go.opentelemetry.io/otel/sdk/trace/tracetest" -) - -func TestInstallTelemetryProvidersSetsTracerAndMeterProviders(t *testing.T) { - previousTracerProvider := otel.GetTracerProvider() - previousMeterProvider := otel.GetMeterProvider() - defer func() { - otel.SetTracerProvider(previousTracerProvider) - otel.SetMeterProvider(previousMeterProvider) - }() - - traceExporter := tracetest.NewInMemoryExporter() - metricReader := sdkmetric.NewManualReader() - - tracerProvider, meterProvider := installTelemetryProviders(traceExporter, metricReader) - t.Cleanup(func() { - _ = tracerProvider.Shutdown(context.Background()) - _ = meterProvider.Shutdown(context.Background()) - }) - - _, span := otel.Tracer("sigil/devex-telemetry-test").Start(context.Background(), "test-span") - span.End() - if err := tracerProvider.ForceFlush(context.Background()); err != nil { - t.Fatalf("force flush tracer provider: %v", err) - } - - if got := len(traceExporter.GetSpans()); got == 0 { - t.Fatalf("expected at least one span to be exported, got %d", got) - } - - counter, err := otel.Meter("sigil/devex-telemetry-test").Int64Counter("devex_telemetry_counter") - if err != nil { - t.Fatalf("create counter: %v", err) - } - counter.Add(context.Background(), 1, metric.WithAttributes(attribute.String("source", "test"))) - - var collected metricdata.ResourceMetrics - if err := metricReader.Collect(context.Background(), &collected); err != nil { - t.Fatalf("collect metrics: %v", err) - } - - foundCounter := false - for _, scopeMetrics := range collected.ScopeMetrics { - for _, metric := range scopeMetrics.Metrics { - if metric.Name == "devex_telemetry_counter" { - foundCounter = true - break - } - } - } - if !foundCounter { - t.Fatal("expected devex_telemetry_counter metric to be collected") - } -} - -var _ sdktrace.SpanExporter = (*tracetest.InMemoryExporter)(nil) diff --git a/go/cmd/devex-emitter/ttft_test.go b/go/cmd/devex-emitter/ttft_test.go deleted file mode 100644 index 6916be0..0000000 --- a/go/cmd/devex-emitter/ttft_test.go +++ /dev/null @@ -1,99 +0,0 @@ -package main - -import ( - "context" - "math/rand" - "testing" - - "github.com/grafana/sigil/sdks/go/sigil" - "go.opentelemetry.io/otel" - sdkmetric "go.opentelemetry.io/otel/sdk/metric" - "go.opentelemetry.io/otel/sdk/metric/metricdata" - "go.opentelemetry.io/otel/sdk/trace/tracetest" -) - -func TestStreamEmittersRecordTTFTMetric(t *testing.T) { - previousTracerProvider := otel.GetTracerProvider() - previousMeterProvider := otel.GetMeterProvider() - metricReader := sdkmetric.NewManualReader() - traceExporter := tracetest.NewInMemoryExporter() - tracerProvider, meterProvider := installTelemetryProviders(traceExporter, metricReader) - t.Cleanup(func() { - otel.SetTracerProvider(previousTracerProvider) - otel.SetMeterProvider(previousMeterProvider) - _ = tracerProvider.Shutdown(context.Background()) - _ = meterProvider.Shutdown(context.Background()) - }) - - clientCfg := sigil.DefaultConfig() - clientCfg.GenerationExport.Protocol = sigil.GenerationExportProtocolNone - clientCfg.GenerationExport.Auth = sigil.AuthConfig{Mode: sigil.ExportAuthModeNone} - client := sigil.NewClient(clientCfg) - t.Cleanup(func() { - _ = client.Shutdown(context.Background()) - }) - - tags := map[string]string{"sigil.devex.test": "true"} - metadata := map[string]any{"conversation_slot": 0} - - if err := emitOpenAIChatCompletionsStream(context.Background(), client, "conv-openai-chat", "Devex GO OpenAI 1", "agent-openai-chat", "v1", tags, metadata, 1); err != nil { - t.Fatalf("emit openai chat stream: %v", err) - } - if err := emitOpenAIResponsesStream(context.Background(), client, "conv-openai-responses", "Devex GO OpenAI 1", "agent-openai-responses", "v1", tags, metadata, 2); err != nil { - t.Fatalf("emit openai responses stream: %v", err) - } - if err := emitAnthropicStream(context.Background(), client, "conv-anthropic", "Devex GO Anthropic 1", "agent-anthropic", "v1", tags, metadata, 3); err != nil { - t.Fatalf("emit anthropic stream: %v", err) - } - if err := emitGeminiStream(context.Background(), client, "conv-gemini", "Devex GO Gemini 1", "agent-gemini", "v1", tags, metadata, 4); err != nil { - t.Fatalf("emit gemini stream: %v", err) - } - if err := emitCustomStream( - context.Background(), - client, - "mistral", - "conv-custom", - "Devex GO Mistral 1", - "agent-custom", - "v1", - tags, - metadata, - 5, - rand.New(rand.NewSource(42)), - ); err != nil { - t.Fatalf("emit custom stream: %v", err) - } - - if err := client.Flush(context.Background()); err != nil { - t.Fatalf("flush client: %v", err) - } - - var collected metricdata.ResourceMetrics - if err := metricReader.Collect(context.Background(), &collected); err != nil { - t.Fatalf("collect metrics: %v", err) - } - - ttftCount := histogramCount(collected, "gen_ai.client.time_to_first_token") - if ttftCount < 5 { - t.Fatalf("expected TTFT histogram count >= 5 from stream emitters, got %d", ttftCount) - } -} - -func histogramCount(collected metricdata.ResourceMetrics, metricName string) uint64 { - var total uint64 - for _, scopeMetrics := range collected.ScopeMetrics { - for _, m := range scopeMetrics.Metrics { - if m.Name != metricName { - continue - } - histogram, ok := m.Data.(metricdata.Histogram[float64]) - if !ok { - continue - } - for _, point := range histogram.DataPoints { - total += point.Count - } - } - } - return total -} diff --git a/go/cmd/sigil-probe/main.go b/go/cmd/sigil-probe/main.go index c9476ac..13bcb24 100644 --- a/go/cmd/sigil-probe/main.go +++ b/go/cmd/sigil-probe/main.go @@ -18,7 +18,7 @@ import ( "strings" "time" - "github.com/grafana/sigil/sdks/go/sigil" + "github.com/grafana/sigil-sdk/go/sigil" ) const ( diff --git a/go/go.mod b/go/go.mod index 431bec9..1514528 100644 --- a/go/go.mod +++ b/go/go.mod @@ -1,4 +1,4 @@ -module github.com/grafana/sigil/sdks/go +module github.com/grafana/sigil-sdk/go go 1.25.6 diff --git a/go/sigil/client.go b/go/sigil/client.go index 06000f2..bf84d05 100644 --- a/go/sigil/client.go +++ b/go/sigil/client.go @@ -93,7 +93,7 @@ type APIConfig struct { Endpoint string } -const instrumentationName = "github.com/grafana/sigil/sdks/go/sigil" +const instrumentationName = "github.com/grafana/sigil-sdk/go/sigil" const ( defaultGRPCMaxSendMessageBytes = 16 << 20 defaultGRPCMaxReceiveMessageBytes = 16 << 20 diff --git a/go/sigil/client_test.go b/go/sigil/client_test.go index f1096a0..bd93b8e 100644 --- a/go/sigil/client_test.go +++ b/go/sigil/client_test.go @@ -9,7 +9,7 @@ import ( "time" "unicode/utf8" - sigilv1 "github.com/grafana/sigil/sdks/go/sigil/internal/gen/sigil/v1" + sigilv1 "github.com/grafana/sigil-sdk/go/sigil/internal/gen/sigil/v1" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/codes" sdktrace "go.opentelemetry.io/otel/sdk/trace" diff --git a/go/sigil/conformance_helpers_test.go b/go/sigil/conformance_helpers_test.go index b6e9fc8..6268d53 100644 --- a/go/sigil/conformance_helpers_test.go +++ b/go/sigil/conformance_helpers_test.go @@ -11,8 +11,8 @@ import ( "testing" "time" - sigil "github.com/grafana/sigil/sdks/go/sigil" - sigilv1 "github.com/grafana/sigil/sdks/go/sigil/internal/gen/sigil/v1" + sigil "github.com/grafana/sigil-sdk/go/sigil" + sigilv1 "github.com/grafana/sigil-sdk/go/sigil/internal/gen/sigil/v1" "go.opentelemetry.io/otel/attribute" sdkmetric "go.opentelemetry.io/otel/sdk/metric" "go.opentelemetry.io/otel/sdk/metric/metricdata" diff --git a/go/sigil/conformance_test.go b/go/sigil/conformance_test.go index d8bdf8f..9f8e64d 100644 --- a/go/sigil/conformance_test.go +++ b/go/sigil/conformance_test.go @@ -9,8 +9,8 @@ import ( "testing" "time" - sigil "github.com/grafana/sigil/sdks/go/sigil" - sigilv1 "github.com/grafana/sigil/sdks/go/sigil/internal/gen/sigil/v1" + sigil "github.com/grafana/sigil-sdk/go/sigil" + sigilv1 "github.com/grafana/sigil-sdk/go/sigil/internal/gen/sigil/v1" "go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/trace" ) diff --git a/go/sigil/example_test.go b/go/sigil/example_test.go index 790682b..0613e67 100644 --- a/go/sigil/example_test.go +++ b/go/sigil/example_test.go @@ -4,7 +4,7 @@ import ( "context" "strings" - "github.com/grafana/sigil/sdks/go/sigil" + "github.com/grafana/sigil-sdk/go/sigil" ) func ExampleClient_StartGeneration() { diff --git a/go/sigil/exporter.go b/go/sigil/exporter.go index 368610a..56bea0c 100644 --- a/go/sigil/exporter.go +++ b/go/sigil/exporter.go @@ -12,7 +12,7 @@ import ( "strings" "time" - sigilv1 "github.com/grafana/sigil/sdks/go/sigil/internal/gen/sigil/v1" + sigilv1 "github.com/grafana/sigil-sdk/go/sigil/internal/gen/sigil/v1" "google.golang.org/grpc" "google.golang.org/grpc/credentials" "google.golang.org/grpc/credentials/insecure" diff --git a/go/sigil/exporter_transport_test.go b/go/sigil/exporter_transport_test.go index c9d5ffa..6ba7b5d 100644 --- a/go/sigil/exporter_transport_test.go +++ b/go/sigil/exporter_transport_test.go @@ -13,7 +13,7 @@ import ( "testing" "time" - sigilv1 "github.com/grafana/sigil/sdks/go/sigil/internal/gen/sigil/v1" + sigilv1 "github.com/grafana/sigil-sdk/go/sigil/internal/gen/sigil/v1" "go.opentelemetry.io/otel/trace/noop" "google.golang.org/grpc" "google.golang.org/protobuf/encoding/protojson" diff --git a/go/sigil/proto_mapping.go b/go/sigil/proto_mapping.go index 8e78114..51a5ede 100644 --- a/go/sigil/proto_mapping.go +++ b/go/sigil/proto_mapping.go @@ -4,7 +4,7 @@ import ( "encoding/json" "fmt" - sigilv1 "github.com/grafana/sigil/sdks/go/sigil/internal/gen/sigil/v1" + sigilv1 "github.com/grafana/sigil-sdk/go/sigil/internal/gen/sigil/v1" "google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/timestamppb" ) diff --git a/go/sigil/sigiltest/env.go b/go/sigil/sigiltest/env.go index 6b02c36..66f5aa0 100644 --- a/go/sigil/sigiltest/env.go +++ b/go/sigil/sigiltest/env.go @@ -9,8 +9,8 @@ import ( "testing" "time" - sigil "github.com/grafana/sigil/sdks/go/sigil" - sigilv1 "github.com/grafana/sigil/sdks/go/sigil/internal/gen/sigil/v1" + sigil "github.com/grafana/sigil-sdk/go/sigil" + sigilv1 "github.com/grafana/sigil-sdk/go/sigil/internal/gen/sigil/v1" sdkmetric "go.opentelemetry.io/otel/sdk/metric" sdktrace "go.opentelemetry.io/otel/sdk/trace" "go.opentelemetry.io/otel/sdk/trace/tracetest" diff --git a/go/sigil/sigiltest/record.go b/go/sigil/sigiltest/record.go index bb87997..e8de10f 100644 --- a/go/sigil/sigiltest/record.go +++ b/go/sigil/sigiltest/record.go @@ -5,7 +5,7 @@ import ( "testing" "time" - sigil "github.com/grafana/sigil/sdks/go/sigil" + sigil "github.com/grafana/sigil-sdk/go/sigil" ) func RecordGeneration(t testing.TB, env *Env, start sigil.GenerationStart, generation sigil.Generation, mapErr error) { diff --git a/java/core/build.gradle.kts b/java/core/build.gradle.kts index ed0653d..0830200 100644 --- a/java/core/build.gradle.kts +++ b/java/core/build.gradle.kts @@ -8,7 +8,7 @@ java { withSourcesJar() } -val protoRoot = rootProject.projectDir.resolve("../../sigil/proto") +val protoRoot = rootProject.projectDir.resolve("../proto") dependencies { api(libs.otel.api) diff --git a/mise.toml b/mise.toml new file mode 100644 index 0000000..96e0dd6 --- /dev/null +++ b/mise.toml @@ -0,0 +1,428 @@ +[env] +PYTHON_BIN = "python3" + +# --- Dependencies --- + +[tasks.deps] +description = "Install JS/TS dependencies" +run = "pnpm install" + +# --- Formatting --- + +[tasks."format:go"] +description = "Format Go code in all modules" +run = """ +#!/usr/bin/env bash +set -euo pipefail +while IFS= read -r moddir; do + echo "==> gofmt ${moddir}" + gofmt -w "${moddir}" +done < <(find . -name go.mod -not -path './node_modules/*' -exec dirname {} \\; | sort) +""" + +[tasks."format:cs"] +description = "Format .NET SDK code" +run = "dotnet format dotnet/Sigil.DotNet.sln" + +[tasks.format] +description = "Format all code" +depends = ["format:go", "format:cs"] + +# --- Linting --- + +[tasks."lint:go"] +description = "Lint Go code in all modules" +run = """ +#!/usr/bin/env bash +set -euo pipefail +while IFS= read -r moddir; do + if [[ -z "$(cd "${moddir}" && GOWORK=off go list ./... 2>/dev/null || true)" ]]; then + echo "==> skip golangci-lint ${moddir} (no Go packages)" + continue + fi + echo "==> golangci-lint ${moddir}" + (cd "${moddir}" && GOWORK=off golangci-lint run ./...) +done < <(find . -name go.mod -not -path './node_modules/*' -exec dirname {} \\; | sort) +""" + +[tasks."lint:cs"] +description = "Verify .NET SDK formatting and analyzer checks" +run = "dotnet format dotnet/Sigil.DotNet.sln --verify-no-changes" + +[tasks.lint] +description = "Run all linting" +depends = ["lint:go", "lint:cs"] + +# --- Type checking --- + +[tasks."typecheck:ts:sdk-js"] +description = "Type-check TypeScript/JavaScript SDK code" +depends = ["deps"] +run = "pnpm --filter @grafana/sigil-sdk-js run typecheck" + +[tasks.typecheck] +description = "Run all type checks" +depends = ["typecheck:ts:sdk-js"] + +# --- Go SDK tests --- + +[tasks."test:go:sdk-core"] +description = "Run Go SDK core tests as standalone module" +dir = "go" +run = "GOWORK=off go test ./..." + +[tasks."test:go:sdk-anthropic"] +description = "Run Anthropic provider helper tests as standalone module" +dir = "go-providers/anthropic" +run = "GOWORK=off go test ./..." + +[tasks."test:go:sdk-openai"] +description = "Run OpenAI provider helper tests as standalone module" +dir = "go-providers/openai" +run = "GOWORK=off go test ./..." + +[tasks."test:go:sdk-gemini"] +description = "Run Gemini provider helper tests as standalone module" +dir = "go-providers/gemini" +run = "GOWORK=off go test ./..." + +[tasks."test:go:sdk-google-adk"] +description = "Run Google ADK framework helper tests as standalone module" +dir = "go-frameworks/google-adk" +run = "GOWORK=off go test ./..." + +[tasks."test:go:sdk-conformance"] +description = "Run the Go SDK core conformance harness" +dir = "go" +run = "GOWORK=off go test ./sigil -run '^TestConformance' -count=1" + +# --- TypeScript/JavaScript SDK tests --- + +[tasks."test:ts:sdk-js"] +description = "Run TypeScript/JavaScript SDK runtime tests" +depends = ["deps"] +run = "pnpm --filter @grafana/sigil-sdk-js run test:ci" + +[tasks."test:ts:sdk-conformance"] +description = "Run TypeScript/JavaScript SDK core conformance tests" +depends = ["deps"] +dir = "js" +run = "pnpm run test:build && node --test test/conformance.test.mjs" + +[tasks."test:ts:sdk-provider-conformance"] +description = "Run TypeScript/JavaScript provider-wrapper conformance tests" +depends = ["deps"] +dir = "js" +run = "pnpm run test:build && node --test test/providers.test.mjs" + +[tasks."test:ts:sdk-framework-conformance"] +description = "Run TypeScript/JavaScript framework-adapter conformance tests" +depends = ["deps"] +run = "mise run test:ts:sdk-js-frameworks" + +[tasks."test:ts:sdk-js-frameworks"] +description = "Run TypeScript/JavaScript SDK framework handler tests" +depends = ["deps"] +dir = "js" +run = "pnpm run test:build && node --test test/frameworks.langchain.test.mjs test/frameworks.langgraph.test.mjs test/frameworks.additional.test.mjs test/frameworks.vercel-ai-sdk.mapping.test.mjs test/frameworks.vercel-ai-sdk.test.mjs" + +# --- Python SDK tests --- + +[tasks."test:py:sdk-core"] +description = "Run Python SDK core parity tests" +run = "uv run --python \"$PYTHON_BIN\" --with '.[dev]' --directory python pytest tests" + +[tasks."test:py:sdk-conformance"] +description = "Run Python SDK core conformance tests" +run = "uv run --python \"$PYTHON_BIN\" --with '.[dev]' --directory python pytest tests/test_conformance.py" + +[tasks."test:py:sdk-openai"] +description = "Run Python OpenAI provider helper tests" +run = "uv run --python \"$PYTHON_BIN\" --with './python[dev]' --with './python-providers/openai[dev]' pytest python-providers/openai/tests" + +[tasks."test:py:sdk-anthropic"] +description = "Run Python Anthropic provider helper tests" +run = "uv run --python \"$PYTHON_BIN\" --with './python[dev]' --with './python-providers/anthropic[dev]' pytest python-providers/anthropic/tests" + +[tasks."test:py:sdk-gemini"] +description = "Run Python Gemini provider helper tests" +run = "uv run --python \"$PYTHON_BIN\" --with './python[dev]' --with './python-providers/gemini[dev]' pytest python-providers/gemini/tests" + +[tasks."test:py:sdk-langchain"] +description = "Run Python LangChain framework handler tests" +run = "uv run --python \"$PYTHON_BIN\" --with './python[dev]' --with './python-frameworks/langchain[dev]' pytest python-frameworks/langchain/tests" + +[tasks."test:py:sdk-langgraph"] +description = "Run Python LangGraph framework handler tests" +run = "uv run --python \"$PYTHON_BIN\" --with './python[dev]' --with './python-frameworks/langgraph[dev]' pytest python-frameworks/langgraph/tests" + +[tasks."test:py:sdk-openai-agents"] +description = "Run Python OpenAI Agents framework handler tests" +run = "uv run --python \"$PYTHON_BIN\" --with './python[dev]' --with './python-frameworks/openai-agents[dev]' pytest python-frameworks/openai-agents/tests" + +[tasks."test:py:sdk-llamaindex"] +description = "Run Python LlamaIndex framework handler tests" +run = "uv run --python \"$PYTHON_BIN\" --with './python[dev]' --with './python-frameworks/llamaindex[dev]' pytest python-frameworks/llamaindex/tests" + +[tasks."test:py:sdk-google-adk"] +description = "Run Python Google ADK framework handler tests" +run = "uv run --python \"$PYTHON_BIN\" --with './python[dev]' --with './python-frameworks/google-adk[dev]' pytest python-frameworks/google-adk/tests" + +[tasks."test:py:sdk-provider-conformance"] +description = "Run Python provider-wrapper conformance tests" +run = """ +#!/usr/bin/env bash +set -euo pipefail +mise run test:py:sdk-openai +mise run test:py:sdk-anthropic +mise run test:py:sdk-gemini +""" + +[tasks."test:py:sdk-framework-conformance"] +description = "Run Python framework-adapter conformance tests" +run = """ +#!/usr/bin/env bash +set -euo pipefail +mise run test:py:sdk-langchain +mise run test:py:sdk-langgraph +mise run test:py:sdk-openai-agents +mise run test:py:sdk-llamaindex +mise run test:py:sdk-google-adk +""" + +# --- .NET SDK tests --- + +[tasks."test:cs:sdk-core"] +description = "Run .NET SDK core runtime tests" +dir = "dotnet" +run = "dotnet test tests/Grafana.Sigil.Tests/Grafana.Sigil.Tests.csproj -c Release" + +[tasks."test:cs:sdk-conformance"] +description = "Run .NET SDK core conformance tests" +dir = "dotnet" +run = "dotnet test tests/Grafana.Sigil.Tests/Grafana.Sigil.Tests.csproj -c Release --filter FullyQualifiedName~ConformanceTests" + +[tasks."test:cs:sdk-openai"] +description = "Run .NET OpenAI provider helper tests" +dir = "dotnet" +run = "dotnet test tests/Grafana.Sigil.OpenAI.Tests/Grafana.Sigil.OpenAI.Tests.csproj -c Release" + +[tasks."test:cs:sdk-anthropic"] +description = "Run .NET Anthropic provider helper tests" +dir = "dotnet" +run = "dotnet test tests/Grafana.Sigil.Anthropic.Tests/Grafana.Sigil.Anthropic.Tests.csproj -c Release" + +[tasks."test:cs:sdk-gemini"] +description = "Run .NET Gemini provider helper tests" +dir = "dotnet" +run = "dotnet test tests/Grafana.Sigil.Gemini.Tests/Grafana.Sigil.Gemini.Tests.csproj -c Release" + +[tasks."test:cs:sdk-provider-conformance"] +description = "Run .NET provider-wrapper conformance tests" +run = """ +#!/usr/bin/env bash +set -euo pipefail +mise run test:cs:sdk-openai +mise run test:cs:sdk-anthropic +mise run test:cs:sdk-gemini +""" + +# --- Java SDK tests --- + +[tasks."test:java:sdk-core"] +description = "Run Java SDK core tests" +dir = "java" +run = "./gradlew --no-daemon :core:test" + +[tasks."test:java:sdk-conformance"] +description = "Run Java SDK core conformance tests" +dir = "java" +run = "./gradlew --no-daemon :core:test --tests 'com.grafana.sigil.sdk.ConformanceTest'" + +[tasks."test:java:sdk-openai"] +description = "Run Java OpenAI provider adapter tests" +dir = "java" +run = "./gradlew --no-daemon :providers:openai:test" + +[tasks."test:java:sdk-anthropic"] +description = "Run Java Anthropic provider adapter tests" +dir = "java" +run = "./gradlew --no-daemon :providers:anthropic:test" + +[tasks."test:java:sdk-gemini"] +description = "Run Java Gemini provider adapter tests" +dir = "java" +run = "./gradlew --no-daemon :providers:gemini:test" + +[tasks."test:java:sdk-google-adk"] +description = "Run Java Google ADK framework adapter tests" +dir = "java" +run = "./gradlew --no-daemon :frameworks:google-adk:test" + +[tasks."test:java:sdk-all"] +description = "Run all Java SDK tests" +dir = "java" +run = "./gradlew --no-daemon :core:test :providers:openai:test :providers:anthropic:test :providers:gemini:test :frameworks:google-adk:test" + +[tasks."test:java:sdk-provider-conformance"] +description = "Run Java provider-wrapper conformance tests" +dir = "java" +run = "./gradlew --no-daemon :providers:openai:test :providers:anthropic:test :providers:gemini:test" + +[tasks."test:java:sdk-framework-conformance"] +description = "Run Java framework-adapter conformance tests" +dir = "java" +run = "./gradlew --no-daemon :frameworks:google-adk:test" + +[tasks."benchmark:java:sdk"] +description = "Run Java SDK JMH benchmarks" +dir = "java" +run = "./gradlew --no-daemon :benchmarks:jmh" + +# --- Cross-SDK conformance --- + +[tasks."test:sdk:core-conformance"] +description = "Run core conformance suites across Go, TypeScript/JavaScript, Python, Java, and .NET SDKs" +run = """ +#!/usr/bin/env bash +set -euo pipefail +mise run test:go:sdk-conformance +mise run test:ts:sdk-conformance +mise run test:py:sdk-conformance +mise run test:java:sdk-conformance +mise run test:cs:sdk-conformance +""" + +[tasks."test:sdk:provider-conformance"] +description = "Run provider-wrapper conformance suites across all shipped SDKs" +run = """ +#!/usr/bin/env bash +set -euo pipefail +mise run test:go:sdk-anthropic +mise run test:go:sdk-openai +mise run test:go:sdk-gemini +mise run test:ts:sdk-provider-conformance +mise run test:py:sdk-provider-conformance +mise run test:cs:sdk-provider-conformance +mise run test:java:sdk-provider-conformance +""" + +[tasks."test:sdk:framework-conformance"] +description = "Run framework-adapter conformance suites across all shipped SDKs" +run = """ +#!/usr/bin/env bash +set -euo pipefail +mise run test:go:sdk-google-adk +mise run test:ts:sdk-framework-conformance +mise run test:py:sdk-framework-conformance +mise run test:java:sdk-framework-conformance +""" + +[tasks."test:sdk:conformance"] +description = "Run core, provider-wrapper, and framework-adapter conformance suites across supported SDKs" +run = """ +#!/usr/bin/env bash +set -euo pipefail +mise run test:sdk:core-conformance +mise run test:sdk:provider-conformance +mise run test:sdk:framework-conformance +""" + +[tasks."test:sdk:all"] +description = "Run all SDK checks (Go, TypeScript/JavaScript, Python, Java, and .NET SDK suites)" +run = """ +#!/usr/bin/env bash +set -euo pipefail +mise run test:go:sdk-core +mise run test:go:sdk-anthropic +mise run test:go:sdk-openai +mise run test:go:sdk-gemini +mise run test:go:sdk-google-adk +mise run test:ts:sdk-js +mise run test:py:sdk-core +mise run test:py:sdk-provider-conformance +mise run test:py:sdk-framework-conformance +mise run test:cs:sdk-core +mise run test:cs:sdk-provider-conformance +mise run test:java:sdk-all +""" + +[tasks."sdk:conformance"] +description = "Alias for test:sdk:conformance" +run = "mise run test:sdk:conformance" + +# --- Python version bump --- + +[tasks."sdk:py:bump"] +description = "Bump version across all 9 Python SDK pyproject.toml files" +run = ''' +#!/usr/bin/env bash +set -euo pipefail + +VERSION="${1:?Usage: mise run sdk:py:bump }" +if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "Error: version must be in MAJOR.MINOR.PATCH format (got: $VERSION)" >&2 + exit 1 +fi + +PACKAGE_DIRS=( + python + python-providers/openai + python-providers/anthropic + python-providers/gemini + python-frameworks/langchain + python-frameworks/langgraph + python-frameworks/openai-agents + python-frameworks/llamaindex + python-frameworks/google-adk +) + +for dir in "${PACKAGE_DIRS[@]}"; do + file="${dir}/pyproject.toml" + perl -i -pe "s/^version = \".*\"/version = \"${VERSION}\"/" "$file" + if [[ "$dir" != "python" ]]; then + perl -i -pe "s/\"sigil-sdk>=.*\"/\"sigil-sdk>=${VERSION}\"/" "$file" + fi + echo " updated ${file}" +done + +echo "All Python SDK versions bumped to ${VERSION}" +''' + +# --- Mock generation (stubs) --- + +[tasks."generate:mocks:sdk-go"] +description = "Generate Go SDK mocks" +run = """ +#!/usr/bin/env bash +set -euo pipefail +echo "==> generating Go SDK mocks" +echo "No Go SDK mocks to generate yet." +""" + +[tasks."generate:mocks:sdk-go-providers"] +description = "Generate Go SDK provider mocks" +run = """ +#!/usr/bin/env bash +set -euo pipefail +echo "==> generating Go SDK provider mocks" +echo "No Go SDK provider mocks to generate yet." +""" + +[tasks."generate:mocks:sdk-go-frameworks"] +description = "Generate Go SDK framework mocks" +run = """ +#!/usr/bin/env bash +set -euo pipefail +echo "==> generating Go SDK framework mocks" +echo "No Go SDK framework mocks to generate yet." +""" + +[tasks."generate:mocks"] +description = "Generate all mock implementations" +depends = ["generate:mocks:sdk-go", "generate:mocks:sdk-go-providers", "generate:mocks:sdk-go-frameworks"] + +[tasks.check] +description = "Run lint + typecheck + tests" +depends = ["lint", "typecheck"] +run = "mise run test:sdk:all" diff --git a/package.json b/package.json new file mode 100644 index 0000000..2c256e4 --- /dev/null +++ b/package.json @@ -0,0 +1,4 @@ +{ + "private": true, + "packageManager": "pnpm@10.12.1" +} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..029f546 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,3 @@ +packages: + - 'js' + - 'plugins/*' diff --git a/python/scripts/generate_proto.sh b/python/scripts/generate_proto.sh index 7bb246d..ecabe0f 100755 --- a/python/scripts/generate_proto.sh +++ b/python/scripts/generate_proto.sh @@ -1,8 +1,8 @@ #!/usr/bin/env bash set -euo pipefail -ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)" -SDK_DIR="${ROOT_DIR}/sdks/python" +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +SDK_DIR="${ROOT_DIR}/python" OUT_DIR="${SDK_DIR}/sigil_sdk/internal/gen" PYTHON_BIN="${PYTHON_BIN:-python3}" @@ -20,11 +20,11 @@ fi PROTO_INCLUDE="$(${PYTHON_BIN} -c 'import pathlib, grpc_tools; print(pathlib.Path(grpc_tools.__file__).parent / "_proto")')" "${PYTHON_BIN}" -m grpc_tools.protoc \ - -I"${ROOT_DIR}/sigil/proto" \ + -I"${ROOT_DIR}/proto" \ -I"${PROTO_INCLUDE}" \ --python_out="${OUT_DIR}" \ --grpc_python_out="${OUT_DIR}" \ - "${ROOT_DIR}/sigil/proto/sigil/v1/generation_ingest.proto" + "${ROOT_DIR}/proto/sigil/v1/generation_ingest.proto" # The grpc plugin emits absolute import paths; normalize to relative package import. TMP_FILE="$(mktemp)" From f5b9dd41d79a7d6f15743954149ce1ec162c2d0d Mon Sep 17 00:00:00 2001 From: Alexander Akhmetov Date: Wed, 1 Apr 2026 10:56:17 +0200 Subject: [PATCH 130/133] fix(CI): add pnpm lockfile, pin node version, fix gitignore paths --- .github/workflows/ci.yml | 3 +- .gitignore | 59 + pnpm-lock.yaml | 6556 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 6616 insertions(+), 2 deletions(-) create mode 100644 .gitignore create mode 100644 pnpm-lock.yaml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b671fea..a0c6057 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -50,8 +50,7 @@ jobs: - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: - node-version-file: js/package.json - cache: pnpm + node-version: '24' - run: pnpm install diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..365244c --- /dev/null +++ b/.gitignore @@ -0,0 +1,59 @@ +# mise +.mise.local.toml + +# dependencies +node_modules/ +.pnpm-store/ + +# logs +logs/ +*.log + +# build output +dist/ +coverage/ +.storybook-static/ +*.tsbuildinfo +js/.test-dist/ + +*.db +coverage.out + +# dotnet artifacts +dotnet/**/bin/ +dotnet/**/obj/ + +# docker and local env +.env +.env.* +!*.example +WORKFLOW.local.md +.venv/ +.venv*/ +.venv-*/ + +# python artifacts +__pycache__/ +*.py[cod] +*.egg-info/ +.pytest_cache/ +.mypy_cache/ + +# e2e outputs +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ +/playwright/.auth/ +/output/playwright/ + +# superpowers brainstorm artifacts +.superpowers/ + +# editor/OS +.DS_Store +.idea/ +.vscode/ +.eslintcache +.turbo/ +scripts/mysql-port-forward.sh diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..5ef2747 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,6556 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: {} + + js: + dependencies: + '@anthropic-ai/sdk': + specifier: ^0.80.0 + version: 0.80.0(zod@4.3.6) + '@google/adk': + specifier: ^0.5.0 + version: 0.5.0(ee6095569807c0f2faf9175cb5eca775) + '@google/genai': + specifier: ^1.41.0 + version: 1.47.0(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6)) + '@grpc/grpc-js': + specifier: ^1.14.1 + version: 1.14.3 + '@grpc/proto-loader': + specifier: ^0.8.0 + version: 0.8.0 + '@langchain/core': + specifier: ^1.0.0 + version: 1.1.38(@opentelemetry/api@1.9.1)(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.1))(openai@6.33.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0) + '@langchain/langgraph': + specifier: ^1.2.0 + version: 1.2.6(@langchain/core@1.1.38(@opentelemetry/api@1.9.1)(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.1))(openai@6.33.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0))(zod-to-json-schema@3.25.2(zod@4.3.6))(zod@4.3.6) + '@openai/agents': + specifier: ^0.8.0 + version: 0.8.2(@cfworker/json-schema@4.1.1)(ws@8.20.0)(zod@4.3.6) + '@opentelemetry/api': + specifier: ^1.9.0 + version: 1.9.1 + '@opentelemetry/exporter-metrics-otlp-grpc': + specifier: ^0.213.0 + version: 0.213.0(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-metrics-otlp-http': + specifier: ^0.213.0 + version: 0.213.0(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-trace-otlp-grpc': + specifier: ^0.213.0 + version: 0.213.0(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-trace-otlp-http': + specifier: ^0.213.0 + version: 0.213.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-metrics': + specifier: ^2.1.0 + version: 2.6.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': + specifier: ^2.5.0 + version: 2.6.1(@opentelemetry/api@1.9.1) + llamaindex: + specifier: ^0.12.1 + version: 0.12.1(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6))(hono@4.12.9)(tree-sitter@0.22.4)(web-tree-sitter@0.24.7)(zod@4.3.6) + openai: + specifier: ^6.27.0 + version: 6.33.0(ws@8.20.0)(zod@4.3.6) + devDependencies: + '@opentelemetry/context-async-hooks': + specifier: ^2.6.0 + version: 2.6.1(@opentelemetry/api@1.9.1) + '@types/node': + specifier: ^24.11.0 + version: 24.12.0 + typescript: + specifier: ^6.0.0 + version: 6.0.2 + + plugins/opencode: + devDependencies: + '@grafana/sigil-sdk-js': + specifier: workspace:* + version: link:../../js + '@opencode-ai/plugin': + specifier: ^1.3.0 + version: 1.3.13 + '@opencode-ai/sdk': + specifier: ^1.3.2 + version: 1.3.13 + '@types/node': + specifier: ^24.0.0 + version: 24.12.0 + esbuild: + specifier: ^0.27.3 + version: 0.27.4 + typescript: + specifier: ^6.0.0 + version: 6.0.2 + vitest: + specifier: ^4.1.0 + version: 4.1.2(@opentelemetry/api@1.9.1)(@types/node@24.12.0)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@24.12.0)(esbuild@0.27.4)) + +packages: + + '@a2a-js/sdk@0.3.13': + resolution: {integrity: sha512-BZr0f9JVNQs3GKOM9xINWCh6OKIJWZFPyqqVqTym5mxO2Eemc6I/0zL7zWnljHzGdaf5aZQyQN5xa6PSH62q+A==} + engines: {node: '>=18'} + peerDependencies: + '@bufbuild/protobuf': ^2.10.2 + '@grpc/grpc-js': ^1.11.0 + express: ^4.21.2 || ^5.1.0 + peerDependenciesMeta: + '@bufbuild/protobuf': + optional: true + '@grpc/grpc-js': + optional: true + express: + optional: true + + '@anthropic-ai/sdk@0.80.0': + resolution: {integrity: sha512-WeXLn7zNVk3yjeshn+xZHvld6AoFUOR3Sep6pSoHho5YbSi6HwcirqgPA5ccFuW8QTVJAAU7N8uQQC6Wa9TG+g==} + hasBin: true + peerDependencies: + zod: ^3.25.0 || ^4.0.0 + peerDependenciesMeta: + zod: + optional: true + + '@aws-crypto/sha256-js@5.2.0': + resolution: {integrity: sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==} + engines: {node: '>=16.0.0'} + + '@aws-crypto/util@5.2.0': + resolution: {integrity: sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==} + + '@aws-sdk/types@3.973.6': + resolution: {integrity: sha512-Atfcy4E++beKtwJHiDln2Nby8W/mam64opFPTiHEqgsthqeydFS1pY+OUlN1ouNOmf8ArPU/6cDS65anOP3KQw==} + engines: {node: '>=20.0.0'} + + '@azure-rest/core-client@2.5.1': + resolution: {integrity: sha512-EHaOXW0RYDKS5CFffnixdyRPak5ytiCtU7uXDcP/uiY+A6jFRwNGzzJBiznkCzvi5EYpY+YWinieqHb0oY916A==} + engines: {node: '>=20.0.0'} + + '@azure/abort-controller@2.1.2': + resolution: {integrity: sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==} + engines: {node: '>=18.0.0'} + + '@azure/core-auth@1.10.1': + resolution: {integrity: sha512-ykRMW8PjVAn+RS6ww5cmK9U2CyH9p4Q88YJwvUslfuMmN98w/2rdGRLPqJYObapBCdzBVeDgYWdJnFPFb7qzpg==} + engines: {node: '>=20.0.0'} + + '@azure/core-client@1.10.1': + resolution: {integrity: sha512-Nh5PhEOeY6PrnxNPsEHRr9eimxLwgLlpmguQaHKBinFYA/RU9+kOYVOQqOrTsCL+KSxrLLl1gD8Dk5BFW/7l/w==} + engines: {node: '>=20.0.0'} + + '@azure/core-http-compat@2.3.2': + resolution: {integrity: sha512-Tf6ltdKzOJEgxZeWLCjMxrxbodB/ZeCbzzA1A2qHbhzAjzjHoBVSUeSl/baT/oHAxhc4qdqVaDKnc2+iE932gw==} + engines: {node: '>=20.0.0'} + peerDependencies: + '@azure/core-client': ^1.10.0 + '@azure/core-rest-pipeline': ^1.22.0 + + '@azure/core-lro@2.7.2': + resolution: {integrity: sha512-0YIpccoX8m/k00O7mDDMdJpbr6mf1yWo2dfmxt5A8XVZVVMz2SSKaEbMCeJRvgQ0IaSlqhjT47p4hVIRRy90xw==} + engines: {node: '>=18.0.0'} + + '@azure/core-paging@1.6.2': + resolution: {integrity: sha512-YKWi9YuCU04B55h25cnOYZHxXYtEvQEbKST5vqRga7hWY9ydd3FZHdeQF8pyh+acWZvppw13M/LMGx0LABUVMA==} + engines: {node: '>=18.0.0'} + + '@azure/core-rest-pipeline@1.23.0': + resolution: {integrity: sha512-Evs1INHo+jUjwHi1T6SG6Ua/LHOQBCLuKEEE6efIpt4ZOoNonaT1kP32GoOcdNDbfqsD2445CPri3MubBy5DEQ==} + engines: {node: '>=20.0.0'} + + '@azure/core-tracing@1.3.1': + resolution: {integrity: sha512-9MWKevR7Hz8kNzzPLfX4EAtGM2b8mr50HPDBvio96bURP/9C+HjdH3sBlLSNNrvRAr5/k/svoH457gB5IKpmwQ==} + engines: {node: '>=20.0.0'} + + '@azure/core-util@1.13.1': + resolution: {integrity: sha512-XPArKLzsvl0Hf0CaGyKHUyVgF7oDnhKoP85Xv6M4StF/1AhfORhZudHtOyf2s+FcbuQ9dPRAjB8J2KvRRMUK2A==} + engines: {node: '>=20.0.0'} + + '@azure/identity@4.13.1': + resolution: {integrity: sha512-5C/2WD5Vb1lHnZS16dNQRPMjN6oV/Upba+C9nBIs15PmOi6A3ZGs4Lr2u60zw4S04gi+u3cEXiqTVP7M4Pz3kw==} + engines: {node: '>=20.0.0'} + + '@azure/keyvault-common@2.0.0': + resolution: {integrity: sha512-wRLVaroQtOqfg60cxkzUkGKrKMsCP6uYXAOomOIysSMyt1/YM0eUn9LqieAWM8DLcU4+07Fio2YGpPeqUbpP9w==} + engines: {node: '>=18.0.0'} + + '@azure/keyvault-keys@4.10.0': + resolution: {integrity: sha512-eDT7iXoBTRZ2n3fLiftuGJFD+yjkiB1GNqzU2KbY1TLYeXeSPVTVgn2eJ5vmRTZ11978jy2Kg2wI7xa9Tyr8ag==} + engines: {node: '>=18.0.0'} + + '@azure/logger@1.3.0': + resolution: {integrity: sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA==} + engines: {node: '>=20.0.0'} + + '@azure/msal-browser@5.6.2': + resolution: {integrity: sha512-ZgcN9ToRJ80f+wNPBBKYJ+DG0jlW7ktEjYtSNkNsTrlHVMhKB8tKMdI1yIG1I9BJtykkXtqnuOjlJaEMC7J6aw==} + engines: {node: '>=0.8.0'} + + '@azure/msal-common@16.4.0': + resolution: {integrity: sha512-twXt09PYtj1PffNNIAzQlrBd0DS91cdA6i1gAfzJ6BnPM4xNk5k9q/5xna7jLIjU3Jnp0slKYtucshGM8OGNAw==} + engines: {node: '>=0.8.0'} + + '@azure/msal-node@5.1.1': + resolution: {integrity: sha512-71grXU6+5hl+3CL3joOxlj/AW6rmhthuTlG0fRqsTrhPArQBpZuUFzCIlKOGdcafLUa/i1hBdV78ZxJdlvRA+g==} + engines: {node: '>=20'} + + '@babel/runtime@7.29.2': + resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==} + engines: {node: '>=6.9.0'} + + '@cfworker/json-schema@4.1.1': + resolution: {integrity: sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==} + + '@colors/colors@1.6.0': + resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==} + engines: {node: '>=0.1.90'} + + '@dabh/diagnostics@2.0.8': + resolution: {integrity: sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==} + + '@emnapi/core@1.9.1': + resolution: {integrity: sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==} + + '@emnapi/runtime@1.9.1': + resolution: {integrity: sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==} + + '@emnapi/wasi-threads@1.2.0': + resolution: {integrity: sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==} + + '@esbuild/aix-ppc64@0.27.4': + resolution: {integrity: sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.27.4': + resolution: {integrity: sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.27.4': + resolution: {integrity: sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.27.4': + resolution: {integrity: sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.27.4': + resolution: {integrity: sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.4': + resolution: {integrity: sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.27.4': + resolution: {integrity: sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.4': + resolution: {integrity: sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.27.4': + resolution: {integrity: sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.27.4': + resolution: {integrity: sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.27.4': + resolution: {integrity: sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.27.4': + resolution: {integrity: sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.27.4': + resolution: {integrity: sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.27.4': + resolution: {integrity: sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.4': + resolution: {integrity: sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.27.4': + resolution: {integrity: sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.27.4': + resolution: {integrity: sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.27.4': + resolution: {integrity: sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.4': + resolution: {integrity: sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.27.4': + resolution: {integrity: sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.4': + resolution: {integrity: sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.4': + resolution: {integrity: sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.27.4': + resolution: {integrity: sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.27.4': + resolution: {integrity: sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.27.4': + resolution: {integrity: sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.27.4': + resolution: {integrity: sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@finom/zod-to-json-schema@3.24.11': + resolution: {integrity: sha512-fL656yBPiWebtfGItvtXLWrFNGlF1NcDFS0WdMQXMs9LluVg0CfT5E2oXYp0pidl0vVG53XkW55ysijNkU5/hA==} + deprecated: 'Use https://www.npmjs.com/package/zod-v3-to-json-schema instead. See issue comment for details: https://github.com/StefanTerdell/zod-to-json-schema/issues/178#issuecomment-3533122539' + peerDependencies: + zod: ^4.0.14 + + '@gar/promisify@1.1.3': + resolution: {integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==} + + '@google-cloud/opentelemetry-cloud-monitoring-exporter@0.21.0': + resolution: {integrity: sha512-+lAew44pWt6rA4l8dQ1gGhH7Uo95wZKfq/GBf9aEyuNDDLQ2XppGEEReu6ujesSqTtZ8ueQFt73+7SReSHbwqg==} + engines: {node: '>=18'} + peerDependencies: + '@opentelemetry/api': ^1.9.0 + '@opentelemetry/core': ^2.0.0 + '@opentelemetry/resources': ^2.0.0 + '@opentelemetry/sdk-metrics': ^2.0.0 + + '@google-cloud/opentelemetry-cloud-trace-exporter@3.0.0': + resolution: {integrity: sha512-mUfLJBFo+ESbO0dAGboErx2VyZ7rbrHcQvTP99yH/J72dGaPbH2IzS+04TFbTbEd1VW5R9uK3xq2CqawQaG+1Q==} + engines: {node: '>=18'} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + '@opentelemetry/core': ^2.0.0 + '@opentelemetry/resources': ^2.0.0 + '@opentelemetry/sdk-trace-base': ^2.0.0 + + '@google-cloud/opentelemetry-resource-util@3.0.0': + resolution: {integrity: sha512-CGR/lNzIfTKlZoZFfS6CkVzx+nsC9gzy6S8VcyaLegfEJbiPjxbMLP7csyhJTvZe/iRRcQJxSk0q8gfrGqD3/Q==} + engines: {node: '>=18'} + peerDependencies: + '@opentelemetry/core': ^2.0.0 + '@opentelemetry/resources': ^2.0.0 + + '@google-cloud/paginator@5.0.2': + resolution: {integrity: sha512-DJS3s0OVH4zFDB1PzjxAsHqJT6sKVbRwwML0ZBP9PbU7Yebtu/7SWMRzvO2J3nUi9pRNITCfu4LJeooM2w4pjg==} + engines: {node: '>=14.0.0'} + + '@google-cloud/precise-date@4.0.0': + resolution: {integrity: sha512-1TUx3KdaU3cN7nfCdNf+UVqA/PSX29Cjcox3fZZBtINlRrXVTmUkQnCKv2MbBUbCopbK4olAT1IHl76uZyCiVA==} + engines: {node: '>=14.0.0'} + + '@google-cloud/projectify@4.0.0': + resolution: {integrity: sha512-MmaX6HeSvyPbWGwFq7mXdo0uQZLGBYCwziiLIGq5JVX+/bdI3SAq6bP98trV5eTWfLuvsMcIC1YJOF2vfteLFA==} + engines: {node: '>=14.0.0'} + + '@google-cloud/promisify@4.0.0': + resolution: {integrity: sha512-Orxzlfb9c67A15cq2JQEyVc7wEsmFBmHjZWZYQMUyJ1qivXyMwdyNOs9odi79hze+2zqdTtu1E19IM/FtqZ10g==} + engines: {node: '>=14'} + + '@google-cloud/storage@7.19.0': + resolution: {integrity: sha512-n2FjE7NAOYyshogdc7KQOl/VZb4sneqPjWouSyia9CMDdMhRX5+RIbqalNmC7LOLzuLAN89VlF2HvG8na9G+zQ==} + engines: {node: '>=14'} + + '@google/adk@0.5.0': + resolution: {integrity: sha512-VQbiGayQewKm3IXJIUX2iq+eqY7WHd71MOT0XwPJTXnViuo3/ysiwxCQ2D8sxAGQt+fol8YHaHxBUkoIpqOBzA==} + peerDependencies: + '@google-cloud/opentelemetry-cloud-monitoring-exporter': ^0.21.0 + '@google-cloud/opentelemetry-cloud-trace-exporter': ^3.0.0 + '@google-cloud/storage': ^7.17.1 + '@mikro-orm/mariadb': ^6.6.6 + '@mikro-orm/mssql': ^6.6.6 + '@mikro-orm/mysql': ^6.6.6 + '@mikro-orm/postgresql': ^6.6.6 + '@mikro-orm/sqlite': ^6.6.6 + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': ^0.205.0 + '@opentelemetry/exporter-logs-otlp-http': ^0.205.0 + '@opentelemetry/exporter-metrics-otlp-http': ^0.205.0 + '@opentelemetry/exporter-trace-otlp-http': ^0.205.0 + '@opentelemetry/resource-detector-gcp': ^0.40.0 + '@opentelemetry/resources': ^2.1.0 + '@opentelemetry/sdk-logs': ^0.205.0 + '@opentelemetry/sdk-metrics': ^2.1.0 + '@opentelemetry/sdk-trace-base': ^2.1.0 + '@opentelemetry/sdk-trace-node': ^2.1.0 + + '@google/genai@1.47.0': + resolution: {integrity: sha512-0VV7AaXm5rQu3oRHNZNEubRAOL2lv5u+YA72eWnDwcOx3B1jFRbvtgL4drRHlocRHOnludvr3xmbQGbR+/RQAQ==} + engines: {node: '>=20.0.0'} + peerDependencies: + '@modelcontextprotocol/sdk': ^1.25.2 + peerDependenciesMeta: + '@modelcontextprotocol/sdk': + optional: true + + '@grpc/grpc-js@1.14.3': + resolution: {integrity: sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==} + engines: {node: '>=12.10.0'} + + '@grpc/proto-loader@0.8.0': + resolution: {integrity: sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ==} + engines: {node: '>=6'} + hasBin: true + + '@hono/node-server@1.19.12': + resolution: {integrity: sha512-txsUW4SQ1iilgE0l9/e9VQWmELXifEFvmdA1j6WFh/aFPj99hIntrSsq/if0UWyGVkmrRPKA1wCeP+UCr1B9Uw==} + engines: {node: '>=18.14.1'} + peerDependencies: + hono: ^4 + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@js-joda/core@5.7.0': + resolution: {integrity: sha512-WBu4ULVVxySLLzK1Ppq+OdfP+adRS4ntmDQT915rzDJ++i95gc2jZkM5B6LWEAwN3lGXpfie3yPABozdD3K3Vg==} + + '@js-sdsl/ordered-map@4.4.2': + resolution: {integrity: sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==} + + '@langchain/core@1.1.38': + resolution: {integrity: sha512-C340wH1YL10CiVOFlEpQMp0zQE85/eBLKX/gi1Lv7shAyUmR3CQ0t/mXlCd5RsZa6ntAN1kDJnp64ArWey9XAA==} + engines: {node: '>=20'} + + '@langchain/langgraph-checkpoint@1.0.1': + resolution: {integrity: sha512-HM0cJLRpIsSlWBQ/xuDC67l52SqZ62Bh2Y61DX+Xorqwoh5e1KxYvfCD7GnSTbWWhjBOutvnR0vPhu4orFkZfw==} + engines: {node: '>=18'} + peerDependencies: + '@langchain/core': ^1.0.1 + + '@langchain/langgraph-sdk@1.8.3': + resolution: {integrity: sha512-Py0S5yVtlOHz410aEsSGLRKjtsK2giDvfPS1JjAjTdcs71khuJufFtUZFwmwdJCbsG4DaGurRLHOAJu9jO4a0g==} + peerDependencies: + '@langchain/core': ^1.1.16 + react: ^18 || ^19 + react-dom: ^18 || ^19 + svelte: ^4.0.0 || ^5.0.0 + vue: ^3.0.0 + peerDependenciesMeta: + '@langchain/core': + optional: true + react: + optional: true + react-dom: + optional: true + svelte: + optional: true + vue: + optional: true + + '@langchain/langgraph@1.2.6': + resolution: {integrity: sha512-5cX402dNGN6w9+0mlMU2dgUiKx6Y1tlENp7x05e9ByDbQCHSDc0kyqRWNFLGc7vatQ92S4ylxQrcCJvi8Fr4SQ==} + engines: {node: '>=18'} + peerDependencies: + '@langchain/core': ^1.1.16 + zod: ^3.25.32 || ^4.2.0 + zod-to-json-schema: ^3.x + peerDependenciesMeta: + zod-to-json-schema: + optional: true + + '@llamaindex/core@0.6.22': + resolution: {integrity: sha512-/BXyemkvpxMaUhOkbwJ2PTvzKjSWkL8+6QLpz/n+pk8xBwMMe1GVBgli/J57gCyi8GbrlBafBj6GaPOgWub2Eg==} + + '@llamaindex/env@0.1.30': + resolution: {integrity: sha512-y6kutMcCevzbmexUgz+HXf7KiZemzAoFEYSjAILfR+cG6FmYSF8XvLbGOB34Kx8mlRi7EI8rZXpezJ5qCqOyZg==} + peerDependencies: + '@huggingface/transformers': ^3.5.0 + gpt-tokenizer: ^2.5.0 + peerDependenciesMeta: + '@huggingface/transformers': + optional: true + gpt-tokenizer: + optional: true + + '@llamaindex/node-parser@2.0.22': + resolution: {integrity: sha512-uj5O89WShAAyiSZ8f8tU7hnLJ6pSmlY2a6hkAOs8odkUgT87dEqaPHpsK7w0iJdEFiob7GoLeRhv2K624FooXg==} + peerDependencies: + '@llamaindex/core': 0.6.22 + '@llamaindex/env': 0.1.30 + tree-sitter: ^0.22.0 + web-tree-sitter: ^0.24.3 + + '@llamaindex/workflow-core@1.3.4': + resolution: {integrity: sha512-nDQ61VEYY5lTJFLHZzN7swdpbYrkoqLHKmct/KXF0wZN2Ih7sB4jk9eaVqWbd6zmkkOADT84I6eSd7hSY9kurg==} + deprecated: 'This package is deprecated. Please use LlamaAgents (Python Workflows) instead: https://developers.llamaindex.ai/python/llamaagents/overview/' + peerDependencies: + '@modelcontextprotocol/sdk': ^1.7.0 + hono: ^4.7.4 + next: ^15.2.2 + p-retry: ^6.2.1 + rxjs: ^7.8.2 + zod: ^3.25.0 || ^4.0.0 + peerDependenciesMeta: + '@modelcontextprotocol/sdk': + optional: true + hono: + optional: true + next: + optional: true + p-retry: + optional: true + rxjs: + optional: true + zod: + optional: true + + '@llamaindex/workflow@1.1.24': + resolution: {integrity: sha512-VyKsbRkFlnT5dRNKbgLXQV+ZpQ+CAFgmC9LaZv6hD/fIKo6wq1wQW/ZqLZgZt569xeHgxmrXPB6KHdqn/AhPbQ==} + peerDependencies: + '@llamaindex/core': 0.6.22 + '@llamaindex/env': 0.1.30 + + '@mikro-orm/core@6.6.11': + resolution: {integrity: sha512-+edc3ctapRi0lyb2B0+QfUpoWkNmXOcaApDT6RhBxyFo74bpoU/tEb9aMobemN86VhAt/rjM1KDKbJYLM9lxTg==} + engines: {node: '>= 18.12.0'} + + '@mikro-orm/knex@6.6.11': + resolution: {integrity: sha512-MUxqw+3COpcM06DC3ufW4Aov5RZWpW1Rv/kMfJkHQX+bO81jPdinXkRtx1l8EVWFRiLJEB+3MNhptFQRlmJNXA==} + engines: {node: '>= 18.12.0'} + peerDependencies: + '@mikro-orm/core': ^6.0.0 + better-sqlite3: '*' + libsql: '*' + mariadb: '*' + peerDependenciesMeta: + better-sqlite3: + optional: true + libsql: + optional: true + mariadb: + optional: true + + '@mikro-orm/mariadb@6.6.11': + resolution: {integrity: sha512-TPUFGJHGPGiQC2LE263iyyBbaG1nwSsa6UVQ8ma2QFxLRt62XqGFEw7XJ1uUXXoqZn/4RW8jrIAWFgbrmfnx3g==} + engines: {node: '>= 18.12.0'} + peerDependencies: + '@mikro-orm/core': ^6.0.0 + + '@mikro-orm/mssql@6.6.11': + resolution: {integrity: sha512-LjMiObzrwKJw9Pt/VUgAgsNU3j/FDZjW8wxvD8522rPdXnNyHtNBDAQQhdWt/e+yJr4bZh7HhoQYekH8Z9G6Yg==} + engines: {node: '>= 18.12.0'} + peerDependencies: + '@mikro-orm/core': ^6.0.0 + + '@mikro-orm/mysql@6.6.11': + resolution: {integrity: sha512-SPtBLl82Qq+pKx/d5rF276LosKz6JO7D8vTaeudadk6/zlXjpE3SciGmyvt5/+htzts4k348F8zQMCf297NdzQ==} + engines: {node: '>= 18.12.0'} + peerDependencies: + '@mikro-orm/core': ^6.0.0 + + '@mikro-orm/postgresql@6.6.11': + resolution: {integrity: sha512-YIQroXsAPXRJc3ruk8M5ynbQEQtGUO0Swjb/MMtjn5o9qypqmPBoq4ANCwUY9P2jVlmheQM1O5VK/1OBm7/EVg==} + engines: {node: '>= 18.12.0'} + peerDependencies: + '@mikro-orm/core': ^6.0.0 + + '@mikro-orm/reflection@6.6.11': + resolution: {integrity: sha512-EG8C79sOzkvqiI1Kvig2TO1ME1YlhxVGLDQaKQur2xUIR31U0cmjWIWd449lCD4mLdrUj1sem7WULLmo2tj7UA==} + engines: {node: '>= 18.12.0'} + peerDependencies: + '@mikro-orm/core': ^6.0.0 + + '@mikro-orm/sqlite@6.6.11': + resolution: {integrity: sha512-WCO9w6JERp7qMRJKXoNF1ELrQ6PrMBU24EwDdhkY8LH76uqDM4jtfSbIcBDafORiZG/D+Rs8JshS1qEQEX9x7w==} + engines: {node: '>= 18.12.0'} + peerDependencies: + '@mikro-orm/core': ^6.0.0 + + '@modelcontextprotocol/sdk@1.29.0': + resolution: {integrity: sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==} + engines: {node: '>=18'} + peerDependencies: + '@cfworker/json-schema': ^4.1.1 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + '@cfworker/json-schema': + optional: true + + '@napi-rs/wasm-runtime@1.1.2': + resolution: {integrity: sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@npmcli/fs@1.1.1': + resolution: {integrity: sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==} + + '@npmcli/move-file@1.1.2': + resolution: {integrity: sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==} + engines: {node: '>=10'} + deprecated: This functionality has been moved to @npmcli/fs + + '@openai/agents-core@0.8.2': + resolution: {integrity: sha512-oxp8XmdFcZwturpfWzqpW/2doNJ75FHwYDfZdBxfSK4Q2vmwLsx9wNPmU67i8dubWUXmIzOSXNUCbdL6+iLNlg==} + peerDependencies: + zod: ^4.0.0 + peerDependenciesMeta: + zod: + optional: true + + '@openai/agents-openai@0.8.2': + resolution: {integrity: sha512-h81JngBj2EcFDPDTnlKZqWp6JZN/SdR4MPR+f6NKYpqNjDWwJQGpq+w4lguLORUrZmXKWwy7xSZwAJzPB0EoCQ==} + peerDependencies: + zod: ^4.0.0 + + '@openai/agents-realtime@0.8.2': + resolution: {integrity: sha512-aY1BGOkhpbb+aq+bU3OkwRMr/CXEc0LZxPFw7vn28NTcVWn2rKdeDEkY2k1EbUv35vk5wo8bwI2LJkQfm+d6uw==} + peerDependencies: + zod: ^4.0.0 + + '@openai/agents@0.8.2': + resolution: {integrity: sha512-chxJPncuVbOqAUUpxUuVnT2tZTIr82hD9eVA59GaNzM0uG13fjaiIYgbNpWpAz9w5jQh84HMybWzXL9QNp7daA==} + peerDependencies: + zod: ^4.0.0 + + '@opencode-ai/plugin@1.3.13': + resolution: {integrity: sha512-zHgtWfdDz8Wu8srE8f8HUtPT9i6c3jTmgQKoFZUZ+RR5CMQF1kAlb1cxeEe9Xm2DRNFVJog9Cv/G1iUHYgXSUQ==} + peerDependencies: + '@opentui/core': '>=0.1.95' + '@opentui/solid': '>=0.1.95' + peerDependenciesMeta: + '@opentui/core': + optional: true + '@opentui/solid': + optional: true + + '@opencode-ai/sdk@1.3.13': + resolution: {integrity: sha512-/M6HlNnba+xf1EId6qFb2tG0cvq0db3PCQDug1glrf8wYOU57LYNF8WvHX9zoDKPTMv0F+O4pcP/8J+WvDaxHA==} + + '@opentelemetry/api-logs@0.205.0': + resolution: {integrity: sha512-wBlPk1nFB37Hsm+3Qy73yQSobVn28F4isnWIBvKpd5IUH/eat8bwcL02H9yzmHyyPmukeccSl2mbN5sDQZYnPg==} + engines: {node: '>=8.0.0'} + + '@opentelemetry/api-logs@0.213.0': + resolution: {integrity: sha512-zRM5/Qj6G84Ej3F1yt33xBVY/3tnMxtL1fiDIxYbDWYaZ/eudVw3/PBiZ8G7JwUxXxjW8gU4g6LnOyfGKYHYgw==} + engines: {node: '>=8.0.0'} + + '@opentelemetry/api@1.9.1': + resolution: {integrity: sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==} + engines: {node: '>=8.0.0'} + + '@opentelemetry/context-async-hooks@2.6.1': + resolution: {integrity: sha512-XHzhwRNkBpeP8Fs/qjGrAf9r9PRv67wkJQ/7ZPaBQQ68DYlTBBx5MF9LvPx7mhuXcDessKK2b+DcxqwpgkcivQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/core@2.1.0': + resolution: {integrity: sha512-RMEtHsxJs/GiHHxYT58IY57UXAQTuUnZVco6ymDEqTNlJKTimM4qPUPVe8InNFyBjhHBEAx4k3Q8LtNayBsbUQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/core@2.6.0': + resolution: {integrity: sha512-HLM1v2cbZ4TgYN6KEOj+Bbj8rAKriOdkF9Ed3tG25FoprSiQl7kYc+RRT6fUZGOvx0oMi5U67GoFdT+XUn8zEg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/core@2.6.1': + resolution: {integrity: sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/exporter-logs-otlp-http@0.205.0': + resolution: {integrity: sha512-5JteMyVWiro4ghF0tHQjfE6OJcF7UBUcoEqX3UIQ5jutKP1H+fxFdyhqjjpmeHMFxzOHaYuLlNR1Bn7FOjGyJg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-metrics-otlp-grpc@0.213.0': + resolution: {integrity: sha512-Z8gYKUAU48qwm+a1tjnGv9xbE7a5lukVIwgF6Z5i3VPXPVMe4Sjra0nN3zU7m277h+V+ZpsPGZJ2Xf0OTkL7/w==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-metrics-otlp-http@0.213.0': + resolution: {integrity: sha512-yw3fTIw4KQIRXC/ZyYQq5gtA3Ogfdfz/g5HVgleobQAcjUUE8Nj3spGMx8iQPp+S+u6/js7BixufRkXhzLmpJA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-trace-otlp-grpc@0.213.0': + resolution: {integrity: sha512-L8y6piP4jBIIx1Nv7/9hkx25ql6/Cro/kQrs+f9e8bPF0Ar5Dm991v7PnbtubKz6Q4fT872H56QXUWVnz/Cs4Q==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-trace-otlp-http@0.213.0': + resolution: {integrity: sha512-tnRmJD39aWrE/Sp7F6AbRNAjKHToDkAqBi6i0lESpGWz3G+f4bhVAV6mgSXH2o18lrDVJXo6jf9bAywQw43wRA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/otlp-exporter-base@0.205.0': + resolution: {integrity: sha512-2MN0C1IiKyo34M6NZzD6P9Nv9Dfuz3OJ3rkZwzFmF6xzjDfqqCTatc9v1EpNfaP55iDOCLHFyYNCgs61FFgtUQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/otlp-exporter-base@0.213.0': + resolution: {integrity: sha512-MegxAP1/n09Ob2dQvY5NBDVjAFkZRuKtWKxYev1R2M8hrsgXzQGkaMgoEKeUOyQ0FUyYcO29UOnYdQWmWa0PXg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/otlp-grpc-exporter-base@0.213.0': + resolution: {integrity: sha512-XgRGuLE9usFNlnw2lgMIM4HTwpcIyjdU/xPoJ8v3LbBLBfjaDkIugjc9HoWa7ZSJ/9Bhzgvm/aD0bGdYUFgnTw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/otlp-transformer@0.205.0': + resolution: {integrity: sha512-KmObgqPtk9k/XTlWPJHdMbGCylRAmMJNXIRh6VYJmvlRDMfe+DonH41G7eenG8t4FXn3fxOGh14o/WiMRR6vPg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/otlp-transformer@0.213.0': + resolution: {integrity: sha512-RSuAlxFFPjeK4d5Y6ps8L2WhaQI6CXWllIjvo5nkAlBpmq2XdYWEBGiAbOF4nDs8CX4QblJDv5BbMUft3sEfDw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/resource-detector-gcp@0.40.3': + resolution: {integrity: sha512-C796YjBA5P1JQldovApYfFA/8bQwFfpxjUbOtGhn1YZkVTLoNQN+kvBwgALfTPWzug6fWsd0xhn9dzeiUcndag==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + + '@opentelemetry/resources@2.1.0': + resolution: {integrity: sha512-1CJjf3LCvoefUOgegxi8h6r4B/wLSzInyhGP2UmIBYNlo4Qk5CZ73e1eEyWmfXvFtm1ybkmfb2DqWvspsYLrWw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/resources@2.6.0': + resolution: {integrity: sha512-D4y/+OGe3JSuYUCBxtH5T9DSAWNcvCb/nQWIga8HNtXTVPQn59j0nTBAgaAXxUVBDl40mG3Tc76b46wPlZaiJQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/resources@2.6.1': + resolution: {integrity: sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/sdk-logs@0.205.0': + resolution: {integrity: sha512-nyqhNQ6eEzPWQU60Nc7+A5LIq8fz3UeIzdEVBQYefB4+msJZ2vuVtRuk9KxPMw1uHoHDtYEwkr2Ct0iG29jU8w==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.4.0 <1.10.0' + + '@opentelemetry/sdk-logs@0.213.0': + resolution: {integrity: sha512-00xlU3GZXo3kXKve4DLdrAL0NAFUaZ9appU/mn00S/5kSUdAvyYsORaDUfR04Mp2CLagAOhrzfUvYozY/EZX2g==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.4.0 <1.10.0' + + '@opentelemetry/sdk-metrics@2.1.0': + resolution: {integrity: sha512-J9QX459mzqHLL9Y6FZ4wQPRZG4TOpMCyPOh6mkr/humxE1W2S3Bvf4i75yiMW9uyed2Kf5rxmLhTm/UK8vNkAw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.9.0 <1.10.0' + + '@opentelemetry/sdk-metrics@2.6.0': + resolution: {integrity: sha512-CicxWZxX6z35HR83jl+PLgtFgUrKRQ9LCXyxgenMnz5A1lgYWfAog7VtdOvGkJYyQgMNPhXQwkYrDLujk7z1Iw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.9.0 <1.10.0' + + '@opentelemetry/sdk-metrics@2.6.1': + resolution: {integrity: sha512-9t9hJHX15meBy2NmTJxL+NJfXmnausR2xUDvE19XQce0Qi/GBtDGamU8nS1RMbdgDmhgpm3VaOu2+fiS/SfTpQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.9.0 <1.10.0' + + '@opentelemetry/sdk-trace-base@2.1.0': + resolution: {integrity: sha512-uTX9FBlVQm4S2gVQO1sb5qyBLq/FPjbp+tmGoxu4tIgtYGmBYB44+KX/725RFDe30yBSaA9Ml9fqphe1hbUyLQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/sdk-trace-base@2.6.0': + resolution: {integrity: sha512-g/OZVkqlxllgFM7qMKqbPV9c1DUPhQ7d4n3pgZFcrnrNft9eJXZM2TNHTPYREJBrtNdRytYyvwjgL5geDKl3EQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/sdk-trace-base@2.6.1': + resolution: {integrity: sha512-r86ut4T1e8vNwB35CqCcKd45yzqH6/6Wzvpk2/cZB8PsPLlZFTvrh8yfOS3CYZYcUmAx4hHTZJ8AO8Dj8nrdhw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/sdk-trace-node@2.6.1': + resolution: {integrity: sha512-Hh2i4FwHWRFhnO2Q/p6svMxy8MPsNCG0uuzUY3glqm0rwM0nQvbTO1dXSp9OqQoTKXcQzaz9q1f65fsurmOhNw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/semantic-conventions@1.40.0': + resolution: {integrity: sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw==} + engines: {node: '>=14'} + + '@oxc-project/types@0.122.0': + resolution: {integrity: sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==} + + '@protobufjs/aspromise@1.1.2': + resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} + + '@protobufjs/base64@1.1.2': + resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} + + '@protobufjs/codegen@2.0.4': + resolution: {integrity: sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==} + + '@protobufjs/eventemitter@1.1.0': + resolution: {integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==} + + '@protobufjs/fetch@1.1.0': + resolution: {integrity: sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==} + + '@protobufjs/float@1.0.2': + resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} + + '@protobufjs/inquire@1.1.0': + resolution: {integrity: sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==} + + '@protobufjs/path@1.1.2': + resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} + + '@protobufjs/pool@1.1.0': + resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} + + '@protobufjs/utf8@1.1.0': + resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} + + '@rolldown/binding-android-arm64@1.0.0-rc.12': + resolution: {integrity: sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.0.0-rc.12': + resolution: {integrity: sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.0.0-rc.12': + resolution: {integrity: sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.0.0-rc.12': + resolution: {integrity: sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.12': + resolution: {integrity: sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.12': + resolution: {integrity: sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.12': + resolution: {integrity: sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.12': + resolution: {integrity: sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.12': + resolution: {integrity: sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.12': + resolution: {integrity: sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.12': + resolution: {integrity: sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.12': + resolution: {integrity: sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.12': + resolution: {integrity: sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.12': + resolution: {integrity: sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.12': + resolution: {integrity: sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.0-rc.12': + resolution: {integrity: sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw==} + + '@selderee/plugin-htmlparser2@0.11.0': + resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==} + + '@smithy/is-array-buffer@2.2.0': + resolution: {integrity: sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==} + engines: {node: '>=14.0.0'} + + '@smithy/types@4.13.1': + resolution: {integrity: sha512-787F3yzE2UiJIQ+wYW1CVg2odHjmaWLGksnKQHUrK/lYZSEcy1msuLVvxaR/sI2/aDe9U+TBuLsXnr3vod1g0g==} + engines: {node: '>=18.0.0'} + + '@smithy/util-buffer-from@2.2.0': + resolution: {integrity: sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==} + engines: {node: '>=14.0.0'} + + '@smithy/util-utf8@2.3.0': + resolution: {integrity: sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==} + engines: {node: '>=14.0.0'} + + '@so-ric/colorspace@1.1.6': + resolution: {integrity: sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==} + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@tootallnate/once@1.1.2': + resolution: {integrity: sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==} + engines: {node: '>= 6'} + + '@tootallnate/once@2.0.0': + resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==} + engines: {node: '>= 10'} + + '@ts-morph/common@0.28.1': + resolution: {integrity: sha512-W74iWf7ILp1ZKNYXY5qbddNaml7e9Sedv5lvU1V8lftlitkc9Pq1A+jlH23ltDgWYeZFFEqGCD1Ies9hqu3O+g==} + + '@tybys/wasm-util@0.10.1': + resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + + '@types/caseless@0.12.5': + resolution: {integrity: sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/geojson@7946.0.16': + resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/lodash@4.17.24': + resolution: {integrity: sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==} + + '@types/node@24.12.0': + resolution: {integrity: sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==} + + '@types/readable-stream@4.0.23': + resolution: {integrity: sha512-wwXrtQvbMHxCbBgjHaMGEmImFTQxxpfMOR/ZoQnXxB1woqkUbdLGFDgauo00Py9IudiaqSeiBiulSV9i6XIPig==} + + '@types/request@2.48.13': + resolution: {integrity: sha512-FGJ6udDNUCjd19pp0Q3iTiDkwhYup7J8hpMW9c4k53NrccQFFWKRho6hvtPPEhnXWKvukfwAlB6DbDz4yhH5Gg==} + + '@types/retry@0.12.0': + resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==} + + '@types/tough-cookie@4.0.5': + resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} + + '@types/triple-beam@1.3.5': + resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==} + + '@types/uuid@10.0.0': + resolution: {integrity: sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==} + + '@types/ws@8.18.1': + resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + + '@typespec/ts-http-runtime@0.3.4': + resolution: {integrity: sha512-CI0NhTrz4EBaa0U+HaaUZrJhPoso8sG7ZFya8uQoBA57fjzrjRSv87ekCjLZOFExN+gXE/z0xuN2QfH4H2HrLQ==} + engines: {node: '>=20.0.0'} + + '@vitest/expect@4.1.2': + resolution: {integrity: sha512-gbu+7B0YgUJ2nkdsRJrFFW6X7NTP44WlhiclHniUhxADQJH5Szt9mZ9hWnJPJ8YwOK5zUOSSlSvyzRf0u1DSBQ==} + + '@vitest/mocker@4.1.2': + resolution: {integrity: sha512-Ize4iQtEALHDttPRCmN+FKqOl2vxTiNUhzobQFFt/BM1lRUTG7zRCLOykG/6Vo4E4hnUdfVLo5/eqKPukcWW7Q==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.1.2': + resolution: {integrity: sha512-dwQga8aejqeuB+TvXCMzSQemvV9hNEtDDpgUKDzOmNQayl2OG241PSWeJwKRH3CiC+sESrmoFd49rfnq7T4RnA==} + + '@vitest/runner@4.1.2': + resolution: {integrity: sha512-Gr+FQan34CdiYAwpGJmQG8PgkyFVmARK8/xSijia3eTFgVfpcpztWLuP6FttGNfPLJhaZVP/euvujeNYar36OQ==} + + '@vitest/snapshot@4.1.2': + resolution: {integrity: sha512-g7yfUmxYS4mNxk31qbOYsSt2F4m1E02LFqO53Xpzg3zKMhLAPZAjjfyl9e6z7HrW6LvUdTwAQR3HHfLjpko16A==} + + '@vitest/spy@4.1.2': + resolution: {integrity: sha512-DU4fBnbVCJGNBwVA6xSToNXrkZNSiw59H8tcuUspVMsBDBST4nfvsPsEHDHGtWRRnqBERBQu7TrTKskmjqTXKA==} + + '@vitest/utils@4.1.2': + resolution: {integrity: sha512-xw2/TiX82lQHA06cgbqRKFb5lCAy3axQ4H4SoUFhUsg+wztiet+co86IAMDtF6Vm1hc7J6j09oh/rgDn+JdKIQ==} + + abbrev@1.1.1: + resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} + + abort-controller@3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} + + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + + agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + + agentkeepalive@4.6.0: + resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==} + engines: {node: '>= 8.0.0'} + + aggregate-error@3.1.0: + resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} + engines: {node: '>=8'} + + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv@8.18.0: + resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + + aproba@2.1.0: + resolution: {integrity: sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==} + + are-we-there-yet@3.0.1: + resolution: {integrity: sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + deprecated: This package is no longer supported. + + array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + + arrify@2.0.1: + resolution: {integrity: sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==} + engines: {node: '>=8'} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + async-retry@1.3.3: + resolution: {integrity: sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==} + + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + aws-ssl-profiles@1.1.2: + resolution: {integrity: sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==} + engines: {node: '>= 6.0.0'} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + bignumber.js@9.3.1: + resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==} + + bindings@1.5.0: + resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} + + bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + + bl@6.1.6: + resolution: {integrity: sha512-jLsPgN/YSvPUg9UX0Kd73CXpm2Psg9FxMeCSXnk3WBO3CMT10JMwijubhGfHCnFu6TPn1ei3b975dxv7K2pWVg==} + + body-parser@2.2.2: + resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} + engines: {node: '>=18'} + + brace-expansion@1.1.13: + resolution: {integrity: sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==} + + brace-expansion@5.0.5: + resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==} + engines: {node: 18 || 20 || >=22} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + + buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + + bundle-name@4.1.0: + resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} + engines: {node: '>=18'} + + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + + cacache@15.3.0: + resolution: {integrity: sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==} + engines: {node: '>= 10'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + camelcase@6.3.0: + resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} + engines: {node: '>=10'} + + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + + chalk@5.6.2: + resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + chownr@1.1.4: + resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + + chownr@2.0.0: + resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} + engines: {node: '>=10'} + + clean-stack@2.2.0: + resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} + engines: {node: '>=6'} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + code-block-writer@13.0.3: + resolution: {integrity: sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-convert@3.1.3: + resolution: {integrity: sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg==} + engines: {node: '>=14.6'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + color-name@2.1.0: + resolution: {integrity: sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==} + engines: {node: '>=12.20'} + + color-string@2.1.4: + resolution: {integrity: sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg==} + engines: {node: '>=18'} + + color-support@1.1.3: + resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==} + hasBin: true + + color@5.0.3: + resolution: {integrity: sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA==} + engines: {node: '>=18'} + + colorette@2.0.19: + resolution: {integrity: sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + commander@10.0.1: + resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} + engines: {node: '>=14'} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + console-control-strings@1.1.0: + resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==} + + console-table-printer@2.15.0: + resolution: {integrity: sha512-SrhBq4hYVjLCkBVOWaTzceJalvn5K1Zq5aQA6wXC/cYjI3frKWNPEMK3sZsJfNNQApvCQmgBcc13ZKmFj8qExw==} + + content-disposition@1.0.1: + resolution: {integrity: sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==} + engines: {node: '>=18'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + + cors@2.8.6: + resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} + engines: {node: '>= 0.10'} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + data-uri-to-buffer@4.0.1: + resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} + engines: {node: '>= 12'} + + dataloader@2.2.3: + resolution: {integrity: sha512-y2krtASINtPFS1rSDjacrFgn1dcUuoREVabwlOGOe4SdxenREqwjwjElAdwvbGM7kgZz9a3KVicWR7vcz8rnzA==} + + debug@4.3.4: + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decamelize@1.2.0: + resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} + engines: {node: '>=0.10.0'} + + decompress-response@6.0.0: + resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} + engines: {node: '>=10'} + + deep-extend@0.6.0: + resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} + engines: {node: '>=4.0.0'} + + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + + default-browser-id@5.0.1: + resolution: {integrity: sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==} + engines: {node: '>=18'} + + default-browser@5.5.0: + resolution: {integrity: sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==} + engines: {node: '>=18'} + + define-lazy-prop@3.0.0: + resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} + engines: {node: '>=12'} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + delegates@1.0.0: + resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} + + denque@2.1.0: + resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} + engines: {node: '>=0.10'} + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + + dom-serializer@2.0.0: + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + + domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + + domhandler@5.0.3: + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} + + domutils@3.2.2: + resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} + + dotenv@17.3.1: + resolution: {integrity: sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==} + engines: {node: '>=12'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + duplexify@4.1.3: + resolution: {integrity: sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==} + + ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + enabled@2.0.0: + resolution: {integrity: sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==} + + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + + encoding@0.1.13: + resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==} + + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + env-paths@2.2.1: + resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} + engines: {node: '>=6'} + + err-code@2.0.3: + resolution: {integrity: sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-module-lexer@2.0.0: + resolution: {integrity: sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + esbuild@0.27.4: + resolution: {integrity: sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + esm@3.2.25: + resolution: {integrity: sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==} + engines: {node: '>=6'} + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + event-target-shim@5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + + eventemitter3@4.0.7: + resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + + eventemitter3@5.0.4: + resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} + + events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + + eventsource-parser@3.0.6: + resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==} + engines: {node: '>=18.0.0'} + + eventsource@3.0.7: + resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} + engines: {node: '>=18.0.0'} + + expand-template@2.0.3: + resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} + engines: {node: '>=6'} + + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + + express-rate-limit@8.3.2: + resolution: {integrity: sha512-77VmFeJkO0/rvimEDuUC5H30oqUC4EyOhyGccfqoLebB0oiEYfM7nwPrsDsBL1gsTpwfzX8SFy2MT3TDyRq+bg==} + engines: {node: '>= 16'} + peerDependencies: + express: '>= 4.11' + + express@5.2.1: + resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} + engines: {node: '>= 18'} + + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-uri@3.1.0: + resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + + fast-xml-builder@1.1.4: + resolution: {integrity: sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg==} + + fast-xml-parser@5.5.9: + resolution: {integrity: sha512-jldvxr1MC6rtiZKgrFnDSvT8xuH+eJqxqOBThUVjYrxssYTo1avZLGql5l0a0BAERR01CadYzZ83kVEkbyDg+g==} + hasBin: true + + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fecha@4.2.3: + resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==} + + fetch-blob@3.2.0: + resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} + engines: {node: ^12.20 || >= 14.13} + + file-uri-to-path@1.0.0: + resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + finalhandler@2.1.1: + resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} + engines: {node: '>= 18.0.0'} + + fn.name@1.1.0: + resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==} + + form-data@2.5.5: + resolution: {integrity: sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A==} + engines: {node: '>= 0.12'} + + formdata-polyfill@4.0.10: + resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} + engines: {node: '>=12.20.0'} + + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + + fs-constants@1.0.0: + resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + + fs-extra@11.3.3: + resolution: {integrity: sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==} + engines: {node: '>=14.14'} + + fs-minipass@2.1.0: + resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} + engines: {node: '>= 8'} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + gauge@4.0.4: + resolution: {integrity: sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + deprecated: This package is no longer supported. + + gaxios@6.7.1: + resolution: {integrity: sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==} + engines: {node: '>=14'} + + gaxios@7.1.4: + resolution: {integrity: sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA==} + engines: {node: '>=18'} + + gcp-metadata@6.1.1: + resolution: {integrity: sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==} + engines: {node: '>=14'} + + gcp-metadata@8.1.2: + resolution: {integrity: sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==} + engines: {node: '>=18'} + + generate-function@2.3.1: + resolution: {integrity: sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-package-type@0.1.0: + resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} + engines: {node: '>=8.0.0'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + getopts@2.3.0: + resolution: {integrity: sha512-5eDf9fuSXwxBL6q5HX+dhDj+dslFGWzU5thZ9kNKUkcPtaPdatmUFKwHFrLb/uf/WpA4BHET+AX3Scl56cAjpA==} + + github-from-package@0.0.0: + resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + + globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + + google-auth-library@10.6.2: + resolution: {integrity: sha512-e27Z6EThmVNNvtYASwQxose/G57rkRuaRbQyxM2bvYLLX/GqWZ5chWq2EBoUchJbCc57eC9ArzO5wMsEmWftCw==} + engines: {node: '>=18'} + + google-auth-library@9.15.1: + resolution: {integrity: sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==} + engines: {node: '>=14'} + + google-logging-utils@0.0.2: + resolution: {integrity: sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==} + engines: {node: '>=14'} + + google-logging-utils@1.1.3: + resolution: {integrity: sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==} + engines: {node: '>=14'} + + googleapis-common@7.2.0: + resolution: {integrity: sha512-/fhDZEJZvOV3X5jmD+fKxMqma5q2Q9nZNSF3kn1F18tpxmA86BcTxAGBQdM0N89Z3bEaIs+HVznSmFJEAmMTjA==} + engines: {node: '>=14.0.0'} + + googleapis@137.1.0: + resolution: {integrity: sha512-2L7SzN0FLHyQtFmyIxrcXhgust77067pkkduqkbIpDuj9JzVnByxsRrcRfUMFQam3rQkWW2B0f1i40IwKDWIVQ==} + engines: {node: '>=14.0.0'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + gtoken@7.1.0: + resolution: {integrity: sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==} + engines: {node: '>=14.0.0'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + has-unicode@2.0.1: + resolution: {integrity: sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + hono@4.12.9: + resolution: {integrity: sha512-wy3T8Zm2bsEvxKZM5w21VdHDDcwVS1yUFFY6i8UobSsKfFceT7TOwhbhfKsDyx7tYQlmRM5FLpIuYvNFyjctiA==} + engines: {node: '>=16.9.0'} + + html-entities@2.6.0: + resolution: {integrity: sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==} + + html-to-text@9.0.5: + resolution: {integrity: sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==} + engines: {node: '>=14'} + + htmlparser2@8.0.2: + resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} + + http-cache-semantics@4.2.0: + resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==} + + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + + http-proxy-agent@4.0.1: + resolution: {integrity: sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==} + engines: {node: '>= 6'} + + http-proxy-agent@5.0.0: + resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==} + engines: {node: '>= 6'} + + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + + https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + + humanize-ms@1.2.1: + resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + iconv-lite@0.7.2: + resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} + engines: {node: '>=0.10.0'} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + + infer-owner@1.0.4: + resolution: {integrity: sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==} + + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + + interpret@2.2.0: + resolution: {integrity: sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==} + engines: {node: '>= 0.10'} + + ip-address@10.1.0: + resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==} + engines: {node: '>= 12'} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + + is-docker@3.0.0: + resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + hasBin: true + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-inside-container@1.0.0: + resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} + engines: {node: '>=14.16'} + hasBin: true + + is-lambda@1.0.1: + resolution: {integrity: sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==} + + is-network-error@1.3.1: + resolution: {integrity: sha512-6QCxa49rQbmUWLfk0nuGqzql9U8uaV2H6279bRErPBHe/109hCzsLUBUHfbEtvLIHBd6hyXbgedBSHevm43Edw==} + engines: {node: '>=16'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + + is-property@1.0.2: + resolution: {integrity: sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==} + + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + + is-wsl@3.1.1: + resolution: {integrity: sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==} + engines: {node: '>=16'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jose@6.2.2: + resolution: {integrity: sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ==} + + js-md4@0.3.2: + resolution: {integrity: sha512-/GDnfQYsltsjRswQhN9fhv3EMw2sCpUdrdxyWDOUK7eyD++r3gRhzgiQgc/x4MAv2i1iuQ4lxO5mvqM3vj4bwA==} + + js-tiktoken@1.0.21: + resolution: {integrity: sha512-biOj/6M5qdgx5TKjDnFT1ymSpM5tbd3ylwDtrQvFQSu0Z7bBYko2dF+W/aUkXUPuk6IVpRxk/3Q2sHOzGlS36g==} + + json-bigint@1.0.0: + resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==} + + json-schema-to-ts@3.1.1: + resolution: {integrity: sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==} + engines: {node: '>=16'} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-schema-typed@8.0.2: + resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} + + jsonfile@6.2.0: + resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} + + jsonwebtoken@9.0.3: + resolution: {integrity: sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==} + engines: {node: '>=12', npm: '>=6'} + + jwa@2.0.1: + resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} + + jws@4.0.1: + resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==} + + knex@3.2.8: + resolution: {integrity: sha512-ElXXxu9Nq+5hWYdBUddYIWIT5yKKs5KNCsmKGbJSHPyaMpAABp3xs4L55GgdQoAs6QQ7dv72ai3M4pxYQ8utEg==} + engines: {node: '>=16'} + hasBin: true + peerDependencies: + better-sqlite3: '*' + mysql: '*' + mysql2: '*' + pg: '*' + pg-native: '*' + pg-query-stream: ^4.14.0 + sqlite3: '*' + tedious: '*' + peerDependenciesMeta: + better-sqlite3: + optional: true + mysql: + optional: true + mysql2: + optional: true + pg: + optional: true + pg-native: + optional: true + pg-query-stream: + optional: true + sqlite3: + optional: true + tedious: + optional: true + + kuler@2.0.0: + resolution: {integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==} + + langsmith@0.5.15: + resolution: {integrity: sha512-S20JnYmIgqGBjA/WEn12ZZJjqd03O5wd8K9KgGBvsKXQBn0bYuFrr1w20L37PpcMmX3/cftpgJ6g2y8KoEmHLw==} + peerDependencies: + '@opentelemetry/api': '*' + '@opentelemetry/exporter-trace-otlp-proto': '*' + '@opentelemetry/sdk-trace-base': '*' + openai: '*' + ws: '>=7' + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + '@opentelemetry/exporter-trace-otlp-proto': + optional: true + '@opentelemetry/sdk-trace-base': + optional: true + openai: + optional: true + ws: + optional: true + + leac@0.6.0: + resolution: {integrity: sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==} + + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + + llamaindex@0.12.1: + resolution: {integrity: sha512-/tXXITk/iVGBycOFaDhev6dgTBIr6Ycu4FoPIt6A5JcEAiB6ujONjiV36flVXUR8JdqwMtS767XMjV+36nV4yQ==} + engines: {node: '>=18.0.0'} + + lodash-es@4.17.23: + resolution: {integrity: sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==} + + lodash.camelcase@4.3.0: + resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} + + lodash.includes@4.3.0: + resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} + + lodash.isboolean@3.0.3: + resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} + + lodash.isinteger@4.0.4: + resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==} + + lodash.isnumber@3.0.3: + resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==} + + lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + + lodash.isstring@4.0.1: + resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} + + lodash.once@4.1.1: + resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + logform@2.7.0: + resolution: {integrity: sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==} + engines: {node: '>= 12.0.0'} + + long@5.3.2: + resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + lru-cache@6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + + lru.min@1.1.4: + resolution: {integrity: sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==} + engines: {bun: '>=1.0.0', deno: '>=1.30.0', node: '>=8.0.0'} + + magic-bytes.js@1.13.0: + resolution: {integrity: sha512-afO2mnxW7GDTXMm5/AoN1WuOcdoKhtgXjIvHmobqTD1grNplhGdv3PFOyjCVmrnOZBIT/gD/koDKpYG+0mvHcg==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + make-fetch-happen@9.1.0: + resolution: {integrity: sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==} + engines: {node: '>= 10'} + + mariadb@3.4.5: + resolution: {integrity: sha512-gThTYkhIS5rRqkVr+Y0cIdzr+GRqJ9sA2Q34e0yzmyhMCwyApf3OKAC1jnF23aSlIOqJuyaUFUcj7O1qZslmmQ==} + engines: {node: '>= 14'} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mikro-orm@6.6.11: + resolution: {integrity: sha512-8z1pS5IfKGys0OR0m5bWDLbmCu7n86DXvozL9v7BYcqW6O3GbsioghmNobzl7PraOOIRy260rS+mO6Z1jLduDQ==} + engines: {node: '>= 18.12.0'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} + + mime@3.0.0: + resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} + engines: {node: '>=10.0.0'} + hasBin: true + + mimic-response@3.1.0: + resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} + engines: {node: '>=10'} + + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} + + minimatch@3.1.5: + resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + minipass-collect@1.0.2: + resolution: {integrity: sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==} + engines: {node: '>= 8'} + + minipass-fetch@1.4.1: + resolution: {integrity: sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==} + engines: {node: '>=8'} + + minipass-flush@1.0.7: + resolution: {integrity: sha512-TbqTz9cUwWyHS2Dy89P3ocAGUGxKjjLuR9z8w4WUTGAVgEj17/4nhgo2Du56i0Fm3Pm30g4iA8Lcqctc76jCzA==} + engines: {node: '>= 8'} + + minipass-pipeline@1.2.4: + resolution: {integrity: sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==} + engines: {node: '>=8'} + + minipass-sized@1.0.3: + resolution: {integrity: sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==} + engines: {node: '>=8'} + + minipass@3.3.6: + resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} + engines: {node: '>=8'} + + minipass@5.0.0: + resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==} + engines: {node: '>=8'} + + minizlib@2.1.2: + resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} + engines: {node: '>= 8'} + + mkdirp-classic@0.5.3: + resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} + + mkdirp@1.0.4: + resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} + engines: {node: '>=10'} + hasBin: true + + ms@2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + mustache@4.2.0: + resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==} + hasBin: true + + mysql2@3.20.0: + resolution: {integrity: sha512-eCLUs7BNbgA6nf/MZXsaBO1SfGs0LtLVrJD3WeWq+jPLDWkSufTD+aGMwykfUVPdZnblaUK1a8G/P63cl9FkKg==} + engines: {node: '>= 8.0'} + peerDependencies: + '@types/node': '>= 8' + + named-placeholders@1.1.6: + resolution: {integrity: sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==} + engines: {node: '>=8.0.0'} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + napi-build-utils@2.0.0: + resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==} + + native-duplexpair@1.0.0: + resolution: {integrity: sha512-E7QQoM+3jvNtlmyfqRZ0/U75VFgCls+fSkbml2MpgWkWyz3ox8Y58gNhfuziuQYGNNQAbFZJQck55LHCnCK6CA==} + + negotiator@0.6.4: + resolution: {integrity: sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==} + engines: {node: '>= 0.6'} + + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + + node-abi@3.89.0: + resolution: {integrity: sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==} + engines: {node: '>=10'} + + node-addon-api@7.1.1: + resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + + node-addon-api@8.7.0: + resolution: {integrity: sha512-9MdFxmkKaOYVTV+XVRG8ArDwwQ77XIgIPyKASB1k3JPq3M8fGQQQE3YpMOrKm6g//Ktx8ivZr8xo1Qmtqub+GA==} + engines: {node: ^18 || ^20 || >= 21} + + node-domexception@1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + deprecated: Use your platform's native DOMException instead + + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + + node-fetch@3.3.2: + resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + node-gyp-build@4.8.4: + resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} + hasBin: true + + node-gyp@8.4.1: + resolution: {integrity: sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==} + engines: {node: '>= 10.12.0'} + hasBin: true + + nopt@5.0.0: + resolution: {integrity: sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==} + engines: {node: '>=6'} + hasBin: true + + npmlog@6.0.2: + resolution: {integrity: sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + deprecated: This package is no longer supported. + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + obug@2.1.1: + resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + one-time@1.0.0: + resolution: {integrity: sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==} + + open@10.2.0: + resolution: {integrity: sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==} + engines: {node: '>=18'} + + openai@6.33.0: + resolution: {integrity: sha512-xAYN1W3YsDXJWA5F277135YfkEk6H7D3D6vWwRhJ3OEkzRgcyK8z/P5P9Gyi/wB4N8kK9kM5ZjprfvyHagKmpw==} + hasBin: true + peerDependencies: + ws: ^8.18.0 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + ws: + optional: true + zod: + optional: true + + p-finally@1.0.0: + resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==} + engines: {node: '>=4'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-map@4.0.0: + resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} + engines: {node: '>=10'} + + p-queue@6.6.2: + resolution: {integrity: sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==} + engines: {node: '>=8'} + + p-queue@9.1.0: + resolution: {integrity: sha512-O/ZPaXuQV29uSLbxWBGGZO1mCQXV2BLIwUr59JUU9SoH76mnYvtms7aafH/isNSNGwuEfP6W/4xD0/TJXxrizw==} + engines: {node: '>=20'} + + p-retry@4.6.2: + resolution: {integrity: sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==} + engines: {node: '>=8'} + + p-retry@7.1.1: + resolution: {integrity: sha512-J5ApzjyRkkf601HpEeykoiCvzHQjWxPAHhyjFcEUP2SWq0+35NKh8TLhpLw+Dkq5TZBFvUM6UigdE9hIVYTl5w==} + engines: {node: '>=20'} + + p-timeout@3.2.0: + resolution: {integrity: sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==} + engines: {node: '>=8'} + + p-timeout@7.0.1: + resolution: {integrity: sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg==} + engines: {node: '>=20'} + + parseley@0.12.1: + resolution: {integrity: sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==} + + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + + path-expression-matcher@1.2.0: + resolution: {integrity: sha512-DwmPWeFn+tq7TiyJ2CxezCAirXjFxvaiD03npak3cRjlP9+OjTmSy1EpIrEbh+l6JgUundniloMLDQ/6VTdhLQ==} + engines: {node: '>=14.0.0'} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-to-regexp@8.4.1: + resolution: {integrity: sha512-fvU78fIjZ+SBM9YwCknCvKOUKkLVqtWDVctl0s7xIqfmfb38t2TT4ZU2gHm+Z8xGwgW+QWEU3oQSAzIbo89Ggw==} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + pathe@1.1.2: + resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + peberminta@0.9.0: + resolution: {integrity: sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==} + + pg-cloudflare@1.3.0: + resolution: {integrity: sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==} + + pg-connection-string@2.12.0: + resolution: {integrity: sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ==} + + pg-connection-string@2.6.2: + resolution: {integrity: sha512-ch6OwaeaPYcova4kKZ15sbJ2hKb/VP48ZD2gE7i1J+L4MspCtBMAx8nMgz7bksc7IojCIIWuEhHibSMFH8m8oA==} + + pg-int8@1.0.1: + resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} + engines: {node: '>=4.0.0'} + + pg-pool@3.13.0: + resolution: {integrity: sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA==} + peerDependencies: + pg: '>=8.0' + + pg-protocol@1.13.0: + resolution: {integrity: sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==} + + pg-types@2.2.0: + resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} + engines: {node: '>=4'} + + pg@8.20.0: + resolution: {integrity: sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==} + engines: {node: '>= 16.0.0'} + peerDependencies: + pg-native: '>=3.0.1' + peerDependenciesMeta: + pg-native: + optional: true + + pgpass@1.0.5: + resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} + engines: {node: '>=8.6'} + + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + + pkce-challenge@5.0.1: + resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} + engines: {node: '>=16.20.0'} + + postcss@8.5.8: + resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} + engines: {node: ^10 || ^12 || >=14} + + postgres-array@2.0.0: + resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} + engines: {node: '>=4'} + + postgres-array@3.0.4: + resolution: {integrity: sha512-nAUSGfSDGOaOAEGwqsRY27GPOea7CNipJPOA7lPbdEpx5Kg3qzdP0AaWC5MlhTWV9s4hFX39nomVZ+C4tnGOJQ==} + engines: {node: '>=12'} + + postgres-bytea@1.0.1: + resolution: {integrity: sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==} + engines: {node: '>=0.10.0'} + + postgres-date@1.0.7: + resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} + engines: {node: '>=0.10.0'} + + postgres-date@2.1.0: + resolution: {integrity: sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA==} + engines: {node: '>=12'} + + postgres-interval@1.2.0: + resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} + engines: {node: '>=0.10.0'} + + postgres-interval@4.0.2: + resolution: {integrity: sha512-EMsphSQ1YkQqKZL2cuG0zHkmjCCzQqQ71l2GXITqRwjhRleCdv00bDk/ktaSi0LnlaPzAc3535KTrjXsTdtx7A==} + engines: {node: '>=12'} + + prebuild-install@7.1.3: + resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} + engines: {node: '>=10'} + deprecated: No longer maintained. Please contact the author of the relevant native addon; alternatives are available. + hasBin: true + + process@0.11.10: + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} + engines: {node: '>= 0.6.0'} + + promise-inflight@1.0.1: + resolution: {integrity: sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==} + peerDependencies: + bluebird: '*' + peerDependenciesMeta: + bluebird: + optional: true + + promise-retry@2.0.1: + resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==} + engines: {node: '>=10'} + + protobufjs@7.5.4: + resolution: {integrity: sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==} + engines: {node: '>=12.0.0'} + + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + + pump@3.0.4: + resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} + + qs@6.15.0: + resolution: {integrity: sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==} + engines: {node: '>=0.6'} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@3.0.2: + resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} + engines: {node: '>= 0.10'} + + rc@1.2.8: + resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} + hasBin: true + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + readable-stream@4.7.0: + resolution: {integrity: sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + rechoir@0.8.0: + resolution: {integrity: sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==} + engines: {node: '>= 10.13.0'} + + reflect-metadata@0.2.2: + resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + + resolve@1.22.11: + resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==} + engines: {node: '>= 0.4'} + hasBin: true + + retry-request@7.0.2: + resolution: {integrity: sha512-dUOvLMJ0/JJYEn8NrpOaGNE7X3vpI5XlZS/u0ANjqtcZVKnIxP7IgCFwrKTxENw29emmwug53awKtaMm4i9g5w==} + engines: {node: '>=14'} + + retry@0.12.0: + resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} + engines: {node: '>= 4'} + + retry@0.13.1: + resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} + engines: {node: '>= 4'} + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + + rolldown@1.0.0-rc.12: + resolution: {integrity: sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + + run-applescript@7.1.0: + resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==} + engines: {node: '>=18'} + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safe-stable-stringify@2.5.0: + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} + engines: {node: '>=10'} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + selderee@0.11.0: + resolution: {integrity: sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==} + + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} + hasBin: true + + send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} + engines: {node: '>= 18'} + + serve-static@2.2.1: + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} + engines: {node: '>= 18'} + + set-blocking@2.0.0: + resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + simple-concat@1.0.1: + resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} + + simple-get@4.0.1: + resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} + + simple-wcswidth@1.1.2: + resolution: {integrity: sha512-j7piyCjAeTDSjzTSQ7DokZtMNwNlEAyxqSZeCS+CXH7fJ4jx3FuJ/mTW3mE+6JLs4VJBbcll0Kjn+KXI5t21Iw==} + + slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + + smart-buffer@4.2.0: + resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} + engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} + + socks-proxy-agent@6.2.1: + resolution: {integrity: sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==} + engines: {node: '>= 10'} + + socks@2.8.7: + resolution: {integrity: sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==} + engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + + sprintf-js@1.1.3: + resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==} + + sql-escaper@1.3.3: + resolution: {integrity: sha512-BsTCV265VpTp8tm1wyIm1xqQCS+Q9NHx2Sr+WcnUrgLrQ6yiDIvHYJV5gHxsj1lMBy2zm5twLaZao8Jd+S8JJw==} + engines: {bun: '>=1.0.0', deno: '>=2.0.0', node: '>=12.0.0'} + + sqlite3@5.1.7: + resolution: {integrity: sha512-GGIyOiFaG+TUra3JIfkI/zGP8yZYLPQ0pl1bH+ODjiX57sPhrLU5sQJn1y9bDKZUFYkX1crlrPfSYt0BKKdkog==} + + sqlstring-sqlite@0.1.1: + resolution: {integrity: sha512-9CAYUJ0lEUPYJrswqiqdINNSfq3jqWo/bFJ7tufdoNeSK0Fy+d1kFTxjqO9PIqza0Kri+ZtYMfPVf1aZaFOvrQ==} + engines: {node: '>= 0.6'} + + sqlstring@2.3.3: + resolution: {integrity: sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==} + engines: {node: '>= 0.6'} + + ssri@8.0.1: + resolution: {integrity: sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==} + engines: {node: '>= 8'} + + stack-trace@0.0.10: + resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + + std-env@4.0.0: + resolution: {integrity: sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==} + + stream-events@1.0.5: + resolution: {integrity: sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==} + + stream-shift@1.0.3: + resolution: {integrity: sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-json-comments@2.0.1: + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} + + strnum@2.2.2: + resolution: {integrity: sha512-DnR90I+jtXNSTXWdwrEy9FakW7UX+qUZg28gj5fk2vxxl7uS/3bpI4fjFYVmdK9etptYBPNkpahuQnEwhwECqA==} + + stubs@3.0.0: + resolution: {integrity: sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + tar-fs@2.1.4: + resolution: {integrity: sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==} + + tar-stream@2.2.0: + resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} + engines: {node: '>=6'} + + tar@6.2.1: + resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} + engines: {node: '>=10'} + deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + + tarn@3.0.2: + resolution: {integrity: sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ==} + engines: {node: '>=8.0.0'} + + tedious@19.2.1: + resolution: {integrity: sha512-pk1Q16Yl62iocuQB+RWbg6rFUFkIyzqOFQ6NfysCltRvQqKwfurgj8v/f2X+CKvDhSL4IJ0cCOfCHDg9PWEEYA==} + engines: {node: '>=18.17'} + + teeny-request@9.0.0: + resolution: {integrity: sha512-resvxdc6Mgb7YEThw6G6bExlXKkv6+YbuzGg9xuXxSgxJF7Ozs+o8Y9+2R3sArdWdW8nOokoQb1yrpFB0pQK2g==} + engines: {node: '>=14'} + + text-hex@1.0.0: + resolution: {integrity: sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==} + + tildify@2.0.0: + resolution: {integrity: sha512-Cc+OraorugtXNfs50hU9KS369rFXCfgGLpfCfvlc+Ud5u6VWmUQsOAa9HbTvheQdYnrdJqqv1e5oIqXppMYnSw==} + engines: {node: '>=8'} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@1.0.4: + resolution: {integrity: sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==} + engines: {node: '>=18'} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + tinyrainbow@3.1.0: + resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} + engines: {node: '>=14.0.0'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + + tree-sitter@0.22.4: + resolution: {integrity: sha512-usbHZP9/oxNsUY65MQUsduGRqDHQOou1cagUSwjhoSYAmSahjQDAVsh9s+SlZkn8X8+O1FULRGwHu7AFP3kjzg==} + + triple-beam@1.4.1: + resolution: {integrity: sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==} + engines: {node: '>= 14.0.0'} + + ts-algebra@2.0.0: + resolution: {integrity: sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==} + + ts-morph@27.0.2: + resolution: {integrity: sha512-fhUhgeljcrdZ+9DZND1De1029PrE+cMkIP7ooqkLRTrRLTqcki2AstsyJm0vRNbTbVCNJ0idGlbBrfqc7/nA8w==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tsqlstring@1.0.1: + resolution: {integrity: sha512-6Nzj/SrVg1SF+egwP4OMAgEa83nLKXIE3EHn+6YKinMUeMj8bGIeLuDCkDC3Cc4OIM+xhw4CD0oXKxal8J/Y6A==} + engines: {node: '>= 8.0'} + + tunnel-agent@0.6.0: + resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} + + type-is@2.0.1: + resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} + engines: {node: '>= 0.6'} + + typescript@6.0.2: + resolution: {integrity: sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@7.16.0: + resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + + unique-filename@1.1.1: + resolution: {integrity: sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==} + + unique-slug@2.0.2: + resolution: {integrity: sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==} + + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + url-template@2.0.8: + resolution: {integrity: sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + uuid@10.0.0: + resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==} + hasBin: true + + uuid@11.1.0: + resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} + hasBin: true + + uuid@13.0.0: + resolution: {integrity: sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==} + hasBin: true + + uuid@8.3.2: + resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + hasBin: true + + uuid@9.0.1: + resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} + hasBin: true + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + + vite@8.0.3: + resolution: {integrity: sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.1.0 + esbuild: ^0.27.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@4.1.2: + resolution: {integrity: sha512-xjR1dMTVHlFLh98JE3i/f/WePqJsah4A0FK9cc8Ehp9Udk0AZk6ccpIZhh1qJ/yxVWRZ+Q54ocnD8TXmkhspGg==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.2 + '@vitest/browser-preview': 4.1.2 + '@vitest/browser-webdriverio': 4.1.2 + '@vitest/ui': 4.1.2 + happy-dom: '*' + jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + web-streams-polyfill@3.3.3: + resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} + engines: {node: '>= 8'} + + web-tree-sitter@0.24.7: + resolution: {integrity: sha512-CdC/TqVFbXqR+C51v38hv6wOPatKEUGxa39scAeFSm98wIhZxAYonhRQPSMmfZ2w7JDI0zQDdzdmgtNk06/krQ==} + + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + wide-align@1.1.5: + resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==} + + winston-transport@4.9.0: + resolution: {integrity: sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==} + engines: {node: '>= 12.0.0'} + + winston@3.19.0: + resolution: {integrity: sha512-LZNJgPzfKR+/J3cHkxcpHKpKKvGfDZVPS4hfJCc4cCG0CgYzvlD6yE/S3CIL/Yt91ak327YCpiF/0MyeZHEHKA==} + engines: {node: '>= 12.0.0'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + ws@8.20.0: + resolution: {integrity: sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + wsl-utils@0.1.0: + resolution: {integrity: sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==} + engines: {node: '>=18'} + + xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + zod-to-json-schema@3.25.2: + resolution: {integrity: sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==} + peerDependencies: + zod: ^3.25.28 || ^4 + + zod@4.1.8: + resolution: {integrity: sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ==} + + zod@4.3.6: + resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} + +snapshots: + + '@a2a-js/sdk@0.3.13(@grpc/grpc-js@1.14.3)(express@5.2.1)': + dependencies: + uuid: 11.1.0 + optionalDependencies: + '@grpc/grpc-js': 1.14.3 + express: 5.2.1 + + '@anthropic-ai/sdk@0.80.0(zod@4.3.6)': + dependencies: + json-schema-to-ts: 3.1.1 + optionalDependencies: + zod: 4.3.6 + + '@aws-crypto/sha256-js@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.6 + tslib: 2.8.1 + + '@aws-crypto/util@5.2.0': + dependencies: + '@aws-sdk/types': 3.973.6 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-sdk/types@3.973.6': + dependencies: + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@azure-rest/core-client@2.5.1': + dependencies: + '@azure/abort-controller': 2.1.2 + '@azure/core-auth': 1.10.1 + '@azure/core-rest-pipeline': 1.23.0 + '@azure/core-tracing': 1.3.1 + '@typespec/ts-http-runtime': 0.3.4 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@azure/abort-controller@2.1.2': + dependencies: + tslib: 2.8.1 + + '@azure/core-auth@1.10.1': + dependencies: + '@azure/abort-controller': 2.1.2 + '@azure/core-util': 1.13.1 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@azure/core-client@1.10.1': + dependencies: + '@azure/abort-controller': 2.1.2 + '@azure/core-auth': 1.10.1 + '@azure/core-rest-pipeline': 1.23.0 + '@azure/core-tracing': 1.3.1 + '@azure/core-util': 1.13.1 + '@azure/logger': 1.3.0 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@azure/core-http-compat@2.3.2(@azure/core-client@1.10.1)(@azure/core-rest-pipeline@1.23.0)': + dependencies: + '@azure/abort-controller': 2.1.2 + '@azure/core-client': 1.10.1 + '@azure/core-rest-pipeline': 1.23.0 + + '@azure/core-lro@2.7.2': + dependencies: + '@azure/abort-controller': 2.1.2 + '@azure/core-util': 1.13.1 + '@azure/logger': 1.3.0 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@azure/core-paging@1.6.2': + dependencies: + tslib: 2.8.1 + + '@azure/core-rest-pipeline@1.23.0': + dependencies: + '@azure/abort-controller': 2.1.2 + '@azure/core-auth': 1.10.1 + '@azure/core-tracing': 1.3.1 + '@azure/core-util': 1.13.1 + '@azure/logger': 1.3.0 + '@typespec/ts-http-runtime': 0.3.4 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@azure/core-tracing@1.3.1': + dependencies: + tslib: 2.8.1 + + '@azure/core-util@1.13.1': + dependencies: + '@azure/abort-controller': 2.1.2 + '@typespec/ts-http-runtime': 0.3.4 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@azure/identity@4.13.1': + dependencies: + '@azure/abort-controller': 2.1.2 + '@azure/core-auth': 1.10.1 + '@azure/core-client': 1.10.1 + '@azure/core-rest-pipeline': 1.23.0 + '@azure/core-tracing': 1.3.1 + '@azure/core-util': 1.13.1 + '@azure/logger': 1.3.0 + '@azure/msal-browser': 5.6.2 + '@azure/msal-node': 5.1.1 + open: 10.2.0 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@azure/keyvault-common@2.0.0': + dependencies: + '@azure/abort-controller': 2.1.2 + '@azure/core-auth': 1.10.1 + '@azure/core-client': 1.10.1 + '@azure/core-rest-pipeline': 1.23.0 + '@azure/core-tracing': 1.3.1 + '@azure/core-util': 1.13.1 + '@azure/logger': 1.3.0 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@azure/keyvault-keys@4.10.0(@azure/core-client@1.10.1)': + dependencies: + '@azure-rest/core-client': 2.5.1 + '@azure/abort-controller': 2.1.2 + '@azure/core-auth': 1.10.1 + '@azure/core-http-compat': 2.3.2(@azure/core-client@1.10.1)(@azure/core-rest-pipeline@1.23.0) + '@azure/core-lro': 2.7.2 + '@azure/core-paging': 1.6.2 + '@azure/core-rest-pipeline': 1.23.0 + '@azure/core-tracing': 1.3.1 + '@azure/core-util': 1.13.1 + '@azure/keyvault-common': 2.0.0 + '@azure/logger': 1.3.0 + tslib: 2.8.1 + transitivePeerDependencies: + - '@azure/core-client' + - supports-color + + '@azure/logger@1.3.0': + dependencies: + '@typespec/ts-http-runtime': 0.3.4 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@azure/msal-browser@5.6.2': + dependencies: + '@azure/msal-common': 16.4.0 + + '@azure/msal-common@16.4.0': {} + + '@azure/msal-node@5.1.1': + dependencies: + '@azure/msal-common': 16.4.0 + jsonwebtoken: 9.0.3 + uuid: 8.3.2 + + '@babel/runtime@7.29.2': {} + + '@cfworker/json-schema@4.1.1': {} + + '@colors/colors@1.6.0': {} + + '@dabh/diagnostics@2.0.8': + dependencies: + '@so-ric/colorspace': 1.1.6 + enabled: 2.0.0 + kuler: 2.0.0 + + '@emnapi/core@1.9.1': + dependencies: + '@emnapi/wasi-threads': 1.2.0 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.9.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@esbuild/aix-ppc64@0.27.4': + optional: true + + '@esbuild/android-arm64@0.27.4': + optional: true + + '@esbuild/android-arm@0.27.4': + optional: true + + '@esbuild/android-x64@0.27.4': + optional: true + + '@esbuild/darwin-arm64@0.27.4': + optional: true + + '@esbuild/darwin-x64@0.27.4': + optional: true + + '@esbuild/freebsd-arm64@0.27.4': + optional: true + + '@esbuild/freebsd-x64@0.27.4': + optional: true + + '@esbuild/linux-arm64@0.27.4': + optional: true + + '@esbuild/linux-arm@0.27.4': + optional: true + + '@esbuild/linux-ia32@0.27.4': + optional: true + + '@esbuild/linux-loong64@0.27.4': + optional: true + + '@esbuild/linux-mips64el@0.27.4': + optional: true + + '@esbuild/linux-ppc64@0.27.4': + optional: true + + '@esbuild/linux-riscv64@0.27.4': + optional: true + + '@esbuild/linux-s390x@0.27.4': + optional: true + + '@esbuild/linux-x64@0.27.4': + optional: true + + '@esbuild/netbsd-arm64@0.27.4': + optional: true + + '@esbuild/netbsd-x64@0.27.4': + optional: true + + '@esbuild/openbsd-arm64@0.27.4': + optional: true + + '@esbuild/openbsd-x64@0.27.4': + optional: true + + '@esbuild/openharmony-arm64@0.27.4': + optional: true + + '@esbuild/sunos-x64@0.27.4': + optional: true + + '@esbuild/win32-arm64@0.27.4': + optional: true + + '@esbuild/win32-ia32@0.27.4': + optional: true + + '@esbuild/win32-x64@0.27.4': + optional: true + + '@finom/zod-to-json-schema@3.24.11(zod@4.3.6)': + dependencies: + zod: 4.3.6 + + '@gar/promisify@1.1.3': + optional: true + + '@google-cloud/opentelemetry-cloud-monitoring-exporter@0.21.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.6.1(@opentelemetry/api@1.9.1))(@opentelemetry/resources@2.6.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.6.1(@opentelemetry/api@1.9.1))(encoding@0.1.13)': + dependencies: + '@google-cloud/opentelemetry-resource-util': 3.0.0(@opentelemetry/core@2.6.1(@opentelemetry/api@1.9.1))(@opentelemetry/resources@2.6.1(@opentelemetry/api@1.9.1))(encoding@0.1.13) + '@google-cloud/precise-date': 4.0.0 + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.6.1(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.6.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-metrics': 2.6.1(@opentelemetry/api@1.9.1) + google-auth-library: 9.15.1(encoding@0.1.13) + googleapis: 137.1.0(encoding@0.1.13) + transitivePeerDependencies: + - encoding + - supports-color + + '@google-cloud/opentelemetry-cloud-trace-exporter@3.0.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.6.1(@opentelemetry/api@1.9.1))(@opentelemetry/resources@2.6.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.1))(encoding@0.1.13)': + dependencies: + '@google-cloud/opentelemetry-resource-util': 3.0.0(@opentelemetry/core@2.6.1(@opentelemetry/api@1.9.1))(@opentelemetry/resources@2.6.1(@opentelemetry/api@1.9.1))(encoding@0.1.13) + '@grpc/grpc-js': 1.14.3 + '@grpc/proto-loader': 0.8.0 + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.6.1(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.6.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 2.6.1(@opentelemetry/api@1.9.1) + google-auth-library: 9.15.1(encoding@0.1.13) + transitivePeerDependencies: + - encoding + - supports-color + + '@google-cloud/opentelemetry-resource-util@3.0.0(@opentelemetry/core@2.6.1(@opentelemetry/api@1.9.1))(@opentelemetry/resources@2.6.1(@opentelemetry/api@1.9.1))(encoding@0.1.13)': + dependencies: + '@opentelemetry/core': 2.6.1(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.6.1(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + gcp-metadata: 6.1.1(encoding@0.1.13) + transitivePeerDependencies: + - encoding + - supports-color + + '@google-cloud/paginator@5.0.2': + dependencies: + arrify: 2.0.1 + extend: 3.0.2 + + '@google-cloud/precise-date@4.0.0': {} + + '@google-cloud/projectify@4.0.0': {} + + '@google-cloud/promisify@4.0.0': {} + + '@google-cloud/storage@7.19.0(encoding@0.1.13)': + dependencies: + '@google-cloud/paginator': 5.0.2 + '@google-cloud/projectify': 4.0.0 + '@google-cloud/promisify': 4.0.0 + abort-controller: 3.0.0 + async-retry: 1.3.3 + duplexify: 4.1.3 + fast-xml-parser: 5.5.9 + gaxios: 6.7.1(encoding@0.1.13) + google-auth-library: 9.15.1(encoding@0.1.13) + html-entities: 2.6.0 + mime: 3.0.0 + p-limit: 3.1.0 + retry-request: 7.0.2(encoding@0.1.13) + teeny-request: 9.0.0(encoding@0.1.13) + uuid: 8.3.2 + transitivePeerDependencies: + - encoding + - supports-color + + '@google/adk@0.5.0(ee6095569807c0f2faf9175cb5eca775)': + dependencies: + '@a2a-js/sdk': 0.3.13(@grpc/grpc-js@1.14.3)(express@5.2.1) + '@google-cloud/opentelemetry-cloud-monitoring-exporter': 0.21.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.6.1(@opentelemetry/api@1.9.1))(@opentelemetry/resources@2.6.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.6.1(@opentelemetry/api@1.9.1))(encoding@0.1.13) + '@google-cloud/opentelemetry-cloud-trace-exporter': 3.0.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.6.1(@opentelemetry/api@1.9.1))(@opentelemetry/resources@2.6.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.1))(encoding@0.1.13) + '@google-cloud/storage': 7.19.0(encoding@0.1.13) + '@google/genai': 1.47.0(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6)) + '@mikro-orm/core': 6.6.11 + '@mikro-orm/mariadb': 6.6.11(@mikro-orm/core@6.6.11)(pg@8.20.0) + '@mikro-orm/mssql': 6.6.11(@azure/core-client@1.10.1)(@mikro-orm/core@6.6.11)(mariadb@3.4.5)(pg@8.20.0) + '@mikro-orm/mysql': 6.6.11(@mikro-orm/core@6.6.11)(@types/node@24.12.0)(mariadb@3.4.5)(pg@8.20.0) + '@mikro-orm/postgresql': 6.6.11(@mikro-orm/core@6.6.11)(mariadb@3.4.5) + '@mikro-orm/reflection': 6.6.11(@mikro-orm/core@6.6.11) + '@mikro-orm/sqlite': 6.6.11(@mikro-orm/core@6.6.11)(mariadb@3.4.5)(pg@8.20.0) + '@modelcontextprotocol/sdk': 1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6) + '@opentelemetry/api': 1.9.1 + '@opentelemetry/api-logs': 0.213.0 + '@opentelemetry/exporter-logs-otlp-http': 0.205.0(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-metrics-otlp-http': 0.213.0(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-trace-otlp-http': 0.213.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resource-detector-gcp': 0.40.3(@opentelemetry/api@1.9.1)(encoding@0.1.13) + '@opentelemetry/resources': 2.6.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-logs': 0.213.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-metrics': 2.6.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 2.6.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-node': 2.6.1(@opentelemetry/api@1.9.1) + google-auth-library: 10.6.2 + lodash-es: 4.17.23 + winston: 3.19.0 + zod: 4.3.6 + zod-to-json-schema: 3.25.2(zod@4.3.6) + transitivePeerDependencies: + - '@bufbuild/protobuf' + - '@cfworker/json-schema' + - '@grpc/grpc-js' + - bufferutil + - express + - supports-color + - utf-8-validate + + '@google/genai@1.47.0(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6))': + dependencies: + google-auth-library: 10.6.2 + p-retry: 4.6.2 + protobufjs: 7.5.4 + ws: 8.20.0 + optionalDependencies: + '@modelcontextprotocol/sdk': 1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6) + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + '@grpc/grpc-js@1.14.3': + dependencies: + '@grpc/proto-loader': 0.8.0 + '@js-sdsl/ordered-map': 4.4.2 + + '@grpc/proto-loader@0.8.0': + dependencies: + lodash.camelcase: 4.3.0 + long: 5.3.2 + protobufjs: 7.5.4 + yargs: 17.7.2 + + '@hono/node-server@1.19.12(hono@4.12.9)': + dependencies: + hono: 4.12.9 + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@js-joda/core@5.7.0': {} + + '@js-sdsl/ordered-map@4.4.2': {} + + '@langchain/core@1.1.38(@opentelemetry/api@1.9.1)(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.1))(openai@6.33.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0)': + dependencies: + '@cfworker/json-schema': 4.1.1 + '@standard-schema/spec': 1.1.0 + ansi-styles: 5.2.0 + camelcase: 6.3.0 + decamelize: 1.2.0 + js-tiktoken: 1.0.21 + langsmith: 0.5.15(@opentelemetry/api@1.9.1)(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.1))(openai@6.33.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0) + mustache: 4.2.0 + p-queue: 6.6.2 + uuid: 11.1.0 + zod: 4.3.6 + transitivePeerDependencies: + - '@opentelemetry/api' + - '@opentelemetry/exporter-trace-otlp-proto' + - '@opentelemetry/sdk-trace-base' + - openai + - ws + + '@langchain/langgraph-checkpoint@1.0.1(@langchain/core@1.1.38(@opentelemetry/api@1.9.1)(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.1))(openai@6.33.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0))': + dependencies: + '@langchain/core': 1.1.38(@opentelemetry/api@1.9.1)(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.1))(openai@6.33.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0) + uuid: 10.0.0 + + '@langchain/langgraph-sdk@1.8.3(@langchain/core@1.1.38(@opentelemetry/api@1.9.1)(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.1))(openai@6.33.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0))': + dependencies: + '@types/json-schema': 7.0.15 + p-queue: 9.1.0 + p-retry: 7.1.1 + uuid: 13.0.0 + optionalDependencies: + '@langchain/core': 1.1.38(@opentelemetry/api@1.9.1)(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.1))(openai@6.33.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0) + + '@langchain/langgraph@1.2.6(@langchain/core@1.1.38(@opentelemetry/api@1.9.1)(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.1))(openai@6.33.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0))(zod-to-json-schema@3.25.2(zod@4.3.6))(zod@4.3.6)': + dependencies: + '@langchain/core': 1.1.38(@opentelemetry/api@1.9.1)(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.1))(openai@6.33.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0) + '@langchain/langgraph-checkpoint': 1.0.1(@langchain/core@1.1.38(@opentelemetry/api@1.9.1)(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.1))(openai@6.33.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0)) + '@langchain/langgraph-sdk': 1.8.3(@langchain/core@1.1.38(@opentelemetry/api@1.9.1)(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.1))(openai@6.33.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0)) + '@standard-schema/spec': 1.1.0 + uuid: 10.0.0 + zod: 4.3.6 + optionalDependencies: + zod-to-json-schema: 3.25.2(zod@4.3.6) + transitivePeerDependencies: + - react + - react-dom + - svelte + - vue + + '@llamaindex/core@0.6.22': + dependencies: + '@finom/zod-to-json-schema': 3.24.11(zod@4.3.6) + '@llamaindex/env': 0.1.30 + '@types/node': 24.12.0 + magic-bytes.js: 1.13.0 + zod: 4.3.6 + transitivePeerDependencies: + - '@huggingface/transformers' + - gpt-tokenizer + + '@llamaindex/env@0.1.30': + dependencies: + '@aws-crypto/sha256-js': 5.2.0 + js-tiktoken: 1.0.21 + pathe: 1.1.2 + + '@llamaindex/node-parser@2.0.22(@llamaindex/core@0.6.22)(@llamaindex/env@0.1.30)(tree-sitter@0.22.4)(web-tree-sitter@0.24.7)': + dependencies: + '@llamaindex/core': 0.6.22 + '@llamaindex/env': 0.1.30 + html-to-text: 9.0.5 + tree-sitter: 0.22.4 + web-tree-sitter: 0.24.7 + + '@llamaindex/workflow-core@1.3.4(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6))(hono@4.12.9)(zod@4.3.6)': + optionalDependencies: + '@modelcontextprotocol/sdk': 1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6) + hono: 4.12.9 + zod: 4.3.6 + + '@llamaindex/workflow@1.1.24(@llamaindex/core@0.6.22)(@llamaindex/env@0.1.30)(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6))(hono@4.12.9)(zod@4.3.6)': + dependencies: + '@llamaindex/core': 0.6.22 + '@llamaindex/env': 0.1.30 + '@llamaindex/workflow-core': 1.3.4(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6))(hono@4.12.9)(zod@4.3.6) + transitivePeerDependencies: + - '@modelcontextprotocol/sdk' + - hono + - next + - p-retry + - rxjs + - zod + + '@mikro-orm/core@6.6.11': + dependencies: + dataloader: 2.2.3 + dotenv: 17.3.1 + esprima: 4.0.1 + fs-extra: 11.3.3 + globby: 11.1.0 + mikro-orm: 6.6.11 + reflect-metadata: 0.2.2 + + '@mikro-orm/knex@6.6.11(@mikro-orm/core@6.6.11)(mariadb@3.4.5)(mysql2@3.20.0(@types/node@24.12.0))(pg@8.20.0)': + dependencies: + '@mikro-orm/core': 6.6.11 + fs-extra: 11.3.3 + knex: 3.2.8(mysql2@3.20.0(@types/node@24.12.0))(pg@8.20.0) + sqlstring: 2.3.3 + optionalDependencies: + mariadb: 3.4.5 + transitivePeerDependencies: + - mysql + - mysql2 + - pg + - pg-native + - pg-query-stream + - sqlite3 + - supports-color + - tedious + + '@mikro-orm/knex@6.6.11(@mikro-orm/core@6.6.11)(mariadb@3.4.5)(pg@8.20.0)': + dependencies: + '@mikro-orm/core': 6.6.11 + fs-extra: 11.3.3 + knex: 3.2.8(pg@8.20.0) + sqlstring: 2.3.3 + optionalDependencies: + mariadb: 3.4.5 + transitivePeerDependencies: + - mysql + - mysql2 + - pg + - pg-native + - pg-query-stream + - sqlite3 + - supports-color + - tedious + + '@mikro-orm/knex@6.6.11(@mikro-orm/core@6.6.11)(mariadb@3.4.5)(pg@8.20.0)(sqlite3@5.1.7)': + dependencies: + '@mikro-orm/core': 6.6.11 + fs-extra: 11.3.3 + knex: 3.2.8(pg@8.20.0)(sqlite3@5.1.7) + sqlstring: 2.3.3 + optionalDependencies: + mariadb: 3.4.5 + transitivePeerDependencies: + - mysql + - mysql2 + - pg + - pg-native + - pg-query-stream + - sqlite3 + - supports-color + - tedious + + '@mikro-orm/knex@6.6.11(@mikro-orm/core@6.6.11)(mariadb@3.4.5)(pg@8.20.0)(tedious@19.2.1(@azure/core-client@1.10.1))': + dependencies: + '@mikro-orm/core': 6.6.11 + fs-extra: 11.3.3 + knex: 3.2.8(pg@8.20.0)(tedious@19.2.1(@azure/core-client@1.10.1)) + sqlstring: 2.3.3 + optionalDependencies: + mariadb: 3.4.5 + transitivePeerDependencies: + - mysql + - mysql2 + - pg + - pg-native + - pg-query-stream + - sqlite3 + - supports-color + - tedious + + '@mikro-orm/mariadb@6.6.11(@mikro-orm/core@6.6.11)(pg@8.20.0)': + dependencies: + '@mikro-orm/core': 6.6.11 + '@mikro-orm/knex': 6.6.11(@mikro-orm/core@6.6.11)(mariadb@3.4.5)(pg@8.20.0) + mariadb: 3.4.5 + transitivePeerDependencies: + - better-sqlite3 + - libsql + - mysql + - mysql2 + - pg + - pg-native + - pg-query-stream + - sqlite3 + - supports-color + - tedious + + '@mikro-orm/mssql@6.6.11(@azure/core-client@1.10.1)(@mikro-orm/core@6.6.11)(mariadb@3.4.5)(pg@8.20.0)': + dependencies: + '@mikro-orm/core': 6.6.11 + '@mikro-orm/knex': 6.6.11(@mikro-orm/core@6.6.11)(mariadb@3.4.5)(pg@8.20.0)(tedious@19.2.1(@azure/core-client@1.10.1)) + tedious: 19.2.1(@azure/core-client@1.10.1) + tsqlstring: 1.0.1 + transitivePeerDependencies: + - '@azure/core-client' + - better-sqlite3 + - libsql + - mariadb + - mysql + - mysql2 + - pg + - pg-native + - pg-query-stream + - sqlite3 + - supports-color + + '@mikro-orm/mysql@6.6.11(@mikro-orm/core@6.6.11)(@types/node@24.12.0)(mariadb@3.4.5)(pg@8.20.0)': + dependencies: + '@mikro-orm/core': 6.6.11 + '@mikro-orm/knex': 6.6.11(@mikro-orm/core@6.6.11)(mariadb@3.4.5)(mysql2@3.20.0(@types/node@24.12.0))(pg@8.20.0) + mysql2: 3.20.0(@types/node@24.12.0) + transitivePeerDependencies: + - '@types/node' + - better-sqlite3 + - libsql + - mariadb + - mysql + - pg + - pg-native + - pg-query-stream + - sqlite3 + - supports-color + - tedious + + '@mikro-orm/postgresql@6.6.11(@mikro-orm/core@6.6.11)(mariadb@3.4.5)': + dependencies: + '@mikro-orm/core': 6.6.11 + '@mikro-orm/knex': 6.6.11(@mikro-orm/core@6.6.11)(mariadb@3.4.5)(pg@8.20.0) + pg: 8.20.0 + postgres-array: 3.0.4 + postgres-date: 2.1.0 + postgres-interval: 4.0.2 + transitivePeerDependencies: + - better-sqlite3 + - libsql + - mariadb + - mysql + - mysql2 + - pg-native + - pg-query-stream + - sqlite3 + - supports-color + - tedious + + '@mikro-orm/reflection@6.6.11(@mikro-orm/core@6.6.11)': + dependencies: + '@mikro-orm/core': 6.6.11 + globby: 11.1.0 + ts-morph: 27.0.2 + + '@mikro-orm/sqlite@6.6.11(@mikro-orm/core@6.6.11)(mariadb@3.4.5)(pg@8.20.0)': + dependencies: + '@mikro-orm/core': 6.6.11 + '@mikro-orm/knex': 6.6.11(@mikro-orm/core@6.6.11)(mariadb@3.4.5)(pg@8.20.0)(sqlite3@5.1.7) + fs-extra: 11.3.3 + sqlite3: 5.1.7 + sqlstring-sqlite: 0.1.1 + transitivePeerDependencies: + - better-sqlite3 + - bluebird + - libsql + - mariadb + - mysql + - mysql2 + - pg + - pg-native + - pg-query-stream + - supports-color + - tedious + + '@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6)': + dependencies: + '@hono/node-server': 1.19.12(hono@4.12.9) + ajv: 8.18.0 + ajv-formats: 3.0.1(ajv@8.18.0) + content-type: 1.0.5 + cors: 2.8.6 + cross-spawn: 7.0.6 + eventsource: 3.0.7 + eventsource-parser: 3.0.6 + express: 5.2.1 + express-rate-limit: 8.3.2(express@5.2.1) + hono: 4.12.9 + jose: 6.2.2 + json-schema-typed: 8.0.2 + pkce-challenge: 5.0.1 + raw-body: 3.0.2 + zod: 4.3.6 + zod-to-json-schema: 3.25.2(zod@4.3.6) + optionalDependencies: + '@cfworker/json-schema': 4.1.1 + transitivePeerDependencies: + - supports-color + + '@napi-rs/wasm-runtime@1.1.2(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)': + dependencies: + '@emnapi/core': 1.9.1 + '@emnapi/runtime': 1.9.1 + '@tybys/wasm-util': 0.10.1 + optional: true + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.1 + + '@npmcli/fs@1.1.1': + dependencies: + '@gar/promisify': 1.1.3 + semver: 7.7.4 + optional: true + + '@npmcli/move-file@1.1.2': + dependencies: + mkdirp: 1.0.4 + rimraf: 3.0.2 + optional: true + + '@openai/agents-core@0.8.2(@cfworker/json-schema@4.1.1)(ws@8.20.0)(zod@4.3.6)': + dependencies: + debug: 4.4.3 + openai: 6.33.0(ws@8.20.0)(zod@4.3.6) + optionalDependencies: + '@modelcontextprotocol/sdk': 1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6) + zod: 4.3.6 + transitivePeerDependencies: + - '@cfworker/json-schema' + - supports-color + - ws + + '@openai/agents-openai@0.8.2(@cfworker/json-schema@4.1.1)(ws@8.20.0)(zod@4.3.6)': + dependencies: + '@openai/agents-core': 0.8.2(@cfworker/json-schema@4.1.1)(ws@8.20.0)(zod@4.3.6) + debug: 4.4.3 + openai: 6.33.0(ws@8.20.0)(zod@4.3.6) + zod: 4.3.6 + transitivePeerDependencies: + - '@cfworker/json-schema' + - supports-color + - ws + + '@openai/agents-realtime@0.8.2(@cfworker/json-schema@4.1.1)(zod@4.3.6)': + dependencies: + '@openai/agents-core': 0.8.2(@cfworker/json-schema@4.1.1)(ws@8.20.0)(zod@4.3.6) + '@types/ws': 8.18.1 + debug: 4.4.3 + ws: 8.20.0 + zod: 4.3.6 + transitivePeerDependencies: + - '@cfworker/json-schema' + - bufferutil + - supports-color + - utf-8-validate + + '@openai/agents@0.8.2(@cfworker/json-schema@4.1.1)(ws@8.20.0)(zod@4.3.6)': + dependencies: + '@openai/agents-core': 0.8.2(@cfworker/json-schema@4.1.1)(ws@8.20.0)(zod@4.3.6) + '@openai/agents-openai': 0.8.2(@cfworker/json-schema@4.1.1)(ws@8.20.0)(zod@4.3.6) + '@openai/agents-realtime': 0.8.2(@cfworker/json-schema@4.1.1)(zod@4.3.6) + debug: 4.4.3 + openai: 6.33.0(ws@8.20.0)(zod@4.3.6) + zod: 4.3.6 + transitivePeerDependencies: + - '@cfworker/json-schema' + - bufferutil + - supports-color + - utf-8-validate + - ws + + '@opencode-ai/plugin@1.3.13': + dependencies: + '@opencode-ai/sdk': 1.3.13 + zod: 4.1.8 + + '@opencode-ai/sdk@1.3.13': {} + + '@opentelemetry/api-logs@0.205.0': + dependencies: + '@opentelemetry/api': 1.9.1 + + '@opentelemetry/api-logs@0.213.0': + dependencies: + '@opentelemetry/api': 1.9.1 + + '@opentelemetry/api@1.9.1': {} + + '@opentelemetry/context-async-hooks@2.6.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + + '@opentelemetry/core@2.1.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/semantic-conventions': 1.40.0 + + '@opentelemetry/core@2.6.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/semantic-conventions': 1.40.0 + + '@opentelemetry/core@2.6.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/semantic-conventions': 1.40.0 + + '@opentelemetry/exporter-logs-otlp-http@0.205.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/api-logs': 0.205.0 + '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-exporter-base': 0.205.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-transformer': 0.205.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-logs': 0.205.0(@opentelemetry/api@1.9.1) + + '@opentelemetry/exporter-metrics-otlp-grpc@0.213.0(@opentelemetry/api@1.9.1)': + dependencies: + '@grpc/grpc-js': 1.14.3 + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-metrics-otlp-http': 0.213.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-exporter-base': 0.213.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-grpc-exporter-base': 0.213.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-transformer': 0.213.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.6.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-metrics': 2.6.0(@opentelemetry/api@1.9.1) + + '@opentelemetry/exporter-metrics-otlp-http@0.213.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-exporter-base': 0.213.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-transformer': 0.213.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.6.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-metrics': 2.6.0(@opentelemetry/api@1.9.1) + + '@opentelemetry/exporter-trace-otlp-grpc@0.213.0(@opentelemetry/api@1.9.1)': + dependencies: + '@grpc/grpc-js': 1.14.3 + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-exporter-base': 0.213.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-grpc-exporter-base': 0.213.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-transformer': 0.213.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.6.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 2.6.0(@opentelemetry/api@1.9.1) + + '@opentelemetry/exporter-trace-otlp-http@0.213.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-exporter-base': 0.213.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-transformer': 0.213.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.6.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 2.6.0(@opentelemetry/api@1.9.1) + + '@opentelemetry/otlp-exporter-base@0.205.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-transformer': 0.205.0(@opentelemetry/api@1.9.1) + + '@opentelemetry/otlp-exporter-base@0.213.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-transformer': 0.213.0(@opentelemetry/api@1.9.1) + + '@opentelemetry/otlp-grpc-exporter-base@0.213.0(@opentelemetry/api@1.9.1)': + dependencies: + '@grpc/grpc-js': 1.14.3 + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-exporter-base': 0.213.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-transformer': 0.213.0(@opentelemetry/api@1.9.1) + + '@opentelemetry/otlp-transformer@0.205.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/api-logs': 0.205.0 + '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.1.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-logs': 0.205.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-metrics': 2.1.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 2.1.0(@opentelemetry/api@1.9.1) + protobufjs: 7.5.4 + + '@opentelemetry/otlp-transformer@0.213.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/api-logs': 0.213.0 + '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.6.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-logs': 0.213.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-metrics': 2.6.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 2.6.0(@opentelemetry/api@1.9.1) + protobufjs: 7.5.4 + + '@opentelemetry/resource-detector-gcp@0.40.3(@opentelemetry/api@1.9.1)(encoding@0.1.13)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.6.1(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.6.1(@opentelemetry/api@1.9.1) + gcp-metadata: 6.1.1(encoding@0.1.13) + transitivePeerDependencies: + - encoding + - supports-color + + '@opentelemetry/resources@2.1.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + + '@opentelemetry/resources@2.6.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + + '@opentelemetry/resources@2.6.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.6.1(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + + '@opentelemetry/sdk-logs@0.205.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/api-logs': 0.205.0 + '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.1.0(@opentelemetry/api@1.9.1) + + '@opentelemetry/sdk-logs@0.213.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/api-logs': 0.213.0 + '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.6.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + + '@opentelemetry/sdk-metrics@2.1.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.1.0(@opentelemetry/api@1.9.1) + + '@opentelemetry/sdk-metrics@2.6.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.6.0(@opentelemetry/api@1.9.1) + + '@opentelemetry/sdk-metrics@2.6.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.6.1(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.6.1(@opentelemetry/api@1.9.1) + + '@opentelemetry/sdk-trace-base@2.1.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.1.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + + '@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.6.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + + '@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.6.1(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.6.1(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + + '@opentelemetry/sdk-trace-node@2.6.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/context-async-hooks': 2.6.1(@opentelemetry/api@1.9.1) + '@opentelemetry/core': 2.6.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 2.6.1(@opentelemetry/api@1.9.1) + + '@opentelemetry/semantic-conventions@1.40.0': {} + + '@oxc-project/types@0.122.0': {} + + '@protobufjs/aspromise@1.1.2': {} + + '@protobufjs/base64@1.1.2': {} + + '@protobufjs/codegen@2.0.4': {} + + '@protobufjs/eventemitter@1.1.0': {} + + '@protobufjs/fetch@1.1.0': + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/inquire': 1.1.0 + + '@protobufjs/float@1.0.2': {} + + '@protobufjs/inquire@1.1.0': {} + + '@protobufjs/path@1.1.2': {} + + '@protobufjs/pool@1.1.0': {} + + '@protobufjs/utf8@1.1.0': {} + + '@rolldown/binding-android-arm64@1.0.0-rc.12': + optional: true + + '@rolldown/binding-darwin-arm64@1.0.0-rc.12': + optional: true + + '@rolldown/binding-darwin-x64@1.0.0-rc.12': + optional: true + + '@rolldown/binding-freebsd-x64@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.12': + optional: true + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.12': + optional: true + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.12(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)': + dependencies: + '@napi-rs/wasm-runtime': 1.1.2(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1) + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.12': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.12': + optional: true + + '@rolldown/pluginutils@1.0.0-rc.12': {} + + '@selderee/plugin-htmlparser2@0.11.0': + dependencies: + domhandler: 5.0.3 + selderee: 0.11.0 + + '@smithy/is-array-buffer@2.2.0': + dependencies: + tslib: 2.8.1 + + '@smithy/types@4.13.1': + dependencies: + tslib: 2.8.1 + + '@smithy/util-buffer-from@2.2.0': + dependencies: + '@smithy/is-array-buffer': 2.2.0 + tslib: 2.8.1 + + '@smithy/util-utf8@2.3.0': + dependencies: + '@smithy/util-buffer-from': 2.2.0 + tslib: 2.8.1 + + '@so-ric/colorspace@1.1.6': + dependencies: + color: 5.0.3 + text-hex: 1.0.0 + + '@standard-schema/spec@1.1.0': {} + + '@tootallnate/once@1.1.2': + optional: true + + '@tootallnate/once@2.0.0': {} + + '@ts-morph/common@0.28.1': + dependencies: + minimatch: 10.2.5 + path-browserify: 1.0.1 + tinyglobby: 0.2.15 + + '@tybys/wasm-util@0.10.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/caseless@0.12.5': {} + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + + '@types/estree@1.0.8': {} + + '@types/geojson@7946.0.16': {} + + '@types/json-schema@7.0.15': {} + + '@types/lodash@4.17.24': {} + + '@types/node@24.12.0': + dependencies: + undici-types: 7.16.0 + + '@types/readable-stream@4.0.23': + dependencies: + '@types/node': 24.12.0 + + '@types/request@2.48.13': + dependencies: + '@types/caseless': 0.12.5 + '@types/node': 24.12.0 + '@types/tough-cookie': 4.0.5 + form-data: 2.5.5 + + '@types/retry@0.12.0': {} + + '@types/tough-cookie@4.0.5': {} + + '@types/triple-beam@1.3.5': {} + + '@types/uuid@10.0.0': {} + + '@types/ws@8.18.1': + dependencies: + '@types/node': 24.12.0 + + '@typespec/ts-http-runtime@0.3.4': + dependencies: + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@vitest/expect@4.1.2': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.2 + '@vitest/utils': 4.1.2 + chai: 6.2.2 + tinyrainbow: 3.1.0 + + '@vitest/mocker@4.1.2(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@24.12.0)(esbuild@0.27.4))': + dependencies: + '@vitest/spy': 4.1.2 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@24.12.0)(esbuild@0.27.4) + + '@vitest/pretty-format@4.1.2': + dependencies: + tinyrainbow: 3.1.0 + + '@vitest/runner@4.1.2': + dependencies: + '@vitest/utils': 4.1.2 + pathe: 2.0.3 + + '@vitest/snapshot@4.1.2': + dependencies: + '@vitest/pretty-format': 4.1.2 + '@vitest/utils': 4.1.2 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.1.2': {} + + '@vitest/utils@4.1.2': + dependencies: + '@vitest/pretty-format': 4.1.2 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.0 + + abbrev@1.1.1: + optional: true + + abort-controller@3.0.0: + dependencies: + event-target-shim: 5.0.1 + + accepts@2.0.0: + dependencies: + mime-types: 3.0.2 + negotiator: 1.0.0 + + agent-base@6.0.2: + dependencies: + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + agent-base@7.1.4: {} + + agentkeepalive@4.6.0: + dependencies: + humanize-ms: 1.2.1 + optional: true + + aggregate-error@3.1.0: + dependencies: + clean-stack: 2.2.0 + indent-string: 4.0.0 + optional: true + + ajv-formats@3.0.1(ajv@8.18.0): + optionalDependencies: + ajv: 8.18.0 + + ajv@8.18.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.0 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ansi-regex@5.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@5.2.0: {} + + aproba@2.1.0: + optional: true + + are-we-there-yet@3.0.1: + dependencies: + delegates: 1.0.0 + readable-stream: 3.6.2 + optional: true + + array-union@2.1.0: {} + + arrify@2.0.1: {} + + assertion-error@2.0.1: {} + + async-retry@1.3.3: + dependencies: + retry: 0.13.1 + + async@3.2.6: {} + + asynckit@0.4.0: {} + + aws-ssl-profiles@1.1.2: {} + + balanced-match@1.0.2: + optional: true + + balanced-match@4.0.4: {} + + base64-js@1.5.1: {} + + bignumber.js@9.3.1: {} + + bindings@1.5.0: + dependencies: + file-uri-to-path: 1.0.0 + + bl@4.1.0: + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + + bl@6.1.6: + dependencies: + '@types/readable-stream': 4.0.23 + buffer: 6.0.3 + inherits: 2.0.4 + readable-stream: 4.7.0 + + body-parser@2.2.2: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 4.4.3 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + on-finished: 2.4.1 + qs: 6.15.0 + raw-body: 3.0.2 + type-is: 2.0.1 + transitivePeerDependencies: + - supports-color + + brace-expansion@1.1.13: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + optional: true + + brace-expansion@5.0.5: + dependencies: + balanced-match: 4.0.4 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + buffer-equal-constant-time@1.0.1: {} + + buffer@5.7.1: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + buffer@6.0.3: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + bundle-name@4.1.0: + dependencies: + run-applescript: 7.1.0 + + bytes@3.1.2: {} + + cacache@15.3.0: + dependencies: + '@npmcli/fs': 1.1.1 + '@npmcli/move-file': 1.1.2 + chownr: 2.0.0 + fs-minipass: 2.1.0 + glob: 7.2.3 + infer-owner: 1.0.4 + lru-cache: 6.0.0 + minipass: 3.3.6 + minipass-collect: 1.0.2 + minipass-flush: 1.0.7 + minipass-pipeline: 1.2.4 + mkdirp: 1.0.4 + p-map: 4.0.0 + promise-inflight: 1.0.1 + rimraf: 3.0.2 + ssri: 8.0.1 + tar: 6.2.1 + unique-filename: 1.1.1 + transitivePeerDependencies: + - bluebird + optional: true + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + camelcase@6.3.0: {} + + chai@6.2.2: {} + + chalk@5.6.2: {} + + chownr@1.1.4: {} + + chownr@2.0.0: {} + + clean-stack@2.2.0: + optional: true + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + code-block-writer@13.0.3: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-convert@3.1.3: + dependencies: + color-name: 2.1.0 + + color-name@1.1.4: {} + + color-name@2.1.0: {} + + color-string@2.1.4: + dependencies: + color-name: 2.1.0 + + color-support@1.1.3: + optional: true + + color@5.0.3: + dependencies: + color-convert: 3.1.3 + color-string: 2.1.4 + + colorette@2.0.19: {} + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + commander@10.0.1: {} + + concat-map@0.0.1: + optional: true + + console-control-strings@1.1.0: + optional: true + + console-table-printer@2.15.0: + dependencies: + simple-wcswidth: 1.1.2 + + content-disposition@1.0.1: {} + + content-type@1.0.5: {} + + convert-source-map@2.0.0: {} + + cookie-signature@1.2.2: {} + + cookie@0.7.2: {} + + cors@2.8.6: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + data-uri-to-buffer@4.0.1: {} + + dataloader@2.2.3: {} + + debug@4.3.4: + dependencies: + ms: 2.1.2 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decamelize@1.2.0: {} + + decompress-response@6.0.0: + dependencies: + mimic-response: 3.1.0 + + deep-extend@0.6.0: {} + + deepmerge@4.3.1: {} + + default-browser-id@5.0.1: {} + + default-browser@5.5.0: + dependencies: + bundle-name: 4.1.0 + default-browser-id: 5.0.1 + + define-lazy-prop@3.0.0: {} + + delayed-stream@1.0.0: {} + + delegates@1.0.0: + optional: true + + denque@2.1.0: {} + + depd@2.0.0: {} + + detect-libc@2.1.2: {} + + dir-glob@3.0.1: + dependencies: + path-type: 4.0.0 + + dom-serializer@2.0.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.5.0 + + domelementtype@2.3.0: {} + + domhandler@5.0.3: + dependencies: + domelementtype: 2.3.0 + + domutils@3.2.2: + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + + dotenv@17.3.1: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + duplexify@4.1.3: + dependencies: + end-of-stream: 1.4.5 + inherits: 2.0.4 + readable-stream: 3.6.2 + stream-shift: 1.0.3 + + ecdsa-sig-formatter@1.0.11: + dependencies: + safe-buffer: 5.2.1 + + ee-first@1.1.1: {} + + emoji-regex@8.0.0: {} + + enabled@2.0.0: {} + + encodeurl@2.0.0: {} + + encoding@0.1.13: + dependencies: + iconv-lite: 0.6.3 + optional: true + + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + + entities@4.5.0: {} + + env-paths@2.2.1: + optional: true + + err-code@2.0.3: + optional: true + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-module-lexer@2.0.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + esbuild@0.27.4: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.4 + '@esbuild/android-arm': 0.27.4 + '@esbuild/android-arm64': 0.27.4 + '@esbuild/android-x64': 0.27.4 + '@esbuild/darwin-arm64': 0.27.4 + '@esbuild/darwin-x64': 0.27.4 + '@esbuild/freebsd-arm64': 0.27.4 + '@esbuild/freebsd-x64': 0.27.4 + '@esbuild/linux-arm': 0.27.4 + '@esbuild/linux-arm64': 0.27.4 + '@esbuild/linux-ia32': 0.27.4 + '@esbuild/linux-loong64': 0.27.4 + '@esbuild/linux-mips64el': 0.27.4 + '@esbuild/linux-ppc64': 0.27.4 + '@esbuild/linux-riscv64': 0.27.4 + '@esbuild/linux-s390x': 0.27.4 + '@esbuild/linux-x64': 0.27.4 + '@esbuild/netbsd-arm64': 0.27.4 + '@esbuild/netbsd-x64': 0.27.4 + '@esbuild/openbsd-arm64': 0.27.4 + '@esbuild/openbsd-x64': 0.27.4 + '@esbuild/openharmony-arm64': 0.27.4 + '@esbuild/sunos-x64': 0.27.4 + '@esbuild/win32-arm64': 0.27.4 + '@esbuild/win32-ia32': 0.27.4 + '@esbuild/win32-x64': 0.27.4 + + escalade@3.2.0: {} + + escape-html@1.0.3: {} + + esm@3.2.25: {} + + esprima@4.0.1: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + + etag@1.8.1: {} + + event-target-shim@5.0.1: {} + + eventemitter3@4.0.7: {} + + eventemitter3@5.0.4: {} + + events@3.3.0: {} + + eventsource-parser@3.0.6: {} + + eventsource@3.0.7: + dependencies: + eventsource-parser: 3.0.6 + + expand-template@2.0.3: {} + + expect-type@1.3.0: {} + + express-rate-limit@8.3.2(express@5.2.1): + dependencies: + express: 5.2.1 + ip-address: 10.1.0 + + express@5.2.1: + dependencies: + accepts: 2.0.0 + body-parser: 2.2.2 + content-disposition: 1.0.1 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.3 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.1 + fresh: 2.0.0 + http-errors: 2.0.1 + merge-descriptors: 2.0.0 + mime-types: 3.0.2 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.15.0 + range-parser: 1.2.1 + router: 2.2.0 + send: 1.2.1 + serve-static: 2.2.1 + statuses: 2.0.2 + type-is: 2.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + extend@3.0.2: {} + + fast-deep-equal@3.1.3: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-uri@3.1.0: {} + + fast-xml-builder@1.1.4: + dependencies: + path-expression-matcher: 1.2.0 + + fast-xml-parser@5.5.9: + dependencies: + fast-xml-builder: 1.1.4 + path-expression-matcher: 1.2.0 + strnum: 2.2.2 + + fastq@1.20.1: + dependencies: + reusify: 1.1.0 + + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + + fecha@4.2.3: {} + + fetch-blob@3.2.0: + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 3.3.3 + + file-uri-to-path@1.0.0: {} + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + finalhandler@2.1.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + fn.name@1.1.0: {} + + form-data@2.5.5: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + safe-buffer: 5.2.1 + + formdata-polyfill@4.0.10: + dependencies: + fetch-blob: 3.2.0 + + forwarded@0.2.0: {} + + fresh@2.0.0: {} + + fs-constants@1.0.0: {} + + fs-extra@11.3.3: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.0 + universalify: 2.0.1 + + fs-minipass@2.1.0: + dependencies: + minipass: 3.3.6 + + fs.realpath@1.0.0: + optional: true + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + gauge@4.0.4: + dependencies: + aproba: 2.1.0 + color-support: 1.1.3 + console-control-strings: 1.1.0 + has-unicode: 2.0.1 + signal-exit: 3.0.7 + string-width: 4.2.3 + strip-ansi: 6.0.1 + wide-align: 1.1.5 + optional: true + + gaxios@6.7.1(encoding@0.1.13): + dependencies: + extend: 3.0.2 + https-proxy-agent: 7.0.6 + is-stream: 2.0.1 + node-fetch: 2.7.0(encoding@0.1.13) + uuid: 9.0.1 + transitivePeerDependencies: + - encoding + - supports-color + + gaxios@7.1.4: + dependencies: + extend: 3.0.2 + https-proxy-agent: 7.0.6 + node-fetch: 3.3.2 + transitivePeerDependencies: + - supports-color + + gcp-metadata@6.1.1(encoding@0.1.13): + dependencies: + gaxios: 6.7.1(encoding@0.1.13) + google-logging-utils: 0.0.2 + json-bigint: 1.0.0 + transitivePeerDependencies: + - encoding + - supports-color + + gcp-metadata@8.1.2: + dependencies: + gaxios: 7.1.4 + google-logging-utils: 1.1.3 + json-bigint: 1.0.0 + transitivePeerDependencies: + - supports-color + + generate-function@2.3.1: + dependencies: + is-property: 1.0.2 + + get-caller-file@2.0.5: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-package-type@0.1.0: {} + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + getopts@2.3.0: {} + + github-from-package@0.0.0: {} + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.5 + once: 1.4.0 + path-is-absolute: 1.0.1 + optional: true + + globby@11.1.0: + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.3 + ignore: 5.3.2 + merge2: 1.4.1 + slash: 3.0.0 + + google-auth-library@10.6.2: + dependencies: + base64-js: 1.5.1 + ecdsa-sig-formatter: 1.0.11 + gaxios: 7.1.4 + gcp-metadata: 8.1.2 + google-logging-utils: 1.1.3 + jws: 4.0.1 + transitivePeerDependencies: + - supports-color + + google-auth-library@9.15.1(encoding@0.1.13): + dependencies: + base64-js: 1.5.1 + ecdsa-sig-formatter: 1.0.11 + gaxios: 6.7.1(encoding@0.1.13) + gcp-metadata: 6.1.1(encoding@0.1.13) + gtoken: 7.1.0(encoding@0.1.13) + jws: 4.0.1 + transitivePeerDependencies: + - encoding + - supports-color + + google-logging-utils@0.0.2: {} + + google-logging-utils@1.1.3: {} + + googleapis-common@7.2.0(encoding@0.1.13): + dependencies: + extend: 3.0.2 + gaxios: 6.7.1(encoding@0.1.13) + google-auth-library: 9.15.1(encoding@0.1.13) + qs: 6.15.0 + url-template: 2.0.8 + uuid: 9.0.1 + transitivePeerDependencies: + - encoding + - supports-color + + googleapis@137.1.0(encoding@0.1.13): + dependencies: + google-auth-library: 9.15.1(encoding@0.1.13) + googleapis-common: 7.2.0(encoding@0.1.13) + transitivePeerDependencies: + - encoding + - supports-color + + gopd@1.2.0: {} + + graceful-fs@4.2.11: {} + + gtoken@7.1.0(encoding@0.1.13): + dependencies: + gaxios: 6.7.1(encoding@0.1.13) + jws: 4.0.1 + transitivePeerDependencies: + - encoding + - supports-color + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + has-unicode@2.0.1: + optional: true + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + hono@4.12.9: {} + + html-entities@2.6.0: {} + + html-to-text@9.0.5: + dependencies: + '@selderee/plugin-htmlparser2': 0.11.0 + deepmerge: 4.3.1 + dom-serializer: 2.0.0 + htmlparser2: 8.0.2 + selderee: 0.11.0 + + htmlparser2@8.0.2: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.2.2 + entities: 4.5.0 + + http-cache-semantics@4.2.0: + optional: true + + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + + http-proxy-agent@4.0.1: + dependencies: + '@tootallnate/once': 1.1.2 + agent-base: 6.0.2 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + optional: true + + http-proxy-agent@5.0.0: + dependencies: + '@tootallnate/once': 2.0.0 + agent-base: 6.0.2 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + https-proxy-agent@5.0.1: + dependencies: + agent-base: 6.0.2 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + humanize-ms@1.2.1: + dependencies: + ms: 2.1.3 + optional: true + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + + iconv-lite@0.7.2: + dependencies: + safer-buffer: 2.1.2 + + ieee754@1.2.1: {} + + ignore@5.3.2: {} + + imurmurhash@0.1.4: + optional: true + + indent-string@4.0.0: + optional: true + + infer-owner@1.0.4: + optional: true + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + optional: true + + inherits@2.0.4: {} + + ini@1.3.8: {} + + interpret@2.2.0: {} + + ip-address@10.1.0: {} + + ipaddr.js@1.9.1: {} + + is-core-module@2.16.1: + dependencies: + hasown: 2.0.2 + + is-docker@3.0.0: {} + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-inside-container@1.0.0: + dependencies: + is-docker: 3.0.0 + + is-lambda@1.0.1: + optional: true + + is-network-error@1.3.1: {} + + is-number@7.0.0: {} + + is-promise@4.0.0: {} + + is-property@1.0.2: {} + + is-stream@2.0.1: {} + + is-wsl@3.1.1: + dependencies: + is-inside-container: 1.0.0 + + isexe@2.0.0: {} + + jose@6.2.2: {} + + js-md4@0.3.2: {} + + js-tiktoken@1.0.21: + dependencies: + base64-js: 1.5.1 + + json-bigint@1.0.0: + dependencies: + bignumber.js: 9.3.1 + + json-schema-to-ts@3.1.1: + dependencies: + '@babel/runtime': 7.29.2 + ts-algebra: 2.0.0 + + json-schema-traverse@1.0.0: {} + + json-schema-typed@8.0.2: {} + + jsonfile@6.2.0: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + + jsonwebtoken@9.0.3: + dependencies: + jws: 4.0.1 + lodash.includes: 4.3.0 + lodash.isboolean: 3.0.3 + lodash.isinteger: 4.0.4 + lodash.isnumber: 3.0.3 + lodash.isplainobject: 4.0.6 + lodash.isstring: 4.0.1 + lodash.once: 4.1.1 + ms: 2.1.3 + semver: 7.7.4 + + jwa@2.0.1: + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + + jws@4.0.1: + dependencies: + jwa: 2.0.1 + safe-buffer: 5.2.1 + + knex@3.2.8(mysql2@3.20.0(@types/node@24.12.0))(pg@8.20.0): + dependencies: + colorette: 2.0.19 + commander: 10.0.1 + debug: 4.3.4 + escalade: 3.2.0 + esm: 3.2.25 + get-package-type: 0.1.0 + getopts: 2.3.0 + interpret: 2.2.0 + lodash: 4.17.21 + pg-connection-string: 2.6.2 + rechoir: 0.8.0 + resolve-from: 5.0.0 + tarn: 3.0.2 + tildify: 2.0.0 + optionalDependencies: + mysql2: 3.20.0(@types/node@24.12.0) + pg: 8.20.0 + transitivePeerDependencies: + - supports-color + + knex@3.2.8(pg@8.20.0): + dependencies: + colorette: 2.0.19 + commander: 10.0.1 + debug: 4.3.4 + escalade: 3.2.0 + esm: 3.2.25 + get-package-type: 0.1.0 + getopts: 2.3.0 + interpret: 2.2.0 + lodash: 4.17.21 + pg-connection-string: 2.6.2 + rechoir: 0.8.0 + resolve-from: 5.0.0 + tarn: 3.0.2 + tildify: 2.0.0 + optionalDependencies: + pg: 8.20.0 + transitivePeerDependencies: + - supports-color + + knex@3.2.8(pg@8.20.0)(sqlite3@5.1.7): + dependencies: + colorette: 2.0.19 + commander: 10.0.1 + debug: 4.3.4 + escalade: 3.2.0 + esm: 3.2.25 + get-package-type: 0.1.0 + getopts: 2.3.0 + interpret: 2.2.0 + lodash: 4.17.21 + pg-connection-string: 2.6.2 + rechoir: 0.8.0 + resolve-from: 5.0.0 + tarn: 3.0.2 + tildify: 2.0.0 + optionalDependencies: + pg: 8.20.0 + sqlite3: 5.1.7 + transitivePeerDependencies: + - supports-color + + knex@3.2.8(pg@8.20.0)(tedious@19.2.1(@azure/core-client@1.10.1)): + dependencies: + colorette: 2.0.19 + commander: 10.0.1 + debug: 4.3.4 + escalade: 3.2.0 + esm: 3.2.25 + get-package-type: 0.1.0 + getopts: 2.3.0 + interpret: 2.2.0 + lodash: 4.17.21 + pg-connection-string: 2.6.2 + rechoir: 0.8.0 + resolve-from: 5.0.0 + tarn: 3.0.2 + tildify: 2.0.0 + optionalDependencies: + pg: 8.20.0 + tedious: 19.2.1(@azure/core-client@1.10.1) + transitivePeerDependencies: + - supports-color + + kuler@2.0.0: {} + + langsmith@0.5.15(@opentelemetry/api@1.9.1)(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.1))(openai@6.33.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0): + dependencies: + '@types/uuid': 10.0.0 + chalk: 5.6.2 + console-table-printer: 2.15.0 + p-queue: 6.6.2 + semver: 7.7.4 + uuid: 10.0.0 + optionalDependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/sdk-trace-base': 2.6.1(@opentelemetry/api@1.9.1) + openai: 6.33.0(ws@8.20.0)(zod@4.3.6) + ws: 8.20.0 + + leac@0.6.0: {} + + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + + llamaindex@0.12.1(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6))(hono@4.12.9)(tree-sitter@0.22.4)(web-tree-sitter@0.24.7)(zod@4.3.6): + dependencies: + '@llamaindex/core': 0.6.22 + '@llamaindex/env': 0.1.30 + '@llamaindex/node-parser': 2.0.22(@llamaindex/core@0.6.22)(@llamaindex/env@0.1.30)(tree-sitter@0.22.4)(web-tree-sitter@0.24.7) + '@llamaindex/workflow': 1.1.24(@llamaindex/core@0.6.22)(@llamaindex/env@0.1.30)(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6))(hono@4.12.9)(zod@4.3.6) + '@types/lodash': 4.17.24 + '@types/node': 24.12.0 + lodash: 4.17.21 + magic-bytes.js: 1.13.0 + transitivePeerDependencies: + - '@huggingface/transformers' + - '@modelcontextprotocol/sdk' + - gpt-tokenizer + - hono + - next + - p-retry + - rxjs + - tree-sitter + - web-tree-sitter + - zod + + lodash-es@4.17.23: {} + + lodash.camelcase@4.3.0: {} + + lodash.includes@4.3.0: {} + + lodash.isboolean@3.0.3: {} + + lodash.isinteger@4.0.4: {} + + lodash.isnumber@3.0.3: {} + + lodash.isplainobject@4.0.6: {} + + lodash.isstring@4.0.1: {} + + lodash.once@4.1.1: {} + + lodash@4.17.21: {} + + logform@2.7.0: + dependencies: + '@colors/colors': 1.6.0 + '@types/triple-beam': 1.3.5 + fecha: 4.2.3 + ms: 2.1.3 + safe-stable-stringify: 2.5.0 + triple-beam: 1.4.1 + + long@5.3.2: {} + + lru-cache@10.4.3: {} + + lru-cache@6.0.0: + dependencies: + yallist: 4.0.0 + optional: true + + lru.min@1.1.4: {} + + magic-bytes.js@1.13.0: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + make-fetch-happen@9.1.0: + dependencies: + agentkeepalive: 4.6.0 + cacache: 15.3.0 + http-cache-semantics: 4.2.0 + http-proxy-agent: 4.0.1 + https-proxy-agent: 5.0.1 + is-lambda: 1.0.1 + lru-cache: 6.0.0 + minipass: 3.3.6 + minipass-collect: 1.0.2 + minipass-fetch: 1.4.1 + minipass-flush: 1.0.7 + minipass-pipeline: 1.2.4 + negotiator: 0.6.4 + promise-retry: 2.0.1 + socks-proxy-agent: 6.2.1 + ssri: 8.0.1 + transitivePeerDependencies: + - bluebird + - supports-color + optional: true + + mariadb@3.4.5: + dependencies: + '@types/geojson': 7946.0.16 + '@types/node': 24.12.0 + denque: 2.1.0 + iconv-lite: 0.6.3 + lru-cache: 10.4.3 + + math-intrinsics@1.1.0: {} + + media-typer@1.1.0: {} + + merge-descriptors@2.0.0: {} + + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.2 + + mikro-orm@6.6.11: {} + + mime-db@1.52.0: {} + + mime-db@1.54.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mime-types@3.0.2: + dependencies: + mime-db: 1.54.0 + + mime@3.0.0: {} + + mimic-response@3.1.0: {} + + minimatch@10.2.5: + dependencies: + brace-expansion: 5.0.5 + + minimatch@3.1.5: + dependencies: + brace-expansion: 1.1.13 + optional: true + + minimist@1.2.8: {} + + minipass-collect@1.0.2: + dependencies: + minipass: 3.3.6 + optional: true + + minipass-fetch@1.4.1: + dependencies: + minipass: 3.3.6 + minipass-sized: 1.0.3 + minizlib: 2.1.2 + optionalDependencies: + encoding: 0.1.13 + optional: true + + minipass-flush@1.0.7: + dependencies: + minipass: 3.3.6 + optional: true + + minipass-pipeline@1.2.4: + dependencies: + minipass: 3.3.6 + optional: true + + minipass-sized@1.0.3: + dependencies: + minipass: 3.3.6 + optional: true + + minipass@3.3.6: + dependencies: + yallist: 4.0.0 + + minipass@5.0.0: {} + + minizlib@2.1.2: + dependencies: + minipass: 3.3.6 + yallist: 4.0.0 + + mkdirp-classic@0.5.3: {} + + mkdirp@1.0.4: {} + + ms@2.1.2: {} + + ms@2.1.3: {} + + mustache@4.2.0: {} + + mysql2@3.20.0(@types/node@24.12.0): + dependencies: + '@types/node': 24.12.0 + aws-ssl-profiles: 1.1.2 + denque: 2.1.0 + generate-function: 2.3.1 + iconv-lite: 0.7.2 + long: 5.3.2 + lru.min: 1.1.4 + named-placeholders: 1.1.6 + sql-escaper: 1.3.3 + + named-placeholders@1.1.6: + dependencies: + lru.min: 1.1.4 + + nanoid@3.3.11: {} + + napi-build-utils@2.0.0: {} + + native-duplexpair@1.0.0: {} + + negotiator@0.6.4: + optional: true + + negotiator@1.0.0: {} + + node-abi@3.89.0: + dependencies: + semver: 7.7.4 + + node-addon-api@7.1.1: {} + + node-addon-api@8.7.0: {} + + node-domexception@1.0.0: {} + + node-fetch@2.7.0(encoding@0.1.13): + dependencies: + whatwg-url: 5.0.0 + optionalDependencies: + encoding: 0.1.13 + + node-fetch@3.3.2: + dependencies: + data-uri-to-buffer: 4.0.1 + fetch-blob: 3.2.0 + formdata-polyfill: 4.0.10 + + node-gyp-build@4.8.4: {} + + node-gyp@8.4.1: + dependencies: + env-paths: 2.2.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + make-fetch-happen: 9.1.0 + nopt: 5.0.0 + npmlog: 6.0.2 + rimraf: 3.0.2 + semver: 7.7.4 + tar: 6.2.1 + which: 2.0.2 + transitivePeerDependencies: + - bluebird + - supports-color + optional: true + + nopt@5.0.0: + dependencies: + abbrev: 1.1.1 + optional: true + + npmlog@6.0.2: + dependencies: + are-we-there-yet: 3.0.1 + console-control-strings: 1.1.0 + gauge: 4.0.4 + set-blocking: 2.0.0 + optional: true + + object-assign@4.1.1: {} + + object-inspect@1.13.4: {} + + obug@2.1.1: {} + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + one-time@1.0.0: + dependencies: + fn.name: 1.1.0 + + open@10.2.0: + dependencies: + default-browser: 5.5.0 + define-lazy-prop: 3.0.0 + is-inside-container: 1.0.0 + wsl-utils: 0.1.0 + + openai@6.33.0(ws@8.20.0)(zod@4.3.6): + optionalDependencies: + ws: 8.20.0 + zod: 4.3.6 + + p-finally@1.0.0: {} + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-map@4.0.0: + dependencies: + aggregate-error: 3.1.0 + optional: true + + p-queue@6.6.2: + dependencies: + eventemitter3: 4.0.7 + p-timeout: 3.2.0 + + p-queue@9.1.0: + dependencies: + eventemitter3: 5.0.4 + p-timeout: 7.0.1 + + p-retry@4.6.2: + dependencies: + '@types/retry': 0.12.0 + retry: 0.13.1 + + p-retry@7.1.1: + dependencies: + is-network-error: 1.3.1 + + p-timeout@3.2.0: + dependencies: + p-finally: 1.0.0 + + p-timeout@7.0.1: {} + + parseley@0.12.1: + dependencies: + leac: 0.6.0 + peberminta: 0.9.0 + + parseurl@1.3.3: {} + + path-browserify@1.0.1: {} + + path-expression-matcher@1.2.0: {} + + path-is-absolute@1.0.1: + optional: true + + path-key@3.1.1: {} + + path-parse@1.0.7: {} + + path-to-regexp@8.4.1: {} + + path-type@4.0.0: {} + + pathe@1.1.2: {} + + pathe@2.0.3: {} + + peberminta@0.9.0: {} + + pg-cloudflare@1.3.0: + optional: true + + pg-connection-string@2.12.0: {} + + pg-connection-string@2.6.2: {} + + pg-int8@1.0.1: {} + + pg-pool@3.13.0(pg@8.20.0): + dependencies: + pg: 8.20.0 + + pg-protocol@1.13.0: {} + + pg-types@2.2.0: + dependencies: + pg-int8: 1.0.1 + postgres-array: 2.0.0 + postgres-bytea: 1.0.1 + postgres-date: 1.0.7 + postgres-interval: 1.2.0 + + pg@8.20.0: + dependencies: + pg-connection-string: 2.12.0 + pg-pool: 3.13.0(pg@8.20.0) + pg-protocol: 1.13.0 + pg-types: 2.2.0 + pgpass: 1.0.5 + optionalDependencies: + pg-cloudflare: 1.3.0 + + pgpass@1.0.5: + dependencies: + split2: 4.2.0 + + picocolors@1.1.1: {} + + picomatch@2.3.2: {} + + picomatch@4.0.4: {} + + pkce-challenge@5.0.1: {} + + postcss@8.5.8: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + postgres-array@2.0.0: {} + + postgres-array@3.0.4: {} + + postgres-bytea@1.0.1: {} + + postgres-date@1.0.7: {} + + postgres-date@2.1.0: {} + + postgres-interval@1.2.0: + dependencies: + xtend: 4.0.2 + + postgres-interval@4.0.2: {} + + prebuild-install@7.1.3: + dependencies: + detect-libc: 2.1.2 + expand-template: 2.0.3 + github-from-package: 0.0.0 + minimist: 1.2.8 + mkdirp-classic: 0.5.3 + napi-build-utils: 2.0.0 + node-abi: 3.89.0 + pump: 3.0.4 + rc: 1.2.8 + simple-get: 4.0.1 + tar-fs: 2.1.4 + tunnel-agent: 0.6.0 + + process@0.11.10: {} + + promise-inflight@1.0.1: + optional: true + + promise-retry@2.0.1: + dependencies: + err-code: 2.0.3 + retry: 0.12.0 + optional: true + + protobufjs@7.5.4: + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.4 + '@protobufjs/eventemitter': 1.1.0 + '@protobufjs/fetch': 1.1.0 + '@protobufjs/float': 1.0.2 + '@protobufjs/inquire': 1.1.0 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.0 + '@types/node': 24.12.0 + long: 5.3.2 + + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + pump@3.0.4: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + + qs@6.15.0: + dependencies: + side-channel: 1.1.0 + + queue-microtask@1.2.3: {} + + range-parser@1.2.1: {} + + raw-body@3.0.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + unpipe: 1.0.0 + + rc@1.2.8: + dependencies: + deep-extend: 0.6.0 + ini: 1.3.8 + minimist: 1.2.8 + strip-json-comments: 2.0.1 + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + readable-stream@4.7.0: + dependencies: + abort-controller: 3.0.0 + buffer: 6.0.3 + events: 3.3.0 + process: 0.11.10 + string_decoder: 1.3.0 + + rechoir@0.8.0: + dependencies: + resolve: 1.22.11 + + reflect-metadata@0.2.2: {} + + require-directory@2.1.1: {} + + require-from-string@2.0.2: {} + + resolve-from@5.0.0: {} + + resolve@1.22.11: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + retry-request@7.0.2(encoding@0.1.13): + dependencies: + '@types/request': 2.48.13 + extend: 3.0.2 + teeny-request: 9.0.0(encoding@0.1.13) + transitivePeerDependencies: + - encoding + - supports-color + + retry@0.12.0: + optional: true + + retry@0.13.1: {} + + reusify@1.1.0: {} + + rimraf@3.0.2: + dependencies: + glob: 7.2.3 + optional: true + + rolldown@1.0.0-rc.12(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1): + dependencies: + '@oxc-project/types': 0.122.0 + '@rolldown/pluginutils': 1.0.0-rc.12 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.0.0-rc.12 + '@rolldown/binding-darwin-arm64': 1.0.0-rc.12 + '@rolldown/binding-darwin-x64': 1.0.0-rc.12 + '@rolldown/binding-freebsd-x64': 1.0.0-rc.12 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.12 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.12 + '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.12 + '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.12 + '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.12 + '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.12 + '@rolldown/binding-linux-x64-musl': 1.0.0-rc.12 + '@rolldown/binding-openharmony-arm64': 1.0.0-rc.12 + '@rolldown/binding-wasm32-wasi': 1.0.0-rc.12(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1) + '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.12 + '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.12 + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + + router@2.2.0: + dependencies: + debug: 4.4.3 + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.4.1 + transitivePeerDependencies: + - supports-color + + run-applescript@7.1.0: {} + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + safe-buffer@5.2.1: {} + + safe-stable-stringify@2.5.0: {} + + safer-buffer@2.1.2: {} + + selderee@0.11.0: + dependencies: + parseley: 0.12.1 + + semver@7.7.4: {} + + send@1.2.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.1 + mime-types: 3.0.2 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + serve-static@2.2.1: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.1 + transitivePeerDependencies: + - supports-color + + set-blocking@2.0.0: + optional: true + + setprototypeof@1.2.0: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + siginfo@2.0.0: {} + + signal-exit@3.0.7: + optional: true + + simple-concat@1.0.1: {} + + simple-get@4.0.1: + dependencies: + decompress-response: 6.0.0 + once: 1.4.0 + simple-concat: 1.0.1 + + simple-wcswidth@1.1.2: {} + + slash@3.0.0: {} + + smart-buffer@4.2.0: + optional: true + + socks-proxy-agent@6.2.1: + dependencies: + agent-base: 6.0.2 + debug: 4.4.3 + socks: 2.8.7 + transitivePeerDependencies: + - supports-color + optional: true + + socks@2.8.7: + dependencies: + ip-address: 10.1.0 + smart-buffer: 4.2.0 + optional: true + + source-map-js@1.2.1: {} + + split2@4.2.0: {} + + sprintf-js@1.1.3: {} + + sql-escaper@1.3.3: {} + + sqlite3@5.1.7: + dependencies: + bindings: 1.5.0 + node-addon-api: 7.1.1 + prebuild-install: 7.1.3 + tar: 6.2.1 + optionalDependencies: + node-gyp: 8.4.1 + transitivePeerDependencies: + - bluebird + - supports-color + + sqlstring-sqlite@0.1.1: {} + + sqlstring@2.3.3: {} + + ssri@8.0.1: + dependencies: + minipass: 3.3.6 + optional: true + + stack-trace@0.0.10: {} + + stackback@0.0.2: {} + + statuses@2.0.2: {} + + std-env@4.0.0: {} + + stream-events@1.0.5: + dependencies: + stubs: 3.0.0 + + stream-shift@1.0.3: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-json-comments@2.0.1: {} + + strnum@2.2.2: {} + + stubs@3.0.0: {} + + supports-preserve-symlinks-flag@1.0.0: {} + + tar-fs@2.1.4: + dependencies: + chownr: 1.1.4 + mkdirp-classic: 0.5.3 + pump: 3.0.4 + tar-stream: 2.2.0 + + tar-stream@2.2.0: + dependencies: + bl: 4.1.0 + end-of-stream: 1.4.5 + fs-constants: 1.0.0 + inherits: 2.0.4 + readable-stream: 3.6.2 + + tar@6.2.1: + dependencies: + chownr: 2.0.0 + fs-minipass: 2.1.0 + minipass: 5.0.0 + minizlib: 2.1.2 + mkdirp: 1.0.4 + yallist: 4.0.0 + + tarn@3.0.2: {} + + tedious@19.2.1(@azure/core-client@1.10.1): + dependencies: + '@azure/core-auth': 1.10.1 + '@azure/identity': 4.13.1 + '@azure/keyvault-keys': 4.10.0(@azure/core-client@1.10.1) + '@js-joda/core': 5.7.0 + '@types/node': 24.12.0 + bl: 6.1.6 + iconv-lite: 0.7.2 + js-md4: 0.3.2 + native-duplexpair: 1.0.0 + sprintf-js: 1.1.3 + transitivePeerDependencies: + - '@azure/core-client' + - supports-color + + teeny-request@9.0.0(encoding@0.1.13): + dependencies: + http-proxy-agent: 5.0.0 + https-proxy-agent: 5.0.1 + node-fetch: 2.7.0(encoding@0.1.13) + stream-events: 1.0.5 + uuid: 9.0.1 + transitivePeerDependencies: + - encoding + - supports-color + + text-hex@1.0.0: {} + + tildify@2.0.0: {} + + tinybench@2.9.0: {} + + tinyexec@1.0.4: {} + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + tinyrainbow@3.1.0: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + toidentifier@1.0.1: {} + + tr46@0.0.3: {} + + tree-sitter@0.22.4: + dependencies: + node-addon-api: 8.7.0 + node-gyp-build: 4.8.4 + + triple-beam@1.4.1: {} + + ts-algebra@2.0.0: {} + + ts-morph@27.0.2: + dependencies: + '@ts-morph/common': 0.28.1 + code-block-writer: 13.0.3 + + tslib@2.8.1: {} + + tsqlstring@1.0.1: {} + + tunnel-agent@0.6.0: + dependencies: + safe-buffer: 5.2.1 + + type-is@2.0.1: + dependencies: + content-type: 1.0.5 + media-typer: 1.1.0 + mime-types: 3.0.2 + + typescript@6.0.2: {} + + undici-types@7.16.0: {} + + unique-filename@1.1.1: + dependencies: + unique-slug: 2.0.2 + optional: true + + unique-slug@2.0.2: + dependencies: + imurmurhash: 0.1.4 + optional: true + + universalify@2.0.1: {} + + unpipe@1.0.0: {} + + url-template@2.0.8: {} + + util-deprecate@1.0.2: {} + + uuid@10.0.0: {} + + uuid@11.1.0: {} + + uuid@13.0.0: {} + + uuid@8.3.2: {} + + uuid@9.0.1: {} + + vary@1.1.2: {} + + vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@24.12.0)(esbuild@0.27.4): + dependencies: + lightningcss: 1.32.0 + picomatch: 4.0.4 + postcss: 8.5.8 + rolldown: 1.0.0-rc.12(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1) + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 24.12.0 + esbuild: 0.27.4 + fsevents: 2.3.3 + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + + vitest@4.1.2(@opentelemetry/api@1.9.1)(@types/node@24.12.0)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@24.12.0)(esbuild@0.27.4)): + dependencies: + '@vitest/expect': 4.1.2 + '@vitest/mocker': 4.1.2(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@24.12.0)(esbuild@0.27.4)) + '@vitest/pretty-format': 4.1.2 + '@vitest/runner': 4.1.2 + '@vitest/snapshot': 4.1.2 + '@vitest/spy': 4.1.2 + '@vitest/utils': 4.1.2 + es-module-lexer: 2.0.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.1 + pathe: 2.0.3 + picomatch: 4.0.4 + std-env: 4.0.0 + tinybench: 2.9.0 + tinyexec: 1.0.4 + tinyglobby: 0.2.15 + tinyrainbow: 3.1.0 + vite: 8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@24.12.0)(esbuild@0.27.4) + why-is-node-running: 2.3.0 + optionalDependencies: + '@opentelemetry/api': 1.9.1 + '@types/node': 24.12.0 + transitivePeerDependencies: + - msw + + web-streams-polyfill@3.3.3: {} + + web-tree-sitter@0.24.7: {} + + webidl-conversions@3.0.1: {} + + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + wide-align@1.1.5: + dependencies: + string-width: 4.2.3 + optional: true + + winston-transport@4.9.0: + dependencies: + logform: 2.7.0 + readable-stream: 3.6.2 + triple-beam: 1.4.1 + + winston@3.19.0: + dependencies: + '@colors/colors': 1.6.0 + '@dabh/diagnostics': 2.0.8 + async: 3.2.6 + is-stream: 2.0.1 + logform: 2.7.0 + one-time: 1.0.0 + readable-stream: 3.6.2 + safe-stable-stringify: 2.5.0 + stack-trace: 0.0.10 + triple-beam: 1.4.1 + winston-transport: 4.9.0 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrappy@1.0.2: {} + + ws@8.20.0: {} + + wsl-utils@0.1.0: + dependencies: + is-wsl: 3.1.1 + + xtend@4.0.2: {} + + y18n@5.0.8: {} + + yallist@4.0.0: {} + + yargs-parser@21.1.1: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + yocto-queue@0.1.0: {} + + zod-to-json-schema@3.25.2(zod@4.3.6): + dependencies: + zod: 4.3.6 + + zod@4.1.8: {} + + zod@4.3.6: {} From 9266ae6e098c7356d229a8d29254217fcdd54147 Mon Sep 17 00:00:00 2001 From: Alexander Akhmetov Date: Wed, 1 Apr 2026 11:25:54 +0200 Subject: [PATCH 131/133] fix(ci): resolve build failures in TS, Java, and .NET jobs --- .github/workflows/ci.yml | 10 ++++++++++ .github/workflows/dotnet-publish.yml | 10 ++++++++-- .github/workflows/go-sdk-tag.yml | 4 +++- .github/workflows/java-publish.yml | 18 +++++++++++++----- .../Grafana.Sigil.Tests/AuthConfigTests.cs | 2 +- .../Grafana.Sigil.Tests.csproj | 16 ++++++++-------- java/settings.gradle.kts | 1 - js/tsconfig.test.json | 3 ++- 8 files changed, 45 insertions(+), 19 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a0c6057..32c0974 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,6 +14,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false - uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0 with: @@ -45,6 +47,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0 @@ -78,6 +82,8 @@ jobs: - { name: google-adk, cmd: "uv run --with './python[dev]' --with './python-frameworks/google-adk[dev]' pytest python-frameworks/google-adk/tests" } steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: @@ -94,6 +100,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false - uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1 with: @@ -110,6 +118,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false - uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 with: diff --git a/.github/workflows/dotnet-publish.yml b/.github/workflows/dotnet-publish.yml index 9a6cf71..9027705 100644 --- a/.github/workflows/dotnet-publish.yml +++ b/.github/workflows/dotnet-publish.yml @@ -32,22 +32,28 @@ jobs: export_env: false - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false - uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1 with: dotnet-version: '8.0.x' - name: Pack + env: + PKG_VERSION: ${{ inputs.version }} run: | dotnet pack dotnet/Sigil.DotNet.sln -c Release \ - -p:PackageVersion=${{ inputs.version }} \ + -p:PackageVersion="${PKG_VERSION}" \ -o nupkgs/ - name: Publish + env: + NUGET_API_KEY: ${{ fromJSON(steps.get-secrets.outputs.secrets).NUGET_API_KEY }} run: | for pkg in nupkgs/*.nupkg; do dotnet nuget push "$pkg" \ - --api-key "${{ fromJSON(steps.get-secrets.outputs.secrets).NUGET_API_KEY }}" \ + --api-key "${NUGET_API_KEY}" \ --source https://api.nuget.org/v3/index.json \ --skip-duplicate done diff --git a/.github/workflows/go-sdk-tag.yml b/.github/workflows/go-sdk-tag.yml index 27c97d9..850077a 100644 --- a/.github/workflows/go-sdk-tag.yml +++ b/.github/workflows/go-sdk-tag.yml @@ -48,8 +48,10 @@ jobs: git config user.email '144369747+grafana-plugins-platform-bot[bot]@users.noreply.github.com' - name: Tag all Go modules + env: + INPUT_VERSION: ${{ inputs.version }} run: | - VERSION="v${{ inputs.version }}" + VERSION="v${INPUT_VERSION}" MODULES=( go go-providers/anthropic diff --git a/.github/workflows/java-publish.yml b/.github/workflows/java-publish.yml index 86a368a..7f6a93d 100644 --- a/.github/workflows/java-publish.yml +++ b/.github/workflows/java-publish.yml @@ -35,6 +35,8 @@ jobs: export_env: false - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false - uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 with: @@ -43,10 +45,16 @@ jobs: - name: Publish working-directory: java + env: + PKG_VERSION: ${{ inputs.version }} + OSSRH_USERNAME: ${{ fromJSON(steps.get-secrets.outputs.secrets).MAVEN_USERNAME }} + OSSRH_PASSWORD: ${{ fromJSON(steps.get-secrets.outputs.secrets).MAVEN_PASSWORD }} + SIGNING_KEY: ${{ fromJSON(steps.get-secrets.outputs.secrets).GPG_PRIVATE_KEY }} + SIGNING_PASSWORD: ${{ fromJSON(steps.get-secrets.outputs.secrets).GPG_PASSPHRASE }} run: | ./gradlew --no-daemon publish \ - -Pversion=${{ inputs.version }} \ - -PossrhUsername="${{ fromJSON(steps.get-secrets.outputs.secrets).MAVEN_USERNAME }}" \ - -PossrhPassword="${{ fromJSON(steps.get-secrets.outputs.secrets).MAVEN_PASSWORD }}" \ - -Psigning.key="${{ fromJSON(steps.get-secrets.outputs.secrets).GPG_PRIVATE_KEY }}" \ - -Psigning.password="${{ fromJSON(steps.get-secrets.outputs.secrets).GPG_PASSPHRASE }}" + -Pversion="${PKG_VERSION}" \ + -PossrhUsername="${OSSRH_USERNAME}" \ + -PossrhPassword="${OSSRH_PASSWORD}" \ + -Psigning.key="${SIGNING_KEY}" \ + -Psigning.password="${SIGNING_PASSWORD}" diff --git a/dotnet/tests/Grafana.Sigil.Tests/AuthConfigTests.cs b/dotnet/tests/Grafana.Sigil.Tests/AuthConfigTests.cs index 41f6bdf..93e6bf6 100644 --- a/dotnet/tests/Grafana.Sigil.Tests/AuthConfigTests.cs +++ b/dotnet/tests/Grafana.Sigil.Tests/AuthConfigTests.cs @@ -28,7 +28,7 @@ public sealed class AuthConfigTests Mode = ExportAuthMode.None, TenantId = "tenant-a", }, - "generation auth mode 'none' does not allow tenant_id or bearer_token" + "generation auth mode 'none' does not allow credentials" }, { new AuthConfig diff --git a/dotnet/tests/Grafana.Sigil.Tests/Grafana.Sigil.Tests.csproj b/dotnet/tests/Grafana.Sigil.Tests/Grafana.Sigil.Tests.csproj index 6dbbeb3..2dce9f9 100644 --- a/dotnet/tests/Grafana.Sigil.Tests/Grafana.Sigil.Tests.csproj +++ b/dotnet/tests/Grafana.Sigil.Tests/Grafana.Sigil.Tests.csproj @@ -22,20 +22,20 @@ - - - - diff --git a/java/settings.gradle.kts b/java/settings.gradle.kts index aa61a8d..e352393 100644 --- a/java/settings.gradle.kts +++ b/java/settings.gradle.kts @@ -6,7 +6,6 @@ include(":providers:anthropic") include(":providers:gemini") include(":frameworks:google-adk") include(":benchmarks") -include(":devex-emitter") project(":providers:openai").projectDir = file("providers/openai") project(":providers:anthropic").projectDir = file("providers/anthropic") diff --git a/js/tsconfig.test.json b/js/tsconfig.test.json index f4844f7..096bea5 100644 --- a/js/tsconfig.test.json +++ b/js/tsconfig.test.json @@ -4,7 +4,8 @@ "module": "NodeNext", "moduleResolution": "NodeNext", "noEmit": false, - "outDir": ".test-dist" + "outDir": ".test-dist", + "rootDir": "./src" }, "include": ["src/**/*.ts"] } From 8074365aa95a34e4efac9e340f387b856c2b6c24 Mon Sep 17 00:00:00 2001 From: Alexander Akhmetov Date: Wed, 1 Apr 2026 11:59:30 +0200 Subject: [PATCH 132/133] Update license --- LICENSE | 862 ++++++------------------ README.md | 2 +- go-providers/LICENSE | 2 +- go-providers/anthropic/LICENSE | 2 +- go-providers/gemini/LICENSE | 2 +- go-providers/openai/LICENSE | 2 +- go/LICENSE | 2 +- js/LICENSE | 2 +- python-frameworks/google-adk/LICENSE | 2 +- python-frameworks/langchain/LICENSE | 2 +- python-frameworks/langgraph/LICENSE | 2 +- python-frameworks/llamaindex/LICENSE | 2 +- python-frameworks/openai-agents/LICENSE | 2 +- python-providers/LICENSE | 2 +- python-providers/anthropic/LICENSE | 2 +- python-providers/gemini/LICENSE | 2 +- python-providers/openai/LICENSE | 2 +- python/LICENSE | 2 +- 18 files changed, 218 insertions(+), 678 deletions(-) diff --git a/LICENSE b/LICENSE index be3f7b2..261eeb9 100644 --- a/LICENSE +++ b/LICENSE @@ -1,661 +1,201 @@ - GNU AFFERO GENERAL PUBLIC LICENSE - Version 3, 19 November 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU Affero General Public License is a free, copyleft license for -software and other kinds of works, specifically designed to ensure -cooperation with the community in the case of network server software. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -our General Public Licenses are intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - Developers that use our General Public Licenses protect your rights -with two steps: (1) assert copyright on the software, and (2) offer -you this License which gives you legal permission to copy, distribute -and/or modify the software. - - A secondary benefit of defending all users' freedom is that -improvements made in alternate versions of the program, if they -receive widespread use, become available for other developers to -incorporate. Many developers of free software are heartened and -encouraged by the resulting cooperation. However, in the case of -software used on network servers, this result may fail to come about. -The GNU General Public License permits making a modified version and -letting the public access it on a server without ever releasing its -source code to the public. - - The GNU Affero General Public License is designed specifically to -ensure that, in such cases, the modified source code becomes available -to the community. It requires the operator of a network server to -provide the source code of the modified version running there to the -users of that server. Therefore, public use of a modified version, on -a publicly accessible server, gives the public access to the source -code of the modified version. - - An older license, called the Affero General Public License and -published by Affero, was designed to accomplish similar goals. This is -a different license, not a version of the Affero GPL, but Affero has -released a new version of the Affero GPL which permits relicensing under -this license. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU Affero General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Remote Network Interaction; Use with the GNU General Public License. - - Notwithstanding any other provision of this License, if you modify the -Program, your modified version must prominently offer all users -interacting with it remotely through a computer network (if your version -supports such interaction) an opportunity to receive the Corresponding -Source of your version by providing access to the Corresponding Source -from a network server at no charge, through some standard or customary -means of facilitating copying of software. This Corresponding Source -shall include the Corresponding Source for any work covered by version 3 -of the GNU General Public License that is incorporated pursuant to the -following paragraph. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the work with which it is combined will remain governed by version -3 of the GNU General Public License. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU Affero General Public License from time to time. Such new versions -will be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU Affero General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU Affero General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU Affero General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If your software can interact with users remotely through a computer -network, you should also make sure that it provides a way for users to -get its source. For example, if your program is a web application, its -interface could display a "Source" link that leads users to an archive -of the code. There are many ways you could offer source, and different -solutions will be better for different programs; see section 13 for the -specific requirements. - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU AGPL, see -. + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md index 0926858..57d6aec 100644 --- a/README.md +++ b/README.md @@ -36,4 +36,4 @@ Vendored protobuf definitions used by SDKs live in [`proto/`](proto/). ## License -[GNU AGPL v3](LICENSE) +[Apache License 2.0](LICENSE) diff --git a/go-providers/LICENSE b/go-providers/LICENSE index ae8c60c..626a3ab 100644 --- a/go-providers/LICENSE +++ b/go-providers/LICENSE @@ -1,3 +1,3 @@ SPDX-License-Identifier: Apache-2.0 -See /sdks/LICENSE at repository root for full license text. +See /LICENSE at repository root for full license text. diff --git a/go-providers/anthropic/LICENSE b/go-providers/anthropic/LICENSE index ae8c60c..626a3ab 100644 --- a/go-providers/anthropic/LICENSE +++ b/go-providers/anthropic/LICENSE @@ -1,3 +1,3 @@ SPDX-License-Identifier: Apache-2.0 -See /sdks/LICENSE at repository root for full license text. +See /LICENSE at repository root for full license text. diff --git a/go-providers/gemini/LICENSE b/go-providers/gemini/LICENSE index ae8c60c..626a3ab 100644 --- a/go-providers/gemini/LICENSE +++ b/go-providers/gemini/LICENSE @@ -1,3 +1,3 @@ SPDX-License-Identifier: Apache-2.0 -See /sdks/LICENSE at repository root for full license text. +See /LICENSE at repository root for full license text. diff --git a/go-providers/openai/LICENSE b/go-providers/openai/LICENSE index ae8c60c..626a3ab 100644 --- a/go-providers/openai/LICENSE +++ b/go-providers/openai/LICENSE @@ -1,3 +1,3 @@ SPDX-License-Identifier: Apache-2.0 -See /sdks/LICENSE at repository root for full license text. +See /LICENSE at repository root for full license text. diff --git a/go/LICENSE b/go/LICENSE index ae8c60c..626a3ab 100644 --- a/go/LICENSE +++ b/go/LICENSE @@ -1,3 +1,3 @@ SPDX-License-Identifier: Apache-2.0 -See /sdks/LICENSE at repository root for full license text. +See /LICENSE at repository root for full license text. diff --git a/js/LICENSE b/js/LICENSE index ae8c60c..626a3ab 100644 --- a/js/LICENSE +++ b/js/LICENSE @@ -1,3 +1,3 @@ SPDX-License-Identifier: Apache-2.0 -See /sdks/LICENSE at repository root for full license text. +See /LICENSE at repository root for full license text. diff --git a/python-frameworks/google-adk/LICENSE b/python-frameworks/google-adk/LICENSE index ae8c60c..626a3ab 100644 --- a/python-frameworks/google-adk/LICENSE +++ b/python-frameworks/google-adk/LICENSE @@ -1,3 +1,3 @@ SPDX-License-Identifier: Apache-2.0 -See /sdks/LICENSE at repository root for full license text. +See /LICENSE at repository root for full license text. diff --git a/python-frameworks/langchain/LICENSE b/python-frameworks/langchain/LICENSE index ae8c60c..626a3ab 100644 --- a/python-frameworks/langchain/LICENSE +++ b/python-frameworks/langchain/LICENSE @@ -1,3 +1,3 @@ SPDX-License-Identifier: Apache-2.0 -See /sdks/LICENSE at repository root for full license text. +See /LICENSE at repository root for full license text. diff --git a/python-frameworks/langgraph/LICENSE b/python-frameworks/langgraph/LICENSE index ae8c60c..626a3ab 100644 --- a/python-frameworks/langgraph/LICENSE +++ b/python-frameworks/langgraph/LICENSE @@ -1,3 +1,3 @@ SPDX-License-Identifier: Apache-2.0 -See /sdks/LICENSE at repository root for full license text. +See /LICENSE at repository root for full license text. diff --git a/python-frameworks/llamaindex/LICENSE b/python-frameworks/llamaindex/LICENSE index ae8c60c..626a3ab 100644 --- a/python-frameworks/llamaindex/LICENSE +++ b/python-frameworks/llamaindex/LICENSE @@ -1,3 +1,3 @@ SPDX-License-Identifier: Apache-2.0 -See /sdks/LICENSE at repository root for full license text. +See /LICENSE at repository root for full license text. diff --git a/python-frameworks/openai-agents/LICENSE b/python-frameworks/openai-agents/LICENSE index ae8c60c..626a3ab 100644 --- a/python-frameworks/openai-agents/LICENSE +++ b/python-frameworks/openai-agents/LICENSE @@ -1,3 +1,3 @@ SPDX-License-Identifier: Apache-2.0 -See /sdks/LICENSE at repository root for full license text. +See /LICENSE at repository root for full license text. diff --git a/python-providers/LICENSE b/python-providers/LICENSE index ae8c60c..626a3ab 100644 --- a/python-providers/LICENSE +++ b/python-providers/LICENSE @@ -1,3 +1,3 @@ SPDX-License-Identifier: Apache-2.0 -See /sdks/LICENSE at repository root for full license text. +See /LICENSE at repository root for full license text. diff --git a/python-providers/anthropic/LICENSE b/python-providers/anthropic/LICENSE index ae8c60c..626a3ab 100644 --- a/python-providers/anthropic/LICENSE +++ b/python-providers/anthropic/LICENSE @@ -1,3 +1,3 @@ SPDX-License-Identifier: Apache-2.0 -See /sdks/LICENSE at repository root for full license text. +See /LICENSE at repository root for full license text. diff --git a/python-providers/gemini/LICENSE b/python-providers/gemini/LICENSE index ae8c60c..626a3ab 100644 --- a/python-providers/gemini/LICENSE +++ b/python-providers/gemini/LICENSE @@ -1,3 +1,3 @@ SPDX-License-Identifier: Apache-2.0 -See /sdks/LICENSE at repository root for full license text. +See /LICENSE at repository root for full license text. diff --git a/python-providers/openai/LICENSE b/python-providers/openai/LICENSE index ae8c60c..626a3ab 100644 --- a/python-providers/openai/LICENSE +++ b/python-providers/openai/LICENSE @@ -1,3 +1,3 @@ SPDX-License-Identifier: Apache-2.0 -See /sdks/LICENSE at repository root for full license text. +See /LICENSE at repository root for full license text. diff --git a/python/LICENSE b/python/LICENSE index ae8c60c..626a3ab 100644 --- a/python/LICENSE +++ b/python/LICENSE @@ -1,3 +1,3 @@ SPDX-License-Identifier: Apache-2.0 -See /sdks/LICENSE at repository root for full license text. +See /LICENSE at repository root for full license text. From 4c5cb76f77410a22ac2c8783fbcb4c4a35c28a5f Mon Sep 17 00:00:00 2001 From: "renovate-sh-app[bot]" <219655108+renovate-sh-app[bot]@users.noreply.github.com> Date: Wed, 1 Apr 2026 14:22:07 +0000 Subject: [PATCH 133/133] chore(deps): update dependency google.genai to 1.6.0 | datasource | package | from | to | | ---------- | ------------ | ----- | ----- | | nuget | Google.GenAI | 1.5.0 | 1.6.0 | Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> --- dotnet/src/Grafana.Sigil.Gemini/Grafana.Sigil.Gemini.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/src/Grafana.Sigil.Gemini/Grafana.Sigil.Gemini.csproj b/dotnet/src/Grafana.Sigil.Gemini/Grafana.Sigil.Gemini.csproj index e283af4..e0cb688 100644 --- a/dotnet/src/Grafana.Sigil.Gemini/Grafana.Sigil.Gemini.csproj +++ b/dotnet/src/Grafana.Sigil.Gemini/Grafana.Sigil.Gemini.csproj @@ -11,7 +11,7 @@ - +