From 63ecbc6e2684e592dff9f782328b333969c402a5 Mon Sep 17 00:00:00 2001 From: Bryan Morgan Date: Wed, 11 Mar 2026 12:32:26 -0400 Subject: [PATCH] feat: differentiate User-Agent for a2a-server and ACP clients --- docs/cli/telemetry.md | 1692 +++++++---------- docs/reference/configuration.md | 43 +- packages/a2a-server/src/config/config.test.ts | 576 ++---- packages/a2a-server/src/config/config.ts | 1 + packages/cli/src/config/config.test.ts | 22 + packages/cli/src/config/config.ts | 23 +- packages/cli/src/config/extension.test.ts | 1 + packages/core/src/config/config.ts | 83 +- .../core/src/core/contentGenerator.test.ts | 46 + packages/core/src/core/contentGenerator.ts | 8 +- packages/core/src/ide/detect-ide.test.ts | 17 +- packages/core/src/ide/detect-ide.ts | 8 + packages/core/src/utils/surface.ts | 54 + 13 files changed, 1087 insertions(+), 1487 deletions(-) create mode 100644 packages/core/src/utils/surface.ts diff --git a/docs/cli/telemetry.md b/docs/cli/telemetry.md index f57badb689..9e58bfe47f 100644 --- a/docs/cli/telemetry.md +++ b/docs/cli/telemetry.md @@ -1,39 +1,81 @@ # Observability with OpenTelemetry -Observability is the key to turning experimental AI into reliable software. -Gemini CLI provides built-in support for OpenTelemetry, transforming every agent -interaction into a rich stream of logs, metrics, and traces. This three-pillar -approach gives you the high-fidelity visibility needed to understand agent -behavior, optimize performance, and ensure reliability across your entire -workflow. +Learn how to enable and setup OpenTelemetry for Gemini CLI. -Whether you are debugging a complex tool interaction locally or monitoring -enterprise-wide usage in the cloud, Gemini CLI's observability system provides -the actionable intelligence needed to move from "black box" AI to predictable, -high-performance systems. +- [Observability with OpenTelemetry](#observability-with-opentelemetry) + - [Key benefits](#key-benefits) + - [OpenTelemetry integration](#opentelemetry-integration) + - [Configuration](#configuration) + - [Google Cloud telemetry](#google-cloud-telemetry) + - [Prerequisites](#prerequisites) + - [Authenticating with CLI Credentials](#authenticating-with-cli-credentials) + - [Direct export (recommended)](#direct-export-recommended) + - [Collector-based export (advanced)](#collector-based-export-advanced) + - [Monitoring Dashboards](#monitoring-dashboards) + - [Local telemetry](#local-telemetry) + - [File-based output (recommended)](#file-based-output-recommended) + - [Collector-based export (advanced)](#collector-based-export-advanced-1) + - [Logs and metrics](#logs-and-metrics) + - [Logs](#logs) + - [Sessions](#sessions) + - [Approval Mode](#approval-mode) + - [Tools](#tools) + - [Files](#files) + - [API](#api) + - [Model routing](#model-routing) + - [Chat and streaming](#chat-and-streaming) + - [Resilience](#resilience) + - [Extensions](#extensions) + - [Agent runs](#agent-runs) + - [IDE](#ide) + - [UI](#ui) + - [Metrics](#metrics) + - [Custom](#custom) + - [Sessions](#sessions-1) + - [Tools](#tools-1) + - [API](#api-1) + - [Token usage](#token-usage) + - [Files](#files-1) + - [Chat and streaming](#chat-and-streaming-1) + - [Model routing](#model-routing-1) + - [Agent runs](#agent-runs-1) + - [UI](#ui-1) + - [Performance](#performance) + - [GenAI semantic convention](#genai-semantic-convention) + +## Key benefits + +- **πŸ” Usage analytics**: Understand interaction patterns and feature adoption + across your team +- **⚑ Performance monitoring**: Track response times, token consumption, and + resource utilization +- **πŸ› Real-time debugging**: Identify bottlenecks, failures, and error patterns + as they occur +- **πŸ“Š Workflow optimization**: Make informed decisions to improve + configurations and processes +- **🏒 Enterprise governance**: Monitor usage across teams, track costs, ensure + compliance, and integrate with existing monitoring infrastructure ## OpenTelemetry integration -Gemini CLI integrates with **[OpenTelemetry]**, a vendor-neutral, -industry-standard observability framework. +Built on **[OpenTelemetry]** β€” the vendor-neutral, industry-standard +observability framework β€” Gemini CLI's observability system provides: -The observability system provides: - -- Universal compatibility: Export to any OpenTelemetry backend (Google Cloud, - Jaeger, Prometheus, Datadog, etc.). -- Standardized data: Use consistent formats and collection methods across your - toolchain. -- Future-proof integration: Connect with existing and future observability - infrastructure. -- No vendor lock-in: Switch between backends without changing your - instrumentation. +- **Universal compatibility**: Export to any OpenTelemetry backend (Google + Cloud, Jaeger, Prometheus, Datadog, etc.) +- **Standardized data**: Use consistent formats and collection methods across + your toolchain +- **Future-proof integration**: Connect with existing and future observability + infrastructure +- **No vendor lock-in**: Switch between backends without changing your + instrumentation [OpenTelemetry]: https://opentelemetry.io/ ## Configuration -You control telemetry behavior through the `.gemini/settings.json` file. -Environment variables can override these settings. +All telemetry behavior is controlled through your `.gemini/settings.json` file. +Environment variables can be used to override the settings in the file. | Setting | Environment Variable | Description | Values | Default | | -------------- | -------------------------------- | --------------------------------------------------- | ----------------- | ----------------------- | @@ -45,146 +87,175 @@ Environment variables can override these settings. | `logPrompts` | `GEMINI_TELEMETRY_LOG_PROMPTS` | Include prompts in telemetry logs | `true`/`false` | `true` | | `useCollector` | `GEMINI_TELEMETRY_USE_COLLECTOR` | Use external OTLP collector (advanced) | `true`/`false` | `false` | | `useCliAuth` | `GEMINI_TELEMETRY_USE_CLI_AUTH` | Use CLI credentials for telemetry (GCP target only) | `true`/`false` | `false` | +| - | `GEMINI_CLI_SURFACE` | Optional custom label for traffic reporting | string | - | -**Note on boolean environment variables:** For boolean settings like `enabled`, -setting the environment variable to `true` or `1` enables the feature. +**Note on boolean environment variables:** For the boolean settings (`enabled`, +`logPrompts`, `useCollector`), setting the corresponding environment variable to +`true` or `1` will enable the feature. Any other value will disable it. -For detailed configuration information, see the +For detailed information about all configuration options, see the [Configuration guide](../reference/configuration.md). ## Google Cloud telemetry -You can export telemetry data directly to Google Cloud Trace, Cloud Monitoring, -and Cloud Logging. - ### Prerequisites -You must complete several setup steps before enabling Google Cloud telemetry. +Before using either method below, complete these steps: -1. Set your Google Cloud project ID: - - To send telemetry to a separate project: +1. Set your Google Cloud project ID: + - For telemetry in a separate project from inference: - **macOS/Linux** + **macOS/Linux** - ```bash - export OTLP_GOOGLE_CLOUD_PROJECT="your-telemetry-project-id" - ``` + ```bash + export OTLP_GOOGLE_CLOUD_PROJECT="your-telemetry-project-id" + ``` - **Windows (PowerShell)** + **Windows (PowerShell)** - ```powershell - $env:OTLP_GOOGLE_CLOUD_PROJECT="your-telemetry-project-id" - ``` + ```powershell + $env:OTLP_GOOGLE_CLOUD_PROJECT="your-telemetry-project-id" + ``` - - To send telemetry to the same project as inference: + - For telemetry in the same project as inference: - **macOS/Linux** + **macOS/Linux** - ```bash - export GOOGLE_CLOUD_PROJECT="your-project-id" - ``` + ```bash + export GOOGLE_CLOUD_PROJECT="your-project-id" + ``` - **Windows (PowerShell)** + **Windows (PowerShell)** - ```powershell - $env:GOOGLE_CLOUD_PROJECT="your-project-id" - ``` + ```powershell + $env:GOOGLE_CLOUD_PROJECT="your-project-id" + ``` -2. Authenticate with Google Cloud using one of these methods: - - **Method A: Application Default Credentials (ADC)**: Use this method for - service accounts or standard `gcloud` authentication. - - For user accounts: - ```bash - gcloud auth application-default login - ``` - - For service accounts: +2. Authenticate with Google Cloud: + - If using a user account: + ```bash + gcloud auth application-default login + ``` + - If using a service account: - **macOS/Linux** + **macOS/Linux** - ```bash - export GOOGLE_APPLICATION_CREDENTIALS="/path/to/your/service-account.json" - ``` + ```bash + export GOOGLE_APPLICATION_CREDENTIALS="/path/to/your/service-account.json" + ``` - **Windows (PowerShell)** + **Windows (PowerShell)** - ```powershell - $env:GOOGLE_APPLICATION_CREDENTIALS="C:\path\to\your\service-account.json" - ``` - * **Method B: CLI Auth** (Direct export only): Simplest method for local - users. Gemini CLI uses the same OAuth credentials you used for login. To - enable this, set `useCliAuth: true` in your `.gemini/settings.json`: + ```powershell + $env:GOOGLE_APPLICATION_CREDENTIALS="C:\path\to\your\service-account.json" + ``` - ```json - { - "telemetry": { - "enabled": true, - "target": "gcp", - "useCliAuth": true - } - } - ``` +3. Make sure your account or service account has these IAM roles: + - Cloud Trace Agent + - Monitoring Metric Writer + - Logs Writer - > **Note:** This setting requires **Direct export** (in-process exporters) - > and cannot be used when `useCollector` is `true`. If both are enabled, - > telemetry will be disabled. +4. Enable the required Google Cloud APIs (if not already enabled): + ```bash + gcloud services enable \ + cloudtrace.googleapis.com \ + monitoring.googleapis.com \ + logging.googleapis.com \ + --project="$OTLP_GOOGLE_CLOUD_PROJECT" + ``` -3. Ensure your account or service account has these IAM roles: - - Cloud Trace Agent - - Monitoring Metric Writer - - Logs Writer +### Authenticating with CLI Credentials -4. Enable the required Google Cloud APIs: - ```bash - gcloud services enable \ - cloudtrace.googleapis.com \ - monitoring.googleapis.com \ - logging.googleapis.com \ - --project="$OTLP_GOOGLE_CLOUD_PROJECT" - ``` +By default, the telemetry collector for Google Cloud uses Application Default +Credentials (ADC). However, you can configure it to use the same OAuth +credentials that you use to log in to the Gemini CLI. This is useful in +environments where you don't have ADC set up. -### Direct export +To enable this, set the `useCliAuth` property in your `telemetry` settings to +`true`: -We recommend using direct export to send telemetry directly to Google Cloud -services. +```json +{ + "telemetry": { + "enabled": true, + "target": "gcp", + "useCliAuth": true + } +} +``` -1. Enable telemetry in `.gemini/settings.json`: - ```json - { - "telemetry": { - "enabled": true, - "target": "gcp" - } - } - ``` -2. Run Gemini CLI and send prompts. -3. View logs, metrics, and traces in the Google Cloud Console. See - [View Google Cloud telemetry](#view-google-cloud-telemetry) for details. +**Important:** -### View Google Cloud telemetry +- This setting requires the use of **Direct Export** (in-process exporters). +- It **cannot** be used with `useCollector: true`. If you enable both, telemetry + will be disabled and an error will be logged. +- The CLI will automatically use your credentials to authenticate with Google + Cloud Trace, Metrics, and Logging APIs. -After you enable telemetry and run Gemini CLI, you can view your data in the -Google Cloud Console. +### Direct export (recommended) -- **Logs:** [Logs Explorer](https://console.cloud.google.com/logs/) -- **Metrics:** - [Metrics Explorer](https://console.cloud.google.com/monitoring/metrics-explorer) -- **Traces:** [Trace Explorer](https://console.cloud.google.com/traces/list) +Sends telemetry directly to Google Cloud services. No collector needed. -For detailed information on how to use these tools, see the following official -Google Cloud documentation: +1. Enable telemetry in your `.gemini/settings.json`: + ```json + { + "telemetry": { + "enabled": true, + "target": "gcp" + } + } + ``` +2. Run Gemini CLI and send prompts. +3. View logs, metrics, and traces: + - Open the Google Cloud Console in your browser after sending prompts: + - Logs (Logs Explorer): https://console.cloud.google.com/logs/ + - Metrics (Metrics Explorer): + https://console.cloud.google.com/monitoring/metrics-explorer + - Traces (Trace Explorer): https://console.cloud.google.com/traces/list -- [View and analyze logs with Logs Explorer](https://cloud.google.com/logging/docs/view/logs-explorer-interface) -- [Create charts with Metrics Explorer](https://cloud.google.com/monitoring/charts/metrics-explorer) -- [Find and explore traces](https://cloud.google.com/trace/docs/finding-traces) +### Collector-based export (advanced) -#### Monitoring dashboards +For custom processing, filtering, or routing, use an OpenTelemetry collector to +forward data to Google Cloud. + +1. Configure your `.gemini/settings.json`: + ```json + { + "telemetry": { + "enabled": true, + "target": "gcp", + "useCollector": true + } + } + ``` +2. Run the automation script: + ```bash + npm run telemetry -- --target=gcp + ``` + This will: + - Start a local OTEL collector that forwards to Google Cloud + - Configure your workspace + - Provide links to view traces, metrics, and logs in Google Cloud Console + - Save collector logs to `~/.gemini/tmp//otel/collector-gcp.log` + - Stop collector on exit (e.g. `Ctrl+C`) +3. Run Gemini CLI and send prompts. +4. View logs, metrics, and traces: + - Open the Google Cloud Console in your browser after sending prompts: + - Logs (Logs Explorer): https://console.cloud.google.com/logs/ + - Metrics (Metrics Explorer): + https://console.cloud.google.com/monitoring/metrics-explorer + - Traces (Trace Explorer): https://console.cloud.google.com/traces/list + - Open `~/.gemini/tmp//otel/collector-gcp.log` to view local + collector logs. + +### Monitoring Dashboards Gemini CLI provides a pre-configured [Google Cloud Monitoring](https://cloud.google.com/monitoring) dashboard to visualize your telemetry. -Find this dashboard under **Google Cloud Monitoring Dashboard Templates** as -"**Gemini CLI Monitoring**". +This dashboard can be found under **Google Cloud Monitoring Dashboard +Templates** as "**Gemini CLI Monitoring**". ![Gemini CLI Monitoring Dashboard Overview](/docs/assets/monitoring-dashboard-overview.png) @@ -192,998 +263,705 @@ Find this dashboard under **Google Cloud Monitoring Dashboard Templates** as ![Gemini CLI Monitoring Dashboard Logs](/docs/assets/monitoring-dashboard-logs.png) -To learn more, see -[Instant insights: Gemini CLI’s pre-configured monitoring dashboards](https://cloud.google.com/blog/topics/developers-practitioners/instant-insights-gemini-clis-new-pre-configured-monitoring-dashboards/). +To learn more, check out this blog post: +[Instant insights: Gemini CLI’s new pre-configured monitoring dashboards](https://cloud.google.com/blog/topics/developers-practitioners/instant-insights-gemini-clis-new-pre-configured-monitoring-dashboards/). ## Local telemetry -You can capture telemetry data locally for development and debugging. We -recommend using file-based output for local development. +For local development and debugging, you can capture telemetry data locally: -1. Enable telemetry in `.gemini/settings.json`: - ```json - { - "telemetry": { - "enabled": true, - "target": "local", - "outfile": ".gemini/telemetry.log" - } - } - ``` -2. Run Gemini CLI and send prompts. -3. View logs and metrics in `.gemini/telemetry.log`. +### File-based output (recommended) -For advanced local telemetry setups (such as Jaeger or Genkit), see the -[Local development guide](../local-development.md#viewing-traces). +1. Enable telemetry in your `.gemini/settings.json`: + ```json + { + "telemetry": { + "enabled": true, + "target": "local", + "otlpEndpoint": "", + "outfile": ".gemini/telemetry.log" + } + } + ``` +2. Run Gemini CLI and send prompts. +3. View logs and metrics in the specified file (e.g., `.gemini/telemetry.log`). + +### Collector-based export (advanced) + +1. Run the automation script: + ```bash + npm run telemetry -- --target=local + ``` + This will: + - Download and start Jaeger and OTEL collector + - Configure your workspace for local telemetry + - Provide a Jaeger UI at http://localhost:16686 + - Save logs/metrics to `~/.gemini/tmp//otel/collector.log` + - Stop collector on exit (e.g. `Ctrl+C`) +2. Run Gemini CLI and send prompts. +3. View traces at http://localhost:16686 and logs/metrics in the collector log + file. + +## Client identification + +Gemini CLI includes identifiers in its `User-Agent` header to help you +differentiate and report on API traffic from different environments (for +example, identifying calls from Gemini Code Assist versus a standard terminal). + +### Automatic identification + +Most integrated environments are identified automatically without additional +configuration. The identifier is included as a prefix to the `User-Agent` and as +a "surface" tag in the parenthetical metadata. + +| Environment | User-Agent Prefix | Surface Tag | +| :---------------------------------- | :--------------------------- | :---------- | +| **Gemini Code Assist (Agent Mode)** | `GeminiCLI-a2a-server` | `vscode` | +| **Zed (via ACP)** | `GeminiCLI-acp-zed` | `zed` | +| **XCode (via ACP)** | `GeminiCLI-acp-xcode` | `xcode` | +| **IntelliJ IDEA (via ACP)** | `GeminiCLI-acp-intellijidea` | `jetbrains` | +| **Standard Terminal** | `GeminiCLI` | `terminal` | + +**Example User-Agent:** +`GeminiCLI-a2a-server/0.34.0/gemini-pro (linux; x64; vscode)` + +### Custom identification + +You can provide a custom identifier for your own scripts or automation by +setting the `GEMINI_CLI_SURFACE` environment variable. This is useful for +tracking specific internal tools or distribution channels in your GCP logs. + +**macOS/Linux** + +```bash +export GEMINI_CLI_SURFACE="my-custom-tool" +``` + +**Windows (PowerShell)** + +```powershell +$env:GEMINI_CLI_SURFACE="my-custom-tool" +``` + +When set, the value appears at the end of the `User-Agent` parenthetical: +`GeminiCLI/0.34.0/gemini-pro (linux; x64; my-custom-tool)` ## Logs, metrics, and traces -This section describes the structure of logs, metrics, and traces generated by -Gemini CLI. +The following section describes the structure of logs, metrics, and traces +generated for Gemini CLI. -Gemini CLI includes `session.id`, `installation.id`, `active_approval_mode`, and -`user.email` (when authenticated) as common attributes on all data. +The `session.id`, `installation.id`, `active_approval_mode`, and `user.email` +(available only when authenticated with a Google account) are included as common +attributes on all logs and metrics. ### Logs -Logs provide timestamped records of specific events. Gemini CLI logs events -across several categories. +Logs are timestamped records of specific events. The following events are logged +for Gemini CLI, grouped by category. #### Sessions -Session logs capture startup configuration and prompt submissions. +Captures startup configuration and user prompt submissions. -##### `gemini_cli.config` +- `gemini_cli.config`: Emitted once at startup with the CLI configuration. + - **Attributes**: + - `model` (string) + - `embedding_model` (string) + - `sandbox_enabled` (boolean) + - `core_tools_enabled` (string) + - `approval_mode` (string) + - `api_key_enabled` (boolean) + - `vertex_ai_enabled` (boolean) + - `log_user_prompts_enabled` (boolean) + - `file_filtering_respect_git_ignore` (boolean) + - `debug_mode` (boolean) + - `mcp_servers` (string) + - `mcp_servers_count` (int) + - `extensions` (string) + - `extension_ids` (string) + - `extension_count` (int) + - `mcp_tools` (string, if applicable) + - `mcp_tools_count` (int, if applicable) + - `output_format` ("text", "json", or "stream-json") -Emitted at startup with the CLI configuration. +- `gemini_cli.user_prompt`: Emitted when a user submits a prompt. + - **Attributes**: + - `prompt_length` (int) + - `prompt_id` (string) + - `prompt` (string; excluded if `telemetry.logPrompts` is `false`) + - `auth_type` (string) -
-Attributes +#### Approval Mode -- `model` (string) -- `embedding_model` (string) -- `sandbox_enabled` (boolean) -- `core_tools_enabled` (string) -- `approval_mode` (string) -- `api_key_enabled` (boolean) -- `vertex_ai_enabled` (boolean) -- `log_user_prompts_enabled` (boolean) -- `file_filtering_respect_git_ignore` (boolean) -- `debug_mode` (boolean) -- `mcp_servers` (string) -- `mcp_servers_count` (int) -- `mcp_tools` (string) -- `mcp_tools_count` (int) -- `output_format` (string) -- `extensions` (string) -- `extension_ids` (string) -- `extensions_count` (int) -- `auth_type` (string) -- `github_workflow_name` (string, optional) -- `github_repository_hash` (string, optional) -- `github_event_name` (string, optional) -- `github_pr_number` (string, optional) -- `github_issue_number` (string, optional) -- `github_custom_tracking_id` (string, optional) - -
- -##### `gemini_cli.user_prompt` - -Emitted when you submit a prompt. - -
-Attributes - -- `prompt_length` (int) -- `prompt_id` (string) -- `prompt` (string; excluded if `telemetry.logPrompts` is `false`) -- `auth_type` (string) - -
- -#### Approval mode - -These logs track changes to and usage of different approval modes. +Tracks changes and duration of approval modes. ##### Lifecycle -##### `approval_mode_switch` +- `approval_mode_switch`: Approval mode was changed. + - **Attributes**: + - `from_mode` (string) + - `to_mode` (string) -Logs when you change the approval mode. - -
-Attributes - -- `from_mode` (string) -- `to_mode` (string) - -
- -##### `approval_mode_duration` - -Records time spent in an approval mode. - -
-Attributes - -- `mode` (string) -- `duration_ms` (int) - -
+- `approval_mode_duration`: Duration spent in an approval mode. + - **Attributes**: + - `mode` (string) + - `duration_ms` (int) ##### Execution -##### `plan_execution` +These events track the execution of an approval mode, such as Plan Mode. -Logs when you execute a plan and switch from plan mode to active execution. - -
-Attributes - -- `approval_mode` (string) - -
+- `plan_execution`: A plan was executed and the session switched from plan mode + to active execution. + - **Attributes**: + - `approval_mode` (string) #### Tools -Tool logs capture executions, truncation, and edit behavior. +Captures tool executions, output truncation, and Edit behavior. -##### `gemini_cli.tool_call` +- `gemini_cli.tool_call`: Emitted for each tool (function) call. + - **Attributes**: + - `function_name` + - `function_args` + - `duration_ms` + - `success` (boolean) + - `decision` ("accept", "reject", "auto_accept", or "modify", if applicable) + - `error` (if applicable) + - `error_type` (if applicable) + - `prompt_id` (string) + - `tool_type` ("native" or "mcp") + - `mcp_server_name` (string, if applicable) + - `extension_name` (string, if applicable) + - `extension_id` (string, if applicable) + - `content_length` (int, if applicable) + - `metadata` (if applicable), which includes for the `AskUser` tool: + - `ask_user` (object): + - `question_types` (array of strings) + - `ask_user_dismissed` (boolean) + - `ask_user_empty_submission` (boolean) + - `ask_user_answer_count` (number) + - `diffStat` (if applicable), which includes: + - `model_added_lines` (number) + - `model_removed_lines` (number) + - `model_added_chars` (number) + - `model_removed_chars` (number) + - `user_added_lines` (number) + - `user_removed_lines` (number) + - `user_added_chars` (number) + - `user_removed_chars` (number) -Emitted for each tool (function) call. +- `gemini_cli.tool_output_truncated`: Output of a tool call was truncated. + - **Attributes**: + - `tool_name` (string) + - `original_content_length` (int) + - `truncated_content_length` (int) + - `threshold` (int) + - `lines` (int) + - `prompt_id` (string) -
-Attributes +- `gemini_cli.edit_strategy`: Edit strategy chosen. + - **Attributes**: + - `strategy` (string) -- `function_name` (string) -- `function_args` (string) -- `duration_ms` (int) -- `success` (boolean) -- `decision` (string: "accept", "reject", "auto_accept", or "modify") -- `error` (string, optional) -- `error_type` (string, optional) -- `prompt_id` (string) -- `tool_type` (string: "native" or "mcp") -- `mcp_server_name` (string, optional) -- `extension_name` (string, optional) -- `extension_id` (string, optional) -- `content_length` (int, optional) -- `start_time` (number, optional) -- `end_time` (number, optional) -- `metadata` (object, optional), which may include: - - `model_added_lines` (number) - - `model_removed_lines` (number) - - `user_added_lines` (number) - - `user_removed_lines` (number) - - `ask_user` (object) +- `gemini_cli.edit_correction`: Edit correction result. + - **Attributes**: + - `correction` ("success" | "failure") -
- -##### `gemini_cli.tool_output_truncated` - -Logs when tool output is truncated. - -
-Attributes - -- `tool_name` (string) -- `original_content_length` (int) -- `truncated_content_length` (int) -- `threshold` (int) -- `lines` (int) -- `prompt_id` (string) - -
- -##### `gemini_cli.edit_strategy` - -Records the chosen edit strategy. - -
-Attributes - -- `strategy` (string) - -
- -##### `gemini_cli.edit_correction` - -Records the result of an edit correction. - -
-Attributes - -- `correction` (string: "success" or "failure") - -
- -##### `gen_ai.client.inference.operation.details` - -Provides detailed GenAI operation data aligned with OpenTelemetry conventions. - -
-Attributes - -- `gen_ai.request.model` (string) -- `gen_ai.provider.name` (string) -- `gen_ai.operation.name` (string) -- `gen_ai.input.messages` (json string) -- `gen_ai.output.messages` (json string) -- `gen_ai.response.finish_reasons` (array of strings) -- `gen_ai.usage.input_tokens` (int) -- `gen_ai.usage.output_tokens` (int) -- `gen_ai.request.temperature` (float) -- `gen_ai.request.top_p` (float) -- `gen_ai.request.top_k` (int) -- `gen_ai.request.max_tokens` (int) -- `gen_ai.system_instructions` (json string) -- `server.address` (string) -- `server.port` (int) - -
+- `gen_ai.client.inference.operation.details`: This event provides detailed + information about the GenAI operation, aligned with [OpenTelemetry GenAI + semantic conventions for events]. + - **Attributes**: + - `gen_ai.request.model` (string) + - `gen_ai.provider.name` (string) + - `gen_ai.operation.name` (string) + - `gen_ai.input.messages` (json string) + - `gen_ai.output.messages` (json string) + - `gen_ai.response.finish_reasons` (array of strings) + - `gen_ai.usage.input_tokens` (int) + - `gen_ai.usage.output_tokens` (int) + - `gen_ai.request.temperature` (float) + - `gen_ai.request.top_p` (float) + - `gen_ai.request.top_k` (int) + - `gen_ai.request.max_tokens` (int) + - `gen_ai.system_instructions` (json string) + - `server.address` (string) + - `server.port` (int) #### Files -File logs track operations performed by tools. +Tracks file operations performed by tools. -##### `gemini_cli.file_operation` - -Emitted for each file creation, read, or update. - -
-Attributes - -- `tool_name` (string) -- `operation` (string: "create", "read", or "update") -- `lines` (int, optional) -- `mimetype` (string, optional) -- `extension` (string, optional) -- `programming_language` (string, optional) - -
+- `gemini_cli.file_operation`: Emitted for each file operation. + - **Attributes**: + - `tool_name` (string) + - `operation` ("create" | "read" | "update") + - `lines` (int, optional) + - `mimetype` (string, optional) + - `extension` (string, optional) + - `programming_language` (string, optional) #### API -API logs capture requests, responses, and errors from Gemini API. +Captures Gemini API requests, responses, and errors. -##### `gemini_cli.api_request` +- `gemini_cli.api_request`: Request sent to Gemini API. + - **Attributes**: + - `model` (string) + - `prompt_id` (string) + - `request_text` (string, optional) -Request sent to Gemini API. +- `gemini_cli.api_response`: Response received from Gemini API. + - **Attributes**: + - `model` (string) + - `status_code` (int|string) + - `duration_ms` (int) + - `input_token_count` (int) + - `output_token_count` (int) + - `cached_content_token_count` (int) + - `thoughts_token_count` (int) + - `tool_token_count` (int) + - `total_token_count` (int) + - `response_text` (string, optional) + - `prompt_id` (string) + - `auth_type` (string) + - `finish_reasons` (array of strings) -
-Attributes +- `gemini_cli.api_error`: API request failed. + - **Attributes**: + - `model` (string) + - `error` (string) + - `error_type` (string) + - `status_code` (int|string) + - `duration_ms` (int) + - `prompt_id` (string) + - `auth_type` (string) -- `model` (string) -- `prompt_id` (string) -- `role` (string: "user", "model", or "system") -- `request_text` (string, optional) - -
- -##### `gemini_cli.api_response` - -Response received from Gemini API. - -
-Attributes - -- `model` (string) -- `status_code` (int or string) -- `duration_ms` (int) -- `input_token_count` (int) -- `output_token_count` (int) -- `cached_content_token_count` (int) -- `thoughts_token_count` (int) -- `tool_token_count` (int) -- `total_token_count` (int) -- `prompt_id` (string) -- `auth_type` (string) -- `finish_reasons` (array of strings) -- `response_text` (string, optional) - -
- -##### `gemini_cli.api_error` - -Logs when an API request fails. - -
-Attributes - -- `error.message` (string) -- `model_name` (string) -- `duration` (int) -- `prompt_id` (string) -- `auth_type` (string) -- `error_type` (string, optional) -- `status_code` (int or string, optional) -- `role` (string, optional) - -
- -##### `gemini_cli.malformed_json_response` - -Logs when a JSON response cannot be parsed. - -
-Attributes - -- `model` (string) - -
+- `gemini_cli.malformed_json_response`: `generateJson` response could not be + parsed. + - **Attributes**: + - `model` (string) #### Model routing -These logs track how Gemini CLI selects and routes requests to models. +- `gemini_cli.slash_command`: A slash command was executed. + - **Attributes**: + - `command` (string) + - `subcommand` (string, optional) + - `status` ("success" | "error") -##### `gemini_cli.slash_command` +- `gemini_cli.slash_command.model`: Model was selected via slash command. + - **Attributes**: + - `model_name` (string) -Logs slash command execution. - -
-Attributes - -- `command` (string) -- `subcommand` (string, optional) -- `status` (string: "success" or "error") - -
- -##### `gemini_cli.slash_command.model` - -Logs model selection via slash command. - -
-Attributes - -- `model_name` (string) - -
- -##### `gemini_cli.model_routing` - -Records model router decisions and reasoning. - -
-Attributes - -- `decision_model` (string) -- `decision_source` (string) -- `routing_latency_ms` (int) -- `reasoning` (string, optional) -- `failed` (boolean) -- `error_message` (string, optional) -- `approval_mode` (string) - -
+- `gemini_cli.model_routing`: Model router made a decision. + - **Attributes**: + - `decision_model` (string) + - `decision_source` (string) + - `routing_latency_ms` (int) + - `reasoning` (string, optional) + - `failed` (boolean) + - `error_message` (string, optional) + - `approval_mode` (string) #### Chat and streaming -These logs track chat context compression and streaming chunk errors. +- `gemini_cli.chat_compression`: Chat context was compressed. + - **Attributes**: + - `tokens_before` (int) + - `tokens_after` (int) -##### `gemini_cli.chat_compression` +- `gemini_cli.chat.invalid_chunk`: Invalid chunk received from a stream. + - **Attributes**: + - `error.message` (string, optional) -Logs chat context compression events. +- `gemini_cli.chat.content_retry`: Retry triggered due to a content error. + - **Attributes**: + - `attempt_number` (int) + - `error_type` (string) + - `retry_delay_ms` (int) + - `model` (string) -
-Attributes +- `gemini_cli.chat.content_retry_failure`: All content retries failed. + - **Attributes**: + - `total_attempts` (int) + - `final_error_type` (string) + - `total_duration_ms` (int, optional) + - `model` (string) -- `tokens_before` (int) -- `tokens_after` (int) +- `gemini_cli.conversation_finished`: Conversation session ended. + - **Attributes**: + - `approvalMode` (string) + - `turnCount` (int) -
- -##### `gemini_cli.chat.invalid_chunk` - -Logs invalid chunks received in a stream. - -
-Attributes - -- `error_message` (string, optional) - -
- -##### `gemini_cli.chat.content_retry` - -Logs retries due to content errors. - -
-Attributes - -- `attempt_number` (int) -- `error_type` (string) -- `retry_delay_ms` (int) -- `model` (string) - -
- -##### `gemini_cli.chat.content_retry_failure` - -Logs when all content retries fail. - -
-Attributes - -- `total_attempts` (int) -- `final_error_type` (string) -- `total_duration_ms` (int, optional) -- `model` (string) - -
- -##### `gemini_cli.conversation_finished` - -Logs when a conversation session ends. - -
-Attributes - -- `approvalMode` (string) -- `turnCount` (int) - -
+- `gemini_cli.next_speaker_check`: Next speaker determination. + - **Attributes**: + - `prompt_id` (string) + - `finish_reason` (string) + - `result` (string) #### Resilience -Resilience logs record fallback mechanisms and recovery attempts. +Records fallback mechanisms for models and network operations. -##### `gemini_cli.flash_fallback` +- `gemini_cli.flash_fallback`: Switched to a flash model as fallback. + - **Attributes**: + - `auth_type` (string) -Logs switch to a flash model fallback. +- `gemini_cli.ripgrep_fallback`: Switched to grep as fallback for file search. + - **Attributes**: + - `error` (string, optional) -
-Attributes - -- `auth_type` (string) - -
- -##### `gemini_cli.ripgrep_fallback` - -Logs fallback to standard grep. - -
-Attributes - -- `error` (string, optional) - -
- -##### `gemini_cli.web_fetch_fallback_attempt` - -Logs web-fetch fallback attempts. - -
-Attributes - -- `reason` (string: "private_ip" or "primary_failed") - -
- -##### `gemini_cli.agent.recovery_attempt` - -Logs attempts to recover from agent errors. - -
-Attributes - -- `agent_name` (string) -- `attempt_number` (int) -- `success` (boolean) -- `error_type` (string, optional) - -
+- `gemini_cli.web_fetch_fallback_attempt`: Attempted web-fetch fallback. + - **Attributes**: + - `reason` ("private_ip" | "primary_failed") #### Extensions -Extension logs track lifecycle events and settings changes. +Tracks extension lifecycle and settings changes. -##### `gemini_cli.extension_install` +- `gemini_cli.extension_install`: An extension was installed. + - **Attributes**: + - `extension_name` (string) + - `extension_version` (string) + - `extension_source` (string) + - `status` (string) -Logs when you install an extension. +- `gemini_cli.extension_uninstall`: An extension was uninstalled. + - **Attributes**: + - `extension_name` (string) + - `status` (string) -
-Attributes +- `gemini_cli.extension_enable`: An extension was enabled. + - **Attributes**: + - `extension_name` (string) + - `setting_scope` (string) -- `extension_name` (string) -- `extension_version` (string) -- `extension_source` (string) -- `status` (string) +- `gemini_cli.extension_disable`: An extension was disabled. + - **Attributes**: + - `extension_name` (string) + - `setting_scope` (string) -
- -##### `gemini_cli.extension_uninstall` - -Logs when you uninstall an extension. - -
-Attributes - -- `extension_name` (string) -- `status` (string) - -
- -##### `gemini_cli.extension_enable` - -Logs when you enable an extension. - -
-Attributes - -- `extension_name` (string) -- `setting_scope` (string) - -
- -##### `gemini_cli.extension_disable` - -Logs when you disable an extension. - -
-Attributes - -- `extension_name` (string) -- `setting_scope` (string) - -
+- `gemini_cli.extension_update`: An extension was updated. + - **Attributes**: + - `extension_name` (string) + - `extension_version` (string) + - `extension_previous_version` (string) + - `extension_source` (string) + - `status` (string) #### Agent runs -Agent logs track the lifecycle of agent executions. +- `gemini_cli.agent.start`: Agent run started. + - **Attributes**: + - `agent_id` (string) + - `agent_name` (string) -##### `gemini_cli.agent.start` - -Logs when an agent run begins. - -
-Attributes - -- `agent_id` (string) -- `agent_name` (string) - -
- -##### `gemini_cli.agent.finish` - -Logs when an agent run completes. - -
-Attributes - -- `agent_id` (string) -- `agent_name` (string) -- `duration_ms` (int) -- `turn_count` (int) -- `terminate_reason` (string) - -
+- `gemini_cli.agent.finish`: Agent run finished. + - **Attributes**: + - `agent_id` (string) + - `agent_name` (string) + - `duration_ms` (int) + - `turn_count` (int) + - `terminate_reason` (string) #### IDE -IDE logs capture connectivity events for the IDE companion. +Captures IDE connectivity and conversation lifecycle events. -##### `gemini_cli.ide_connection` - -Logs IDE companion connections. - -
-Attributes - -- `connection_type` (string) - -
+- `gemini_cli.ide_connection`: IDE companion connection. + - **Attributes**: + - `connection_type` (string) #### UI -UI logs track terminal rendering issues. +Tracks terminal rendering issues and related signals. -##### `kitty_sequence_overflow` - -Logs terminal control sequence overflows. - -
-Attributes - -- `sequence_length` (int) -- `truncated_sequence` (string) - -
- -#### Miscellaneous - -##### `gemini_cli.rewind` - -Logs when the conversation state is rewound. - -
-Attributes - -- `outcome` (string) - -
- -##### `gemini_cli.conseca.verdict` - -Logs security verdicts from ConSeca. - -
-Attributes - -- `verdict` (string) -- `decision` (string: "accept", "reject", or "modify") -- `reason` (string, optional) -- `tool_name` (string, optional) - -
- -##### `gemini_cli.hook_call` - -Logs execution of lifecycle hooks. - -
-Attributes - -- `hook_name` (string) -- `hook_type` (string) -- `duration_ms` (int) -- `success` (boolean) - -
- -##### `gemini_cli.tool_output_masking` - -Logs when tool output is masked for privacy. - -
-Attributes - -- `tokens_before` (int) -- `tokens_after` (int) -- `masked_count` (int) -- `total_prunable_tokens` (int) - -
- -##### `gemini_cli.keychain.availability` - -Logs keychain availability checks. - -
-Attributes - -- `available` (boolean) - -
+- `kitty_sequence_overflow`: Terminal kitty control sequence overflow. + - **Attributes**: + - `sequence_length` (int) + - `truncated_sequence` (string) ### Metrics -Metrics provide numerical measurements of behavior over time. +Metrics are numerical measurements of behavior over time. -#### Custom metrics - -Gemini CLI exports several custom metrics. +#### Custom ##### Sessions -##### `gemini_cli.session.count` +Counts CLI sessions at startup. -Incremented once per CLI startup. +- `gemini_cli.session.count` (Counter, Int): Incremented once per CLI startup. ##### Tools -##### `gemini_cli.tool.call.count` +Measures tool usage and latency. -Counts tool calls. +- `gemini_cli.tool.call.count` (Counter, Int): Counts tool calls. + - **Attributes**: + - `function_name` + - `success` (boolean) + - `decision` (string: "accept", "reject", "modify", or "auto_accept", if + applicable) + - `tool_type` (string: "mcp" or "native", if applicable) -
-Attributes - -- `function_name` (string) -- `success` (boolean) -- `decision` (string: "accept", "reject", "modify", or "auto_accept") -- `tool_type` (string: "mcp" or "native") - -
- -##### `gemini_cli.tool.call.latency` - -Measures tool call latency (in ms). - -
-Attributes - -- `function_name` (string) - -
+- `gemini_cli.tool.call.latency` (Histogram, ms): Measures tool call latency. + - **Attributes**: + - `function_name` ##### API -##### `gemini_cli.api.request.count` +Tracks API request volume and latency. -Counts all API requests. +- `gemini_cli.api.request.count` (Counter, Int): Counts all API requests. + - **Attributes**: + - `model` + - `status_code` + - `error_type` (if applicable) -
-Attributes - -- `model` (string) -- `status_code` (int or string) -- `error_type` (string, optional) - -
- -##### `gemini_cli.api.request.latency` - -Measures API request latency (in ms). - -
-Attributes - -- `model` (string) - -
+- `gemini_cli.api.request.latency` (Histogram, ms): Measures API request + latency. + - **Attributes**: + - `model` + - Note: Overlaps with `gen_ai.client.operation.duration` (GenAI conventions). ##### Token usage -##### `gemini_cli.token.usage` +Tracks tokens used by model and type. -Counts input, output, thought, cache, and tool tokens. - -
-Attributes - -- `model` (string) -- `type` (string: "input", "output", "thought", "cache", or "tool") - -
+- `gemini_cli.token.usage` (Counter, Int): Counts tokens used. + - **Attributes**: + - `model` + - `type` ("input", "output", "thought", "cache", or "tool") + - Note: Overlaps with `gen_ai.client.token.usage` for `input`/`output`. ##### Files -##### `gemini_cli.file.operation.count` +Counts file operations with basic context. -Counts file operations. +- `gemini_cli.file.operation.count` (Counter, Int): Counts file operations. + - **Attributes**: + - `operation` ("create", "read", "update") + - `lines` (Int, optional) + - `mimetype` (string, optional) + - `extension` (string, optional) + - `programming_language` (string, optional) -
-Attributes - -- `operation` (string: "create", "read", or "update") -- `lines` (int, optional) -- `mimetype` (string, optional) -- `extension` (string, optional) -- `programming_language` (string, optional) - -
- -##### `gemini_cli.lines.changed` - -Counts added or removed lines. - -
-Attributes - -- `function_name` (string, optional) -- `type` (string: "added" or "removed") - -
+- `gemini_cli.lines.changed` (Counter, Int): Number of lines changed (from file + diffs). + - **Attributes**: + - `function_name` + - `type` ("added" or "removed") ##### Chat and streaming -##### `gemini_cli.chat_compression` +Resilience counters for compression, invalid chunks, and retries. -Counts compression operations. +- `gemini_cli.chat_compression` (Counter, Int): Counts chat compression + operations. + - **Attributes**: + - `tokens_before` (Int) + - `tokens_after` (Int) -
-Attributes +- `gemini_cli.chat.invalid_chunk.count` (Counter, Int): Counts invalid chunks + from streams. -- `tokens_before` (int) -- `tokens_after` (int) +- `gemini_cli.chat.content_retry.count` (Counter, Int): Counts retries due to + content errors. -
- -##### `gemini_cli.chat.invalid_chunk.count` - -Counts invalid stream chunks. - -##### `gemini_cli.chat.content_retry.count` - -Counts content error retries. - -##### `gemini_cli.chat.content_retry_failure.count` - -Counts requests where all retries failed. +- `gemini_cli.chat.content_retry_failure.count` (Counter, Int): Counts requests + where all content retries failed. ##### Model routing -##### `gemini_cli.slash_command.model.call_count` +Routing latency/failures and slash-command selections. -Counts model selections. +- `gemini_cli.slash_command.model.call_count` (Counter, Int): Counts model + selections via slash command. + - **Attributes**: + - `slash_command.model.model_name` (string) -
-Attributes +- `gemini_cli.model_routing.latency` (Histogram, ms): Model routing decision + latency. + - **Attributes**: + - `routing.decision_model` (string) + - `routing.decision_source` (string) + - `routing.approval_mode` (string) -- `slash_command.model.model_name` (string) - -
- -##### `gemini_cli.model_routing.latency` - -Measures routing decision latency. - -
-Attributes - -- `routing.decision_model` (string) -- `routing.decision_source` (string) -- `routing.approval_mode` (string) - -
- -##### `gemini_cli.model_routing.failure.count` - -Counts routing failures. - -
-Attributes - -- `routing.decision_source` (string) -- `routing.error_message` (string) -- `routing.approval_mode` (string) - -
+- `gemini_cli.model_routing.failure.count` (Counter, Int): Counts model routing + failures. + - **Attributes**: + - `routing.decision_source` (string) + - `routing.error_message` (string) + - `routing.approval_mode` (string) ##### Agent runs -##### `gemini_cli.agent.run.count` +Agent lifecycle metrics: runs, durations, and turns. -Counts agent runs. +- `gemini_cli.agent.run.count` (Counter, Int): Counts agent runs. + - **Attributes**: + - `agent_name` (string) + - `terminate_reason` (string) -
-Attributes +- `gemini_cli.agent.duration` (Histogram, ms): Agent run durations. + - **Attributes**: + - `agent_name` (string) -- `agent_name` (string) -- `terminate_reason` (string) +- `gemini_cli.agent.turns` (Histogram, turns): Turns taken per agent run. + - **Attributes**: + - `agent_name` (string) -
+##### Approval Mode -##### `gemini_cli.agent.duration` +###### Execution -Measures agent run duration. +These metrics track the adoption and usage of specific approval workflows, such +as Plan Mode. -
-Attributes - -- `agent_name` (string) - -
- -##### `gemini_cli.agent.turns` - -Counts turns per agent run. - -
-Attributes - -- `agent_name` (string) - -
- -##### Approval mode - -##### `gemini_cli.plan.execution.count` - -Counts plan executions. - -
-Attributes - -- `approval_mode` (string) - -
+- `gemini_cli.plan.execution.count` (Counter, Int): Counts plan executions. + - **Attributes**: + - `approval_mode` (string) ##### UI -##### `gemini_cli.ui.flicker.count` +UI stability signals such as flicker count. -Counts terminal flicker events. +- `gemini_cli.ui.flicker.count` (Counter, Int): Counts UI frames that flicker + (render taller than terminal). ##### Performance -Gemini CLI provides detailed performance metrics for advanced monitoring. +Optional performance monitoring for startup, CPU/memory, and phase timing. -##### `gemini_cli.startup.duration` +- `gemini_cli.startup.duration` (Histogram, ms): CLI startup time by phase. + - **Attributes**: + - `phase` (string) + - `details` (map, optional) -Measures startup time by phase. +- `gemini_cli.memory.usage` (Histogram, bytes): Memory usage. + - **Attributes**: + - `memory_type` ("heap_used", "heap_total", "external", "rss") + - `component` (string, optional) -
-Attributes +- `gemini_cli.cpu.usage` (Histogram, percent): CPU usage percentage. + - **Attributes**: + - `component` (string, optional) -- `phase` (string) -- `details` (map, optional) +- `gemini_cli.tool.queue.depth` (Histogram, count): Number of tools in the + execution queue. -
+- `gemini_cli.tool.execution.breakdown` (Histogram, ms): Tool time by phase. + - **Attributes**: + - `function_name` (string) + - `phase` ("validation", "preparation", "execution", "result_processing") -##### `gemini_cli.memory.usage` +- `gemini_cli.api.request.breakdown` (Histogram, ms): API request time by phase. + - **Attributes**: + - `model` (string) + - `phase` ("request_preparation", "network_latency", "response_processing", + "token_processing") -Measures heap and RSS memory. +- `gemini_cli.token.efficiency` (Histogram, ratio): Token efficiency metrics. + - **Attributes**: + - `model` (string) + - `metric` (string) + - `context` (string, optional) -
-Attributes +- `gemini_cli.performance.score` (Histogram, score): Composite performance + score. + - **Attributes**: + - `category` (string) + - `baseline` (number, optional) -- `memory_type` (string: "heap_used", "heap_total", "external", "rss") -- `component` (string, optional) +- `gemini_cli.performance.regression` (Counter, Int): Regression detection + events. + - **Attributes**: + - `metric` (string) + - `severity` ("low", "medium", "high") + - `current_value` (number) + - `baseline_value` (number) -
+- `gemini_cli.performance.regression.percentage_change` (Histogram, percent): + Percent change from baseline when regression detected. + - **Attributes**: + - `metric` (string) + - `severity` ("low", "medium", "high") + - `current_value` (number) + - `baseline_value` (number) -##### `gemini_cli.cpu.usage` - -Measures CPU usage percentage. - -
-Attributes - -- `component` (string, optional) - -
- -##### `gemini_cli.tool.queue.depth` - -Measures tool execution queue depth. - -##### `gemini_cli.tool.execution.breakdown` - -Breaks down tool time by phase. - -
-Attributes - -- `function_name` (string) -- `phase` (string: "validation", "preparation", "execution", - "result_processing") - -
- -#### GenAI semantic convention - -These metrics follow standard [OpenTelemetry GenAI semantic conventions]. - -- `gen_ai.client.token.usage`: Counts tokens used per operation. -- `gen_ai.client.operation.duration`: Measures operation duration in seconds. - -[OpenTelemetry GenAI semantic conventions]: - https://github.com/open-telemetry/semantic-conventions/blob/main/docs/gen-ai/gen-ai-metrics.md +- `gemini_cli.performance.baseline.comparison` (Histogram, percent): Comparison + to baseline. + - **Attributes**: + - `metric` (string) + - `category` (string) + - `current_value` (number) + - `baseline_value` (number) ### Traces -Traces provide an "under-the-hood" view of agent and backend operations. Use -traces to debug tool interactions and optimize performance. +Traces offer a granular, "under-the-hood" view of every agent and backend +operation. By providing a high-fidelity execution map, they enable precise +debugging of complex tool interactions and deep performance optimization. Each +trace captures rich, consistent metadata via custom span attributes: -Every trace captures rich metadata via standard span attributes. +- `gen_ai.operation.name` (string): The high-level operation kind (e.g. + "tool_call", "llm_call"). +- `gen_ai.agent.name` (string): The service agent identifier ("gemini-cli"). +- `gen_ai.agent.description` (string): The service agent description. +- `gen_ai.input.messages` (string): Input messages or metadata specific to the + operation. +- `gen_ai.output.messages` (string): Output messages or metadata generated from + the operation. +- `gen_ai.request.model` (string): The request model name. +- `gen_ai.response.model` (string): The response model name. +- `gen_ai.system_instructions` (json string): The system instructions. +- `gen_ai.prompt.name` (string): The prompt name. +- `gen_ai.tool.name` (string): The executed tool's name. +- `gen_ai.tool.call_id` (string): The generated specific ID of the tool call. +- `gen_ai.tool.description` (string): The executed tool's description. +- `gen_ai.tool.definitions` (json string): The executed tool's description. +- `gen_ai.conversation.id` (string): The current CLI session ID. +- Additional user-defined Custom Attributes passed via the span's configuration. -
-Standard span attributes +#### GenAI semantic convention -- `gen_ai.operation.name`: High-level operation (for example, `tool_call`, - `llm_call`, `user_prompt`, `system_prompt`, `agent_call`, or - `schedule_tool_calls`). -- `gen_ai.agent.name`: Set to `gemini-cli`. -- `gen_ai.agent.description`: The service agent description. -- `gen_ai.input.messages`: Input data or metadata. -- `gen_ai.output.messages`: Output data or results. -- `gen_ai.request.model`: Request model name. -- `gen_ai.response.model`: Response model name. -- `gen_ai.prompt.name`: The prompt name. -- `gen_ai.tool.name`: Executed tool name. -- `gen_ai.tool.call_id`: Unique ID for the tool call. -- `gen_ai.tool.description`: Tool description. -- `gen_ai.tool.definitions`: Tool definitions in JSON format. -- `gen_ai.usage.input_tokens`: Number of input tokens. -- `gen_ai.usage.output_tokens`: Number of output tokens. -- `gen_ai.system_instructions`: System instructions in JSON format. -- `gen_ai.conversation.id`: The CLI session ID. +The following metrics comply with [OpenTelemetry GenAI semantic conventions] for +standardized observability across GenAI applications: -
+- `gen_ai.client.token.usage` (Histogram, token): Number of input and output + tokens used per operation. + - **Attributes**: + - `gen_ai.operation.name` (string): The operation type (e.g., + "generate_content", "chat") + - `gen_ai.provider.name` (string): The GenAI provider ("gcp.gen_ai" or + "gcp.vertex_ai") + - `gen_ai.token.type` (string): The token type ("input" or "output") + - `gen_ai.request.model` (string, optional): The model name used for the + request + - `gen_ai.response.model` (string, optional): The model name that generated + the response + - `server.address` (string, optional): GenAI server address + - `server.port` (int, optional): GenAI server port -For more details on semantic conventions for events, see the -[OpenTelemetry documentation](https://github.com/open-telemetry/semantic-conventions/blob/8b4f210f43136e57c1f6f47292eb6d38e3bf30bb/docs/gen-ai/gen-ai-events.md). +- `gen_ai.client.operation.duration` (Histogram, s): GenAI operation duration in + seconds. + - **Attributes**: + - `gen_ai.operation.name` (string): The operation type (e.g., + "generate_content", "chat") + - `gen_ai.provider.name` (string): The GenAI provider ("gcp.gen_ai" or + "gcp.vertex_ai") + - `gen_ai.request.model` (string, optional): The model name used for the + request + - `gen_ai.response.model` (string, optional): The model name that generated + the response + - `server.address` (string, optional): GenAI server address + - `server.port` (int, optional): GenAI server port + - `error.type` (string, optional): Error type if the operation failed + +[OpenTelemetry GenAI semantic conventions]: + https://github.com/open-telemetry/semantic-conventions/blob/main/docs/gen-ai/gen-ai-metrics.md +[OpenTelemetry GenAI semantic conventions for events]: + https://github.com/open-telemetry/semantic-conventions/blob/8b4f210f43136e57c1f6f47292eb6d38e3bf30bb/docs/gen-ai/gen-ai-events.md diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index 55571678de..ef95e1727e 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -146,7 +146,7 @@ their corresponding top-level category object in your `settings.json` file. - **`general.retryFetchErrors`** (boolean): - **Description:** Retry on "exception TypeError: fetch failed sending request" errors. - - **Default:** `true` + - **Default:** `false` - **`general.maxAttempts`** (number): - **Description:** Maximum number of attempts for requests to the main chat @@ -297,7 +297,7 @@ their corresponding top-level category object in your `settings.json` file. - **Default:** `false` - **`ui.showUserIdentity`** (boolean): - - **Description:** Show the signed-in user's identity (e.g. email) in the UI. + - **Description:** Show the logged-in user's identity (e.g. email) in the UI. - **Default:** `true` - **`ui.useAlternateBuffer`** (boolean): @@ -872,11 +872,6 @@ their corresponding top-level category object in your `settings.json` file. confirmation dialogs. - **Default:** `false` -- **`security.autoAddToPolicyByDefault`** (boolean): - - **Description:** When enabled, the "Allow for all future sessions" option - becomes the default choice for low-risk tools in trusted workspaces. - - **Default:** `false` - - **`security.blockGitExtensions`** (boolean): - **Description:** Blocks installing and loading extensions from Git. - **Default:** `false` @@ -1003,12 +998,6 @@ their corresponding top-level category object in your `settings.json` file. - **Default:** `false` - **Requires restart:** Yes -- **`experimental.extensionRegistryURI`** (string): - - **Description:** The URI (web URL or local file path) of the extension - registry. - - **Default:** `"https://geminicli.com/extensions.json"` - - **Requires restart:** Yes - - **`experimental.extensionReloading`** (boolean): - **Description:** Enables extension loading/unloading within the CLI session. - **Default:** `false` @@ -1182,20 +1171,13 @@ their corresponding top-level category object in your `settings.json` file. Configures connections to one or more Model-Context Protocol (MCP) servers for discovering and using custom tools. Gemini CLI attempts to connect to each -configured MCP server to discover available tools. Every discovered tool is -prepended with the `mcp_` prefix and its server alias to form a fully qualified -name (FQN) (e.g., `mcp_serverAlias_actualToolName`) to avoid conflicts. Note -that the system might strip certain schema properties from MCP tool definitions -for compatibility. At least one of `command`, `url`, or `httpUrl` must be -provided. If multiple are specified, the order of precedence is `httpUrl`, then -`url`, then `command`. - -> **Warning:** Avoid using underscores (`_`) in your server aliases (e.g., use -> `my-server` instead of `my_server`). The underlying policy engine parses Fully -> Qualified Names (`mcp_server_tool`) using the first underscore after the -> `mcp_` prefix. An underscore in your server alias will cause the parser to -> misidentify the server name, which can cause security policies to fail -> silently. +configured MCP server to discover available tools. If multiple MCP servers +expose a tool with the same name, the tool names will be prefixed with the +server alias you defined in the configuration (e.g., +`serverAlias__actualToolName`) to avoid conflicts. Note that the system might +strip certain schema properties from MCP tool definitions for compatibility. At +least one of `command`, `url`, or `httpUrl` must be provided. If multiple are +specified, the order of precedence is `httpUrl`, then `url`, then `command`. - **`mcpServers.`** (object): The server parameters for the named server. @@ -1376,6 +1358,13 @@ the `advanced.excludedEnvVars` setting in your `settings.json` file. - Useful for shared compute environments or keeping CLI state isolated. - Example: `export GEMINI_CLI_HOME="/path/to/user/config"` (Windows PowerShell: `$env:GEMINI_CLI_HOME="C:\path\to\user\config"`) +- **`GEMINI_CLI_SURFACE`**: + - Specifies a custom label to include in the `User-Agent` header for API + traffic reporting. + - This is useful for tracking specific internal tools or distribution + channels. + - Example: `export GEMINI_CLI_SURFACE="my-custom-tool"` (Windows PowerShell: + `$env:GEMINI_CLI_SURFACE="my-custom-tool"`) - **`GOOGLE_API_KEY`**: - Your Google Cloud API key. - Required for using Vertex AI in express mode. diff --git a/packages/a2a-server/src/config/config.test.ts b/packages/a2a-server/src/config/config.test.ts index ee63df36f7..0300f101d1 100644 --- a/packages/a2a-server/src/config/config.test.ts +++ b/packages/a2a-server/src/config/config.test.ts @@ -4,64 +4,25 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { + describe, + it, + expect, + vi, + beforeEach, + afterEach, + type MockInstance, +} from 'vitest'; import * as path from 'node:path'; import { loadConfig } from './config.js'; import type { Settings } from './settings.js'; -import { - type ExtensionLoader, - FileDiscoveryService, - getCodeAssistServer, - Config, - ExperimentFlags, - fetchAdminControlsOnce, - type FetchAdminControlsResponse, - AuthType, - isHeadlessMode, - FatalAuthenticationError, -} from '@google/gemini-cli-core'; -// Mock dependencies -vi.mock('@google/gemini-cli-core', async (importOriginal) => { - const actual = - await importOriginal(); - return { - ...actual, - Config: vi.fn().mockImplementation((params) => { - const mockConfig = { - ...params, - initialize: vi.fn(), - waitForMcpInit: vi.fn(), - refreshAuth: vi.fn(), - getExperiments: vi.fn().mockReturnValue({ - flags: { - [actual.ExperimentFlags.ENABLE_ADMIN_CONTROLS]: { - boolValue: false, - }, - }, - }), - getRemoteAdminSettings: vi.fn(), - setRemoteAdminSettings: vi.fn(), - }; - return mockConfig; - }), - loadServerHierarchicalMemory: vi.fn().mockResolvedValue({ - memoryContent: { global: '', extension: '', project: '' }, - fileCount: 0, - filePaths: [], - }), - startupProfiler: { - flush: vi.fn(), - }, - isHeadlessMode: vi.fn().mockReturnValue(false), - FileDiscoveryService: vi.fn(), - getCodeAssistServer: vi.fn(), - fetchAdminControlsOnce: vi.fn(), - coreEvents: { - emitAdminSettingsChanged: vi.fn(), - }, - }; -}); +import { + fetchAdminControlsOnce, + Config, + type FetchAdminControlsResponse, + type ExtensionLoader, +} from '@google/gemini-cli-core'; vi.mock('../utils/logger.js', () => ({ logger: { @@ -71,9 +32,84 @@ vi.mock('../utils/logger.js', () => ({ }, })); +interface MockConfigParams { + clientName?: string; + allowedTools?: string[]; + approvalMode?: string; +} + +// Mock dependencies +vi.mock('@google/gemini-cli-core', async (importOriginal) => { + const actual = + await importOriginal(); + + const mockConfigCtor = vi.fn().mockImplementation(function ( + this: Record, + params: MockConfigParams, + ) { + Object.assign(this, params); + this['initialize'] = vi.fn().mockResolvedValue(undefined); + this['waitForMcpInit'] = vi.fn().mockResolvedValue(undefined); + this['refreshAuth'] = vi.fn().mockResolvedValue(undefined); + this['getExperiments'] = vi.fn().mockReturnValue({ + flags: { + ['enable_admin_controls']: { + boolValue: false, + }, + }, + }); + this['getRemoteAdminSettings'] = vi.fn().mockReturnValue({}); + this['setRemoteAdminSettings'] = vi.fn(); + this['getModel'] = vi.fn().mockReturnValue('gemini-2.0-flash'); + this['getUserTier'] = vi.fn().mockReturnValue('free'); + this['getClientName'] = vi.fn().mockReturnValue(params.clientName); + this['getAllowedTools'] = vi + .fn() + .mockReturnValue(params.allowedTools || []); + this['getApprovalMode'] = vi + .fn() + .mockReturnValue(params.approvalMode || 'default'); + return this as unknown as Config; + }); + + return { + ...actual, + PREVIEW_GEMINI_MODEL: 'gemini-2.0-flash', + DEFAULT_GEMINI_EMBEDDING_MODEL: 'text-embedding-004', + ApprovalMode: { + DEFAULT: 'default', + YOLO: 'yolo', + }, + AuthType: { + USE_GEMINI: 'use_gemini', + LOGIN_WITH_GOOGLE: 'login_with_google', + COMPUTE_ADC: 'compute_adc', + }, + ExperimentFlags: { + ENABLE_ADMIN_CONTROLS: 'enable_admin_controls', + }, + Config: mockConfigCtor, + loadServerHierarchicalMemory: vi.fn().mockResolvedValue({ + memoryContent: { global: '', extension: '', project: '' }, + fileCount: 0, + filePaths: [], + }), + startupProfiler: { + flush: vi.fn(), + }, + isHeadlessMode: vi.fn().mockReturnValue(false), + FileDiscoveryService: vi.fn().mockImplementation(() => ({})), + getCodeAssistServer: vi.fn(), + fetchAdminControlsOnce: vi.fn(), + coreEvents: { + emitAdminSettingsChanged: vi.fn(), + }, + }; +}); + describe('loadConfig', () => { const mockSettings = {} as Settings; - const mockExtensionLoader = {} as ExtensionLoader; + const mockExtensionLoader = {} as unknown as ExtensionLoader; const taskId = 'test-task-id'; beforeEach(() => { @@ -91,32 +127,44 @@ describe('loadConfig', () => { expect(fetchAdminControlsOnce).not.toHaveBeenCalled(); }); + it('should set clientName to a2a-server in config', async () => { + await loadConfig(mockSettings, mockExtensionLoader, taskId); + expect(Config).toHaveBeenCalledWith( + expect.objectContaining({ + clientName: 'a2a-server', + }), + ); + }); + describe('when admin controls experiment is enabled', () => { beforeEach(() => { - // We need to cast to any here to modify the mock implementation - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (Config as any).mockImplementation((params: unknown) => { - const mockConfig = { - ...(params as object), - initialize: vi.fn(), - waitForMcpInit: vi.fn(), - refreshAuth: vi.fn(), - getExperiments: vi.fn().mockReturnValue({ - flags: { - [ExperimentFlags.ENABLE_ADMIN_CONTROLS]: { - boolValue: true, - }, + const mockConfig = Config as unknown as MockInstance; + mockConfig.mockImplementation(function ( + this: Record, + params: MockConfigParams, + ) { + Object.assign(this, params); + this['initialize'] = vi.fn().mockResolvedValue(undefined); + this['waitForMcpInit'] = vi.fn().mockResolvedValue(undefined); + this['refreshAuth'] = vi.fn().mockResolvedValue(undefined); + this['getExperiments'] = vi.fn().mockReturnValue({ + flags: { + ['enable_admin_controls']: { + boolValue: true, }, - }), - getRemoteAdminSettings: vi.fn().mockReturnValue({}), - setRemoteAdminSettings: vi.fn(), - }; - return mockConfig; + }, + }); + this['getRemoteAdminSettings'] = vi.fn().mockReturnValue({}); + this['setRemoteAdminSettings'] = vi.fn(); + this['getModel'] = vi.fn().mockReturnValue('gemini-2.0-flash'); + this['getUserTier'] = vi.fn().mockReturnValue('free'); + this['getClientName'] = vi.fn().mockReturnValue(params.clientName); + return this as unknown as Config; }); }); it('should fetch admin controls and apply them', async () => { - const mockAdminSettings: FetchAdminControlsResponse = { + const mockAdminSettings: Partial = { mcpSetting: { mcpEnabled: false, }, @@ -127,7 +175,9 @@ describe('loadConfig', () => { }, strictModeDisabled: false, }; - vi.mocked(fetchAdminControlsOnce).mockResolvedValue(mockAdminSettings); + vi.mocked(fetchAdminControlsOnce).mockResolvedValue( + mockAdminSettings as FetchAdminControlsResponse, + ); await loadConfig(mockSettings, mockExtensionLoader, taskId); @@ -141,137 +191,20 @@ describe('loadConfig', () => { }), ); }); - - it('should treat unset admin settings as false when admin settings are passed', async () => { - const mockAdminSettings: FetchAdminControlsResponse = { - mcpSetting: { - mcpEnabled: true, - }, - }; - vi.mocked(fetchAdminControlsOnce).mockResolvedValue(mockAdminSettings); - - await loadConfig(mockSettings, mockExtensionLoader, taskId); - - expect(Config).toHaveBeenLastCalledWith( - expect.objectContaining({ - disableYoloMode: !false, - mcpEnabled: mockAdminSettings.mcpSetting?.mcpEnabled, - extensionsEnabled: undefined, - }), - ); - }); - - it('should not pass default unset admin settings when no admin settings are present', async () => { - const mockAdminSettings: FetchAdminControlsResponse = {}; - vi.mocked(fetchAdminControlsOnce).mockResolvedValue(mockAdminSettings); - - await loadConfig(mockSettings, mockExtensionLoader, taskId); - - expect(Config).toHaveBeenLastCalledWith(expect.objectContaining({})); - }); - - it('should fetch admin controls using the code assist server when available', async () => { - const mockAdminSettings: FetchAdminControlsResponse = { - mcpSetting: { - mcpEnabled: true, - }, - strictModeDisabled: true, - }; - const mockCodeAssistServer = { projectId: 'test-project' }; - vi.mocked(getCodeAssistServer).mockReturnValue( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - mockCodeAssistServer as any, - ); - vi.mocked(fetchAdminControlsOnce).mockResolvedValue(mockAdminSettings); - - await loadConfig(mockSettings, mockExtensionLoader, taskId); - - expect(fetchAdminControlsOnce).toHaveBeenCalledWith( - mockCodeAssistServer, - true, - ); - expect(Config).toHaveBeenLastCalledWith( - expect.objectContaining({ - disableYoloMode: !mockAdminSettings.strictModeDisabled, - mcpEnabled: mockAdminSettings.mcpSetting?.mcpEnabled, - extensionsEnabled: undefined, - }), - ); - }); }); }); - it('should set customIgnoreFilePaths when CUSTOM_IGNORE_FILE_PATHS env var is present', async () => { - const testPath = '/tmp/ignore'; - vi.stubEnv('CUSTOM_IGNORE_FILE_PATHS', testPath); - const config = await loadConfig(mockSettings, mockExtensionLoader, taskId); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - expect((config as any).fileFiltering.customIgnoreFilePaths).toEqual([ - testPath, - ]); - }); - - it('should set customIgnoreFilePaths when settings.fileFiltering.customIgnoreFilePaths is present', async () => { - const testPath = '/settings/ignore'; - const settings: Settings = { - fileFiltering: { - customIgnoreFilePaths: [testPath], - }, - }; - const config = await loadConfig(settings, mockExtensionLoader, taskId); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - expect((config as any).fileFiltering.customIgnoreFilePaths).toEqual([ - testPath, - ]); - }); - - it('should merge customIgnoreFilePaths from settings and env var', async () => { - const envPath = '/env/ignore'; - const settingsPath = '/settings/ignore'; - vi.stubEnv('CUSTOM_IGNORE_FILE_PATHS', envPath); - const settings: Settings = { - fileFiltering: { - customIgnoreFilePaths: [settingsPath], - }, - }; - const config = await loadConfig(settings, mockExtensionLoader, taskId); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - expect((config as any).fileFiltering.customIgnoreFilePaths).toEqual([ - settingsPath, - envPath, - ]); - }); - it('should split CUSTOM_IGNORE_FILE_PATHS using system delimiter', async () => { const paths = ['/path/one', '/path/two']; vi.stubEnv('CUSTOM_IGNORE_FILE_PATHS', paths.join(path.delimiter)); - const config = await loadConfig(mockSettings, mockExtensionLoader, taskId); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - expect((config as any).fileFiltering.customIgnoreFilePaths).toEqual(paths); - }); - - it('should have empty customIgnoreFilePaths when both are missing', async () => { - const config = await loadConfig(mockSettings, mockExtensionLoader, taskId); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - expect((config as any).fileFiltering.customIgnoreFilePaths).toEqual([]); - }); - - it('should initialize FileDiscoveryService with correct options', async () => { - const testPath = '/tmp/ignore'; - vi.stubEnv('CUSTOM_IGNORE_FILE_PATHS', testPath); - const settings: Settings = { - fileFiltering: { - respectGitIgnore: false, - }, - }; - - await loadConfig(settings, mockExtensionLoader, taskId); - - expect(FileDiscoveryService).toHaveBeenCalledWith(expect.any(String), { - respectGitIgnore: false, - respectGeminiIgnore: undefined, - customIgnoreFilePaths: [testPath], - }); + await loadConfig(mockSettings, mockExtensionLoader, taskId); + expect(Config).toHaveBeenCalledWith( + expect.objectContaining({ + fileFiltering: expect.objectContaining({ + customIgnoreFilePaths: paths, + }), + }), + ); }); describe('tool configuration', () => { @@ -286,248 +219,5 @@ describe('loadConfig', () => { }), ); }); - - it('should pass V2 tools.allowed to Config properly', async () => { - const settings: Settings = { - tools: { - allowed: ['shell', 'fetch'], - }, - }; - await loadConfig(settings, mockExtensionLoader, taskId); - expect(Config).toHaveBeenCalledWith( - expect.objectContaining({ - allowedTools: ['shell', 'fetch'], - }), - ); - }); - - it('should prefer V1 allowedTools over V2 tools.allowed if both present', async () => { - const settings: Settings = { - allowedTools: ['v1-tool'], - tools: { - allowed: ['v2-tool'], - }, - }; - await loadConfig(settings, mockExtensionLoader, taskId); - expect(Config).toHaveBeenCalledWith( - expect.objectContaining({ - allowedTools: ['v1-tool'], - }), - ); - }); - - describe('interactivity', () => { - it('should set interactive true when not headless', async () => { - vi.mocked(isHeadlessMode).mockReturnValue(false); - await loadConfig(mockSettings, mockExtensionLoader, taskId); - expect(Config).toHaveBeenCalledWith( - expect.objectContaining({ - interactive: true, - enableInteractiveShell: true, - }), - ); - }); - - it('should set interactive false when headless', async () => { - vi.mocked(isHeadlessMode).mockReturnValue(true); - await loadConfig(mockSettings, mockExtensionLoader, taskId); - expect(Config).toHaveBeenCalledWith( - expect.objectContaining({ - interactive: false, - enableInteractiveShell: false, - }), - ); - }); - }); - - describe('authentication fallback', () => { - beforeEach(() => { - vi.stubEnv('USE_CCPA', 'true'); - vi.stubEnv('GEMINI_API_KEY', ''); - }); - - afterEach(() => { - vi.unstubAllEnvs(); - }); - - it('should fall back to COMPUTE_ADC in Cloud Shell if LOGIN_WITH_GOOGLE fails', async () => { - vi.stubEnv('CLOUD_SHELL', 'true'); - vi.mocked(isHeadlessMode).mockReturnValue(false); - const refreshAuthMock = vi.fn().mockImplementation((authType) => { - if (authType === AuthType.LOGIN_WITH_GOOGLE) { - throw new FatalAuthenticationError('Non-interactive session'); - } - return Promise.resolve(); - }); - - // Update the mock implementation for this test - vi.mocked(Config).mockImplementation( - (params: unknown) => - ({ - ...(params as object), - initialize: vi.fn(), - waitForMcpInit: vi.fn(), - refreshAuth: refreshAuthMock, - getExperiments: vi.fn().mockReturnValue({ flags: {} }), - getRemoteAdminSettings: vi.fn(), - setRemoteAdminSettings: vi.fn(), - }) as unknown as Config, - ); - - await loadConfig(mockSettings, mockExtensionLoader, taskId); - - expect(refreshAuthMock).toHaveBeenCalledWith( - AuthType.LOGIN_WITH_GOOGLE, - ); - expect(refreshAuthMock).toHaveBeenCalledWith(AuthType.COMPUTE_ADC); - }); - - it('should not fall back to COMPUTE_ADC if not in cloud environment', async () => { - vi.mocked(isHeadlessMode).mockReturnValue(false); - const refreshAuthMock = vi.fn().mockImplementation((authType) => { - if (authType === AuthType.LOGIN_WITH_GOOGLE) { - throw new FatalAuthenticationError('Non-interactive session'); - } - return Promise.resolve(); - }); - - vi.mocked(Config).mockImplementation( - (params: unknown) => - ({ - ...(params as object), - initialize: vi.fn(), - waitForMcpInit: vi.fn(), - refreshAuth: refreshAuthMock, - getExperiments: vi.fn().mockReturnValue({ flags: {} }), - getRemoteAdminSettings: vi.fn(), - setRemoteAdminSettings: vi.fn(), - }) as unknown as Config, - ); - - await expect( - loadConfig(mockSettings, mockExtensionLoader, taskId), - ).rejects.toThrow('Non-interactive session'); - - expect(refreshAuthMock).toHaveBeenCalledWith( - AuthType.LOGIN_WITH_GOOGLE, - ); - expect(refreshAuthMock).not.toHaveBeenCalledWith(AuthType.COMPUTE_ADC); - }); - - it('should skip LOGIN_WITH_GOOGLE and use COMPUTE_ADC directly in headless Cloud Shell', async () => { - vi.stubEnv('CLOUD_SHELL', 'true'); - vi.mocked(isHeadlessMode).mockReturnValue(true); - - const refreshAuthMock = vi.fn().mockResolvedValue(undefined); - - vi.mocked(Config).mockImplementation( - (params: unknown) => - ({ - ...(params as object), - initialize: vi.fn(), - waitForMcpInit: vi.fn(), - refreshAuth: refreshAuthMock, - getExperiments: vi.fn().mockReturnValue({ flags: {} }), - getRemoteAdminSettings: vi.fn(), - setRemoteAdminSettings: vi.fn(), - }) as unknown as Config, - ); - - await loadConfig(mockSettings, mockExtensionLoader, taskId); - - expect(refreshAuthMock).not.toHaveBeenCalledWith( - AuthType.LOGIN_WITH_GOOGLE, - ); - expect(refreshAuthMock).toHaveBeenCalledWith(AuthType.COMPUTE_ADC); - }); - - it('should skip LOGIN_WITH_GOOGLE and use COMPUTE_ADC directly if GEMINI_CLI_USE_COMPUTE_ADC is true', async () => { - vi.stubEnv('GEMINI_CLI_USE_COMPUTE_ADC', 'true'); - vi.mocked(isHeadlessMode).mockReturnValue(false); // Even if not headless - - const refreshAuthMock = vi.fn().mockResolvedValue(undefined); - - vi.mocked(Config).mockImplementation( - (params: unknown) => - ({ - ...(params as object), - initialize: vi.fn(), - waitForMcpInit: vi.fn(), - refreshAuth: refreshAuthMock, - getExperiments: vi.fn().mockReturnValue({ flags: {} }), - getRemoteAdminSettings: vi.fn(), - setRemoteAdminSettings: vi.fn(), - }) as unknown as Config, - ); - - await loadConfig(mockSettings, mockExtensionLoader, taskId); - - expect(refreshAuthMock).not.toHaveBeenCalledWith( - AuthType.LOGIN_WITH_GOOGLE, - ); - expect(refreshAuthMock).toHaveBeenCalledWith(AuthType.COMPUTE_ADC); - }); - - it('should throw FatalAuthenticationError in headless mode if no ADC fallback available', async () => { - vi.mocked(isHeadlessMode).mockReturnValue(true); - - const refreshAuthMock = vi.fn().mockResolvedValue(undefined); - - vi.mocked(Config).mockImplementation( - (params: unknown) => - ({ - ...(params as object), - initialize: vi.fn(), - waitForMcpInit: vi.fn(), - refreshAuth: refreshAuthMock, - getExperiments: vi.fn().mockReturnValue({ flags: {} }), - getRemoteAdminSettings: vi.fn(), - setRemoteAdminSettings: vi.fn(), - }) as unknown as Config, - ); - - await expect( - loadConfig(mockSettings, mockExtensionLoader, taskId), - ).rejects.toThrow( - 'Interactive terminal required for LOGIN_WITH_GOOGLE. Run in an interactive terminal or set GEMINI_CLI_USE_COMPUTE_ADC=true to use Application Default Credentials.', - ); - - expect(refreshAuthMock).not.toHaveBeenCalled(); - }); - - it('should include both original and fallback error when COMPUTE_ADC fallback fails', async () => { - vi.stubEnv('CLOUD_SHELL', 'true'); - vi.mocked(isHeadlessMode).mockReturnValue(false); - - const refreshAuthMock = vi.fn().mockImplementation((authType) => { - if (authType === AuthType.LOGIN_WITH_GOOGLE) { - throw new FatalAuthenticationError('OAuth failed'); - } - if (authType === AuthType.COMPUTE_ADC) { - throw new Error('ADC failed'); - } - return Promise.resolve(); - }); - - vi.mocked(Config).mockImplementation( - (params: unknown) => - ({ - ...(params as object), - initialize: vi.fn(), - waitForMcpInit: vi.fn(), - refreshAuth: refreshAuthMock, - getExperiments: vi.fn().mockReturnValue({ flags: {} }), - getRemoteAdminSettings: vi.fn(), - setRemoteAdminSettings: vi.fn(), - }) as unknown as Config, - ); - - await expect( - loadConfig(mockSettings, mockExtensionLoader, taskId), - ).rejects.toThrow( - 'OAuth failed. Fallback to COMPUTE_ADC also failed: ADC failed', - ); - }); - }); }); }); diff --git a/packages/a2a-server/src/config/config.ts b/packages/a2a-server/src/config/config.ts index 5b6757701d..607695f173 100644 --- a/packages/a2a-server/src/config/config.ts +++ b/packages/a2a-server/src/config/config.ts @@ -62,6 +62,7 @@ export async function loadConfig( const configParams: ConfigParameters = { sessionId: taskId, + clientName: 'a2a-server', model: PREVIEW_GEMINI_MODEL, embeddingModel: DEFAULT_GEMINI_EMBEDDING_MODEL, sandbox: undefined, // Sandbox might not be relevant for a server-side agent diff --git a/packages/cli/src/config/config.test.ts b/packages/cli/src/config/config.test.ts index 22ff209cb6..70a4cfe848 100644 --- a/packages/cli/src/config/config.test.ts +++ b/packages/cli/src/config/config.test.ts @@ -112,6 +112,14 @@ vi.mock('@google/gemini-cli-core', async () => { }), }, loadEnvironment: vi.fn(), + detectIdeFromEnv: vi.fn().mockImplementation(() => { + if (process.env['TERM_PROGRAM'] === 'Zed') + return actualServer.IDE_DEFINITIONS.zed; + if (process.env['XCODE_VERSION_ACTUAL']) + return actualServer.IDE_DEFINITIONS.xcode; + return actualServer.IDE_DEFINITIONS.vscode; + }), + IDE_DEFINITIONS: actualServer.IDE_DEFINITIONS, loadServerHierarchicalMemory: vi.fn( ( cwd, @@ -311,6 +319,20 @@ describe('parseArguments', () => { }); }); + it('should set clientName to acp-vscode when using --acp flag', async () => { + process.argv = ['node', 'script.js', '--acp']; + // Mock TERM_PROGRAM to ensure a known IDE is detected (default is vscode if nothing else matches) + vi.stubEnv('TERM_PROGRAM', 'vscode'); + + const args = await parseArguments(createTestMergedSettings()); + const config = await loadCliConfig( + createTestMergedSettings(), + 'test-session', + args, + ); + expect(config.getClientName()).toBe('acp-vscode'); + }); + it.each([ { description: diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts index 320e47a380..65ea7eaadd 100755 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -7,7 +7,6 @@ import yargs from 'yargs/yargs'; import { hideBin } from 'yargs/helpers'; import process from 'node:process'; -import * as path from 'node:path'; import { mcpCommand } from '../commands/mcp.js'; import { extensionsCommand } from '../commands/extensions.js'; import { skillsCommand } from '../commands/skills.js'; @@ -34,9 +33,9 @@ import { getAdminErrorMessage, isHeadlessMode, Config, - resolveToRealPath, applyAdminAllowlist, getAdminBlockedMcpServersMessage, + detectIdeFromEnv, type HookDefinition, type HookEventName, type OutputFormat, @@ -490,15 +489,6 @@ export async function loadCliConfig( const experimentalJitContext = settings.experimental?.jitContext ?? false; - let extensionRegistryURI: string | undefined = trustedFolder - ? settings.experimental?.extensionRegistryURI - : undefined; - if (extensionRegistryURI && !extensionRegistryURI.startsWith('http')) { - extensionRegistryURI = resolveToRealPath( - path.resolve(cwd, resolvePath(extensionRegistryURI)), - ); - } - let memoryContent: string | HierarchicalMemory = ''; let fileCount = 0; let filePaths: string[] = []; @@ -704,8 +694,16 @@ export async function loadCliConfig( } } + const acpMode = !!argv.acp || !!argv.experimentalAcp; + let clientName: string | undefined = undefined; + if (acpMode) { + const ide = detectIdeFromEnv(); + clientName = `acp-${ide.name}`; + } + return new Config({ - acpMode: !!argv.acp || !!argv.experimentalAcp, + acpMode, + clientName, sessionId, clientVersion: await getVersion(), embeddingModel: DEFAULT_GEMINI_EMBEDDING_MODEL, @@ -775,7 +773,6 @@ export async function loadCliConfig( deleteSession: argv.deleteSession, enabledExtensions: argv.extensions, extensionLoader: extensionManager, - extensionRegistryURI, enableExtensionReloading: settings.experimental?.extensionReloading, enableAgents: settings.experimental?.enableAgents, plan: settings.experimental?.plan, diff --git a/packages/cli/src/config/extension.test.ts b/packages/cli/src/config/extension.test.ts index 38264b285a..8b0fe43934 100644 --- a/packages/cli/src/config/extension.test.ts +++ b/packages/cli/src/config/extension.test.ts @@ -118,6 +118,7 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => { ExtensionInstallEvent: vi.fn(), ExtensionUninstallEvent: vi.fn(), ExtensionDisableEvent: vi.fn(), + ExtensionUpdateEvent: vi.fn(), KeychainTokenStorage: vi.fn().mockImplementation(() => ({ getSecret: vi.fn(), setSecret: vi.fn(), diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index bc52050286..70ebfc0369 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -476,6 +476,7 @@ export interface PolicyUpdateConfirmationRequest { export interface ConfigParameters { sessionId: string; + clientName?: string; clientVersion?: string; embeddingModel?: string; sandbox?: SandboxConfig; @@ -550,11 +551,9 @@ export interface ConfigParameters { skipNextSpeakerCheck?: boolean; shellExecutionConfig?: ShellExecutionConfig; extensionManagement?: boolean; - extensionRegistryURI?: string; truncateToolOutputThreshold?: number; eventEmitter?: EventEmitter; useWriteTodos?: boolean; - workspacePoliciesDir?: string; policyEngineConfig?: PolicyEngineConfig; directWebFetch?: boolean; policyUpdateConfirmationRequest?: PolicyUpdateConfirmationRequest; @@ -620,6 +619,7 @@ export class Config implements McpContext, AgentLoopContext { private readonly acknowledgedAgentsService: AcknowledgedAgentsService; private skillManager!: SkillManager; private _sessionId: string; + private readonly clientName: string | undefined; private clientVersion: string; private fileSystemService: FileSystemService; private trackerService?: TrackerService; @@ -739,7 +739,6 @@ export class Config implements McpContext, AgentLoopContext { private readonly useAlternateBuffer: boolean; private shellExecutionConfig: ShellExecutionConfig; private readonly extensionManagement: boolean = true; - private readonly extensionRegistryURI: string | undefined; private readonly truncateToolOutputThreshold: number; private compressionTruncationCounter = 0; private initialized = false; @@ -749,7 +748,6 @@ export class Config implements McpContext, AgentLoopContext { private readonly fileExclusions: FileExclusions; private readonly eventEmitter?: EventEmitter; private readonly useWriteTodos: boolean; - private readonly workspacePoliciesDir: string | undefined; private readonly _messageBus: MessageBus; private readonly policyEngine: PolicyEngine; private policyUpdateConfirmationRequest: @@ -817,6 +815,7 @@ export class Config implements McpContext, AgentLoopContext { constructor(params: ConfigParameters) { this._sessionId = params.sessionId; + this.clientName = params.clientName; this.clientVersion = params.clientVersion ?? 'unknown'; this.approvedPlanPath = undefined; this.embeddingModel = @@ -960,7 +959,6 @@ export class Config implements McpContext, AgentLoopContext { this.useWriteTodos = isPreviewModel(this.model) ? false : (params.useWriteTodos ?? true); - this.workspacePoliciesDir = params.workspacePoliciesDir; this.enableHooksUI = params.enableHooksUI ?? true; this.enableHooks = params.enableHooks ?? true; this.disabledHooks = params.disabledHooks ?? []; @@ -971,7 +969,6 @@ export class Config implements McpContext, AgentLoopContext { this.shellToolInactivityTimeout = (params.shellToolInactivityTimeout ?? 300) * 1000; // 5 minutes this.extensionManagement = params.extensionManagement ?? true; - this.extensionRegistryURI = params.extensionRegistryURI; this.enableExtensionReloading = params.enableExtensionReloading ?? false; this.storage = new Storage(this.targetDir, this._sessionId); this.storage.setCustomPlansDir(params.planSettings?.directory); @@ -1024,7 +1021,7 @@ export class Config implements McpContext, AgentLoopContext { params.gemmaModelRouter?.classifier?.model ?? 'gemma3-1b-gpu-custom', }, }; - this.retryFetchErrors = params.retryFetchErrors ?? true; + this.retryFetchErrors = params.retryFetchErrors ?? false; this.maxAttempts = Math.min( params.maxAttempts ?? DEFAULT_MAX_ATTEMPTS, DEFAULT_MAX_ATTEMPTS, @@ -1100,10 +1097,6 @@ export class Config implements McpContext, AgentLoopContext { ); } - get config(): Config { - return this; - } - isInitialized(): boolean { return this.initialized; } @@ -1197,7 +1190,7 @@ export class Config implements McpContext, AgentLoopContext { if (this.getSkillManager().getSkills().length > 0) { this.getToolRegistry().unregisterTool(ActivateSkillTool.Name); this.getToolRegistry().registerTool( - new ActivateSkillTool(this, this.messageBus), + new ActivateSkillTool(this, this._messageBus), ); } } @@ -1847,10 +1840,6 @@ export class Config implements McpContext, AgentLoopContext { return this.extensionsEnabled; } - getExtensionRegistryURI(): string | undefined { - return this.extensionRegistryURI; - } - getMcpClientManager(): McpClientManager | undefined { return this.mcpClientManager; } @@ -2013,10 +2002,6 @@ export class Config implements McpContext, AgentLoopContext { return this.geminiMdFilePaths; } - getWorkspacePoliciesDir(): string | undefined { - return this.workspacePoliciesDir; - } - setGeminiMdFilePaths(paths: string[]): void { this.geminiMdFilePaths = paths; } @@ -2390,6 +2375,10 @@ export class Config implements McpContext, AgentLoopContext { return this.ideMode; } + getClientName(): string | undefined { + return this.clientName; + } + /** * Returns 'true' if the folder trust feature is enabled. */ @@ -2639,7 +2628,7 @@ export class Config implements McpContext, AgentLoopContext { if (this.getSkillManager().getSkills().length > 0) { this.getToolRegistry().unregisterTool(ActivateSkillTool.Name); this.getToolRegistry().registerTool( - new ActivateSkillTool(this, this.messageBus), + new ActivateSkillTool(this, this._messageBus), ); } else { this.getToolRegistry().unregisterTool(ActivateSkillTool.Name); @@ -2823,7 +2812,7 @@ export class Config implements McpContext, AgentLoopContext { } async createToolRegistry(): Promise { - const registry = new ToolRegistry(this, this.messageBus); + const registry = new ToolRegistry(this, this._messageBus); // helper to create & register core tools that are enabled const maybeRegister = ( @@ -2853,10 +2842,10 @@ export class Config implements McpContext, AgentLoopContext { }; maybeRegister(LSTool, () => - registry.registerTool(new LSTool(this, this.messageBus)), + registry.registerTool(new LSTool(this, this._messageBus)), ); maybeRegister(ReadFileTool, () => - registry.registerTool(new ReadFileTool(this, this.messageBus)), + registry.registerTool(new ReadFileTool(this, this._messageBus)), ); if (this.getUseRipgrep()) { @@ -2869,81 +2858,85 @@ export class Config implements McpContext, AgentLoopContext { } if (useRipgrep) { maybeRegister(RipGrepTool, () => - registry.registerTool(new RipGrepTool(this, this.messageBus)), + registry.registerTool(new RipGrepTool(this, this._messageBus)), ); } else { logRipgrepFallback(this, new RipgrepFallbackEvent(errorString)); maybeRegister(GrepTool, () => - registry.registerTool(new GrepTool(this, this.messageBus)), + registry.registerTool(new GrepTool(this, this._messageBus)), ); } } else { maybeRegister(GrepTool, () => - registry.registerTool(new GrepTool(this, this.messageBus)), + registry.registerTool(new GrepTool(this, this._messageBus)), ); } maybeRegister(GlobTool, () => - registry.registerTool(new GlobTool(this, this.messageBus)), + registry.registerTool(new GlobTool(this, this._messageBus)), ); maybeRegister(ActivateSkillTool, () => - registry.registerTool(new ActivateSkillTool(this, this.messageBus)), + registry.registerTool(new ActivateSkillTool(this, this._messageBus)), ); maybeRegister(EditTool, () => - registry.registerTool(new EditTool(this, this.messageBus)), + registry.registerTool(new EditTool(this, this._messageBus)), ); maybeRegister(WriteFileTool, () => - registry.registerTool(new WriteFileTool(this, this.messageBus)), + registry.registerTool(new WriteFileTool(this, this._messageBus)), ); maybeRegister(WebFetchTool, () => - registry.registerTool(new WebFetchTool(this, this.messageBus)), + registry.registerTool(new WebFetchTool(this, this._messageBus)), ); maybeRegister(ShellTool, () => - registry.registerTool(new ShellTool(this, this.messageBus)), + registry.registerTool(new ShellTool(this, this._messageBus)), ); maybeRegister(MemoryTool, () => - registry.registerTool(new MemoryTool(this.messageBus)), + registry.registerTool(new MemoryTool(this._messageBus)), ); maybeRegister(WebSearchTool, () => - registry.registerTool(new WebSearchTool(this, this.messageBus)), + registry.registerTool(new WebSearchTool(this, this._messageBus)), ); maybeRegister(AskUserTool, () => - registry.registerTool(new AskUserTool(this.messageBus)), + registry.registerTool(new AskUserTool(this._messageBus)), ); if (this.getUseWriteTodos()) { maybeRegister(WriteTodosTool, () => - registry.registerTool(new WriteTodosTool(this.messageBus)), + registry.registerTool(new WriteTodosTool(this._messageBus)), ); } if (this.isPlanEnabled()) { maybeRegister(ExitPlanModeTool, () => - registry.registerTool(new ExitPlanModeTool(this, this.messageBus)), + registry.registerTool(new ExitPlanModeTool(this, this._messageBus)), ); maybeRegister(EnterPlanModeTool, () => - registry.registerTool(new EnterPlanModeTool(this, this.messageBus)), + registry.registerTool(new EnterPlanModeTool(this, this._messageBus)), ); } if (this.isTrackerEnabled()) { maybeRegister(TrackerCreateTaskTool, () => - registry.registerTool(new TrackerCreateTaskTool(this, this.messageBus)), + registry.registerTool( + new TrackerCreateTaskTool(this, this._messageBus), + ), ); maybeRegister(TrackerUpdateTaskTool, () => - registry.registerTool(new TrackerUpdateTaskTool(this, this.messageBus)), + registry.registerTool( + new TrackerUpdateTaskTool(this, this._messageBus), + ), ); maybeRegister(TrackerGetTaskTool, () => - registry.registerTool(new TrackerGetTaskTool(this, this.messageBus)), + registry.registerTool(new TrackerGetTaskTool(this, this._messageBus)), ); maybeRegister(TrackerListTasksTool, () => - registry.registerTool(new TrackerListTasksTool(this, this.messageBus)), + registry.registerTool(new TrackerListTasksTool(this, this._messageBus)), ); maybeRegister(TrackerAddDependencyTool, () => registry.registerTool( - new TrackerAddDependencyTool(this, this.messageBus), + new TrackerAddDependencyTool(this, this._messageBus), ), ); maybeRegister(TrackerVisualizeTool, () => - registry.registerTool(new TrackerVisualizeTool(this, this.messageBus)), + registry.registerTool(new TrackerVisualizeTool(this, this._messageBus)), ); } diff --git a/packages/core/src/core/contentGenerator.test.ts b/packages/core/src/core/contentGenerator.test.ts index d86eb6f738..2d5d61bec7 100644 --- a/packages/core/src/core/contentGenerator.test.ts +++ b/packages/core/src/core/contentGenerator.test.ts @@ -32,6 +32,7 @@ const mockConfig = { getModel: vi.fn().mockReturnValue('gemini-pro'), getProxy: vi.fn().mockReturnValue(undefined), getUsageStatisticsEnabled: vi.fn().mockReturnValue(true), + getClientName: vi.fn().mockReturnValue(undefined), } as unknown as Config; describe('createContentGenerator', () => { @@ -52,6 +53,7 @@ describe('createContentGenerator', () => { const fakeResponsesFile = 'fake/responses.yaml'; const mockConfigWithFake = { fakeResponses: fakeResponsesFile, + getClientName: vi.fn().mockReturnValue(undefined), } as unknown as Config; const generator = await createContentGenerator( { @@ -73,6 +75,7 @@ describe('createContentGenerator', () => { const mockConfigWithRecordResponses = { fakeResponses: fakeResponsesFile, recordResponses: recordResponsesFile, + getClientName: vi.fn().mockReturnValue(undefined), } as unknown as Config; const generator = await createContentGenerator( { @@ -122,6 +125,7 @@ describe('createContentGenerator', () => { getModel: vi.fn().mockReturnValue('gemini-pro'), getProxy: vi.fn().mockReturnValue(undefined), getUsageStatisticsEnabled: () => true, + getClientName: vi.fn().mockReturnValue(undefined), } as unknown as Config; // Set a fixed version for testing @@ -188,6 +192,7 @@ describe('createContentGenerator', () => { getModel: vi.fn().mockReturnValue('gemini-pro'), getProxy: vi.fn().mockReturnValue(undefined), getUsageStatisticsEnabled: () => false, + getClientName: vi.fn().mockReturnValue(undefined), } as unknown as Config; const mockGenerator = { @@ -229,11 +234,46 @@ describe('createContentGenerator', () => { ); }); + it('should include clientName in User-Agent if provided', async () => { + const mockConfigWithClientName = { + getModel: vi.fn().mockReturnValue('gemini-pro'), + getClientName: vi.fn().mockReturnValue('a2a-server'), + getProxy: vi.fn().mockReturnValue(undefined), + getUsageStatisticsEnabled: () => false, + } as unknown as Config; + + const mockGenerator = { + models: {}, + } as unknown as GoogleGenAI; + vi.mocked(GoogleGenAI).mockImplementation(() => mockGenerator as never); + + await createContentGenerator( + { + apiKey: 'test-api-key', + authType: AuthType.USE_GEMINI, + }, + mockConfigWithClientName, + ); + + expect(GoogleGenAI).toHaveBeenCalledWith( + expect.objectContaining({ + httpOptions: expect.objectContaining({ + headers: expect.objectContaining({ + 'User-Agent': expect.stringMatching( + /GeminiCLI-a2a-server\/.*\/gemini-pro \(.*; .*; .*\)/, + ), + }), + }), + }), + ); + }); + it('should pass api key as Authorization Header when GEMINI_API_KEY_AUTH_MECHANISM is set to bearer', async () => { const mockConfig = { getModel: vi.fn().mockReturnValue('gemini-pro'), getProxy: vi.fn().mockReturnValue(undefined), getUsageStatisticsEnabled: () => false, + getClientName: vi.fn().mockReturnValue(undefined), } as unknown as Config; const mockGenerator = { @@ -267,6 +307,7 @@ describe('createContentGenerator', () => { getModel: vi.fn().mockReturnValue('gemini-pro'), getProxy: vi.fn().mockReturnValue(undefined), getUsageStatisticsEnabled: () => false, + getClientName: vi.fn().mockReturnValue(undefined), } as unknown as Config; const mockGenerator = { @@ -308,6 +349,7 @@ describe('createContentGenerator', () => { const mockConfig = { getModel: vi.fn().mockReturnValue('gemini-pro'), getUsageStatisticsEnabled: () => false, + getClientName: vi.fn().mockReturnValue(undefined), } as unknown as Config; const mockGenerator = { models: {}, @@ -339,6 +381,7 @@ describe('createContentGenerator', () => { getModel: vi.fn().mockReturnValue('gemini-pro'), getProxy: vi.fn().mockReturnValue(undefined), getUsageStatisticsEnabled: () => false, + getClientName: vi.fn().mockReturnValue(undefined), } as unknown as Config; const mockGenerator = { @@ -372,6 +415,7 @@ describe('createContentGenerator', () => { getModel: vi.fn().mockReturnValue('gemini-pro'), getProxy: vi.fn().mockReturnValue(undefined), getUsageStatisticsEnabled: () => false, + getClientName: vi.fn().mockReturnValue(undefined), } as unknown as Config; const mockGenerator = { @@ -409,6 +453,7 @@ describe('createContentGenerator', () => { getModel: vi.fn().mockReturnValue('gemini-pro'), getProxy: vi.fn().mockReturnValue(undefined), getUsageStatisticsEnabled: () => false, + getClientName: vi.fn().mockReturnValue(undefined), } as unknown as Config; const mockGenerator = { @@ -447,6 +492,7 @@ describe('createContentGenerator', () => { getModel: vi.fn().mockReturnValue('gemini-pro'), getProxy: vi.fn().mockReturnValue(undefined), getUsageStatisticsEnabled: () => false, + getClientName: vi.fn().mockReturnValue(undefined), } as unknown as Config; const mockGenerator = { diff --git a/packages/core/src/core/contentGenerator.ts b/packages/core/src/core/contentGenerator.ts index 2ce5420335..1a93fbfeb5 100644 --- a/packages/core/src/core/contentGenerator.ts +++ b/packages/core/src/core/contentGenerator.ts @@ -22,6 +22,7 @@ import { LoggingContentGenerator } from './loggingContentGenerator.js'; import { InstallationManager } from '../utils/installationManager.js'; import { FakeContentGenerator } from './fakeContentGenerator.js'; import { parseCustomHeaders } from '../utils/customHeaderUtils.js'; +import { determineSurface } from '../utils/surface.js'; import { RecordingContentGenerator } from './recordingContentGenerator.js'; import { getVersion, resolveModel } from '../../index.js'; import type { LlmRole } from '../telemetry/llmRole.js'; @@ -173,7 +174,12 @@ export async function createContentGenerator( ); const customHeadersEnv = process.env['GEMINI_CLI_CUSTOM_HEADERS'] || undefined; - const userAgent = `GeminiCLI/${version}/${model} (${process.platform}; ${process.arch})`; + const clientName = gcConfig.getClientName(); + const userAgentPrefix = clientName + ? `GeminiCLI-${clientName}` + : 'GeminiCLI'; + const surface = determineSurface(); + const userAgent = `${userAgentPrefix}/${version}/${model} (${process.platform}; ${process.arch}; ${surface})`; const customHeadersMap = parseCustomHeaders(customHeadersEnv); const apiKeyAuthMechanism = process.env['GEMINI_API_KEY_AUTH_MECHANISM'] || 'x-goog-api-key'; diff --git a/packages/core/src/ide/detect-ide.test.ts b/packages/core/src/ide/detect-ide.test.ts index 0b27b27560..4631bead48 100644 --- a/packages/core/src/ide/detect-ide.test.ts +++ b/packages/core/src/ide/detect-ide.test.ts @@ -5,7 +5,7 @@ */ import { describe, it, expect, vi, afterEach, beforeEach } from 'vitest'; -import { detectIde, IDE_DEFINITIONS } from './detect-ide.js'; +import { detectIde, IDE_DEFINITIONS, detectIdeFromEnv } from './detect-ide.js'; beforeEach(() => { // Ensure Antigravity detection doesn't interfere with other tests @@ -97,6 +97,21 @@ describe('detectIde', () => { expect(detectIde(ideProcessInfo)).toBe(IDE_DEFINITIONS.firebasestudio); }); + it('should detect Zed via ZED_SESSION_ID', () => { + vi.stubEnv('ZED_SESSION_ID', 'some-id'); + expect(detectIdeFromEnv()).toBe(IDE_DEFINITIONS.zed); + }); + + it('should detect Zed via TERM_PROGRAM', () => { + vi.stubEnv('TERM_PROGRAM', 'Zed'); + expect(detectIdeFromEnv()).toBe(IDE_DEFINITIONS.zed); + }); + + it('should detect XCode via XCODE_VERSION_ACTUAL', () => { + vi.stubEnv('XCODE_VERSION_ACTUAL', '1500'); + expect(detectIdeFromEnv()).toBe(IDE_DEFINITIONS.xcode); + }); + it('should detect VSCode when no other IDE is detected and command includes "code"', () => { vi.stubEnv('TERM_PROGRAM', 'vscode'); vi.stubEnv('MONOSPACE_ENV', ''); diff --git a/packages/core/src/ide/detect-ide.ts b/packages/core/src/ide/detect-ide.ts index c07ef8254c..fe0030bfe0 100644 --- a/packages/core/src/ide/detect-ide.ts +++ b/packages/core/src/ide/detect-ide.ts @@ -27,6 +27,8 @@ export const IDE_DEFINITIONS = { rustrover: { name: 'rustrover', displayName: 'RustRover' }, datagrip: { name: 'datagrip', displayName: 'DataGrip' }, phpstorm: { name: 'phpstorm', displayName: 'PhpStorm' }, + zed: { name: 'zed', displayName: 'Zed' }, + xcode: { name: 'xcode', displayName: 'XCode' }, } as const; export interface IdeInfo { @@ -75,6 +77,12 @@ export function detectIdeFromEnv(): IdeInfo { if (process.env['TERM_PROGRAM'] === 'sublime') { return IDE_DEFINITIONS.sublimetext; } + if (process.env['ZED_SESSION_ID'] || process.env['TERM_PROGRAM'] === 'Zed') { + return IDE_DEFINITIONS.zed; + } + if (process.env['XCODE_VERSION_ACTUAL']) { + return IDE_DEFINITIONS.xcode; + } if (isJetBrains()) { return IDE_DEFINITIONS.jetbrains; } diff --git a/packages/core/src/utils/surface.ts b/packages/core/src/utils/surface.ts new file mode 100644 index 0000000000..e4b1241d84 --- /dev/null +++ b/packages/core/src/utils/surface.ts @@ -0,0 +1,54 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { detectIdeFromEnv } from '../ide/detect-ide.js'; + +/** Default surface value when no IDE/environment is detected. */ +export const SURFACE_NOT_SET = 'terminal'; + +/** + * Determines the surface/distribution channel the CLI is running in. + * + * Priority: + * 1. `GEMINI_CLI_SURFACE` env var (first-class override for enterprise customers) + * 2. `SURFACE` env var (legacy override, kept for backward compatibility) + * 3. Auto-detection via environment variables (Cloud Shell, GitHub Actions, IDE, etc.) + * + * @returns A human-readable surface identifier (e.g., "vscode", "cursor", "terminal"). + */ +export function determineSurface(): string { + // Priority 1 & 2: Explicit overrides from environment variables. + const customSurface = + process.env['GEMINI_CLI_SURFACE'] || process.env['SURFACE']; + if (customSurface) { + return customSurface; + } + + // Priority 3: Auto-detect IDE/environment. + const ide = detectIdeFromEnv(); + + // `detectIdeFromEnv` falls back to 'vscode' for generic terminals. + // If a specific IDE (e.g., Cloud Shell, Cursor, JetBrains) was detected, + // its name will be something other than 'vscode', and we can use it directly. + if (ide.name !== 'vscode') { + return ide.name; + } + + // If the detected IDE is 'vscode', we only accept it if TERM_PROGRAM confirms it. + // This prevents generic terminals from being misidentified as VSCode. + if (process.env['TERM_PROGRAM'] === 'vscode') { + return ide.name; + } + + // Priority 4: GitHub Actions (checked after IDE detection so that + // specific environments like Cloud Shell take precedence). + if (process.env['GITHUB_SHA']) { + return 'GitHub'; + } + + // Priority 5: Fallback for all other cases (e.g., a generic terminal). + return SURFACE_NOT_SET; +}