mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-07-30 11:41:00 -07:00
Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2ef872e73a | |||
| b3014337d5 | |||
| 069641f65a | |||
| e36dfc9fc9 | |||
| 2ef6149684 | |||
| b70cf35df3 | |||
| f581ebdd19 | |||
| 54f24574e5 | |||
| 7f9808ce6b | |||
| 8429f559f0 | |||
| 9a947f5544 | |||
| d924ed0128 | |||
| 9f864ccd25 | |||
| b3ac1299e8 | |||
| 95b973e7fc | |||
| 13249bc518 | |||
| d1fcb09afa | |||
| f0473f3385 | |||
| 3d8edc5d97 | |||
| 6a2f2d3a91 | |||
| c32969c5a1 | |||
| 31a7a8187b | |||
| 70305907de |
@@ -59,7 +59,7 @@ Other ways to start in Plan Mode:
|
|||||||
You can enter Plan Mode in three ways:
|
You can enter Plan Mode in three ways:
|
||||||
|
|
||||||
1. **Keyboard Shortcut:** Press `Shift+Tab` to cycle through approval modes
|
1. **Keyboard Shortcut:** Press `Shift+Tab` to cycle through approval modes
|
||||||
(`Default` -> `Plan` -> `Auto-Edit`).
|
(`Default` -> `Auto-Edit` -> `Plan`).
|
||||||
2. **Command:** Type `/plan` in the input box.
|
2. **Command:** Type `/plan` in the input box.
|
||||||
3. **Natural Language:** Ask the agent to "start a plan for...".
|
3. **Natural Language:** Ask the agent to "start a plan for...".
|
||||||
|
|
||||||
|
|||||||
@@ -739,21 +739,10 @@ The MCP integration tracks several states:
|
|||||||
cautiously and only for servers you completely control
|
cautiously and only for servers you completely control
|
||||||
- **Access tokens:** Be security-aware when configuring environment variables
|
- **Access tokens:** Be security-aware when configuring environment variables
|
||||||
containing API keys or tokens
|
containing API keys or tokens
|
||||||
- **Environment variable redaction:** By default, the Gemini CLI redacts
|
|
||||||
sensitive environment variables (such as `GEMINI_API_KEY`, `GOOGLE_API_KEY`,
|
|
||||||
and variables matching patterns like `*TOKEN*`, `*SECRET*`, `*PASSWORD*`) when
|
|
||||||
spawning MCP servers using the `stdio` transport. This prevents unintended
|
|
||||||
exposure of your credentials to third-party servers.
|
|
||||||
- **Explicit environment variables:** If you need to pass a specific environment
|
|
||||||
variable to an MCP server, you should define it explicitly in the `env`
|
|
||||||
property of the server configuration in `settings.json`.
|
|
||||||
- **Sandbox compatibility:** When using sandboxing, ensure MCP servers are
|
- **Sandbox compatibility:** When using sandboxing, ensure MCP servers are
|
||||||
available within the sandbox environment.
|
available within the sandbox environment
|
||||||
- **Private data:** Using broadly scoped personal access tokens can lead to
|
- **Private data:** Using broadly scoped personal access tokens can lead to
|
||||||
information leakage between repositories.
|
information leakage between repositories.
|
||||||
- **Untrusted servers:** Be extremely cautious when adding MCP servers from
|
|
||||||
untrusted or third-party sources. Malicious servers could attempt to
|
|
||||||
exfiltrate data or perform unauthorized actions through the tools they expose.
|
|
||||||
|
|
||||||
### Performance and resource management
|
### Performance and resource management
|
||||||
|
|
||||||
|
|||||||
@@ -75,6 +75,10 @@ const baseConfig = {
|
|||||||
write: true,
|
write: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const commonAliases = {
|
||||||
|
punycode: 'punycode/',
|
||||||
|
};
|
||||||
|
|
||||||
const cliConfig = {
|
const cliConfig = {
|
||||||
...baseConfig,
|
...baseConfig,
|
||||||
banner: {
|
banner: {
|
||||||
@@ -88,6 +92,7 @@ const cliConfig = {
|
|||||||
plugins: createWasmPlugins(),
|
plugins: createWasmPlugins(),
|
||||||
alias: {
|
alias: {
|
||||||
'is-in-ci': path.resolve(__dirname, 'packages/cli/src/patches/is-in-ci.ts'),
|
'is-in-ci': path.resolve(__dirname, 'packages/cli/src/patches/is-in-ci.ts'),
|
||||||
|
...commonAliases,
|
||||||
},
|
},
|
||||||
metafile: true,
|
metafile: true,
|
||||||
};
|
};
|
||||||
@@ -103,6 +108,7 @@ const a2aServerConfig = {
|
|||||||
'process.env.CLI_VERSION': JSON.stringify(pkg.version),
|
'process.env.CLI_VERSION': JSON.stringify(pkg.version),
|
||||||
},
|
},
|
||||||
plugins: createWasmPlugins(),
|
plugins: createWasmPlugins(),
|
||||||
|
alias: commonAliases,
|
||||||
};
|
};
|
||||||
|
|
||||||
Promise.allSettled([
|
Promise.allSettled([
|
||||||
|
|||||||
Generated
+10
-33
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@google/gemini-cli",
|
"name": "@google/gemini-cli",
|
||||||
"version": "0.29.0-nightly.20260203.71f46f116",
|
"version": "0.29.4",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@google/gemini-cli",
|
"name": "@google/gemini-cli",
|
||||||
"version": "0.29.0-nightly.20260203.71f46f116",
|
"version": "0.29.4",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
],
|
],
|
||||||
@@ -14,6 +14,7 @@
|
|||||||
"ink": "npm:@jrichman/ink@6.4.8",
|
"ink": "npm:@jrichman/ink@6.4.8",
|
||||||
"latest-version": "^9.0.0",
|
"latest-version": "^9.0.0",
|
||||||
"proper-lockfile": "^4.1.2",
|
"proper-lockfile": "^4.1.2",
|
||||||
|
"punycode": "^2.3.1",
|
||||||
"simple-git": "^3.28.0"
|
"simple-git": "^3.28.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -2232,7 +2233,6 @@
|
|||||||
"integrity": "sha512-t54CUOsFMappY1Jbzb7fetWeO0n6K0k/4+/ZpkS+3Joz8I4VcvY9OiEBFRYISqaI2fq5sCiPtAjRDOzVYG8m+Q==",
|
"integrity": "sha512-t54CUOsFMappY1Jbzb7fetWeO0n6K0k/4+/ZpkS+3Joz8I4VcvY9OiEBFRYISqaI2fq5sCiPtAjRDOzVYG8m+Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@octokit/auth-token": "^6.0.0",
|
"@octokit/auth-token": "^6.0.0",
|
||||||
"@octokit/graphql": "^9.0.2",
|
"@octokit/graphql": "^9.0.2",
|
||||||
@@ -2413,7 +2413,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
|
||||||
"integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
|
"integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8.0.0"
|
"node": ">=8.0.0"
|
||||||
}
|
}
|
||||||
@@ -2447,7 +2446,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.0.1.tgz",
|
||||||
"integrity": "sha512-MaZk9SJIDgo1peKevlbhP6+IwIiNPNmswNL4AF0WaQJLbHXjr9SrZMgS12+iqr9ToV4ZVosCcc0f8Rg67LXjxw==",
|
"integrity": "sha512-MaZk9SJIDgo1peKevlbhP6+IwIiNPNmswNL4AF0WaQJLbHXjr9SrZMgS12+iqr9ToV4ZVosCcc0f8Rg67LXjxw==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opentelemetry/semantic-conventions": "^1.29.0"
|
"@opentelemetry/semantic-conventions": "^1.29.0"
|
||||||
},
|
},
|
||||||
@@ -2816,7 +2814,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.0.1.tgz",
|
||||||
"integrity": "sha512-dZOB3R6zvBwDKnHDTB4X1xtMArB/d324VsbiPkX/Yu0Q8T2xceRthoIVFhJdvgVM2QhGVUyX9tzwiNxGtoBJUw==",
|
"integrity": "sha512-dZOB3R6zvBwDKnHDTB4X1xtMArB/d324VsbiPkX/Yu0Q8T2xceRthoIVFhJdvgVM2QhGVUyX9tzwiNxGtoBJUw==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opentelemetry/core": "2.0.1",
|
"@opentelemetry/core": "2.0.1",
|
||||||
"@opentelemetry/semantic-conventions": "^1.29.0"
|
"@opentelemetry/semantic-conventions": "^1.29.0"
|
||||||
@@ -2850,7 +2847,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.0.1.tgz",
|
||||||
"integrity": "sha512-wf8OaJoSnujMAHWR3g+/hGvNcsC16rf9s1So4JlMiFaFHiE4HpIA3oUh+uWZQ7CNuK8gVW/pQSkgoa5HkkOl0g==",
|
"integrity": "sha512-wf8OaJoSnujMAHWR3g+/hGvNcsC16rf9s1So4JlMiFaFHiE4HpIA3oUh+uWZQ7CNuK8gVW/pQSkgoa5HkkOl0g==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opentelemetry/core": "2.0.1",
|
"@opentelemetry/core": "2.0.1",
|
||||||
"@opentelemetry/resources": "2.0.1"
|
"@opentelemetry/resources": "2.0.1"
|
||||||
@@ -2903,7 +2899,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.0.1.tgz",
|
||||||
"integrity": "sha512-xYLlvk/xdScGx1aEqvxLwf6sXQLXCjk3/1SQT9X9AoN5rXRhkdvIFShuNNmtTEPRBqcsMbS4p/gJLNI2wXaDuQ==",
|
"integrity": "sha512-xYLlvk/xdScGx1aEqvxLwf6sXQLXCjk3/1SQT9X9AoN5rXRhkdvIFShuNNmtTEPRBqcsMbS4p/gJLNI2wXaDuQ==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opentelemetry/core": "2.0.1",
|
"@opentelemetry/core": "2.0.1",
|
||||||
"@opentelemetry/resources": "2.0.1",
|
"@opentelemetry/resources": "2.0.1",
|
||||||
@@ -4021,7 +4016,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.1.tgz",
|
||||||
"integrity": "sha512-jJD50LtlD2dodAEO653i3YF04NWak6jN3ky+Ri3Em3mGR39/glWiboM/IePaRbgwSfqM1TpGXfAg8ohn/4dTgA==",
|
"integrity": "sha512-jJD50LtlD2dodAEO653i3YF04NWak6jN3ky+Ri3Em3mGR39/glWiboM/IePaRbgwSfqM1TpGXfAg8ohn/4dTgA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~6.21.0"
|
"undici-types": "~6.21.0"
|
||||||
}
|
}
|
||||||
@@ -4087,7 +4081,6 @@
|
|||||||
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
|
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": "^3.0.2"
|
"csstype": "^3.0.2"
|
||||||
}
|
}
|
||||||
@@ -4362,7 +4355,6 @@
|
|||||||
"integrity": "sha512-6sMvZePQrnZH2/cJkwRpkT7DxoAWh+g6+GFRK6bV3YQo7ogi3SX5rgF6099r5Q53Ma5qeT7LGmOmuIutF4t3lA==",
|
"integrity": "sha512-6sMvZePQrnZH2/cJkwRpkT7DxoAWh+g6+GFRK6bV3YQo7ogi3SX5rgF6099r5Q53Ma5qeT7LGmOmuIutF4t3lA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/scope-manager": "8.35.0",
|
"@typescript-eslint/scope-manager": "8.35.0",
|
||||||
"@typescript-eslint/types": "8.35.0",
|
"@typescript-eslint/types": "8.35.0",
|
||||||
@@ -5355,7 +5347,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
@@ -7895,7 +7886,6 @@
|
|||||||
"integrity": "sha512-GsGizj2Y1rCWDu6XoEekL3RLilp0voSePurjZIkxL3wlm5o5EC9VpgaP7lrCvjnkuLvzFBQWB3vWB3K5KQTveQ==",
|
"integrity": "sha512-GsGizj2Y1rCWDu6XoEekL3RLilp0voSePurjZIkxL3wlm5o5EC9VpgaP7lrCvjnkuLvzFBQWB3vWB3K5KQTveQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.2.0",
|
"@eslint-community/eslint-utils": "^4.2.0",
|
||||||
"@eslint-community/regexpp": "^4.12.1",
|
"@eslint-community/regexpp": "^4.12.1",
|
||||||
@@ -8416,7 +8406,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
|
||||||
"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
|
"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"accepts": "^2.0.0",
|
"accepts": "^2.0.0",
|
||||||
"body-parser": "^2.2.1",
|
"body-parser": "^2.2.1",
|
||||||
@@ -10023,7 +10012,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@jrichman/ink/-/ink-6.4.8.tgz",
|
"resolved": "https://registry.npmjs.org/@jrichman/ink/-/ink-6.4.8.tgz",
|
||||||
"integrity": "sha512-v0thcXIKl9hqF/1w4HqA6MKxIcMoWSP3YtEZIAA+eeJngXpN5lGnMkb6rllB7FnOdwyEyYaFTcu1ZVr4/JZpWQ==",
|
"integrity": "sha512-v0thcXIKl9hqF/1w4HqA6MKxIcMoWSP3YtEZIAA+eeJngXpN5lGnMkb6rllB7FnOdwyEyYaFTcu1ZVr4/JZpWQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@alcalzone/ansi-tokenize": "^0.2.1",
|
"@alcalzone/ansi-tokenize": "^0.2.1",
|
||||||
"ansi-escapes": "^7.0.0",
|
"ansi-escapes": "^7.0.0",
|
||||||
@@ -13527,7 +13515,6 @@
|
|||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||||
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
|
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
@@ -13693,7 +13680,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
|
||||||
"integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
|
"integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@@ -13704,7 +13690,6 @@
|
|||||||
"integrity": "sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA==",
|
"integrity": "sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"shell-quote": "^1.6.1",
|
"shell-quote": "^1.6.1",
|
||||||
"ws": "^7"
|
"ws": "^7"
|
||||||
@@ -15866,7 +15851,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@@ -16090,8 +16074,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "0BSD",
|
"license": "0BSD"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/tsx": {
|
"node_modules/tsx": {
|
||||||
"version": "4.20.3",
|
"version": "4.20.3",
|
||||||
@@ -16099,7 +16082,6 @@
|
|||||||
"integrity": "sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==",
|
"integrity": "sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "~0.25.0",
|
"esbuild": "~0.25.0",
|
||||||
"get-tsconfig": "^4.7.5"
|
"get-tsconfig": "^4.7.5"
|
||||||
@@ -16260,7 +16242,6 @@
|
|||||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
@@ -16468,7 +16449,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-7.2.2.tgz",
|
||||||
"integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==",
|
"integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.25.0",
|
"esbuild": "^0.25.0",
|
||||||
"fdir": "^6.5.0",
|
"fdir": "^6.5.0",
|
||||||
@@ -16582,7 +16562,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@@ -16595,7 +16574,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz",
|
||||||
"integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
|
"integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/chai": "^5.2.2",
|
"@types/chai": "^5.2.2",
|
||||||
"@vitest/expect": "3.2.4",
|
"@vitest/expect": "3.2.4",
|
||||||
@@ -17228,7 +17206,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
||||||
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
}
|
}
|
||||||
@@ -17244,7 +17221,7 @@
|
|||||||
},
|
},
|
||||||
"packages/a2a-server": {
|
"packages/a2a-server": {
|
||||||
"name": "@google/gemini-cli-a2a-server",
|
"name": "@google/gemini-cli-a2a-server",
|
||||||
"version": "0.29.0-nightly.20260203.71f46f116",
|
"version": "0.29.4",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@a2a-js/sdk": "^0.3.8",
|
"@a2a-js/sdk": "^0.3.8",
|
||||||
"@google-cloud/storage": "^7.16.0",
|
"@google-cloud/storage": "^7.16.0",
|
||||||
@@ -17300,7 +17277,7 @@
|
|||||||
},
|
},
|
||||||
"packages/cli": {
|
"packages/cli": {
|
||||||
"name": "@google/gemini-cli",
|
"name": "@google/gemini-cli",
|
||||||
"version": "0.29.0-nightly.20260203.71f46f116",
|
"version": "0.29.4",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@agentclientprotocol/sdk": "^0.12.0",
|
"@agentclientprotocol/sdk": "^0.12.0",
|
||||||
@@ -17313,6 +17290,7 @@
|
|||||||
"chalk": "^4.1.2",
|
"chalk": "^4.1.2",
|
||||||
"cli-spinners": "^2.9.2",
|
"cli-spinners": "^2.9.2",
|
||||||
"clipboardy": "^5.0.0",
|
"clipboardy": "^5.0.0",
|
||||||
|
"color-convert": "^2.0.1",
|
||||||
"command-exists": "^1.2.9",
|
"command-exists": "^1.2.9",
|
||||||
"comment-json": "^4.2.5",
|
"comment-json": "^4.2.5",
|
||||||
"diff": "^8.0.3",
|
"diff": "^8.0.3",
|
||||||
@@ -17397,7 +17375,7 @@
|
|||||||
},
|
},
|
||||||
"packages/core": {
|
"packages/core": {
|
||||||
"name": "@google/gemini-cli-core",
|
"name": "@google/gemini-cli-core",
|
||||||
"version": "0.29.0-nightly.20260203.71f46f116",
|
"version": "0.29.4",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@a2a-js/sdk": "^0.3.8",
|
"@a2a-js/sdk": "^0.3.8",
|
||||||
@@ -17534,7 +17512,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@@ -17557,7 +17534,7 @@
|
|||||||
},
|
},
|
||||||
"packages/test-utils": {
|
"packages/test-utils": {
|
||||||
"name": "@google/gemini-cli-test-utils",
|
"name": "@google/gemini-cli-test-utils",
|
||||||
"version": "0.29.0-nightly.20260203.71f46f116",
|
"version": "0.29.4",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@google/gemini-cli-core": "file:../core",
|
"@google/gemini-cli-core": "file:../core",
|
||||||
@@ -17574,7 +17551,7 @@
|
|||||||
},
|
},
|
||||||
"packages/vscode-ide-companion": {
|
"packages/vscode-ide-companion": {
|
||||||
"name": "gemini-cli-vscode-ide-companion",
|
"name": "gemini-cli-vscode-ide-companion",
|
||||||
"version": "0.29.0-nightly.20260203.71f46f116",
|
"version": "0.29.4",
|
||||||
"license": "LICENSE",
|
"license": "LICENSE",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modelcontextprotocol/sdk": "^1.23.0",
|
"@modelcontextprotocol/sdk": "^1.23.0",
|
||||||
|
|||||||
+3
-2
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@google/gemini-cli",
|
"name": "@google/gemini-cli",
|
||||||
"version": "0.29.0-nightly.20260203.71f46f116",
|
"version": "0.29.4",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20.0.0"
|
"node": ">=20.0.0"
|
||||||
},
|
},
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
"url": "git+https://github.com/google-gemini/gemini-cli.git"
|
"url": "git+https://github.com/google-gemini/gemini-cli.git"
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.29.0-nightly.20260203.71f46f116"
|
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.29.4"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "cross-env NODE_ENV=development node scripts/start.js",
|
"start": "cross-env NODE_ENV=development node scripts/start.js",
|
||||||
@@ -129,6 +129,7 @@
|
|||||||
"ink": "npm:@jrichman/ink@6.4.8",
|
"ink": "npm:@jrichman/ink@6.4.8",
|
||||||
"latest-version": "^9.0.0",
|
"latest-version": "^9.0.0",
|
||||||
"proper-lockfile": "^4.1.2",
|
"proper-lockfile": "^4.1.2",
|
||||||
|
"punycode": "^2.3.1",
|
||||||
"simple-git": "^3.28.0"
|
"simple-git": "^3.28.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@google/gemini-cli-a2a-server",
|
"name": "@google/gemini-cli-a2a-server",
|
||||||
"version": "0.29.0-nightly.20260203.71f46f116",
|
"version": "0.29.4",
|
||||||
"description": "Gemini CLI A2A Server",
|
"description": "Gemini CLI A2A Server",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@google/gemini-cli",
|
"name": "@google/gemini-cli",
|
||||||
"version": "0.29.0-nightly.20260203.71f46f116",
|
"version": "0.29.4",
|
||||||
"description": "Gemini CLI",
|
"description": "Gemini CLI",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"repository": {
|
"repository": {
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
"dist"
|
"dist"
|
||||||
],
|
],
|
||||||
"config": {
|
"config": {
|
||||||
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.29.0-nightly.20260203.71f46f116"
|
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.29.4"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@agentclientprotocol/sdk": "^0.12.0",
|
"@agentclientprotocol/sdk": "^0.12.0",
|
||||||
@@ -39,6 +39,7 @@
|
|||||||
"chalk": "^4.1.2",
|
"chalk": "^4.1.2",
|
||||||
"cli-spinners": "^2.9.2",
|
"cli-spinners": "^2.9.2",
|
||||||
"clipboardy": "^5.0.0",
|
"clipboardy": "^5.0.0",
|
||||||
|
"color-convert": "^2.0.1",
|
||||||
"command-exists": "^1.2.9",
|
"command-exists": "^1.2.9",
|
||||||
"comment-json": "^4.2.5",
|
"comment-json": "^4.2.5",
|
||||||
"diff": "^8.0.3",
|
"diff": "^8.0.3",
|
||||||
|
|||||||
@@ -128,13 +128,6 @@ async function addMcpServer(
|
|||||||
|
|
||||||
settings.setValue(settingsScope, 'mcpServers', mcpServers);
|
settings.setValue(settingsScope, 'mcpServers', mcpServers);
|
||||||
|
|
||||||
if (transport === 'stdio') {
|
|
||||||
debugLogger.warn(
|
|
||||||
'Security Warning: Running MCP servers with stdio transport can expose inherited environment variables. ' +
|
|
||||||
'While the Gemini CLI redacts common API keys and secrets by default, you should only run servers from trusted sources.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isExistingServer) {
|
if (isExistingServer) {
|
||||||
debugLogger.log(`MCP server "${name}" updated in ${scope} settings.`);
|
debugLogger.log(`MCP server "${name}" updated in ${scope} settings.`);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -445,7 +445,11 @@ export async function loadCliConfig(
|
|||||||
process.env['VITEST'] === 'true'
|
process.env['VITEST'] === 'true'
|
||||||
? false
|
? false
|
||||||
: (settings.security?.folderTrust?.enabled ?? false);
|
: (settings.security?.folderTrust?.enabled ?? false);
|
||||||
const trustedFolder = isWorkspaceTrusted(settings, cwd)?.isTrusted ?? false;
|
const trustedFolder =
|
||||||
|
isWorkspaceTrusted(settings, cwd, undefined, {
|
||||||
|
prompt: argv.prompt,
|
||||||
|
query: argv.query,
|
||||||
|
})?.isTrusted ?? false;
|
||||||
|
|
||||||
// Set the context filename in the server's memoryTool module BEFORE loading memory
|
// Set the context filename in the server's memoryTool module BEFORE loading memory
|
||||||
// TODO(b/343434939): This is a bit of a hack. The contextFileName should ideally be passed
|
// TODO(b/343434939): This is a bit of a hack. The contextFileName should ideally be passed
|
||||||
@@ -602,8 +606,7 @@ export async function loadCliConfig(
|
|||||||
const interactive =
|
const interactive =
|
||||||
!!argv.promptInteractive ||
|
!!argv.promptInteractive ||
|
||||||
!!argv.experimentalAcp ||
|
!!argv.experimentalAcp ||
|
||||||
(!isHeadlessMode({ prompt: argv.prompt }) &&
|
(!isHeadlessMode({ prompt: argv.prompt, query: argv.query }) &&
|
||||||
!argv.query &&
|
|
||||||
!argv.isCommand);
|
!argv.isCommand);
|
||||||
|
|
||||||
const allowedTools = argv.allowedTools || settings.tools?.allowed || [];
|
const allowedTools = argv.allowedTools || settings.tools?.allowed || [];
|
||||||
|
|||||||
@@ -449,6 +449,14 @@ describe('Trusted Folders', () => {
|
|||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should return true for isPathTrusted when isHeadlessMode is true', async () => {
|
||||||
|
const geminiCore = await import('@google/gemini-cli-core');
|
||||||
|
vi.spyOn(geminiCore, 'isHeadlessMode').mockReturnValue(true);
|
||||||
|
|
||||||
|
const folders = loadTrustedFolders();
|
||||||
|
expect(folders.isPathTrusted('/any-untrusted-path')).toBe(true);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Trusted Folders Caching', () => {
|
describe('Trusted Folders Caching', () => {
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
homedir,
|
homedir,
|
||||||
isHeadlessMode,
|
isHeadlessMode,
|
||||||
coreEvents,
|
coreEvents,
|
||||||
|
type HeadlessModeOptions,
|
||||||
} from '@google/gemini-cli-core';
|
} from '@google/gemini-cli-core';
|
||||||
import type { Settings } from './settings.js';
|
import type { Settings } from './settings.js';
|
||||||
import stripJsonComments from 'strip-json-comments';
|
import stripJsonComments from 'strip-json-comments';
|
||||||
@@ -128,7 +129,11 @@ export class LoadedTrustedFolders {
|
|||||||
isPathTrusted(
|
isPathTrusted(
|
||||||
location: string,
|
location: string,
|
||||||
config?: Record<string, TrustLevel>,
|
config?: Record<string, TrustLevel>,
|
||||||
|
headlessOptions?: HeadlessModeOptions,
|
||||||
): boolean | undefined {
|
): boolean | undefined {
|
||||||
|
if (isHeadlessMode(headlessOptions)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
const configToUse = config ?? this.user.config;
|
const configToUse = config ?? this.user.config;
|
||||||
|
|
||||||
// Resolve location to its realpath for canonical comparison
|
// Resolve location to its realpath for canonical comparison
|
||||||
@@ -333,6 +338,7 @@ export function isFolderTrustEnabled(settings: Settings): boolean {
|
|||||||
function getWorkspaceTrustFromLocalConfig(
|
function getWorkspaceTrustFromLocalConfig(
|
||||||
workspaceDir: string,
|
workspaceDir: string,
|
||||||
trustConfig?: Record<string, TrustLevel>,
|
trustConfig?: Record<string, TrustLevel>,
|
||||||
|
headlessOptions?: HeadlessModeOptions,
|
||||||
): TrustResult {
|
): TrustResult {
|
||||||
const folders = loadTrustedFolders();
|
const folders = loadTrustedFolders();
|
||||||
const configToUse = trustConfig ?? folders.user.config;
|
const configToUse = trustConfig ?? folders.user.config;
|
||||||
@@ -346,7 +352,11 @@ function getWorkspaceTrustFromLocalConfig(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const isTrusted = folders.isPathTrusted(workspaceDir, configToUse);
|
const isTrusted = folders.isPathTrusted(
|
||||||
|
workspaceDir,
|
||||||
|
configToUse,
|
||||||
|
headlessOptions,
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
isTrusted,
|
isTrusted,
|
||||||
source: isTrusted !== undefined ? 'file' : undefined,
|
source: isTrusted !== undefined ? 'file' : undefined,
|
||||||
@@ -357,8 +367,9 @@ export function isWorkspaceTrusted(
|
|||||||
settings: Settings,
|
settings: Settings,
|
||||||
workspaceDir: string = process.cwd(),
|
workspaceDir: string = process.cwd(),
|
||||||
trustConfig?: Record<string, TrustLevel>,
|
trustConfig?: Record<string, TrustLevel>,
|
||||||
|
headlessOptions?: HeadlessModeOptions,
|
||||||
): TrustResult {
|
): TrustResult {
|
||||||
if (isHeadlessMode()) {
|
if (isHeadlessMode(headlessOptions)) {
|
||||||
return { isTrusted: true, source: undefined };
|
return { isTrusted: true, source: undefined };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -372,5 +383,9 @@ export function isWorkspaceTrusted(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fall back to the local user configuration
|
// Fall back to the local user configuration
|
||||||
return getWorkspaceTrustFromLocalConfig(workspaceDir, trustConfig);
|
return getWorkspaceTrustFromLocalConfig(
|
||||||
|
workspaceDir,
|
||||||
|
trustConfig,
|
||||||
|
headlessOptions,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -671,6 +671,7 @@ export const AppContainer = (props: AppContainerProps) => {
|
|||||||
settings.setValue(scope, 'security.auth.selectedType', authType);
|
settings.setValue(scope, 'security.auth.selectedType', authType);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
config.setRemoteAdminSettings(undefined);
|
||||||
await config.refreshAuth(authType);
|
await config.refreshAuth(authType);
|
||||||
setAuthState(AuthState.Authenticated);
|
setAuthState(AuthState.Authenticated);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { Box, Text } from 'ink';
|
|||||||
import { theme } from '../semantic-colors.js';
|
import { theme } from '../semantic-colors.js';
|
||||||
import { GIT_COMMIT_INFO } from '../../generated/git-commit.js';
|
import { GIT_COMMIT_INFO } from '../../generated/git-commit.js';
|
||||||
import { useSettings } from '../contexts/SettingsContext.js';
|
import { useSettings } from '../contexts/SettingsContext.js';
|
||||||
|
import { getDisplayString } from '@google/gemini-cli-core';
|
||||||
|
|
||||||
interface AboutBoxProps {
|
interface AboutBoxProps {
|
||||||
cliVersion: string;
|
cliVersion: string;
|
||||||
@@ -79,7 +80,9 @@ export const AboutBox: React.FC<AboutBoxProps> = ({
|
|||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<Box>
|
<Box>
|
||||||
<Text color={theme.text.primary}>{modelVersion}</Text>
|
<Text color={theme.text.primary}>
|
||||||
|
{getDisplayString(modelVersion)}
|
||||||
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
<Box flexDirection="row">
|
<Box flexDirection="row">
|
||||||
|
|||||||
@@ -14,9 +14,7 @@ describe('ApprovalModeIndicator', () => {
|
|||||||
const { lastFrame } = render(
|
const { lastFrame } = render(
|
||||||
<ApprovalModeIndicator approvalMode={ApprovalMode.AUTO_EDIT} />,
|
<ApprovalModeIndicator approvalMode={ApprovalMode.AUTO_EDIT} />,
|
||||||
);
|
);
|
||||||
const output = lastFrame();
|
expect(lastFrame()).toMatchSnapshot();
|
||||||
expect(output).toContain('auto-accept edits');
|
|
||||||
expect(output).toContain('shift+tab to manual');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders correctly for AUTO_EDIT mode with plan enabled', () => {
|
it('renders correctly for AUTO_EDIT mode with plan enabled', () => {
|
||||||
@@ -26,35 +24,28 @@ describe('ApprovalModeIndicator', () => {
|
|||||||
isPlanEnabled={true}
|
isPlanEnabled={true}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
const output = lastFrame();
|
expect(lastFrame()).toMatchSnapshot();
|
||||||
expect(output).toContain('auto-accept edits');
|
|
||||||
expect(output).toContain('shift+tab to manual');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders correctly for PLAN mode', () => {
|
it('renders correctly for PLAN mode', () => {
|
||||||
const { lastFrame } = render(
|
const { lastFrame } = render(
|
||||||
<ApprovalModeIndicator approvalMode={ApprovalMode.PLAN} />,
|
<ApprovalModeIndicator approvalMode={ApprovalMode.PLAN} />,
|
||||||
);
|
);
|
||||||
const output = lastFrame();
|
expect(lastFrame()).toMatchSnapshot();
|
||||||
expect(output).toContain('plan');
|
|
||||||
expect(output).toContain('shift+tab to accept edits');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders correctly for YOLO mode', () => {
|
it('renders correctly for YOLO mode', () => {
|
||||||
const { lastFrame } = render(
|
const { lastFrame } = render(
|
||||||
<ApprovalModeIndicator approvalMode={ApprovalMode.YOLO} />,
|
<ApprovalModeIndicator approvalMode={ApprovalMode.YOLO} />,
|
||||||
);
|
);
|
||||||
const output = lastFrame();
|
expect(lastFrame()).toMatchSnapshot();
|
||||||
expect(output).toContain('YOLO');
|
|
||||||
expect(output).toContain('ctrl+y');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders correctly for DEFAULT mode', () => {
|
it('renders correctly for DEFAULT mode', () => {
|
||||||
const { lastFrame } = render(
|
const { lastFrame } = render(
|
||||||
<ApprovalModeIndicator approvalMode={ApprovalMode.DEFAULT} />,
|
<ApprovalModeIndicator approvalMode={ApprovalMode.DEFAULT} />,
|
||||||
);
|
);
|
||||||
const output = lastFrame();
|
expect(lastFrame()).toMatchSnapshot();
|
||||||
expect(output).toContain('shift+tab to accept edits');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders correctly for DEFAULT mode with plan enabled', () => {
|
it('renders correctly for DEFAULT mode with plan enabled', () => {
|
||||||
@@ -64,7 +55,6 @@ describe('ApprovalModeIndicator', () => {
|
|||||||
isPlanEnabled={true}
|
isPlanEnabled={true}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
const output = lastFrame();
|
expect(lastFrame()).toMatchSnapshot();
|
||||||
expect(output).toContain('shift+tab to plan');
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,6 +14,16 @@ interface ApprovalModeIndicatorProps {
|
|||||||
isPlanEnabled?: boolean;
|
isPlanEnabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const APPROVAL_MODE_TEXT = {
|
||||||
|
AUTO_EDIT: 'auto-accept edits',
|
||||||
|
PLAN: 'plan',
|
||||||
|
YOLO: 'YOLO',
|
||||||
|
HINT_SWITCH_TO_PLAN_MODE: 'shift+tab to plan',
|
||||||
|
HINT_SWITCH_TO_MANUAL_MODE: 'shift+tab to manual',
|
||||||
|
HINT_SWITCH_TO_AUTO_EDIT_MODE: 'shift+tab to accept edits',
|
||||||
|
HINT_SWITCH_TO_YOLO_MODE: 'ctrl+y',
|
||||||
|
};
|
||||||
|
|
||||||
export const ApprovalModeIndicator: React.FC<ApprovalModeIndicatorProps> = ({
|
export const ApprovalModeIndicator: React.FC<ApprovalModeIndicatorProps> = ({
|
||||||
approvalMode,
|
approvalMode,
|
||||||
isPlanEnabled,
|
isPlanEnabled,
|
||||||
@@ -25,26 +35,26 @@ export const ApprovalModeIndicator: React.FC<ApprovalModeIndicatorProps> = ({
|
|||||||
switch (approvalMode) {
|
switch (approvalMode) {
|
||||||
case ApprovalMode.AUTO_EDIT:
|
case ApprovalMode.AUTO_EDIT:
|
||||||
textColor = theme.status.warning;
|
textColor = theme.status.warning;
|
||||||
textContent = 'auto-accept edits';
|
textContent = APPROVAL_MODE_TEXT.AUTO_EDIT;
|
||||||
subText = 'shift+tab to manual';
|
subText = isPlanEnabled
|
||||||
|
? APPROVAL_MODE_TEXT.HINT_SWITCH_TO_PLAN_MODE
|
||||||
|
: APPROVAL_MODE_TEXT.HINT_SWITCH_TO_MANUAL_MODE;
|
||||||
break;
|
break;
|
||||||
case ApprovalMode.PLAN:
|
case ApprovalMode.PLAN:
|
||||||
textColor = theme.status.success;
|
textColor = theme.status.success;
|
||||||
textContent = 'plan';
|
textContent = APPROVAL_MODE_TEXT.PLAN;
|
||||||
subText = 'shift+tab to accept edits';
|
subText = APPROVAL_MODE_TEXT.HINT_SWITCH_TO_MANUAL_MODE;
|
||||||
break;
|
break;
|
||||||
case ApprovalMode.YOLO:
|
case ApprovalMode.YOLO:
|
||||||
textColor = theme.status.error;
|
textColor = theme.status.error;
|
||||||
textContent = 'YOLO';
|
textContent = APPROVAL_MODE_TEXT.YOLO;
|
||||||
subText = 'ctrl+y';
|
subText = APPROVAL_MODE_TEXT.HINT_SWITCH_TO_YOLO_MODE;
|
||||||
break;
|
break;
|
||||||
case ApprovalMode.DEFAULT:
|
case ApprovalMode.DEFAULT:
|
||||||
default:
|
default:
|
||||||
textColor = theme.text.accent;
|
textColor = theme.text.accent;
|
||||||
textContent = '';
|
textContent = '';
|
||||||
subText = isPlanEnabled
|
subText = APPROVAL_MODE_TEXT.HINT_SWITCH_TO_AUTO_EDIT_MODE;
|
||||||
? 'shift+tab to plan'
|
|
||||||
: 'shift+tab to accept edits';
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,8 +14,14 @@ import {
|
|||||||
DEFAULT_GEMINI_MODEL_AUTO,
|
DEFAULT_GEMINI_MODEL_AUTO,
|
||||||
DEFAULT_GEMINI_FLASH_MODEL,
|
DEFAULT_GEMINI_FLASH_MODEL,
|
||||||
DEFAULT_GEMINI_FLASH_LITE_MODEL,
|
DEFAULT_GEMINI_FLASH_LITE_MODEL,
|
||||||
|
PREVIEW_GEMINI_MODEL,
|
||||||
|
PREVIEW_GEMINI_3_1_MODEL,
|
||||||
|
PREVIEW_GEMINI_3_1_CUSTOM_TOOLS_MODEL,
|
||||||
|
PREVIEW_GEMINI_FLASH_MODEL,
|
||||||
|
AuthType,
|
||||||
} from '@google/gemini-cli-core';
|
} from '@google/gemini-cli-core';
|
||||||
import type { Config, ModelSlashCommandEvent } from '@google/gemini-cli-core';
|
import type { Config, ModelSlashCommandEvent } from '@google/gemini-cli-core';
|
||||||
|
import { createMockSettings } from '../../test-utils/settings.js';
|
||||||
|
|
||||||
// Mock dependencies
|
// Mock dependencies
|
||||||
const mockGetDisplayString = vi.fn();
|
const mockGetDisplayString = vi.fn();
|
||||||
@@ -42,12 +48,14 @@ describe('<ModelDialog />', () => {
|
|||||||
const mockGetModel = vi.fn();
|
const mockGetModel = vi.fn();
|
||||||
const mockOnClose = vi.fn();
|
const mockOnClose = vi.fn();
|
||||||
const mockGetHasAccessToPreviewModel = vi.fn();
|
const mockGetHasAccessToPreviewModel = vi.fn();
|
||||||
|
const mockGetGemini31LaunchedSync = vi.fn();
|
||||||
|
|
||||||
interface MockConfig extends Partial<Config> {
|
interface MockConfig extends Partial<Config> {
|
||||||
setModel: (model: string, isTemporary?: boolean) => void;
|
setModel: (model: string, isTemporary?: boolean) => void;
|
||||||
getModel: () => string;
|
getModel: () => string;
|
||||||
getHasAccessToPreviewModel: () => boolean;
|
getHasAccessToPreviewModel: () => boolean;
|
||||||
getIdeMode: () => boolean;
|
getIdeMode: () => boolean;
|
||||||
|
getGemini31LaunchedSync: () => boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const mockConfig: MockConfig = {
|
const mockConfig: MockConfig = {
|
||||||
@@ -55,12 +63,14 @@ describe('<ModelDialog />', () => {
|
|||||||
getModel: mockGetModel,
|
getModel: mockGetModel,
|
||||||
getHasAccessToPreviewModel: mockGetHasAccessToPreviewModel,
|
getHasAccessToPreviewModel: mockGetHasAccessToPreviewModel,
|
||||||
getIdeMode: () => false,
|
getIdeMode: () => false,
|
||||||
|
getGemini31LaunchedSync: mockGetGemini31LaunchedSync,
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.resetAllMocks();
|
vi.resetAllMocks();
|
||||||
mockGetModel.mockReturnValue(DEFAULT_GEMINI_MODEL_AUTO);
|
mockGetModel.mockReturnValue(DEFAULT_GEMINI_MODEL_AUTO);
|
||||||
mockGetHasAccessToPreviewModel.mockReturnValue(false);
|
mockGetHasAccessToPreviewModel.mockReturnValue(false);
|
||||||
|
mockGetGemini31LaunchedSync.mockReturnValue(false);
|
||||||
|
|
||||||
// Default implementation for getDisplayString
|
// Default implementation for getDisplayString
|
||||||
mockGetDisplayString.mockImplementation((val: string) => {
|
mockGetDisplayString.mockImplementation((val: string) => {
|
||||||
@@ -70,11 +80,24 @@ describe('<ModelDialog />', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const renderComponent = (configValue = mockConfig as Config) =>
|
const renderComponent = (
|
||||||
renderWithProviders(<ModelDialog onClose={mockOnClose} />, {
|
configValue = mockConfig as Config,
|
||||||
config: configValue,
|
authType = AuthType.LOGIN_WITH_GOOGLE,
|
||||||
|
) => {
|
||||||
|
const settings = createMockSettings({
|
||||||
|
security: {
|
||||||
|
auth: {
|
||||||
|
selectedType: authType,
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return renderWithProviders(<ModelDialog onClose={mockOnClose} />, {
|
||||||
|
config: configValue,
|
||||||
|
settings,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
it('renders the initial "main" view correctly', () => {
|
it('renders the initial "main" view correctly', () => {
|
||||||
const { lastFrame } = renderComponent();
|
const { lastFrame } = renderComponent();
|
||||||
expect(lastFrame()).toContain('Select Model');
|
expect(lastFrame()).toContain('Select Model');
|
||||||
@@ -210,4 +233,96 @@ describe('<ModelDialog />', () => {
|
|||||||
expect(lastFrame()).toContain('Manual');
|
expect(lastFrame()).toContain('Manual');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('shows the preferred manual model in the main view option', () => {
|
||||||
|
mockGetModel.mockReturnValue(DEFAULT_GEMINI_MODEL);
|
||||||
|
const { lastFrame, unmount } = renderComponent();
|
||||||
|
|
||||||
|
expect(lastFrame()).toContain(`Manual (${DEFAULT_GEMINI_MODEL})`);
|
||||||
|
unmount();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Preview Models', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mockGetHasAccessToPreviewModel.mockReturnValue(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows Auto (Preview) in main view when access is granted', () => {
|
||||||
|
const { lastFrame, unmount } = renderComponent();
|
||||||
|
expect(lastFrame()).toContain('Auto (Preview)');
|
||||||
|
unmount();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows Gemini 3 models in manual view when Gemini 3.1 is NOT launched', async () => {
|
||||||
|
mockGetGemini31LaunchedSync.mockReturnValue(false);
|
||||||
|
const { lastFrame, stdin, unmount } = renderComponent();
|
||||||
|
|
||||||
|
// Go to manual view
|
||||||
|
await act(async () => {
|
||||||
|
stdin.write('\u001B[B'); // Manual
|
||||||
|
});
|
||||||
|
await act(async () => {
|
||||||
|
stdin.write('\r');
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
const output = lastFrame();
|
||||||
|
expect(output).toContain(PREVIEW_GEMINI_MODEL);
|
||||||
|
expect(output).toContain(PREVIEW_GEMINI_FLASH_MODEL);
|
||||||
|
});
|
||||||
|
unmount();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows Gemini 3.1 models in manual view when Gemini 3.1 IS launched', async () => {
|
||||||
|
mockGetGemini31LaunchedSync.mockReturnValue(true);
|
||||||
|
const { lastFrame, stdin, unmount } = renderComponent(
|
||||||
|
mockConfig as Config,
|
||||||
|
AuthType.USE_VERTEX_AI,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Go to manual view
|
||||||
|
await act(async () => {
|
||||||
|
stdin.write('\u001B[B'); // Manual
|
||||||
|
});
|
||||||
|
await act(async () => {
|
||||||
|
stdin.write('\r');
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
const output = lastFrame();
|
||||||
|
expect(output).toContain(PREVIEW_GEMINI_3_1_MODEL);
|
||||||
|
expect(output).toContain(PREVIEW_GEMINI_FLASH_MODEL);
|
||||||
|
});
|
||||||
|
unmount();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses custom tools model when Gemini 3.1 IS launched and auth is Gemini API Key', async () => {
|
||||||
|
mockGetGemini31LaunchedSync.mockReturnValue(true);
|
||||||
|
const { stdin, unmount } = renderComponent(
|
||||||
|
mockConfig as Config,
|
||||||
|
AuthType.USE_GEMINI,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Go to manual view
|
||||||
|
await act(async () => {
|
||||||
|
stdin.write('\u001B[B'); // Manual
|
||||||
|
});
|
||||||
|
await act(async () => {
|
||||||
|
stdin.write('\r');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Select Gemini 3.1 (first item in preview section)
|
||||||
|
await act(async () => {
|
||||||
|
stdin.write('\r');
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockSetModel).toHaveBeenCalledWith(
|
||||||
|
PREVIEW_GEMINI_3_1_CUSTOM_TOOLS_MODEL,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
unmount();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { useCallback, useContext, useMemo, useState } from 'react';
|
|||||||
import { Box, Text } from 'ink';
|
import { Box, Text } from 'ink';
|
||||||
import {
|
import {
|
||||||
PREVIEW_GEMINI_MODEL,
|
PREVIEW_GEMINI_MODEL,
|
||||||
|
PREVIEW_GEMINI_3_1_MODEL,
|
||||||
PREVIEW_GEMINI_FLASH_MODEL,
|
PREVIEW_GEMINI_FLASH_MODEL,
|
||||||
PREVIEW_GEMINI_MODEL_AUTO,
|
PREVIEW_GEMINI_MODEL_AUTO,
|
||||||
DEFAULT_GEMINI_MODEL,
|
DEFAULT_GEMINI_MODEL,
|
||||||
@@ -18,11 +19,14 @@ import {
|
|||||||
ModelSlashCommandEvent,
|
ModelSlashCommandEvent,
|
||||||
logModelSlashCommand,
|
logModelSlashCommand,
|
||||||
getDisplayString,
|
getDisplayString,
|
||||||
|
AuthType,
|
||||||
|
PREVIEW_GEMINI_3_1_CUSTOM_TOOLS_MODEL,
|
||||||
} from '@google/gemini-cli-core';
|
} from '@google/gemini-cli-core';
|
||||||
import { useKeypress } from '../hooks/useKeypress.js';
|
import { useKeypress } from '../hooks/useKeypress.js';
|
||||||
import { theme } from '../semantic-colors.js';
|
import { theme } from '../semantic-colors.js';
|
||||||
import { DescriptiveRadioButtonSelect } from './shared/DescriptiveRadioButtonSelect.js';
|
import { DescriptiveRadioButtonSelect } from './shared/DescriptiveRadioButtonSelect.js';
|
||||||
import { ConfigContext } from '../contexts/ConfigContext.js';
|
import { ConfigContext } from '../contexts/ConfigContext.js';
|
||||||
|
import { useSettings } from '../contexts/SettingsContext.js';
|
||||||
|
|
||||||
interface ModelDialogProps {
|
interface ModelDialogProps {
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
@@ -30,6 +34,7 @@ interface ModelDialogProps {
|
|||||||
|
|
||||||
export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element {
|
export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element {
|
||||||
const config = useContext(ConfigContext);
|
const config = useContext(ConfigContext);
|
||||||
|
const settings = useSettings();
|
||||||
const [view, setView] = useState<'main' | 'manual'>('main');
|
const [view, setView] = useState<'main' | 'manual'>('main');
|
||||||
const [persistMode, setPersistMode] = useState(false);
|
const [persistMode, setPersistMode] = useState(false);
|
||||||
|
|
||||||
@@ -37,6 +42,10 @@ export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element {
|
|||||||
const preferredModel = config?.getModel() || DEFAULT_GEMINI_MODEL_AUTO;
|
const preferredModel = config?.getModel() || DEFAULT_GEMINI_MODEL_AUTO;
|
||||||
|
|
||||||
const shouldShowPreviewModels = config?.getHasAccessToPreviewModel();
|
const shouldShowPreviewModels = config?.getHasAccessToPreviewModel();
|
||||||
|
const useGemini31 = config?.getGemini31LaunchedSync?.() ?? false;
|
||||||
|
const selectedAuthType = settings.merged.security.auth.selectedType;
|
||||||
|
const useCustomToolModel =
|
||||||
|
useGemini31 && selectedAuthType === AuthType.USE_GEMINI;
|
||||||
|
|
||||||
const manualModelSelected = useMemo(() => {
|
const manualModelSelected = useMemo(() => {
|
||||||
const manualModels = [
|
const manualModels = [
|
||||||
@@ -44,6 +53,8 @@ export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element {
|
|||||||
DEFAULT_GEMINI_FLASH_MODEL,
|
DEFAULT_GEMINI_FLASH_MODEL,
|
||||||
DEFAULT_GEMINI_FLASH_LITE_MODEL,
|
DEFAULT_GEMINI_FLASH_LITE_MODEL,
|
||||||
PREVIEW_GEMINI_MODEL,
|
PREVIEW_GEMINI_MODEL,
|
||||||
|
PREVIEW_GEMINI_3_1_MODEL,
|
||||||
|
PREVIEW_GEMINI_3_1_CUSTOM_TOOLS_MODEL,
|
||||||
PREVIEW_GEMINI_FLASH_MODEL,
|
PREVIEW_GEMINI_FLASH_MODEL,
|
||||||
];
|
];
|
||||||
if (manualModels.includes(preferredModel)) {
|
if (manualModels.includes(preferredModel)) {
|
||||||
@@ -94,13 +105,14 @@ export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element {
|
|||||||
list.unshift({
|
list.unshift({
|
||||||
value: PREVIEW_GEMINI_MODEL_AUTO,
|
value: PREVIEW_GEMINI_MODEL_AUTO,
|
||||||
title: getDisplayString(PREVIEW_GEMINI_MODEL_AUTO),
|
title: getDisplayString(PREVIEW_GEMINI_MODEL_AUTO),
|
||||||
description:
|
description: useGemini31
|
||||||
'Let Gemini CLI decide the best model for the task: gemini-3-pro, gemini-3-flash',
|
? 'Let Gemini CLI decide the best model for the task: gemini-3.1-pro, gemini-3-flash'
|
||||||
|
: 'Let Gemini CLI decide the best model for the task: gemini-3-pro, gemini-3-flash',
|
||||||
key: PREVIEW_GEMINI_MODEL_AUTO,
|
key: PREVIEW_GEMINI_MODEL_AUTO,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return list;
|
return list;
|
||||||
}, [shouldShowPreviewModels, manualModelSelected]);
|
}, [shouldShowPreviewModels, manualModelSelected, useGemini31]);
|
||||||
|
|
||||||
const manualOptions = useMemo(() => {
|
const manualOptions = useMemo(() => {
|
||||||
const list = [
|
const list = [
|
||||||
@@ -122,11 +134,19 @@ export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element {
|
|||||||
];
|
];
|
||||||
|
|
||||||
if (shouldShowPreviewModels) {
|
if (shouldShowPreviewModels) {
|
||||||
|
const previewProModel = useGemini31
|
||||||
|
? PREVIEW_GEMINI_3_1_MODEL
|
||||||
|
: PREVIEW_GEMINI_MODEL;
|
||||||
|
|
||||||
|
const previewProValue = useCustomToolModel
|
||||||
|
? PREVIEW_GEMINI_3_1_CUSTOM_TOOLS_MODEL
|
||||||
|
: previewProModel;
|
||||||
|
|
||||||
list.unshift(
|
list.unshift(
|
||||||
{
|
{
|
||||||
value: PREVIEW_GEMINI_MODEL,
|
value: previewProValue,
|
||||||
title: PREVIEW_GEMINI_MODEL,
|
title: previewProModel,
|
||||||
key: PREVIEW_GEMINI_MODEL,
|
key: previewProModel,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: PREVIEW_GEMINI_FLASH_MODEL,
|
value: PREVIEW_GEMINI_FLASH_MODEL,
|
||||||
@@ -136,7 +156,7 @@ export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
return list;
|
return list;
|
||||||
}, [shouldShowPreviewModels]);
|
}, [shouldShowPreviewModels, useGemini31, useCustomToolModel]);
|
||||||
|
|
||||||
const options = view === 'main' ? mainOptions : manualOptions;
|
const options = view === 'main' ? mainOptions : manualOptions;
|
||||||
|
|
||||||
|
|||||||
@@ -23,11 +23,13 @@ import {
|
|||||||
import { computeSessionStats } from '../utils/computeStats.js';
|
import { computeSessionStats } from '../utils/computeStats.js';
|
||||||
import {
|
import {
|
||||||
type RetrieveUserQuotaResponse,
|
type RetrieveUserQuotaResponse,
|
||||||
VALID_GEMINI_MODELS,
|
isActiveModel,
|
||||||
getDisplayString,
|
getDisplayString,
|
||||||
isAutoModel,
|
isAutoModel,
|
||||||
|
AuthType,
|
||||||
} from '@google/gemini-cli-core';
|
} from '@google/gemini-cli-core';
|
||||||
import { useSettings } from '../contexts/SettingsContext.js';
|
import { useSettings } from '../contexts/SettingsContext.js';
|
||||||
|
import { useConfig } from '../contexts/ConfigContext.js';
|
||||||
import type { QuotaStats } from '../types.js';
|
import type { QuotaStats } from '../types.js';
|
||||||
import { QuotaStatsInfo } from './QuotaStatsInfo.js';
|
import { QuotaStatsInfo } from './QuotaStatsInfo.js';
|
||||||
|
|
||||||
@@ -82,9 +84,13 @@ const Section: React.FC<SectionProps> = ({ title, children }) => (
|
|||||||
const buildModelRows = (
|
const buildModelRows = (
|
||||||
models: Record<string, ModelMetrics>,
|
models: Record<string, ModelMetrics>,
|
||||||
quotas?: RetrieveUserQuotaResponse,
|
quotas?: RetrieveUserQuotaResponse,
|
||||||
|
useGemini3_1 = false,
|
||||||
|
useCustomToolModel = false,
|
||||||
) => {
|
) => {
|
||||||
const getBaseModelName = (name: string) => name.replace('-001', '');
|
const getBaseModelName = (name: string) => name.replace('-001', '');
|
||||||
const usedModelNames = new Set(Object.keys(models).map(getBaseModelName));
|
const usedModelNames = new Set(
|
||||||
|
Object.keys(models).map(getBaseModelName).map(getDisplayString),
|
||||||
|
);
|
||||||
|
|
||||||
// 1. Models with active usage
|
// 1. Models with active usage
|
||||||
const activeRows = Object.entries(models).map(([name, metrics]) => {
|
const activeRows = Object.entries(models).map(([name, metrics]) => {
|
||||||
@@ -93,7 +99,7 @@ const buildModelRows = (
|
|||||||
const inputTokens = metrics.tokens.input;
|
const inputTokens = metrics.tokens.input;
|
||||||
return {
|
return {
|
||||||
key: name,
|
key: name,
|
||||||
modelName,
|
modelName: getDisplayString(modelName),
|
||||||
requests: metrics.api.totalRequests,
|
requests: metrics.api.totalRequests,
|
||||||
cachedTokens: cachedTokens.toLocaleString(),
|
cachedTokens: cachedTokens.toLocaleString(),
|
||||||
inputTokens: inputTokens.toLocaleString(),
|
inputTokens: inputTokens.toLocaleString(),
|
||||||
@@ -109,12 +115,12 @@ const buildModelRows = (
|
|||||||
?.filter(
|
?.filter(
|
||||||
(b) =>
|
(b) =>
|
||||||
b.modelId &&
|
b.modelId &&
|
||||||
VALID_GEMINI_MODELS.has(b.modelId) &&
|
isActiveModel(b.modelId, useGemini3_1, useCustomToolModel) &&
|
||||||
!usedModelNames.has(b.modelId),
|
!usedModelNames.has(getDisplayString(b.modelId)),
|
||||||
)
|
)
|
||||||
.map((bucket) => ({
|
.map((bucket) => ({
|
||||||
key: bucket.modelId!,
|
key: bucket.modelId!,
|
||||||
modelName: bucket.modelId!,
|
modelName: getDisplayString(bucket.modelId!),
|
||||||
requests: '-',
|
requests: '-',
|
||||||
cachedTokens: '-',
|
cachedTokens: '-',
|
||||||
inputTokens: '-',
|
inputTokens: '-',
|
||||||
@@ -135,6 +141,8 @@ const ModelUsageTable: React.FC<{
|
|||||||
pooledRemaining?: number;
|
pooledRemaining?: number;
|
||||||
pooledLimit?: number;
|
pooledLimit?: number;
|
||||||
pooledResetTime?: string;
|
pooledResetTime?: string;
|
||||||
|
useGemini3_1?: boolean;
|
||||||
|
useCustomToolModel?: boolean;
|
||||||
}> = ({
|
}> = ({
|
||||||
models,
|
models,
|
||||||
quotas,
|
quotas,
|
||||||
@@ -144,8 +152,10 @@ const ModelUsageTable: React.FC<{
|
|||||||
pooledRemaining,
|
pooledRemaining,
|
||||||
pooledLimit,
|
pooledLimit,
|
||||||
pooledResetTime,
|
pooledResetTime,
|
||||||
|
useGemini3_1,
|
||||||
|
useCustomToolModel,
|
||||||
}) => {
|
}) => {
|
||||||
const rows = buildModelRows(models, quotas);
|
const rows = buildModelRows(models, quotas, useGemini3_1, useCustomToolModel);
|
||||||
|
|
||||||
if (rows.length === 0) {
|
if (rows.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
@@ -401,7 +411,11 @@ export const StatsDisplay: React.FC<StatsDisplayProps> = ({
|
|||||||
const { models, tools, files } = metrics;
|
const { models, tools, files } = metrics;
|
||||||
const computed = computeSessionStats(metrics);
|
const computed = computeSessionStats(metrics);
|
||||||
const settings = useSettings();
|
const settings = useSettings();
|
||||||
|
const config = useConfig();
|
||||||
|
const useGemini3_1 = config.getGemini31LaunchedSync?.() ?? false;
|
||||||
|
const useCustomToolModel =
|
||||||
|
useGemini3_1 &&
|
||||||
|
config.getContentGeneratorConfig().authType === AuthType.USE_GEMINI;
|
||||||
const pooledRemaining = quotaStats?.remaining;
|
const pooledRemaining = quotaStats?.remaining;
|
||||||
const pooledLimit = quotaStats?.limit;
|
const pooledLimit = quotaStats?.limit;
|
||||||
const pooledResetTime = quotaStats?.resetTime;
|
const pooledResetTime = quotaStats?.resetTime;
|
||||||
@@ -535,6 +549,8 @@ export const StatsDisplay: React.FC<StatsDisplayProps> = ({
|
|||||||
pooledRemaining={pooledRemaining}
|
pooledRemaining={pooledRemaining}
|
||||||
pooledLimit={pooledLimit}
|
pooledLimit={pooledLimit}
|
||||||
pooledResetTime={pooledResetTime}
|
pooledResetTime={pooledResetTime}
|
||||||
|
useGemini3_1={useGemini3_1}
|
||||||
|
useCustomToolModel={useCustomToolModel}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||||
|
|
||||||
|
exports[`ApprovalModeIndicator > renders correctly for AUTO_EDIT mode 1`] = `"auto-accept edits shift+tab to manual"`;
|
||||||
|
|
||||||
|
exports[`ApprovalModeIndicator > renders correctly for AUTO_EDIT mode with plan enabled 1`] = `"auto-accept edits shift+tab to plan"`;
|
||||||
|
|
||||||
|
exports[`ApprovalModeIndicator > renders correctly for DEFAULT mode 1`] = `"shift+tab to accept edits"`;
|
||||||
|
|
||||||
|
exports[`ApprovalModeIndicator > renders correctly for DEFAULT mode with plan enabled 1`] = `"shift+tab to accept edits"`;
|
||||||
|
|
||||||
|
exports[`ApprovalModeIndicator > renders correctly for PLAN mode 1`] = `"plan shift+tab to manual"`;
|
||||||
|
|
||||||
|
exports[`ApprovalModeIndicator > renders correctly for YOLO mode 1`] = `"YOLO ctrl+y"`;
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
import type React from 'react';
|
import type React from 'react';
|
||||||
import { Text, Box } from 'ink';
|
import { Text, Box } from 'ink';
|
||||||
import { theme } from '../../semantic-colors.js';
|
import { theme } from '../../semantic-colors.js';
|
||||||
|
import { getDisplayString } from '@google/gemini-cli-core';
|
||||||
|
|
||||||
interface ModelMessageProps {
|
interface ModelMessageProps {
|
||||||
model: string;
|
model: string;
|
||||||
@@ -15,7 +16,7 @@ interface ModelMessageProps {
|
|||||||
export const ModelMessage: React.FC<ModelMessageProps> = ({ model }) => (
|
export const ModelMessage: React.FC<ModelMessageProps> = ({ model }) => (
|
||||||
<Box marginLeft={2}>
|
<Box marginLeft={2}>
|
||||||
<Text color={theme.ui.comment} italic>
|
<Text color={theme.ui.comment} italic>
|
||||||
Responding with {model}
|
Responding with {getDisplayString(model)}
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -202,7 +202,7 @@ describe('useApprovalModeIndicator', () => {
|
|||||||
);
|
);
|
||||||
expect(result.current).toBe(ApprovalMode.YOLO);
|
expect(result.current).toBe(ApprovalMode.YOLO);
|
||||||
|
|
||||||
// Shift+Tab cycles back to DEFAULT (since PLAN is disabled by default in mock)
|
// Shift+Tab cycles back to AUTO_EDIT (from YOLO)
|
||||||
act(() => {
|
act(() => {
|
||||||
capturedUseKeypressHandler({
|
capturedUseKeypressHandler({
|
||||||
name: 'tab',
|
name: 'tab',
|
||||||
@@ -236,7 +236,7 @@ describe('useApprovalModeIndicator', () => {
|
|||||||
expect(result.current).toBe(ApprovalMode.AUTO_EDIT);
|
expect(result.current).toBe(ApprovalMode.AUTO_EDIT);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should cycle through DEFAULT -> PLAN -> AUTO_EDIT -> DEFAULT when plan is enabled', () => {
|
it('should cycle through DEFAULT -> AUTO_EDIT -> PLAN -> DEFAULT when plan is enabled', () => {
|
||||||
mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.DEFAULT);
|
mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.DEFAULT);
|
||||||
mockConfigInstance.isPlanEnabled.mockReturnValue(true);
|
mockConfigInstance.isPlanEnabled.mockReturnValue(true);
|
||||||
renderHook(() =>
|
renderHook(() =>
|
||||||
@@ -246,15 +246,7 @@ describe('useApprovalModeIndicator', () => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
// DEFAULT -> PLAN
|
// DEFAULT -> AUTO_EDIT
|
||||||
act(() => {
|
|
||||||
capturedUseKeypressHandler({ name: 'tab', shift: true } as Key);
|
|
||||||
});
|
|
||||||
expect(mockConfigInstance.setApprovalMode).toHaveBeenCalledWith(
|
|
||||||
ApprovalMode.PLAN,
|
|
||||||
);
|
|
||||||
|
|
||||||
// PLAN -> AUTO_EDIT
|
|
||||||
act(() => {
|
act(() => {
|
||||||
capturedUseKeypressHandler({ name: 'tab', shift: true } as Key);
|
capturedUseKeypressHandler({ name: 'tab', shift: true } as Key);
|
||||||
});
|
});
|
||||||
@@ -262,7 +254,15 @@ describe('useApprovalModeIndicator', () => {
|
|||||||
ApprovalMode.AUTO_EDIT,
|
ApprovalMode.AUTO_EDIT,
|
||||||
);
|
);
|
||||||
|
|
||||||
// AUTO_EDIT -> DEFAULT
|
// AUTO_EDIT -> PLAN
|
||||||
|
act(() => {
|
||||||
|
capturedUseKeypressHandler({ name: 'tab', shift: true } as Key);
|
||||||
|
});
|
||||||
|
expect(mockConfigInstance.setApprovalMode).toHaveBeenCalledWith(
|
||||||
|
ApprovalMode.PLAN,
|
||||||
|
);
|
||||||
|
|
||||||
|
// PLAN -> DEFAULT
|
||||||
act(() => {
|
act(() => {
|
||||||
capturedUseKeypressHandler({ name: 'tab', shift: true } as Key);
|
capturedUseKeypressHandler({ name: 'tab', shift: true } as Key);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -72,14 +72,14 @@ export function useApprovalModeIndicator({
|
|||||||
const currentMode = config.getApprovalMode();
|
const currentMode = config.getApprovalMode();
|
||||||
switch (currentMode) {
|
switch (currentMode) {
|
||||||
case ApprovalMode.DEFAULT:
|
case ApprovalMode.DEFAULT:
|
||||||
nextApprovalMode = config.isPlanEnabled()
|
|
||||||
? ApprovalMode.PLAN
|
|
||||||
: ApprovalMode.AUTO_EDIT;
|
|
||||||
break;
|
|
||||||
case ApprovalMode.PLAN:
|
|
||||||
nextApprovalMode = ApprovalMode.AUTO_EDIT;
|
nextApprovalMode = ApprovalMode.AUTO_EDIT;
|
||||||
break;
|
break;
|
||||||
case ApprovalMode.AUTO_EDIT:
|
case ApprovalMode.AUTO_EDIT:
|
||||||
|
nextApprovalMode = config.isPlanEnabled()
|
||||||
|
? ApprovalMode.PLAN
|
||||||
|
: ApprovalMode.DEFAULT;
|
||||||
|
break;
|
||||||
|
case ApprovalMode.PLAN:
|
||||||
nextApprovalMode = ApprovalMode.DEFAULT;
|
nextApprovalMode = ApprovalMode.DEFAULT;
|
||||||
break;
|
break;
|
||||||
case ApprovalMode.YOLO:
|
case ApprovalMode.YOLO:
|
||||||
|
|||||||
@@ -155,9 +155,10 @@ describe('useQuotaAndFallback', () => {
|
|||||||
expect(request?.isTerminalQuotaError).toBe(true);
|
expect(request?.isTerminalQuotaError).toBe(true);
|
||||||
|
|
||||||
const message = request!.message;
|
const message = request!.message;
|
||||||
expect(message).toContain('Usage limit reached for gemini-pro.');
|
expect(message).toContain('Usage limit reached for all Pro models.');
|
||||||
expect(message).toContain('Access resets at'); // From getResetTimeMessage
|
expect(message).toContain('Access resets at'); // From getResetTimeMessage
|
||||||
expect(message).toContain('/stats model for usage details');
|
expect(message).toContain('/stats model for usage details');
|
||||||
|
expect(message).toContain('/model to switch models.');
|
||||||
expect(message).toContain('/auth to switch to API key.');
|
expect(message).toContain('/auth to switch to API key.');
|
||||||
|
|
||||||
expect(mockHistoryManager.addItem).not.toHaveBeenCalled();
|
expect(mockHistoryManager.addItem).not.toHaveBeenCalled();
|
||||||
@@ -176,6 +177,77 @@ describe('useQuotaAndFallback', () => {
|
|||||||
expect(mockHistoryManager.addItem).toHaveBeenCalledTimes(1);
|
expect(mockHistoryManager.addItem).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should show the model name for a terminal quota error on a non-pro model', async () => {
|
||||||
|
const { result } = renderHook(() =>
|
||||||
|
useQuotaAndFallback({
|
||||||
|
config: mockConfig,
|
||||||
|
historyManager: mockHistoryManager,
|
||||||
|
userTier: UserTierId.FREE,
|
||||||
|
setModelSwitchedFromQuotaError: mockSetModelSwitchedFromQuotaError,
|
||||||
|
onShowAuthSelection: mockOnShowAuthSelection,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const handler = setFallbackHandlerSpy.mock
|
||||||
|
.calls[0][0] as FallbackModelHandler;
|
||||||
|
|
||||||
|
let promise: Promise<FallbackIntent | null>;
|
||||||
|
const error = new TerminalQuotaError(
|
||||||
|
'flash quota',
|
||||||
|
mockGoogleApiError,
|
||||||
|
1000 * 60 * 5,
|
||||||
|
);
|
||||||
|
act(() => {
|
||||||
|
promise = handler('gemini-flash', 'gemini-pro', error);
|
||||||
|
});
|
||||||
|
|
||||||
|
const request = result.current.proQuotaRequest;
|
||||||
|
expect(request).not.toBeNull();
|
||||||
|
expect(request?.failedModel).toBe('gemini-flash');
|
||||||
|
|
||||||
|
const message = request!.message;
|
||||||
|
expect(message).toContain('Usage limit reached for gemini-flash.');
|
||||||
|
expect(message).not.toContain('all Pro models');
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.handleProQuotaChoice('retry_later');
|
||||||
|
});
|
||||||
|
|
||||||
|
await promise!;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle terminal quota error without retry delay', async () => {
|
||||||
|
const { result } = renderHook(() =>
|
||||||
|
useQuotaAndFallback({
|
||||||
|
config: mockConfig,
|
||||||
|
historyManager: mockHistoryManager,
|
||||||
|
userTier: UserTierId.FREE,
|
||||||
|
setModelSwitchedFromQuotaError: mockSetModelSwitchedFromQuotaError,
|
||||||
|
onShowAuthSelection: mockOnShowAuthSelection,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const handler = setFallbackHandlerSpy.mock
|
||||||
|
.calls[0][0] as FallbackModelHandler;
|
||||||
|
|
||||||
|
let promise: Promise<FallbackIntent | null>;
|
||||||
|
const error = new TerminalQuotaError('no delay', mockGoogleApiError);
|
||||||
|
act(() => {
|
||||||
|
promise = handler('gemini-pro', 'gemini-flash', error);
|
||||||
|
});
|
||||||
|
|
||||||
|
const request = result.current.proQuotaRequest;
|
||||||
|
const message = request!.message;
|
||||||
|
expect(message).not.toContain('Access resets at');
|
||||||
|
expect(message).toContain('Usage limit reached for all Pro models.');
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.handleProQuotaChoice('retry_later');
|
||||||
|
});
|
||||||
|
|
||||||
|
await promise!;
|
||||||
|
});
|
||||||
|
|
||||||
it('should handle race conditions by stopping subsequent requests', async () => {
|
it('should handle race conditions by stopping subsequent requests', async () => {
|
||||||
const { result } = renderHook(() =>
|
const { result } = renderHook(() =>
|
||||||
useQuotaAndFallback({
|
useQuotaAndFallback({
|
||||||
|
|||||||
@@ -14,9 +14,9 @@ import {
|
|||||||
TerminalQuotaError,
|
TerminalQuotaError,
|
||||||
ModelNotFoundError,
|
ModelNotFoundError,
|
||||||
type UserTierId,
|
type UserTierId,
|
||||||
PREVIEW_GEMINI_MODEL,
|
|
||||||
DEFAULT_GEMINI_MODEL,
|
|
||||||
VALID_GEMINI_MODELS,
|
VALID_GEMINI_MODELS,
|
||||||
|
isProModel,
|
||||||
|
getDisplayString,
|
||||||
} from '@google/gemini-cli-core';
|
} from '@google/gemini-cli-core';
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { type UseHistoryManagerReturn } from './useHistoryManager.js';
|
import { type UseHistoryManagerReturn } from './useHistoryManager.js';
|
||||||
@@ -67,11 +67,9 @@ export function useQuotaAndFallback({
|
|||||||
let message: string;
|
let message: string;
|
||||||
let isTerminalQuotaError = false;
|
let isTerminalQuotaError = false;
|
||||||
let isModelNotFoundError = false;
|
let isModelNotFoundError = false;
|
||||||
const usageLimitReachedModel =
|
const usageLimitReachedModel = isProModel(failedModel)
|
||||||
failedModel === DEFAULT_GEMINI_MODEL ||
|
? 'all Pro models'
|
||||||
failedModel === PREVIEW_GEMINI_MODEL
|
: failedModel;
|
||||||
? 'all Pro models'
|
|
||||||
: failedModel;
|
|
||||||
if (error instanceof TerminalQuotaError) {
|
if (error instanceof TerminalQuotaError) {
|
||||||
isTerminalQuotaError = true;
|
isTerminalQuotaError = true;
|
||||||
// Common part of the message for both tiers
|
// Common part of the message for both tiers
|
||||||
@@ -89,7 +87,7 @@ export function useQuotaAndFallback({
|
|||||||
) {
|
) {
|
||||||
isModelNotFoundError = true;
|
isModelNotFoundError = true;
|
||||||
const messageLines = [
|
const messageLines = [
|
||||||
`It seems like you don't have access to ${failedModel}.`,
|
`It seems like you don't have access to ${getDisplayString(failedModel)}.`,
|
||||||
`Your admin might have disabled the access. Contact them to enable the Preview Release Channel.`,
|
`Your admin might have disabled the access. Contact them to enable the Preview Release Channel.`,
|
||||||
];
|
];
|
||||||
message = messageLines.join('\n');
|
message = messageLines.join('\n');
|
||||||
|
|||||||
@@ -483,7 +483,10 @@ export class Session {
|
|||||||
const functionCalls: FunctionCall[] = [];
|
const functionCalls: FunctionCall[] = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const model = resolveModel(this.config.getModel());
|
const model = resolveModel(
|
||||||
|
this.config.getModel(),
|
||||||
|
(await this.config.getGemini31Launched?.()) ?? false,
|
||||||
|
);
|
||||||
const responseStream = await chat.sendMessageStream(
|
const responseStream = await chat.sendMessageStream(
|
||||||
{ model },
|
{ model },
|
||||||
nextMessage?.parts ?? [],
|
nextMessage?.parts ?? [],
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@google/gemini-cli-core",
|
"name": "@google/gemini-cli-core",
|
||||||
"version": "0.29.0-nightly.20260203.71f46f116",
|
"version": "0.29.4",
|
||||||
"description": "Gemini CLI Core",
|
"description": "Gemini CLI Core",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|||||||
@@ -43,7 +43,10 @@ export function resolvePolicyChain(
|
|||||||
const configuredModel = config.getModel();
|
const configuredModel = config.getModel();
|
||||||
|
|
||||||
let chain;
|
let chain;
|
||||||
const resolvedModel = resolveModel(modelFromConfig);
|
const resolvedModel = resolveModel(
|
||||||
|
modelFromConfig,
|
||||||
|
config.getGemini31LaunchedSync?.() ?? false,
|
||||||
|
);
|
||||||
const isAutoPreferred = preferredModel ? isAutoModel(preferredModel) : false;
|
const isAutoPreferred = preferredModel ? isAutoModel(preferredModel) : false;
|
||||||
const isAutoConfigured = isAutoModel(configuredModel);
|
const isAutoConfigured = isAutoModel(configuredModel);
|
||||||
|
|
||||||
|
|||||||
@@ -345,6 +345,7 @@ describe('Admin Controls', () => {
|
|||||||
// Should still start polling
|
// Should still start polling
|
||||||
(mockServer.fetchAdminControls as Mock).mockResolvedValue({
|
(mockServer.fetchAdminControls as Mock).mockResolvedValue({
|
||||||
strictModeDisabled: true,
|
strictModeDisabled: true,
|
||||||
|
adminControlsApplicable: true,
|
||||||
});
|
});
|
||||||
await vi.advanceTimersByTimeAsync(5 * 60 * 1000);
|
await vi.advanceTimersByTimeAsync(5 * 60 * 1000);
|
||||||
|
|
||||||
@@ -363,7 +364,10 @@ describe('Admin Controls', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should fetch from server if no cachedSettings provided', async () => {
|
it('should fetch from server if no cachedSettings provided', async () => {
|
||||||
const serverResponse = { strictModeDisabled: false };
|
const serverResponse = {
|
||||||
|
strictModeDisabled: false,
|
||||||
|
adminControlsApplicable: true,
|
||||||
|
};
|
||||||
(mockServer.fetchAdminControls as Mock).mockResolvedValue(serverResponse);
|
(mockServer.fetchAdminControls as Mock).mockResolvedValue(serverResponse);
|
||||||
|
|
||||||
const result = await fetchAdminControls(
|
const result = await fetchAdminControls(
|
||||||
@@ -386,31 +390,24 @@ describe('Admin Controls', () => {
|
|||||||
expect(mockServer.fetchAdminControls).toHaveBeenCalledTimes(1);
|
expect(mockServer.fetchAdminControls).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return empty object on fetch error and still start polling', async () => {
|
it('should throw error on fetch error and NOT start polling', async () => {
|
||||||
(mockServer.fetchAdminControls as Mock).mockRejectedValue(
|
const error = new Error('Network error');
|
||||||
new Error('Network error'),
|
(mockServer.fetchAdminControls as Mock).mockRejectedValue(error);
|
||||||
);
|
|
||||||
const result = await fetchAdminControls(
|
|
||||||
mockServer,
|
|
||||||
undefined,
|
|
||||||
true,
|
|
||||||
mockOnSettingsChanged,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(result).toEqual({});
|
await expect(
|
||||||
|
fetchAdminControls(mockServer, undefined, true, mockOnSettingsChanged),
|
||||||
|
).rejects.toThrow(error);
|
||||||
|
|
||||||
// Polling should have been started and should retry
|
// Polling should NOT have been started
|
||||||
(mockServer.fetchAdminControls as Mock).mockResolvedValue({
|
// Advance timers just to be absolutely sure
|
||||||
strictModeDisabled: false,
|
|
||||||
});
|
|
||||||
await vi.advanceTimersByTimeAsync(5 * 60 * 1000);
|
await vi.advanceTimersByTimeAsync(5 * 60 * 1000);
|
||||||
expect(mockServer.fetchAdminControls).toHaveBeenCalledTimes(2); // Initial + poll
|
expect(mockServer.fetchAdminControls).toHaveBeenCalledTimes(1); // Only initial fetch
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return empty object on 403 fetch error and STOP polling', async () => {
|
it('should return empty object on adminControlsApplicable false and STOP polling', async () => {
|
||||||
const error403 = new Error('Forbidden');
|
(mockServer.fetchAdminControls as Mock).mockResolvedValue({
|
||||||
Object.assign(error403, { status: 403 });
|
adminControlsApplicable: false,
|
||||||
(mockServer.fetchAdminControls as Mock).mockRejectedValue(error403);
|
});
|
||||||
|
|
||||||
const result = await fetchAdminControls(
|
const result = await fetchAdminControls(
|
||||||
mockServer,
|
mockServer,
|
||||||
@@ -421,7 +418,7 @@ describe('Admin Controls', () => {
|
|||||||
|
|
||||||
expect(result).toEqual({});
|
expect(result).toEqual({});
|
||||||
|
|
||||||
// Advance time - should NOT poll because of 403
|
// Advance time - should NOT poll because of adminControlsApplicable: false
|
||||||
await vi.advanceTimersByTimeAsync(5 * 60 * 1000);
|
await vi.advanceTimersByTimeAsync(5 * 60 * 1000);
|
||||||
expect(mockServer.fetchAdminControls).toHaveBeenCalledTimes(1); // Only the initial call
|
expect(mockServer.fetchAdminControls).toHaveBeenCalledTimes(1); // Only the initial call
|
||||||
});
|
});
|
||||||
@@ -430,6 +427,7 @@ describe('Admin Controls', () => {
|
|||||||
(mockServer.fetchAdminControls as Mock).mockResolvedValue({
|
(mockServer.fetchAdminControls as Mock).mockResolvedValue({
|
||||||
strictModeDisabled: false,
|
strictModeDisabled: false,
|
||||||
unknownField: 'bad',
|
unknownField: 'bad',
|
||||||
|
adminControlsApplicable: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await fetchAdminControls(
|
const result = await fetchAdminControls(
|
||||||
@@ -455,7 +453,9 @@ describe('Admin Controls', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should reset polling interval if called again', async () => {
|
it('should reset polling interval if called again', async () => {
|
||||||
(mockServer.fetchAdminControls as Mock).mockResolvedValue({});
|
(mockServer.fetchAdminControls as Mock).mockResolvedValue({
|
||||||
|
adminControlsApplicable: true,
|
||||||
|
});
|
||||||
|
|
||||||
// First call
|
// First call
|
||||||
await fetchAdminControls(
|
await fetchAdminControls(
|
||||||
@@ -514,6 +514,7 @@ describe('Admin Controls', () => {
|
|||||||
const serverResponse = {
|
const serverResponse = {
|
||||||
strictModeDisabled: true,
|
strictModeDisabled: true,
|
||||||
unknownField: 'should be removed',
|
unknownField: 'should be removed',
|
||||||
|
adminControlsApplicable: true,
|
||||||
};
|
};
|
||||||
(mockServer.fetchAdminControls as Mock).mockResolvedValue(serverResponse);
|
(mockServer.fetchAdminControls as Mock).mockResolvedValue(serverResponse);
|
||||||
|
|
||||||
@@ -532,22 +533,22 @@ describe('Admin Controls', () => {
|
|||||||
expect(mockServer.fetchAdminControls).toHaveBeenCalledTimes(1);
|
expect(mockServer.fetchAdminControls).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return empty object on 403 fetch error', async () => {
|
it('should return empty object on adminControlsApplicable false', async () => {
|
||||||
const error403 = new Error('Forbidden');
|
(mockServer.fetchAdminControls as Mock).mockResolvedValue({
|
||||||
Object.assign(error403, { status: 403 });
|
adminControlsApplicable: false,
|
||||||
(mockServer.fetchAdminControls as Mock).mockRejectedValue(error403);
|
});
|
||||||
|
|
||||||
const result = await fetchAdminControlsOnce(mockServer, true);
|
const result = await fetchAdminControlsOnce(mockServer, true);
|
||||||
expect(result).toEqual({});
|
expect(result).toEqual({});
|
||||||
expect(mockServer.fetchAdminControls).toHaveBeenCalledTimes(1);
|
expect(mockServer.fetchAdminControls).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return empty object on any other fetch error', async () => {
|
it('should throw error on any other fetch error', async () => {
|
||||||
(mockServer.fetchAdminControls as Mock).mockRejectedValue(
|
const error = new Error('Network error');
|
||||||
new Error('Network error'),
|
(mockServer.fetchAdminControls as Mock).mockRejectedValue(error);
|
||||||
|
await expect(fetchAdminControlsOnce(mockServer, true)).rejects.toThrow(
|
||||||
|
error,
|
||||||
);
|
);
|
||||||
const result = await fetchAdminControlsOnce(mockServer, true);
|
|
||||||
expect(result).toEqual({});
|
|
||||||
expect(mockServer.fetchAdminControls).toHaveBeenCalledTimes(1);
|
expect(mockServer.fetchAdminControls).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -555,7 +556,9 @@ describe('Admin Controls', () => {
|
|||||||
const setIntervalSpy = vi.spyOn(global, 'setInterval');
|
const setIntervalSpy = vi.spyOn(global, 'setInterval');
|
||||||
const clearIntervalSpy = vi.spyOn(global, 'clearInterval');
|
const clearIntervalSpy = vi.spyOn(global, 'clearInterval');
|
||||||
|
|
||||||
(mockServer.fetchAdminControls as Mock).mockResolvedValue({});
|
(mockServer.fetchAdminControls as Mock).mockResolvedValue({
|
||||||
|
adminControlsApplicable: true,
|
||||||
|
});
|
||||||
await fetchAdminControlsOnce(mockServer, true);
|
await fetchAdminControlsOnce(mockServer, true);
|
||||||
|
|
||||||
expect(setIntervalSpy).not.toHaveBeenCalled();
|
expect(setIntervalSpy).not.toHaveBeenCalled();
|
||||||
@@ -568,6 +571,7 @@ describe('Admin Controls', () => {
|
|||||||
// Initial fetch
|
// Initial fetch
|
||||||
(mockServer.fetchAdminControls as Mock).mockResolvedValue({
|
(mockServer.fetchAdminControls as Mock).mockResolvedValue({
|
||||||
strictModeDisabled: true,
|
strictModeDisabled: true,
|
||||||
|
adminControlsApplicable: true,
|
||||||
});
|
});
|
||||||
await fetchAdminControls(
|
await fetchAdminControls(
|
||||||
mockServer,
|
mockServer,
|
||||||
@@ -579,6 +583,7 @@ describe('Admin Controls', () => {
|
|||||||
// Update for next poll
|
// Update for next poll
|
||||||
(mockServer.fetchAdminControls as Mock).mockResolvedValue({
|
(mockServer.fetchAdminControls as Mock).mockResolvedValue({
|
||||||
strictModeDisabled: false,
|
strictModeDisabled: false,
|
||||||
|
adminControlsApplicable: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fast forward
|
// Fast forward
|
||||||
@@ -598,7 +603,10 @@ describe('Admin Controls', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should NOT emit if settings are deeply equal but not the same instance', async () => {
|
it('should NOT emit if settings are deeply equal but not the same instance', async () => {
|
||||||
const settings = { strictModeDisabled: false };
|
const settings = {
|
||||||
|
strictModeDisabled: false,
|
||||||
|
adminControlsApplicable: true,
|
||||||
|
};
|
||||||
(mockServer.fetchAdminControls as Mock).mockResolvedValue(settings);
|
(mockServer.fetchAdminControls as Mock).mockResolvedValue(settings);
|
||||||
|
|
||||||
await fetchAdminControls(
|
await fetchAdminControls(
|
||||||
@@ -613,6 +621,7 @@ describe('Admin Controls', () => {
|
|||||||
// Next poll returns a different object with the same values
|
// Next poll returns a different object with the same values
|
||||||
(mockServer.fetchAdminControls as Mock).mockResolvedValue({
|
(mockServer.fetchAdminControls as Mock).mockResolvedValue({
|
||||||
strictModeDisabled: false,
|
strictModeDisabled: false,
|
||||||
|
adminControlsApplicable: true,
|
||||||
});
|
});
|
||||||
await vi.advanceTimersByTimeAsync(5 * 60 * 1000);
|
await vi.advanceTimersByTimeAsync(5 * 60 * 1000);
|
||||||
|
|
||||||
@@ -623,6 +632,7 @@ describe('Admin Controls', () => {
|
|||||||
// Initial fetch is successful
|
// Initial fetch is successful
|
||||||
(mockServer.fetchAdminControls as Mock).mockResolvedValue({
|
(mockServer.fetchAdminControls as Mock).mockResolvedValue({
|
||||||
strictModeDisabled: true,
|
strictModeDisabled: true,
|
||||||
|
adminControlsApplicable: true,
|
||||||
});
|
});
|
||||||
await fetchAdminControls(
|
await fetchAdminControls(
|
||||||
mockServer,
|
mockServer,
|
||||||
@@ -643,6 +653,7 @@ describe('Admin Controls', () => {
|
|||||||
// Subsequent poll succeeds with new data
|
// Subsequent poll succeeds with new data
|
||||||
(mockServer.fetchAdminControls as Mock).mockResolvedValue({
|
(mockServer.fetchAdminControls as Mock).mockResolvedValue({
|
||||||
strictModeDisabled: false,
|
strictModeDisabled: false,
|
||||||
|
adminControlsApplicable: true,
|
||||||
});
|
});
|
||||||
await vi.advanceTimersByTimeAsync(5 * 60 * 1000);
|
await vi.advanceTimersByTimeAsync(5 * 60 * 1000);
|
||||||
expect(mockServer.fetchAdminControls).toHaveBeenCalledTimes(3);
|
expect(mockServer.fetchAdminControls).toHaveBeenCalledTimes(3);
|
||||||
@@ -659,10 +670,11 @@ describe('Admin Controls', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should STOP polling if server returns 403', async () => {
|
it('should STOP polling if server returns adminControlsApplicable false', async () => {
|
||||||
// Initial fetch is successful
|
// Initial fetch is successful
|
||||||
(mockServer.fetchAdminControls as Mock).mockResolvedValue({
|
(mockServer.fetchAdminControls as Mock).mockResolvedValue({
|
||||||
strictModeDisabled: true,
|
strictModeDisabled: true,
|
||||||
|
adminControlsApplicable: true,
|
||||||
});
|
});
|
||||||
await fetchAdminControls(
|
await fetchAdminControls(
|
||||||
mockServer,
|
mockServer,
|
||||||
@@ -672,10 +684,10 @@ describe('Admin Controls', () => {
|
|||||||
);
|
);
|
||||||
expect(mockServer.fetchAdminControls).toHaveBeenCalledTimes(1);
|
expect(mockServer.fetchAdminControls).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
// Next poll returns 403
|
// Next poll returns adminControlsApplicable: false
|
||||||
const error403 = new Error('Forbidden');
|
(mockServer.fetchAdminControls as Mock).mockResolvedValue({
|
||||||
Object.assign(error403, { status: 403 });
|
adminControlsApplicable: false,
|
||||||
(mockServer.fetchAdminControls as Mock).mockRejectedValue(error403);
|
});
|
||||||
|
|
||||||
await vi.advanceTimersByTimeAsync(5 * 60 * 1000);
|
await vi.advanceTimersByTimeAsync(5 * 60 * 1000);
|
||||||
expect(mockServer.fetchAdminControls).toHaveBeenCalledTimes(2);
|
expect(mockServer.fetchAdminControls).toHaveBeenCalledTimes(2);
|
||||||
@@ -688,7 +700,9 @@ describe('Admin Controls', () => {
|
|||||||
|
|
||||||
describe('stopAdminControlsPolling', () => {
|
describe('stopAdminControlsPolling', () => {
|
||||||
it('should stop polling after it has started', async () => {
|
it('should stop polling after it has started', async () => {
|
||||||
(mockServer.fetchAdminControls as Mock).mockResolvedValue({});
|
(mockServer.fetchAdminControls as Mock).mockResolvedValue({
|
||||||
|
adminControlsApplicable: true,
|
||||||
|
});
|
||||||
|
|
||||||
// Start polling
|
// Start polling
|
||||||
await fetchAdminControls(
|
await fetchAdminControls(
|
||||||
|
|||||||
@@ -80,15 +80,6 @@ export function sanitizeAdminSettings(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function isGaxiosError(error: unknown): error is { status: number } {
|
|
||||||
return (
|
|
||||||
typeof error === 'object' &&
|
|
||||||
error !== null &&
|
|
||||||
'status' in error &&
|
|
||||||
typeof (error as { status: unknown }).status === 'number'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches the admin controls from the server if enabled by experiment flag.
|
* Fetches the admin controls from the server if enabled by experiment flag.
|
||||||
* Safely handles polling start/stop based on the flag and server availability.
|
* Safely handles polling start/stop based on the flag and server availability.
|
||||||
@@ -113,7 +104,7 @@ export async function fetchAdminControls(
|
|||||||
|
|
||||||
// If we already have settings (e.g. from IPC during relaunch), use them
|
// If we already have settings (e.g. from IPC during relaunch), use them
|
||||||
// to avoid blocking startup with another fetch. We'll still start polling.
|
// to avoid blocking startup with another fetch. We'll still start polling.
|
||||||
if (cachedSettings) {
|
if (cachedSettings && Object.keys(cachedSettings).length !== 0) {
|
||||||
currentSettings = cachedSettings;
|
currentSettings = cachedSettings;
|
||||||
startAdminControlsPolling(server, server.projectId, onSettingsChanged);
|
startAdminControlsPolling(server, server.projectId, onSettingsChanged);
|
||||||
return cachedSettings;
|
return cachedSettings;
|
||||||
@@ -123,22 +114,20 @@ export async function fetchAdminControls(
|
|||||||
const rawSettings = await server.fetchAdminControls({
|
const rawSettings = await server.fetchAdminControls({
|
||||||
project: server.projectId,
|
project: server.projectId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (rawSettings.adminControlsApplicable !== true) {
|
||||||
|
stopAdminControlsPolling();
|
||||||
|
currentSettings = undefined;
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
const sanitizedSettings = sanitizeAdminSettings(rawSettings);
|
const sanitizedSettings = sanitizeAdminSettings(rawSettings);
|
||||||
currentSettings = sanitizedSettings;
|
currentSettings = sanitizedSettings;
|
||||||
startAdminControlsPolling(server, server.projectId, onSettingsChanged);
|
startAdminControlsPolling(server, server.projectId, onSettingsChanged);
|
||||||
return sanitizedSettings;
|
return sanitizedSettings;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Non-enterprise users don't have access to fetch settings.
|
|
||||||
if (isGaxiosError(e) && e.status === 403) {
|
|
||||||
stopAdminControlsPolling();
|
|
||||||
currentSettings = undefined;
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
debugLogger.error('Failed to fetch admin controls: ', e);
|
debugLogger.error('Failed to fetch admin controls: ', e);
|
||||||
// If initial fetch fails, start polling to retry.
|
throw e;
|
||||||
currentSettings = {};
|
|
||||||
startAdminControlsPolling(server, server.projectId, onSettingsChanged);
|
|
||||||
return {};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,17 +151,18 @@ export async function fetchAdminControlsOnce(
|
|||||||
const rawSettings = await server.fetchAdminControls({
|
const rawSettings = await server.fetchAdminControls({
|
||||||
project: server.projectId,
|
project: server.projectId,
|
||||||
});
|
});
|
||||||
return sanitizeAdminSettings(rawSettings);
|
|
||||||
} catch (e) {
|
if (rawSettings.adminControlsApplicable !== true) {
|
||||||
// Non-enterprise users don't have access to fetch settings.
|
|
||||||
if (isGaxiosError(e) && e.status === 403) {
|
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return sanitizeAdminSettings(rawSettings);
|
||||||
|
} catch (e) {
|
||||||
debugLogger.error(
|
debugLogger.error(
|
||||||
'Failed to fetch admin controls: ',
|
'Failed to fetch admin controls: ',
|
||||||
e instanceof Error ? e.message : e,
|
e instanceof Error ? e.message : e,
|
||||||
);
|
);
|
||||||
return {};
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,6 +182,13 @@ function startAdminControlsPolling(
|
|||||||
const rawSettings = await server.fetchAdminControls({
|
const rawSettings = await server.fetchAdminControls({
|
||||||
project,
|
project,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (rawSettings.adminControlsApplicable !== true) {
|
||||||
|
stopAdminControlsPolling();
|
||||||
|
currentSettings = undefined;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const newSettings = sanitizeAdminSettings(rawSettings);
|
const newSettings = sanitizeAdminSettings(rawSettings);
|
||||||
|
|
||||||
if (!isDeepStrictEqual(newSettings, currentSettings)) {
|
if (!isDeepStrictEqual(newSettings, currentSettings)) {
|
||||||
@@ -199,12 +196,6 @@ function startAdminControlsPolling(
|
|||||||
onSettingsChanged(newSettings);
|
onSettingsChanged(newSettings);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Non-enterprise users don't have access to fetch settings.
|
|
||||||
if (isGaxiosError(e) && e.status === 403) {
|
|
||||||
stopAdminControlsPolling();
|
|
||||||
currentSettings = undefined;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
debugLogger.error('Failed to poll admin controls: ', e);
|
debugLogger.error('Failed to poll admin controls: ', e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export const ExperimentFlags = {
|
|||||||
MASKING_PROTECTION_THRESHOLD: 45758817,
|
MASKING_PROTECTION_THRESHOLD: 45758817,
|
||||||
MASKING_PRUNABLE_THRESHOLD: 45758818,
|
MASKING_PRUNABLE_THRESHOLD: 45758818,
|
||||||
MASKING_PROTECT_LATEST_TURN: 45758819,
|
MASKING_PROTECT_LATEST_TURN: 45758819,
|
||||||
|
GEMINI_3_1_PRO_LAUNCHED: 45760185,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export type ExperimentFlagName =
|
export type ExperimentFlagName =
|
||||||
|
|||||||
@@ -355,4 +355,5 @@ export const FetchAdminControlsResponseSchema = z.object({
|
|||||||
strictModeDisabled: z.boolean().optional(),
|
strictModeDisabled: z.boolean().optional(),
|
||||||
mcpSetting: McpSettingSchema.optional(),
|
mcpSetting: McpSettingSchema.optional(),
|
||||||
cliFeatureSetting: CliFeatureSettingSchema.optional(),
|
cliFeatureSetting: CliFeatureSettingSchema.optional(),
|
||||||
|
adminControlsApplicable: z.boolean().optional(),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1023,6 +1023,12 @@ export class Config {
|
|||||||
// Reset availability status when switching auth (e.g. from limited key to OAuth)
|
// Reset availability status when switching auth (e.g. from limited key to OAuth)
|
||||||
this.modelAvailabilityService.reset();
|
this.modelAvailabilityService.reset();
|
||||||
|
|
||||||
|
// Clear stale authType to ensure getGemini31LaunchedSync doesn't return stale results
|
||||||
|
// during the transition.
|
||||||
|
if (this.contentGeneratorConfig) {
|
||||||
|
this.contentGeneratorConfig.authType = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
const newContentGeneratorConfig = await createContentGeneratorConfig(
|
const newContentGeneratorConfig = await createContentGeneratorConfig(
|
||||||
this,
|
this,
|
||||||
authMethod,
|
authMethod,
|
||||||
@@ -1145,7 +1151,7 @@ export class Config {
|
|||||||
return this.remoteAdminSettings;
|
return this.remoteAdminSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
setRemoteAdminSettings(settings: AdminControlsSettings): void {
|
setRemoteAdminSettings(settings: AdminControlsSettings | undefined): void {
|
||||||
this.remoteAdminSettings = settings;
|
this.remoteAdminSettings = settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1298,7 +1304,10 @@ export class Config {
|
|||||||
if (pooled.remaining !== undefined) {
|
if (pooled.remaining !== undefined) {
|
||||||
return pooled.remaining;
|
return pooled.remaining;
|
||||||
}
|
}
|
||||||
const primaryModel = resolveModel(this.getModel());
|
const primaryModel = resolveModel(
|
||||||
|
this.getModel(),
|
||||||
|
this.getGemini31LaunchedSync(),
|
||||||
|
);
|
||||||
return this.modelQuotas.get(primaryModel)?.remaining;
|
return this.modelQuotas.get(primaryModel)?.remaining;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1307,7 +1316,10 @@ export class Config {
|
|||||||
if (pooled.limit !== undefined) {
|
if (pooled.limit !== undefined) {
|
||||||
return pooled.limit;
|
return pooled.limit;
|
||||||
}
|
}
|
||||||
const primaryModel = resolveModel(this.getModel());
|
const primaryModel = resolveModel(
|
||||||
|
this.getModel(),
|
||||||
|
this.getGemini31LaunchedSync(),
|
||||||
|
);
|
||||||
return this.modelQuotas.get(primaryModel)?.limit;
|
return this.modelQuotas.get(primaryModel)?.limit;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1316,7 +1328,10 @@ export class Config {
|
|||||||
if (pooled.resetTime !== undefined) {
|
if (pooled.resetTime !== undefined) {
|
||||||
return pooled.resetTime;
|
return pooled.resetTime;
|
||||||
}
|
}
|
||||||
const primaryModel = resolveModel(this.getModel());
|
const primaryModel = resolveModel(
|
||||||
|
this.getModel(),
|
||||||
|
this.getGemini31LaunchedSync(),
|
||||||
|
);
|
||||||
return this.modelQuotas.get(primaryModel)?.resetTime;
|
return this.modelQuotas.get(primaryModel)?.resetTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2145,6 +2160,36 @@ export class Config {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether Gemini 3.1 has been launched.
|
||||||
|
* This method is async and ensures that experiments are loaded before returning the result.
|
||||||
|
*/
|
||||||
|
async getGemini31Launched(): Promise<boolean> {
|
||||||
|
await this.ensureExperimentsLoaded();
|
||||||
|
return this.getGemini31LaunchedSync();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether Gemini 3.1 has been launched.
|
||||||
|
*
|
||||||
|
* Note: This method should only be called after startup, once experiments have been loaded.
|
||||||
|
* If you need to call this during startup or from an async context, use
|
||||||
|
* getGemini31Launched instead.
|
||||||
|
*/
|
||||||
|
getGemini31LaunchedSync(): boolean {
|
||||||
|
const authType = this.contentGeneratorConfig?.authType;
|
||||||
|
if (
|
||||||
|
authType === AuthType.USE_GEMINI ||
|
||||||
|
authType === AuthType.USE_VERTEX_AI
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
this.experiments?.flags[ExperimentFlags.GEMINI_3_1_PRO_LAUNCHED]
|
||||||
|
?.boolValue ?? false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private async ensureExperimentsLoaded(): Promise<void> {
|
private async ensureExperimentsLoaded(): Promise<void> {
|
||||||
if (!this.experimentsPromise) {
|
if (!this.experimentsPromise) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -23,8 +23,41 @@ import {
|
|||||||
PREVIEW_GEMINI_FLASH_MODEL,
|
PREVIEW_GEMINI_FLASH_MODEL,
|
||||||
PREVIEW_GEMINI_MODEL_AUTO,
|
PREVIEW_GEMINI_MODEL_AUTO,
|
||||||
DEFAULT_GEMINI_MODEL_AUTO,
|
DEFAULT_GEMINI_MODEL_AUTO,
|
||||||
|
isActiveModel,
|
||||||
|
PREVIEW_GEMINI_3_1_MODEL,
|
||||||
|
PREVIEW_GEMINI_3_1_CUSTOM_TOOLS_MODEL,
|
||||||
|
isPreviewModel,
|
||||||
|
isProModel,
|
||||||
} from './models.js';
|
} from './models.js';
|
||||||
|
|
||||||
|
describe('isPreviewModel', () => {
|
||||||
|
it('should return true for preview models', () => {
|
||||||
|
expect(isPreviewModel(PREVIEW_GEMINI_MODEL)).toBe(true);
|
||||||
|
expect(isPreviewModel(PREVIEW_GEMINI_3_1_MODEL)).toBe(true);
|
||||||
|
expect(isPreviewModel(PREVIEW_GEMINI_FLASH_MODEL)).toBe(true);
|
||||||
|
expect(isPreviewModel(PREVIEW_GEMINI_MODEL_AUTO)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for non-preview models', () => {
|
||||||
|
expect(isPreviewModel(DEFAULT_GEMINI_MODEL)).toBe(false);
|
||||||
|
expect(isPreviewModel('gemini-1.5-pro')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isProModel', () => {
|
||||||
|
it('should return true for models containing "pro"', () => {
|
||||||
|
expect(isProModel('gemini-3-pro-preview')).toBe(true);
|
||||||
|
expect(isProModel('gemini-2.5-pro')).toBe(true);
|
||||||
|
expect(isProModel('pro')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for models without "pro"', () => {
|
||||||
|
expect(isProModel('gemini-3-flash-preview')).toBe(false);
|
||||||
|
expect(isProModel('gemini-2.5-flash')).toBe(false);
|
||||||
|
expect(isProModel('auto')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('isGemini3Model', () => {
|
describe('isGemini3Model', () => {
|
||||||
it('should return true for gemini-3 models', () => {
|
it('should return true for gemini-3 models', () => {
|
||||||
expect(isGemini3Model('gemini-3-pro-preview')).toBe(true);
|
expect(isGemini3Model('gemini-3-pro-preview')).toBe(true);
|
||||||
@@ -69,6 +102,12 @@ describe('getDisplayString', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should return PREVIEW_GEMINI_3_1_MODEL for PREVIEW_GEMINI_3_1_CUSTOM_TOOLS_MODEL', () => {
|
||||||
|
expect(getDisplayString(PREVIEW_GEMINI_3_1_CUSTOM_TOOLS_MODEL)).toBe(
|
||||||
|
PREVIEW_GEMINI_3_1_MODEL,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('should return the model name as is for other models', () => {
|
it('should return the model name as is for other models', () => {
|
||||||
expect(getDisplayString('custom-model')).toBe('custom-model');
|
expect(getDisplayString('custom-model')).toBe('custom-model');
|
||||||
expect(getDisplayString(DEFAULT_GEMINI_FLASH_LITE_MODEL)).toBe(
|
expect(getDisplayString(DEFAULT_GEMINI_FLASH_LITE_MODEL)).toBe(
|
||||||
@@ -100,6 +139,16 @@ describe('resolveModel', () => {
|
|||||||
expect(model).toBe(PREVIEW_GEMINI_MODEL);
|
expect(model).toBe(PREVIEW_GEMINI_MODEL);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should return Gemini 3.1 Pro when auto-gemini-3 is requested and useGemini3_1 is true', () => {
|
||||||
|
const model = resolveModel(PREVIEW_GEMINI_MODEL_AUTO, true);
|
||||||
|
expect(model).toBe(PREVIEW_GEMINI_3_1_MODEL);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return Gemini 3.1 Pro Custom Tools when auto-gemini-3 is requested, useGemini3_1 is true, and useCustomToolModel is true', () => {
|
||||||
|
const model = resolveModel(PREVIEW_GEMINI_MODEL_AUTO, true, true);
|
||||||
|
expect(model).toBe(PREVIEW_GEMINI_3_1_CUSTOM_TOOLS_MODEL);
|
||||||
|
});
|
||||||
|
|
||||||
it('should return the Default Pro model when auto-gemini-2.5 is requested', () => {
|
it('should return the Default Pro model when auto-gemini-2.5 is requested', () => {
|
||||||
const model = resolveModel(DEFAULT_GEMINI_MODEL_AUTO);
|
const model = resolveModel(DEFAULT_GEMINI_MODEL_AUTO);
|
||||||
expect(model).toBe(DEFAULT_GEMINI_MODEL);
|
expect(model).toBe(DEFAULT_GEMINI_MODEL);
|
||||||
@@ -193,4 +242,71 @@ describe('resolveClassifierModel', () => {
|
|||||||
resolveClassifierModel(PREVIEW_GEMINI_MODEL_AUTO, GEMINI_MODEL_ALIAS_PRO),
|
resolveClassifierModel(PREVIEW_GEMINI_MODEL_AUTO, GEMINI_MODEL_ALIAS_PRO),
|
||||||
).toBe(PREVIEW_GEMINI_MODEL);
|
).toBe(PREVIEW_GEMINI_MODEL);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should return Gemini 3.1 Pro when alias is pro and useGemini3_1 is true', () => {
|
||||||
|
expect(
|
||||||
|
resolveClassifierModel(
|
||||||
|
PREVIEW_GEMINI_MODEL_AUTO,
|
||||||
|
GEMINI_MODEL_ALIAS_PRO,
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
).toBe(PREVIEW_GEMINI_3_1_MODEL);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return Gemini 3.1 Pro Custom Tools when alias is pro, useGemini3_1 is true, and useCustomToolModel is true', () => {
|
||||||
|
expect(
|
||||||
|
resolveClassifierModel(
|
||||||
|
PREVIEW_GEMINI_MODEL_AUTO,
|
||||||
|
GEMINI_MODEL_ALIAS_PRO,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
).toBe(PREVIEW_GEMINI_3_1_CUSTOM_TOOLS_MODEL);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isActiveModel', () => {
|
||||||
|
it('should return true for valid models when useGemini3_1 is false', () => {
|
||||||
|
expect(isActiveModel(DEFAULT_GEMINI_MODEL)).toBe(true);
|
||||||
|
expect(isActiveModel(PREVIEW_GEMINI_MODEL)).toBe(true);
|
||||||
|
expect(isActiveModel(DEFAULT_GEMINI_FLASH_MODEL)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true for unknown models and aliases', () => {
|
||||||
|
expect(isActiveModel('invalid-model')).toBe(false);
|
||||||
|
expect(isActiveModel(GEMINI_MODEL_ALIAS_AUTO)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for PREVIEW_GEMINI_MODEL when useGemini3_1 is true', () => {
|
||||||
|
expect(isActiveModel(PREVIEW_GEMINI_MODEL, true)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true for other valid models when useGemini3_1 is true', () => {
|
||||||
|
expect(isActiveModel(DEFAULT_GEMINI_MODEL, true)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly filter Gemini 3.1 models based on useCustomToolModel when useGemini3_1 is true', () => {
|
||||||
|
// When custom tools are preferred, standard 3.1 should be inactive
|
||||||
|
expect(isActiveModel(PREVIEW_GEMINI_3_1_MODEL, true, true)).toBe(false);
|
||||||
|
expect(
|
||||||
|
isActiveModel(PREVIEW_GEMINI_3_1_CUSTOM_TOOLS_MODEL, true, true),
|
||||||
|
).toBe(true);
|
||||||
|
|
||||||
|
// When custom tools are NOT preferred, custom tools 3.1 should be inactive
|
||||||
|
expect(isActiveModel(PREVIEW_GEMINI_3_1_MODEL, true, false)).toBe(true);
|
||||||
|
expect(
|
||||||
|
isActiveModel(PREVIEW_GEMINI_3_1_CUSTOM_TOOLS_MODEL, true, false),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for both Gemini 3.1 models when useGemini3_1 is false', () => {
|
||||||
|
expect(isActiveModel(PREVIEW_GEMINI_3_1_MODEL, false, true)).toBe(false);
|
||||||
|
expect(isActiveModel(PREVIEW_GEMINI_3_1_MODEL, false, false)).toBe(false);
|
||||||
|
expect(
|
||||||
|
isActiveModel(PREVIEW_GEMINI_3_1_CUSTOM_TOOLS_MODEL, false, true),
|
||||||
|
).toBe(false);
|
||||||
|
expect(
|
||||||
|
isActiveModel(PREVIEW_GEMINI_3_1_CUSTOM_TOOLS_MODEL, false, false),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,6 +5,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export const PREVIEW_GEMINI_MODEL = 'gemini-3-pro-preview';
|
export const PREVIEW_GEMINI_MODEL = 'gemini-3-pro-preview';
|
||||||
|
export const PREVIEW_GEMINI_3_1_MODEL = 'gemini-3.1-pro-preview';
|
||||||
|
export const PREVIEW_GEMINI_3_1_CUSTOM_TOOLS_MODEL =
|
||||||
|
'gemini-3.1-pro-preview-customtools';
|
||||||
export const PREVIEW_GEMINI_FLASH_MODEL = 'gemini-3-flash-preview';
|
export const PREVIEW_GEMINI_FLASH_MODEL = 'gemini-3-flash-preview';
|
||||||
export const DEFAULT_GEMINI_MODEL = 'gemini-2.5-pro';
|
export const DEFAULT_GEMINI_MODEL = 'gemini-2.5-pro';
|
||||||
export const DEFAULT_GEMINI_FLASH_MODEL = 'gemini-2.5-flash';
|
export const DEFAULT_GEMINI_FLASH_MODEL = 'gemini-2.5-flash';
|
||||||
@@ -12,6 +15,8 @@ export const DEFAULT_GEMINI_FLASH_LITE_MODEL = 'gemini-2.5-flash-lite';
|
|||||||
|
|
||||||
export const VALID_GEMINI_MODELS = new Set([
|
export const VALID_GEMINI_MODELS = new Set([
|
||||||
PREVIEW_GEMINI_MODEL,
|
PREVIEW_GEMINI_MODEL,
|
||||||
|
PREVIEW_GEMINI_3_1_MODEL,
|
||||||
|
PREVIEW_GEMINI_3_1_CUSTOM_TOOLS_MODEL,
|
||||||
PREVIEW_GEMINI_FLASH_MODEL,
|
PREVIEW_GEMINI_FLASH_MODEL,
|
||||||
DEFAULT_GEMINI_MODEL,
|
DEFAULT_GEMINI_MODEL,
|
||||||
DEFAULT_GEMINI_FLASH_MODEL,
|
DEFAULT_GEMINI_FLASH_MODEL,
|
||||||
@@ -37,20 +42,29 @@ export const DEFAULT_THINKING_MODE = 8192;
|
|||||||
* to a concrete model name.
|
* to a concrete model name.
|
||||||
*
|
*
|
||||||
* @param requestedModel The model alias or concrete model name requested by the user.
|
* @param requestedModel The model alias or concrete model name requested by the user.
|
||||||
|
* @param useGemini3_1 Whether to use Gemini 3.1 Pro Preview for auto/pro aliases.
|
||||||
* @returns The resolved concrete model name.
|
* @returns The resolved concrete model name.
|
||||||
*/
|
*/
|
||||||
export function resolveModel(requestedModel: string): string {
|
export function resolveModel(
|
||||||
|
requestedModel: string,
|
||||||
|
useGemini3_1: boolean = false,
|
||||||
|
useCustomToolModel: boolean = false,
|
||||||
|
): string {
|
||||||
switch (requestedModel) {
|
switch (requestedModel) {
|
||||||
case PREVIEW_GEMINI_MODEL_AUTO: {
|
case PREVIEW_GEMINI_MODEL:
|
||||||
|
case PREVIEW_GEMINI_MODEL_AUTO:
|
||||||
|
case GEMINI_MODEL_ALIAS_AUTO:
|
||||||
|
case GEMINI_MODEL_ALIAS_PRO: {
|
||||||
|
if (useGemini3_1) {
|
||||||
|
return useCustomToolModel
|
||||||
|
? PREVIEW_GEMINI_3_1_CUSTOM_TOOLS_MODEL
|
||||||
|
: PREVIEW_GEMINI_3_1_MODEL;
|
||||||
|
}
|
||||||
return PREVIEW_GEMINI_MODEL;
|
return PREVIEW_GEMINI_MODEL;
|
||||||
}
|
}
|
||||||
case DEFAULT_GEMINI_MODEL_AUTO: {
|
case DEFAULT_GEMINI_MODEL_AUTO: {
|
||||||
return DEFAULT_GEMINI_MODEL;
|
return DEFAULT_GEMINI_MODEL;
|
||||||
}
|
}
|
||||||
case GEMINI_MODEL_ALIAS_AUTO:
|
|
||||||
case GEMINI_MODEL_ALIAS_PRO: {
|
|
||||||
return PREVIEW_GEMINI_MODEL;
|
|
||||||
}
|
|
||||||
case GEMINI_MODEL_ALIAS_FLASH: {
|
case GEMINI_MODEL_ALIAS_FLASH: {
|
||||||
return PREVIEW_GEMINI_FLASH_MODEL;
|
return PREVIEW_GEMINI_FLASH_MODEL;
|
||||||
}
|
}
|
||||||
@@ -73,6 +87,8 @@ export function resolveModel(requestedModel: string): string {
|
|||||||
export function resolveClassifierModel(
|
export function resolveClassifierModel(
|
||||||
requestedModel: string,
|
requestedModel: string,
|
||||||
modelAlias: string,
|
modelAlias: string,
|
||||||
|
useGemini3_1: boolean = false,
|
||||||
|
useCustomToolModel: boolean = false,
|
||||||
): string {
|
): string {
|
||||||
if (modelAlias === GEMINI_MODEL_ALIAS_FLASH) {
|
if (modelAlias === GEMINI_MODEL_ALIAS_FLASH) {
|
||||||
if (
|
if (
|
||||||
@@ -89,7 +105,7 @@ export function resolveClassifierModel(
|
|||||||
}
|
}
|
||||||
return resolveModel(GEMINI_MODEL_ALIAS_FLASH);
|
return resolveModel(GEMINI_MODEL_ALIAS_FLASH);
|
||||||
}
|
}
|
||||||
return resolveModel(requestedModel);
|
return resolveModel(requestedModel, useGemini3_1, useCustomToolModel);
|
||||||
}
|
}
|
||||||
export function getDisplayString(model: string) {
|
export function getDisplayString(model: string) {
|
||||||
switch (model) {
|
switch (model) {
|
||||||
@@ -101,6 +117,8 @@ export function getDisplayString(model: string) {
|
|||||||
return PREVIEW_GEMINI_MODEL;
|
return PREVIEW_GEMINI_MODEL;
|
||||||
case GEMINI_MODEL_ALIAS_FLASH:
|
case GEMINI_MODEL_ALIAS_FLASH:
|
||||||
return PREVIEW_GEMINI_FLASH_MODEL;
|
return PREVIEW_GEMINI_FLASH_MODEL;
|
||||||
|
case PREVIEW_GEMINI_3_1_CUSTOM_TOOLS_MODEL:
|
||||||
|
return PREVIEW_GEMINI_3_1_MODEL;
|
||||||
default:
|
default:
|
||||||
return model;
|
return model;
|
||||||
}
|
}
|
||||||
@@ -115,11 +133,22 @@ export function getDisplayString(model: string) {
|
|||||||
export function isPreviewModel(model: string): boolean {
|
export function isPreviewModel(model: string): boolean {
|
||||||
return (
|
return (
|
||||||
model === PREVIEW_GEMINI_MODEL ||
|
model === PREVIEW_GEMINI_MODEL ||
|
||||||
|
model === PREVIEW_GEMINI_3_1_MODEL ||
|
||||||
model === PREVIEW_GEMINI_FLASH_MODEL ||
|
model === PREVIEW_GEMINI_FLASH_MODEL ||
|
||||||
model === PREVIEW_GEMINI_MODEL_AUTO
|
model === PREVIEW_GEMINI_MODEL_AUTO
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the model is a Pro model.
|
||||||
|
*
|
||||||
|
* @param model The model name to check.
|
||||||
|
* @returns True if the model is a Pro model.
|
||||||
|
*/
|
||||||
|
export function isProModel(model: string): boolean {
|
||||||
|
return model.toLowerCase().includes('pro');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if the model is a Gemini 3 model.
|
* Checks if the model is a Gemini 3 model.
|
||||||
*
|
*
|
||||||
@@ -165,3 +194,35 @@ export function isAutoModel(model: string): boolean {
|
|||||||
export function supportsMultimodalFunctionResponse(model: string): boolean {
|
export function supportsMultimodalFunctionResponse(model: string): boolean {
|
||||||
return model.startsWith('gemini-3-');
|
return model.startsWith('gemini-3-');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the given model is considered active based on the current configuration.
|
||||||
|
*
|
||||||
|
* @param model The model name to check.
|
||||||
|
* @param useGemini3_1 Whether Gemini 3.1 Pro Preview is enabled.
|
||||||
|
* @returns True if the model is active.
|
||||||
|
*/
|
||||||
|
export function isActiveModel(
|
||||||
|
model: string,
|
||||||
|
useGemini3_1: boolean = false,
|
||||||
|
useCustomToolModel: boolean = false,
|
||||||
|
): boolean {
|
||||||
|
if (!VALID_GEMINI_MODELS.has(model)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (useGemini3_1) {
|
||||||
|
if (model === PREVIEW_GEMINI_MODEL) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (useCustomToolModel) {
|
||||||
|
return model !== PREVIEW_GEMINI_3_1_MODEL;
|
||||||
|
} else {
|
||||||
|
return model !== PREVIEW_GEMINI_3_1_CUSTOM_TOOLS_MODEL;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
model !== PREVIEW_GEMINI_3_1_MODEL &&
|
||||||
|
model !== PREVIEW_GEMINI_3_1_CUSTOM_TOOLS_MODEL
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -541,7 +541,10 @@ export class GeminiClient {
|
|||||||
|
|
||||||
// Availability logic: The configured model is the source of truth,
|
// Availability logic: The configured model is the source of truth,
|
||||||
// including any permanent fallbacks (config.setModel) or manual overrides.
|
// including any permanent fallbacks (config.setModel) or manual overrides.
|
||||||
return resolveModel(this.config.getActiveModel());
|
return resolveModel(
|
||||||
|
this.config.getActiveModel(),
|
||||||
|
this.config.getGemini31LaunchedSync?.() ?? false,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async *processTurn(
|
private async *processTurn(
|
||||||
|
|||||||
@@ -122,7 +122,12 @@ export async function createContentGenerator(
|
|||||||
return new LoggingContentGenerator(fakeGenerator, gcConfig);
|
return new LoggingContentGenerator(fakeGenerator, gcConfig);
|
||||||
}
|
}
|
||||||
const version = await getVersion();
|
const version = await getVersion();
|
||||||
const model = resolveModel(gcConfig.getModel());
|
const model = resolveModel(
|
||||||
|
gcConfig.getModel(),
|
||||||
|
config.authType === AuthType.USE_GEMINI ||
|
||||||
|
config.authType === AuthType.USE_VERTEX_AI ||
|
||||||
|
((await gcConfig.getGemini31Launched?.()) ?? false),
|
||||||
|
);
|
||||||
const customHeadersEnv =
|
const customHeadersEnv =
|
||||||
process.env['GEMINI_CLI_CUSTOM_HEADERS'] || undefined;
|
process.env['GEMINI_CLI_CUSTOM_HEADERS'] || undefined;
|
||||||
const userAgent = `GeminiCLI/${version}/${model} (${process.platform}; ${process.arch})`;
|
const userAgent = `GeminiCLI/${version}/${model} (${process.platform}; ${process.arch})`;
|
||||||
|
|||||||
@@ -492,13 +492,14 @@ export class GeminiChat {
|
|||||||
const initialActiveModel = this.config.getActiveModel();
|
const initialActiveModel = this.config.getActiveModel();
|
||||||
|
|
||||||
const apiCall = async () => {
|
const apiCall = async () => {
|
||||||
|
const useGemini3_1 = (await this.config.getGemini31Launched?.()) ?? false;
|
||||||
// Default to the last used model (which respects arguments/availability selection)
|
// Default to the last used model (which respects arguments/availability selection)
|
||||||
let modelToUse = resolveModel(lastModelToUse);
|
let modelToUse = resolveModel(lastModelToUse, useGemini3_1);
|
||||||
|
|
||||||
// If the active model has changed (e.g. due to a fallback updating the config),
|
// If the active model has changed (e.g. due to a fallback updating the config),
|
||||||
// we switch to the new active model.
|
// we switch to the new active model.
|
||||||
if (this.config.getActiveModel() !== initialActiveModel) {
|
if (this.config.getActiveModel() !== initialActiveModel) {
|
||||||
modelToUse = resolveModel(this.config.getActiveModel());
|
modelToUse = resolveModel(this.config.getActiveModel(), useGemini3_1);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (modelToUse !== lastModelToUse) {
|
if (modelToUse !== lastModelToUse) {
|
||||||
|
|||||||
@@ -58,7 +58,10 @@ export class PromptProvider {
|
|||||||
const enabledToolNames = new Set(toolNames);
|
const enabledToolNames = new Set(toolNames);
|
||||||
const approvedPlanPath = config.getApprovedPlanPath();
|
const approvedPlanPath = config.getApprovedPlanPath();
|
||||||
|
|
||||||
const desiredModel = resolveModel(config.getActiveModel());
|
const desiredModel = resolveModel(
|
||||||
|
config.getActiveModel(),
|
||||||
|
config.getGemini31LaunchedSync?.() ?? false,
|
||||||
|
);
|
||||||
const isGemini3 = isPreviewModel(desiredModel);
|
const isGemini3 = isPreviewModel(desiredModel);
|
||||||
const activeSnippets = isGemini3 ? snippets : legacySnippets;
|
const activeSnippets = isGemini3 ? snippets : legacySnippets;
|
||||||
const contextFilenames = getAllGeminiMdFilenames();
|
const contextFilenames = getAllGeminiMdFilenames();
|
||||||
@@ -233,7 +236,10 @@ export class PromptProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getCompressionPrompt(config: Config): string {
|
getCompressionPrompt(config: Config): string {
|
||||||
const desiredModel = resolveModel(config.getActiveModel());
|
const desiredModel = resolveModel(
|
||||||
|
config.getActiveModel(),
|
||||||
|
config.getGemini31LaunchedSync?.() ?? false,
|
||||||
|
);
|
||||||
const isGemini3 = isPreviewModel(desiredModel);
|
const isGemini3 = isPreviewModel(desiredModel);
|
||||||
const activeSnippets = isGemini3 ? snippets : legacySnippets;
|
const activeSnippets = isGemini3 ? snippets : legacySnippets;
|
||||||
return activeSnippets.getCompressionPrompt();
|
return activeSnippets.getCompressionPrompt();
|
||||||
|
|||||||
@@ -18,11 +18,14 @@ import {
|
|||||||
DEFAULT_GEMINI_MODEL,
|
DEFAULT_GEMINI_MODEL,
|
||||||
DEFAULT_GEMINI_MODEL_AUTO,
|
DEFAULT_GEMINI_MODEL_AUTO,
|
||||||
PREVIEW_GEMINI_MODEL_AUTO,
|
PREVIEW_GEMINI_MODEL_AUTO,
|
||||||
|
PREVIEW_GEMINI_3_1_MODEL,
|
||||||
|
PREVIEW_GEMINI_3_1_CUSTOM_TOOLS_MODEL,
|
||||||
} from '../../config/models.js';
|
} from '../../config/models.js';
|
||||||
import { promptIdContext } from '../../utils/promptIdContext.js';
|
import { promptIdContext } from '../../utils/promptIdContext.js';
|
||||||
import type { Content } from '@google/genai';
|
import type { Content } from '@google/genai';
|
||||||
import type { ResolvedModelConfig } from '../../services/modelConfigService.js';
|
import type { ResolvedModelConfig } from '../../services/modelConfigService.js';
|
||||||
import { debugLogger } from '../../utils/debugLogger.js';
|
import { debugLogger } from '../../utils/debugLogger.js';
|
||||||
|
import { AuthType } from '../../core/contentGenerator.js';
|
||||||
|
|
||||||
vi.mock('../../core/baseLlmClient.js');
|
vi.mock('../../core/baseLlmClient.js');
|
||||||
|
|
||||||
@@ -53,6 +56,10 @@ describe('ClassifierStrategy', () => {
|
|||||||
},
|
},
|
||||||
getModel: vi.fn().mockReturnValue(DEFAULT_GEMINI_MODEL_AUTO),
|
getModel: vi.fn().mockReturnValue(DEFAULT_GEMINI_MODEL_AUTO),
|
||||||
getNumericalRoutingEnabled: vi.fn().mockResolvedValue(false),
|
getNumericalRoutingEnabled: vi.fn().mockResolvedValue(false),
|
||||||
|
getGemini31Launched: vi.fn().mockResolvedValue(false),
|
||||||
|
getContentGeneratorConfig: vi.fn().mockReturnValue({
|
||||||
|
authType: AuthType.LOGIN_WITH_GOOGLE,
|
||||||
|
}),
|
||||||
} as unknown as Config;
|
} as unknown as Config;
|
||||||
mockBaseLlmClient = {
|
mockBaseLlmClient = {
|
||||||
generateJson: vi.fn(),
|
generateJson: vi.fn(),
|
||||||
@@ -339,4 +346,49 @@ describe('ClassifierStrategy', () => {
|
|||||||
// Since requestedModel is Pro, and choice is flash, it should resolve to Flash
|
// Since requestedModel is Pro, and choice is flash, it should resolve to Flash
|
||||||
expect(decision?.model).toBe(DEFAULT_GEMINI_FLASH_MODEL);
|
expect(decision?.model).toBe(DEFAULT_GEMINI_FLASH_MODEL);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Gemini 3.1 and Custom Tools Routing', () => {
|
||||||
|
it('should route to PREVIEW_GEMINI_3_1_MODEL when Gemini 3.1 is launched', async () => {
|
||||||
|
vi.mocked(mockConfig.getGemini31Launched).mockResolvedValue(true);
|
||||||
|
vi.mocked(mockConfig.getModel).mockReturnValue(PREVIEW_GEMINI_MODEL_AUTO);
|
||||||
|
const mockApiResponse = {
|
||||||
|
reasoning: 'Complex task',
|
||||||
|
model_choice: 'pro',
|
||||||
|
};
|
||||||
|
vi.mocked(mockBaseLlmClient.generateJson).mockResolvedValue(
|
||||||
|
mockApiResponse,
|
||||||
|
);
|
||||||
|
|
||||||
|
const decision = await strategy.route(
|
||||||
|
mockContext,
|
||||||
|
mockConfig,
|
||||||
|
mockBaseLlmClient,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(decision?.model).toBe(PREVIEW_GEMINI_3_1_MODEL);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should route to PREVIEW_GEMINI_3_1_CUSTOM_TOOLS_MODEL when Gemini 3.1 is launched and auth is USE_GEMINI', async () => {
|
||||||
|
vi.mocked(mockConfig.getGemini31Launched).mockResolvedValue(true);
|
||||||
|
vi.mocked(mockConfig.getModel).mockReturnValue(PREVIEW_GEMINI_MODEL_AUTO);
|
||||||
|
vi.mocked(mockConfig.getContentGeneratorConfig).mockReturnValue({
|
||||||
|
authType: AuthType.USE_GEMINI,
|
||||||
|
});
|
||||||
|
const mockApiResponse = {
|
||||||
|
reasoning: 'Complex task',
|
||||||
|
model_choice: 'pro',
|
||||||
|
};
|
||||||
|
vi.mocked(mockBaseLlmClient.generateJson).mockResolvedValue(
|
||||||
|
mockApiResponse,
|
||||||
|
);
|
||||||
|
|
||||||
|
const decision = await strategy.route(
|
||||||
|
mockContext,
|
||||||
|
mockConfig,
|
||||||
|
mockBaseLlmClient,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(decision?.model).toBe(PREVIEW_GEMINI_3_1_CUSTOM_TOOLS_MODEL);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
isFunctionResponse,
|
isFunctionResponse,
|
||||||
} from '../../utils/messageInspectors.js';
|
} from '../../utils/messageInspectors.js';
|
||||||
import { debugLogger } from '../../utils/debugLogger.js';
|
import { debugLogger } from '../../utils/debugLogger.js';
|
||||||
|
import { AuthType } from '../../core/contentGenerator.js';
|
||||||
|
|
||||||
// The number of recent history turns to provide to the router for context.
|
// The number of recent history turns to provide to the router for context.
|
||||||
const HISTORY_TURNS_FOR_CONTEXT = 4;
|
const HISTORY_TURNS_FOR_CONTEXT = 4;
|
||||||
@@ -167,9 +168,15 @@ export class ClassifierStrategy implements RoutingStrategy {
|
|||||||
|
|
||||||
const reasoning = routerResponse.reasoning;
|
const reasoning = routerResponse.reasoning;
|
||||||
const latencyMs = Date.now() - startTime;
|
const latencyMs = Date.now() - startTime;
|
||||||
|
const useGemini3_1 = (await config.getGemini31Launched?.()) ?? false;
|
||||||
|
const useCustomToolModel =
|
||||||
|
useGemini3_1 &&
|
||||||
|
config.getContentGeneratorConfig().authType === AuthType.USE_GEMINI;
|
||||||
const selectedModel = resolveClassifierModel(
|
const selectedModel = resolveClassifierModel(
|
||||||
model,
|
model,
|
||||||
routerResponse.model_choice,
|
routerResponse.model_choice,
|
||||||
|
useGemini3_1,
|
||||||
|
useCustomToolModel,
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -21,7 +21,10 @@ export class DefaultStrategy implements TerminalStrategy {
|
|||||||
config: Config,
|
config: Config,
|
||||||
_baseLlmClient: BaseLlmClient,
|
_baseLlmClient: BaseLlmClient,
|
||||||
): Promise<RoutingDecision> {
|
): Promise<RoutingDecision> {
|
||||||
const defaultModel = resolveModel(config.getModel());
|
const defaultModel = resolveModel(
|
||||||
|
config.getModel(),
|
||||||
|
config.getGemini31LaunchedSync?.() ?? false,
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
model: defaultModel,
|
model: defaultModel,
|
||||||
metadata: {
|
metadata: {
|
||||||
|
|||||||
@@ -23,7 +23,10 @@ export class FallbackStrategy implements RoutingStrategy {
|
|||||||
_baseLlmClient: BaseLlmClient,
|
_baseLlmClient: BaseLlmClient,
|
||||||
): Promise<RoutingDecision | null> {
|
): Promise<RoutingDecision | null> {
|
||||||
const requestedModel = context.requestedModel ?? config.getModel();
|
const requestedModel = context.requestedModel ?? config.getModel();
|
||||||
const resolvedModel = resolveModel(requestedModel);
|
const resolvedModel = resolveModel(
|
||||||
|
requestedModel,
|
||||||
|
config.getGemini31LaunchedSync?.() ?? false,
|
||||||
|
);
|
||||||
const service = config.getModelAvailabilityService();
|
const service = config.getModelAvailabilityService();
|
||||||
const snapshot = service.snapshot(resolvedModel);
|
const snapshot = service.snapshot(resolvedModel);
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import type { BaseLlmClient } from '../../core/baseLlmClient.js';
|
|||||||
import {
|
import {
|
||||||
PREVIEW_GEMINI_FLASH_MODEL,
|
PREVIEW_GEMINI_FLASH_MODEL,
|
||||||
PREVIEW_GEMINI_MODEL,
|
PREVIEW_GEMINI_MODEL,
|
||||||
|
PREVIEW_GEMINI_3_1_MODEL,
|
||||||
|
PREVIEW_GEMINI_3_1_CUSTOM_TOOLS_MODEL,
|
||||||
PREVIEW_GEMINI_MODEL_AUTO,
|
PREVIEW_GEMINI_MODEL_AUTO,
|
||||||
DEFAULT_GEMINI_MODEL_AUTO,
|
DEFAULT_GEMINI_MODEL_AUTO,
|
||||||
DEFAULT_GEMINI_MODEL,
|
DEFAULT_GEMINI_MODEL,
|
||||||
@@ -20,6 +22,7 @@ import { promptIdContext } from '../../utils/promptIdContext.js';
|
|||||||
import type { Content } from '@google/genai';
|
import type { Content } from '@google/genai';
|
||||||
import type { ResolvedModelConfig } from '../../services/modelConfigService.js';
|
import type { ResolvedModelConfig } from '../../services/modelConfigService.js';
|
||||||
import { debugLogger } from '../../utils/debugLogger.js';
|
import { debugLogger } from '../../utils/debugLogger.js';
|
||||||
|
import { AuthType } from '../../core/contentGenerator.js';
|
||||||
|
|
||||||
vi.mock('../../core/baseLlmClient.js');
|
vi.mock('../../core/baseLlmClient.js');
|
||||||
|
|
||||||
@@ -52,6 +55,10 @@ describe('NumericalClassifierStrategy', () => {
|
|||||||
getSessionId: vi.fn().mockReturnValue('control-group-id'), // Default to Control Group (Hash 71 >= 50)
|
getSessionId: vi.fn().mockReturnValue('control-group-id'), // Default to Control Group (Hash 71 >= 50)
|
||||||
getNumericalRoutingEnabled: vi.fn().mockResolvedValue(true),
|
getNumericalRoutingEnabled: vi.fn().mockResolvedValue(true),
|
||||||
getClassifierThreshold: vi.fn().mockResolvedValue(undefined),
|
getClassifierThreshold: vi.fn().mockResolvedValue(undefined),
|
||||||
|
getGemini31Launched: vi.fn().mockResolvedValue(false),
|
||||||
|
getContentGeneratorConfig: vi.fn().mockReturnValue({
|
||||||
|
authType: AuthType.LOGIN_WITH_GOOGLE,
|
||||||
|
}),
|
||||||
} as unknown as Config;
|
} as unknown as Config;
|
||||||
mockBaseLlmClient = {
|
mockBaseLlmClient = {
|
||||||
generateJson: vi.fn(),
|
generateJson: vi.fn(),
|
||||||
@@ -535,4 +542,68 @@ describe('NumericalClassifierStrategy', () => {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Gemini 3.1 and Custom Tools Routing', () => {
|
||||||
|
it('should route to PREVIEW_GEMINI_3_1_MODEL when Gemini 3.1 is launched', async () => {
|
||||||
|
vi.mocked(mockConfig.getGemini31Launched).mockResolvedValue(true);
|
||||||
|
const mockApiResponse = {
|
||||||
|
complexity_reasoning: 'Complex task',
|
||||||
|
complexity_score: 80,
|
||||||
|
};
|
||||||
|
vi.mocked(mockBaseLlmClient.generateJson).mockResolvedValue(
|
||||||
|
mockApiResponse,
|
||||||
|
);
|
||||||
|
|
||||||
|
const decision = await strategy.route(
|
||||||
|
mockContext,
|
||||||
|
mockConfig,
|
||||||
|
mockBaseLlmClient,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(decision?.model).toBe(PREVIEW_GEMINI_3_1_MODEL);
|
||||||
|
});
|
||||||
|
it('should route to PREVIEW_GEMINI_3_1_CUSTOM_TOOLS_MODEL when Gemini 3.1 is launched and auth is USE_GEMINI', async () => {
|
||||||
|
vi.mocked(mockConfig.getGemini31Launched).mockResolvedValue(true);
|
||||||
|
vi.mocked(mockConfig.getContentGeneratorConfig).mockReturnValue({
|
||||||
|
authType: AuthType.USE_GEMINI,
|
||||||
|
});
|
||||||
|
const mockApiResponse = {
|
||||||
|
complexity_reasoning: 'Complex task',
|
||||||
|
complexity_score: 80,
|
||||||
|
};
|
||||||
|
vi.mocked(mockBaseLlmClient.generateJson).mockResolvedValue(
|
||||||
|
mockApiResponse,
|
||||||
|
);
|
||||||
|
|
||||||
|
const decision = await strategy.route(
|
||||||
|
mockContext,
|
||||||
|
mockConfig,
|
||||||
|
mockBaseLlmClient,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(decision?.model).toBe(PREVIEW_GEMINI_3_1_CUSTOM_TOOLS_MODEL);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should NOT route to custom tools model when auth is USE_VERTEX_AI', async () => {
|
||||||
|
vi.mocked(mockConfig.getGemini31Launched).mockResolvedValue(true);
|
||||||
|
vi.mocked(mockConfig.getContentGeneratorConfig).mockReturnValue({
|
||||||
|
authType: AuthType.USE_VERTEX_AI,
|
||||||
|
});
|
||||||
|
const mockApiResponse = {
|
||||||
|
complexity_reasoning: 'Complex task',
|
||||||
|
complexity_score: 80,
|
||||||
|
};
|
||||||
|
vi.mocked(mockBaseLlmClient.generateJson).mockResolvedValue(
|
||||||
|
mockApiResponse,
|
||||||
|
);
|
||||||
|
|
||||||
|
const decision = await strategy.route(
|
||||||
|
mockContext,
|
||||||
|
mockConfig,
|
||||||
|
mockBaseLlmClient,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(decision?.model).toBe(PREVIEW_GEMINI_3_1_MODEL);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { resolveClassifierModel, isGemini3Model } from '../../config/models.js';
|
|||||||
import { createUserContent, Type } from '@google/genai';
|
import { createUserContent, Type } from '@google/genai';
|
||||||
import type { Config } from '../../config/config.js';
|
import type { Config } from '../../config/config.js';
|
||||||
import { debugLogger } from '../../utils/debugLogger.js';
|
import { debugLogger } from '../../utils/debugLogger.js';
|
||||||
|
import { AuthType } from '../../core/contentGenerator.js';
|
||||||
|
|
||||||
// The number of recent history turns to provide to the router for context.
|
// The number of recent history turns to provide to the router for context.
|
||||||
const HISTORY_TURNS_FOR_CONTEXT = 8;
|
const HISTORY_TURNS_FOR_CONTEXT = 8;
|
||||||
@@ -180,8 +181,16 @@ export class NumericalClassifierStrategy implements RoutingStrategy {
|
|||||||
config,
|
config,
|
||||||
config.getSessionId() || 'unknown-session',
|
config.getSessionId() || 'unknown-session',
|
||||||
);
|
);
|
||||||
|
const useGemini3_1 = (await config.getGemini31Launched?.()) ?? false;
|
||||||
const selectedModel = resolveClassifierModel(model, modelAlias);
|
const useCustomToolModel =
|
||||||
|
useGemini3_1 &&
|
||||||
|
config.getContentGeneratorConfig().authType === AuthType.USE_GEMINI;
|
||||||
|
const selectedModel = resolveClassifierModel(
|
||||||
|
model,
|
||||||
|
modelAlias,
|
||||||
|
useGemini3_1,
|
||||||
|
useCustomToolModel,
|
||||||
|
);
|
||||||
|
|
||||||
const latencyMs = Date.now() - startTime;
|
const latencyMs = Date.now() - startTime;
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,10 @@ export class OverrideStrategy implements RoutingStrategy {
|
|||||||
|
|
||||||
// Return the overridden model name.
|
// Return the overridden model name.
|
||||||
return {
|
return {
|
||||||
model: resolveModel(overrideModel),
|
model: resolveModel(
|
||||||
|
overrideModel,
|
||||||
|
config.getGemini31LaunchedSync?.() ?? false,
|
||||||
|
),
|
||||||
metadata: {
|
metadata: {
|
||||||
source: this.name,
|
source: this.name,
|
||||||
latencyMs: 0,
|
latencyMs: 0,
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import {
|
|||||||
DEFAULT_GEMINI_MODEL,
|
DEFAULT_GEMINI_MODEL,
|
||||||
PREVIEW_GEMINI_MODEL,
|
PREVIEW_GEMINI_MODEL,
|
||||||
PREVIEW_GEMINI_FLASH_MODEL,
|
PREVIEW_GEMINI_FLASH_MODEL,
|
||||||
|
PREVIEW_GEMINI_3_1_MODEL,
|
||||||
} from '../config/models.js';
|
} from '../config/models.js';
|
||||||
import { PreCompressTrigger } from '../hooks/types.js';
|
import { PreCompressTrigger } from '../hooks/types.js';
|
||||||
|
|
||||||
@@ -100,6 +101,7 @@ export function findCompressSplitPoint(
|
|||||||
export function modelStringToModelConfigAlias(model: string): string {
|
export function modelStringToModelConfigAlias(model: string): string {
|
||||||
switch (model) {
|
switch (model) {
|
||||||
case PREVIEW_GEMINI_MODEL:
|
case PREVIEW_GEMINI_MODEL:
|
||||||
|
case PREVIEW_GEMINI_3_1_MODEL:
|
||||||
return 'chat-compression-3-pro';
|
return 'chat-compression-3-pro';
|
||||||
case PREVIEW_GEMINI_FLASH_MODEL:
|
case PREVIEW_GEMINI_FLASH_MODEL:
|
||||||
return 'chat-compression-3-flash';
|
return 'chat-compression-3-flash';
|
||||||
|
|||||||
@@ -46,9 +46,6 @@ describe('sanitizeEnvironment', () => {
|
|||||||
CLIENT_ID: 'sensitive-id',
|
CLIENT_ID: 'sensitive-id',
|
||||||
DB_URI: 'sensitive-uri',
|
DB_URI: 'sensitive-uri',
|
||||||
DATABASE_URL: 'sensitive-url',
|
DATABASE_URL: 'sensitive-url',
|
||||||
GEMINI_API_KEY: 'sensitive-gemini-key',
|
|
||||||
GOOGLE_API_KEY: 'sensitive-google-key',
|
|
||||||
GOOGLE_APPLICATION_CREDENTIALS: '/path/to/creds.json',
|
|
||||||
SAFE_VAR: 'is-safe',
|
SAFE_VAR: 'is-safe',
|
||||||
};
|
};
|
||||||
const sanitized = sanitizeEnvironment(env, EMPTY_OPTIONS);
|
const sanitized = sanitizeEnvironment(env, EMPTY_OPTIONS);
|
||||||
|
|||||||
@@ -103,9 +103,6 @@ export const NEVER_ALLOWED_ENVIRONMENT_VARIABLES: ReadonlySet<string> = new Set(
|
|||||||
'GOOGLE_CLOUD_PROJECT',
|
'GOOGLE_CLOUD_PROJECT',
|
||||||
'GOOGLE_CLOUD_ACCOUNT',
|
'GOOGLE_CLOUD_ACCOUNT',
|
||||||
'FIREBASE_PROJECT_ID',
|
'FIREBASE_PROJECT_ID',
|
||||||
'GEMINI_API_KEY',
|
|
||||||
'GOOGLE_API_KEY',
|
|
||||||
'GOOGLE_APPLICATION_CREDENTIALS',
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -901,9 +901,9 @@ describe('mcp-client', () => {
|
|||||||
vi.mocked(ClientLib.Client).mockReturnValue(
|
vi.mocked(ClientLib.Client).mockReturnValue(
|
||||||
mockedClient as unknown as ClientLib.Client,
|
mockedClient as unknown as ClientLib.Client,
|
||||||
);
|
);
|
||||||
vi.spyOn(SdkClientStdioLib, 'StdioClientTransport').mockReturnValue({
|
vi.spyOn(SdkClientStdioLib, 'StdioClientTransport').mockReturnValue(
|
||||||
close: vi.fn(),
|
{} as SdkClientStdioLib.StdioClientTransport,
|
||||||
} as unknown as SdkClientStdioLib.StdioClientTransport);
|
);
|
||||||
const mockedToolRegistry = {
|
const mockedToolRegistry = {
|
||||||
registerTool: vi.fn(),
|
registerTool: vi.fn(),
|
||||||
unregisterTool: vi.fn(),
|
unregisterTool: vi.fn(),
|
||||||
@@ -1623,7 +1623,7 @@ describe('mcp-client', () => {
|
|||||||
{
|
{
|
||||||
command: 'test-command',
|
command: 'test-command',
|
||||||
args: ['--foo', 'bar'],
|
args: ['--foo', 'bar'],
|
||||||
env: { GEMINI_CLI_FOO: 'bar' },
|
env: { FOO: 'bar' },
|
||||||
cwd: 'test/cwd',
|
cwd: 'test/cwd',
|
||||||
},
|
},
|
||||||
false,
|
false,
|
||||||
@@ -1634,80 +1634,11 @@ describe('mcp-client', () => {
|
|||||||
command: 'test-command',
|
command: 'test-command',
|
||||||
args: ['--foo', 'bar'],
|
args: ['--foo', 'bar'],
|
||||||
cwd: 'test/cwd',
|
cwd: 'test/cwd',
|
||||||
env: expect.objectContaining({ GEMINI_CLI_FOO: 'bar' }),
|
env: expect.objectContaining({ FOO: 'bar' }),
|
||||||
stderr: 'pipe',
|
stderr: 'pipe',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should redact sensitive environment variables for command transport', async () => {
|
|
||||||
const mockedTransport = vi
|
|
||||||
.spyOn(SdkClientStdioLib, 'StdioClientTransport')
|
|
||||||
.mockReturnValue({} as SdkClientStdioLib.StdioClientTransport);
|
|
||||||
|
|
||||||
const originalEnv = process.env;
|
|
||||||
process.env = {
|
|
||||||
...originalEnv,
|
|
||||||
GEMINI_API_KEY: 'sensitive-key',
|
|
||||||
GEMINI_CLI_SAFE_VAR: 'safe-value',
|
|
||||||
};
|
|
||||||
// Ensure strict sanitization is not triggered for this test
|
|
||||||
delete process.env['GITHUB_SHA'];
|
|
||||||
delete process.env['SURFACE'];
|
|
||||||
|
|
||||||
try {
|
|
||||||
await createTransport(
|
|
||||||
'test-server',
|
|
||||||
{
|
|
||||||
command: 'test-command',
|
|
||||||
},
|
|
||||||
false,
|
|
||||||
EMPTY_CONFIG,
|
|
||||||
);
|
|
||||||
|
|
||||||
const callArgs = mockedTransport.mock.calls[0][0];
|
|
||||||
expect(callArgs.env).toBeDefined();
|
|
||||||
expect(callArgs.env!['GEMINI_CLI_SAFE_VAR']).toBe('safe-value');
|
|
||||||
expect(callArgs.env!['GEMINI_API_KEY']).toBeUndefined();
|
|
||||||
} finally {
|
|
||||||
process.env = originalEnv;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should include extension settings in environment', async () => {
|
|
||||||
const mockedTransport = vi
|
|
||||||
.spyOn(SdkClientStdioLib, 'StdioClientTransport')
|
|
||||||
.mockReturnValue({} as SdkClientStdioLib.StdioClientTransport);
|
|
||||||
|
|
||||||
await createTransport(
|
|
||||||
'test-server',
|
|
||||||
{
|
|
||||||
command: 'test-command',
|
|
||||||
extension: {
|
|
||||||
name: 'test-ext',
|
|
||||||
resolvedSettings: [
|
|
||||||
{
|
|
||||||
envVar: 'GEMINI_CLI_EXT_VAR',
|
|
||||||
value: 'ext-value',
|
|
||||||
sensitive: false,
|
|
||||||
name: 'ext-setting',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
version: '',
|
|
||||||
isActive: false,
|
|
||||||
path: '',
|
|
||||||
contextFiles: [],
|
|
||||||
id: '',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
false,
|
|
||||||
EMPTY_CONFIG,
|
|
||||||
);
|
|
||||||
|
|
||||||
const callArgs = mockedTransport.mock.calls[0][0];
|
|
||||||
expect(callArgs.env).toBeDefined();
|
|
||||||
expect(callArgs.env!['GEMINI_CLI_EXT_VAR']).toBe('ext-value');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should exclude extension settings with undefined values from environment', async () => {
|
it('should exclude extension settings with undefined values from environment', async () => {
|
||||||
const mockedTransport = vi
|
const mockedTransport = vi
|
||||||
.spyOn(SdkClientStdioLib, 'StdioClientTransport')
|
.spyOn(SdkClientStdioLib, 'StdioClientTransport')
|
||||||
@@ -2040,7 +1971,7 @@ describe('connectToMcpServer with OAuth', () => {
|
|||||||
EMPTY_CONFIG,
|
EMPTY_CONFIG,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(client.client).toBe(mockedClient);
|
expect(client).toBe(mockedClient);
|
||||||
expect(mockedClient.connect).toHaveBeenCalledTimes(2);
|
expect(mockedClient.connect).toHaveBeenCalledTimes(2);
|
||||||
expect(mockAuthProvider.authenticate).toHaveBeenCalledOnce();
|
expect(mockAuthProvider.authenticate).toHaveBeenCalledOnce();
|
||||||
|
|
||||||
@@ -2086,7 +2017,7 @@ describe('connectToMcpServer with OAuth', () => {
|
|||||||
EMPTY_CONFIG,
|
EMPTY_CONFIG,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(client.client).toBe(mockedClient);
|
expect(client).toBe(mockedClient);
|
||||||
expect(mockedClient.connect).toHaveBeenCalledTimes(2);
|
expect(mockedClient.connect).toHaveBeenCalledTimes(2);
|
||||||
expect(mockAuthProvider.authenticate).toHaveBeenCalledOnce();
|
expect(mockAuthProvider.authenticate).toHaveBeenCalledOnce();
|
||||||
expect(OAuthUtils.discoverOAuthConfig).toHaveBeenCalledWith(serverUrl);
|
expect(OAuthUtils.discoverOAuthConfig).toHaveBeenCalledWith(serverUrl);
|
||||||
@@ -2181,7 +2112,7 @@ describe('connectToMcpServer - HTTP→SSE fallback', () => {
|
|||||||
EMPTY_CONFIG,
|
EMPTY_CONFIG,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(client.client).toBe(mockedClient);
|
expect(client).toBe(mockedClient);
|
||||||
// First HTTP attempt fails, second SSE attempt succeeds
|
// First HTTP attempt fails, second SSE attempt succeeds
|
||||||
expect(mockedClient.connect).toHaveBeenCalledTimes(2);
|
expect(mockedClient.connect).toHaveBeenCalledTimes(2);
|
||||||
});
|
});
|
||||||
@@ -2222,7 +2153,7 @@ describe('connectToMcpServer - HTTP→SSE fallback', () => {
|
|||||||
EMPTY_CONFIG,
|
EMPTY_CONFIG,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(client.client).toBe(mockedClient);
|
expect(client).toBe(mockedClient);
|
||||||
expect(mockedClient.connect).toHaveBeenCalledTimes(2);
|
expect(mockedClient.connect).toHaveBeenCalledTimes(2);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -2307,7 +2238,7 @@ describe('connectToMcpServer - OAuth with transport fallback', () => {
|
|||||||
EMPTY_CONFIG,
|
EMPTY_CONFIG,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(client.client).toBe(mockedClient);
|
expect(client).toBe(mockedClient);
|
||||||
expect(mockedClient.connect).toHaveBeenCalledTimes(3);
|
expect(mockedClient.connect).toHaveBeenCalledTimes(3);
|
||||||
expect(mockAuthProvider.authenticate).toHaveBeenCalledOnce();
|
expect(mockAuthProvider.authenticate).toHaveBeenCalledOnce();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -34,11 +34,7 @@ import {
|
|||||||
} from '@modelcontextprotocol/sdk/types.js';
|
} from '@modelcontextprotocol/sdk/types.js';
|
||||||
import { ApprovalMode, PolicyDecision } from '../policy/types.js';
|
import { ApprovalMode, PolicyDecision } from '../policy/types.js';
|
||||||
import { parse } from 'shell-quote';
|
import { parse } from 'shell-quote';
|
||||||
import type {
|
import type { Config, MCPServerConfig } from '../config/config.js';
|
||||||
Config,
|
|
||||||
GeminiCLIExtension,
|
|
||||||
MCPServerConfig,
|
|
||||||
} from '../config/config.js';
|
|
||||||
import { AuthProviderType } from '../config/config.js';
|
import { AuthProviderType } from '../config/config.js';
|
||||||
import { GoogleCredentialProvider } from '../mcp/google-auth-provider.js';
|
import { GoogleCredentialProvider } from '../mcp/google-auth-provider.js';
|
||||||
import { ServiceAccountImpersonationProvider } from '../mcp/sa-impersonation-provider.js';
|
import { ServiceAccountImpersonationProvider } from '../mcp/sa-impersonation-provider.js';
|
||||||
@@ -146,7 +142,7 @@ export class McpClient {
|
|||||||
}
|
}
|
||||||
this.updateStatus(MCPServerStatus.CONNECTING);
|
this.updateStatus(MCPServerStatus.CONNECTING);
|
||||||
try {
|
try {
|
||||||
const { client, transport } = await connectToMcpServer(
|
this.client = await connectToMcpServer(
|
||||||
this.clientVersion,
|
this.clientVersion,
|
||||||
this.serverName,
|
this.serverName,
|
||||||
this.serverConfig,
|
this.serverConfig,
|
||||||
@@ -154,13 +150,11 @@ export class McpClient {
|
|||||||
this.workspaceContext,
|
this.workspaceContext,
|
||||||
this.cliConfig.sanitizationConfig,
|
this.cliConfig.sanitizationConfig,
|
||||||
);
|
);
|
||||||
this.client = client;
|
|
||||||
this.transport = transport;
|
|
||||||
|
|
||||||
this.registerNotificationHandlers();
|
this.registerNotificationHandlers();
|
||||||
|
|
||||||
const originalOnError = this.client.onerror;
|
const originalOnError = this.client.onerror;
|
||||||
this.client.onerror = async (error) => {
|
this.client.onerror = (error) => {
|
||||||
if (this.status !== MCPServerStatus.CONNECTED) {
|
if (this.status !== MCPServerStatus.CONNECTED) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -171,14 +165,6 @@ export class McpClient {
|
|||||||
error,
|
error,
|
||||||
);
|
);
|
||||||
this.updateStatus(MCPServerStatus.DISCONNECTED);
|
this.updateStatus(MCPServerStatus.DISCONNECTED);
|
||||||
// Close transport to prevent memory leaks
|
|
||||||
if (this.transport) {
|
|
||||||
try {
|
|
||||||
await this.transport.close();
|
|
||||||
} catch {
|
|
||||||
// Ignore errors when closing transport on error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
this.updateStatus(MCPServerStatus.CONNECTED);
|
this.updateStatus(MCPServerStatus.CONNECTED);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -927,9 +913,8 @@ export async function connectAndDiscover(
|
|||||||
updateMCPServerStatus(mcpServerName, MCPServerStatus.CONNECTING);
|
updateMCPServerStatus(mcpServerName, MCPServerStatus.CONNECTING);
|
||||||
|
|
||||||
let mcpClient: Client | undefined;
|
let mcpClient: Client | undefined;
|
||||||
let transport: Transport | undefined;
|
|
||||||
try {
|
try {
|
||||||
const result = await connectToMcpServer(
|
mcpClient = await connectToMcpServer(
|
||||||
clientVersion,
|
clientVersion,
|
||||||
mcpServerName,
|
mcpServerName,
|
||||||
mcpServerConfig,
|
mcpServerConfig,
|
||||||
@@ -937,20 +922,10 @@ export async function connectAndDiscover(
|
|||||||
workspaceContext,
|
workspaceContext,
|
||||||
cliConfig.sanitizationConfig,
|
cliConfig.sanitizationConfig,
|
||||||
);
|
);
|
||||||
mcpClient = result.client;
|
|
||||||
transport = result.transport;
|
|
||||||
|
|
||||||
mcpClient.onerror = async (error) => {
|
mcpClient.onerror = (error) => {
|
||||||
coreEvents.emitFeedback('error', `MCP ERROR (${mcpServerName}):`, error);
|
coreEvents.emitFeedback('error', `MCP ERROR (${mcpServerName}):`, error);
|
||||||
updateMCPServerStatus(mcpServerName, MCPServerStatus.DISCONNECTED);
|
updateMCPServerStatus(mcpServerName, MCPServerStatus.DISCONNECTED);
|
||||||
// Close transport to prevent memory leaks
|
|
||||||
if (transport) {
|
|
||||||
try {
|
|
||||||
await transport.close();
|
|
||||||
} catch {
|
|
||||||
// Ignore errors when closing transport on error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Attempt to discover both prompts and tools
|
// Attempt to discover both prompts and tools
|
||||||
@@ -1348,18 +1323,16 @@ function createSSETransportWithAuth(
|
|||||||
* @param client The MCP client to connect
|
* @param client The MCP client to connect
|
||||||
* @param config The MCP server configuration
|
* @param config The MCP server configuration
|
||||||
* @param accessToken Optional OAuth access token for authentication
|
* @param accessToken Optional OAuth access token for authentication
|
||||||
* @returns The transport used for connection
|
|
||||||
*/
|
*/
|
||||||
async function connectWithSSETransport(
|
async function connectWithSSETransport(
|
||||||
client: Client,
|
client: Client,
|
||||||
config: MCPServerConfig,
|
config: MCPServerConfig,
|
||||||
accessToken?: string | null,
|
accessToken?: string | null,
|
||||||
): Promise<Transport> {
|
): Promise<void> {
|
||||||
const transport = createSSETransportWithAuth(config, accessToken);
|
const transport = createSSETransportWithAuth(config, accessToken);
|
||||||
await client.connect(transport, {
|
await client.connect(transport, {
|
||||||
timeout: config.timeout ?? MCP_DEFAULT_TIMEOUT_MSEC,
|
timeout: config.timeout ?? MCP_DEFAULT_TIMEOUT_MSEC,
|
||||||
});
|
});
|
||||||
return transport;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1389,7 +1362,6 @@ async function showAuthRequiredMessage(serverName: string): Promise<never> {
|
|||||||
* @param config The MCP server configuration
|
* @param config The MCP server configuration
|
||||||
* @param accessToken The OAuth access token to use
|
* @param accessToken The OAuth access token to use
|
||||||
* @param httpReturned404 Whether the HTTP transport returned 404 (indicating SSE-only server)
|
* @param httpReturned404 Whether the HTTP transport returned 404 (indicating SSE-only server)
|
||||||
* @returns The transport used for connection
|
|
||||||
*/
|
*/
|
||||||
async function retryWithOAuth(
|
async function retryWithOAuth(
|
||||||
client: Client,
|
client: Client,
|
||||||
@@ -1397,21 +1369,17 @@ async function retryWithOAuth(
|
|||||||
config: MCPServerConfig,
|
config: MCPServerConfig,
|
||||||
accessToken: string,
|
accessToken: string,
|
||||||
httpReturned404: boolean,
|
httpReturned404: boolean,
|
||||||
): Promise<Transport> {
|
): Promise<void> {
|
||||||
if (httpReturned404) {
|
if (httpReturned404) {
|
||||||
// HTTP returned 404, only try SSE
|
// HTTP returned 404, only try SSE
|
||||||
debugLogger.log(
|
debugLogger.log(
|
||||||
`Retrying SSE connection to '${serverName}' with OAuth token...`,
|
`Retrying SSE connection to '${serverName}' with OAuth token...`,
|
||||||
);
|
);
|
||||||
const transport = await connectWithSSETransport(
|
await connectWithSSETransport(client, config, accessToken);
|
||||||
client,
|
|
||||||
config,
|
|
||||||
accessToken,
|
|
||||||
);
|
|
||||||
debugLogger.log(
|
debugLogger.log(
|
||||||
`Successfully connected to '${serverName}' using SSE with OAuth.`,
|
`Successfully connected to '${serverName}' using SSE with OAuth.`,
|
||||||
);
|
);
|
||||||
return transport;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// HTTP returned 401, try HTTP with OAuth first
|
// HTTP returned 401, try HTTP with OAuth first
|
||||||
@@ -1435,7 +1403,6 @@ async function retryWithOAuth(
|
|||||||
debugLogger.log(
|
debugLogger.log(
|
||||||
`Successfully connected to '${serverName}' using HTTP with OAuth.`,
|
`Successfully connected to '${serverName}' using HTTP with OAuth.`,
|
||||||
);
|
);
|
||||||
return httpTransport;
|
|
||||||
} catch (httpError) {
|
} catch (httpError) {
|
||||||
await httpTransport.close();
|
await httpTransport.close();
|
||||||
|
|
||||||
@@ -1447,15 +1414,10 @@ async function retryWithOAuth(
|
|||||||
!config.httpUrl
|
!config.httpUrl
|
||||||
) {
|
) {
|
||||||
debugLogger.log(`HTTP with OAuth returned 404, trying SSE with OAuth...`);
|
debugLogger.log(`HTTP with OAuth returned 404, trying SSE with OAuth...`);
|
||||||
const sseTransport = await connectWithSSETransport(
|
await connectWithSSETransport(client, config, accessToken);
|
||||||
client,
|
|
||||||
config,
|
|
||||||
accessToken,
|
|
||||||
);
|
|
||||||
debugLogger.log(
|
debugLogger.log(
|
||||||
`Successfully connected to '${serverName}' using SSE with OAuth.`,
|
`Successfully connected to '${serverName}' using SSE with OAuth.`,
|
||||||
);
|
);
|
||||||
return sseTransport;
|
|
||||||
} else {
|
} else {
|
||||||
throw httpError;
|
throw httpError;
|
||||||
}
|
}
|
||||||
@@ -1469,7 +1431,7 @@ async function retryWithOAuth(
|
|||||||
*
|
*
|
||||||
* @param mcpServerName The name of the MCP server, used for logging and identification.
|
* @param mcpServerName The name of the MCP server, used for logging and identification.
|
||||||
* @param mcpServerConfig The configuration specifying how to connect to the server.
|
* @param mcpServerConfig The configuration specifying how to connect to the server.
|
||||||
* @returns A promise that resolves to a connected MCP `Client` instance and its transport.
|
* @returns A promise that resolves to a connected MCP `Client` instance.
|
||||||
* @throws An error if the connection fails or the configuration is invalid.
|
* @throws An error if the connection fails or the configuration is invalid.
|
||||||
*/
|
*/
|
||||||
export async function connectToMcpServer(
|
export async function connectToMcpServer(
|
||||||
@@ -1479,7 +1441,7 @@ export async function connectToMcpServer(
|
|||||||
debugMode: boolean,
|
debugMode: boolean,
|
||||||
workspaceContext: WorkspaceContext,
|
workspaceContext: WorkspaceContext,
|
||||||
sanitizationConfig: EnvironmentSanitizationConfig,
|
sanitizationConfig: EnvironmentSanitizationConfig,
|
||||||
): Promise<{ client: Client; transport: Transport }> {
|
): Promise<Client> {
|
||||||
const mcpClient = new Client(
|
const mcpClient = new Client(
|
||||||
{
|
{
|
||||||
name: 'gemini-cli-mcp-client',
|
name: 'gemini-cli-mcp-client',
|
||||||
@@ -1551,7 +1513,7 @@ export async function connectToMcpServer(
|
|||||||
await mcpClient.connect(transport, {
|
await mcpClient.connect(transport, {
|
||||||
timeout: mcpServerConfig.timeout ?? MCP_DEFAULT_TIMEOUT_MSEC,
|
timeout: mcpServerConfig.timeout ?? MCP_DEFAULT_TIMEOUT_MSEC,
|
||||||
});
|
});
|
||||||
return { client: mcpClient, transport };
|
return mcpClient;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await transport.close();
|
await transport.close();
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||||
@@ -1583,7 +1545,7 @@ export async function connectToMcpServer(
|
|||||||
try {
|
try {
|
||||||
// Try SSE with stored OAuth token if available
|
// Try SSE with stored OAuth token if available
|
||||||
// This ensures that SSE fallback works for authenticated servers
|
// This ensures that SSE fallback works for authenticated servers
|
||||||
const sseTransport = await connectWithSSETransport(
|
await connectWithSSETransport(
|
||||||
mcpClient,
|
mcpClient,
|
||||||
mcpServerConfig,
|
mcpServerConfig,
|
||||||
await getStoredOAuthToken(mcpServerName),
|
await getStoredOAuthToken(mcpServerName),
|
||||||
@@ -1592,7 +1554,7 @@ export async function connectToMcpServer(
|
|||||||
debugLogger.log(
|
debugLogger.log(
|
||||||
`MCP server '${mcpServerName}': Successfully connected using SSE transport.`,
|
`MCP server '${mcpServerName}': Successfully connected using SSE transport.`,
|
||||||
);
|
);
|
||||||
return { client: mcpClient, transport: sseTransport };
|
return mcpClient;
|
||||||
} catch (sseFallbackError) {
|
} catch (sseFallbackError) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||||
sseError = sseFallbackError as Error;
|
sseError = sseFallbackError as Error;
|
||||||
@@ -1700,14 +1662,14 @@ export async function connectToMcpServer(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const oauthTransport = await retryWithOAuth(
|
await retryWithOAuth(
|
||||||
mcpClient,
|
mcpClient,
|
||||||
mcpServerName,
|
mcpServerName,
|
||||||
mcpServerConfig,
|
mcpServerConfig,
|
||||||
accessToken,
|
accessToken,
|
||||||
httpReturned404,
|
httpReturned404,
|
||||||
);
|
);
|
||||||
return { client: mcpClient, transport: oauthTransport };
|
return mcpClient;
|
||||||
} else {
|
} else {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Failed to handle automatic OAuth for server '${mcpServerName}'`,
|
`Failed to handle automatic OAuth for server '${mcpServerName}'`,
|
||||||
@@ -1788,7 +1750,7 @@ export async function connectToMcpServer(
|
|||||||
timeout: mcpServerConfig.timeout ?? MCP_DEFAULT_TIMEOUT_MSEC,
|
timeout: mcpServerConfig.timeout ?? MCP_DEFAULT_TIMEOUT_MSEC,
|
||||||
});
|
});
|
||||||
// Connection successful with OAuth
|
// Connection successful with OAuth
|
||||||
return { client: mcpClient, transport: oauthTransport };
|
return mcpClient;
|
||||||
} else {
|
} else {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`OAuth configuration failed for '${mcpServerName}'. Please authenticate manually with /mcp auth ${mcpServerName}`,
|
`OAuth configuration failed for '${mcpServerName}'. Please authenticate manually with /mcp auth ${mcpServerName}`,
|
||||||
@@ -1936,23 +1898,10 @@ export async function createTransport(
|
|||||||
command: mcpServerConfig.command,
|
command: mcpServerConfig.command,
|
||||||
args: mcpServerConfig.args || [],
|
args: mcpServerConfig.args || [],
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||||
env: sanitizeEnvironment(
|
env: {
|
||||||
{
|
...sanitizeEnvironment(process.env, sanitizationConfig),
|
||||||
...process.env,
|
...(mcpServerConfig.env || {}),
|
||||||
...getExtensionEnvironment(mcpServerConfig.extension),
|
} as Record<string, string>,
|
||||||
...(mcpServerConfig.env || {}),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
...sanitizationConfig,
|
|
||||||
allowedEnvironmentVariables: [
|
|
||||||
...(sanitizationConfig.allowedEnvironmentVariables ?? []),
|
|
||||||
...(mcpServerConfig.extension?.resolvedSettings?.map(
|
|
||||||
(s) => s.envVar,
|
|
||||||
) ?? []),
|
|
||||||
],
|
|
||||||
enableEnvironmentVariableRedaction: true,
|
|
||||||
},
|
|
||||||
) as Record<string, string>,
|
|
||||||
cwd: mcpServerConfig.cwd,
|
cwd: mcpServerConfig.cwd,
|
||||||
stderr: 'pipe',
|
stderr: 'pipe',
|
||||||
});
|
});
|
||||||
@@ -2027,17 +1976,3 @@ export function isEnabled(
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getExtensionEnvironment(
|
|
||||||
extension?: GeminiCLIExtension,
|
|
||||||
): Record<string, string> {
|
|
||||||
const env: Record<string, string> = {};
|
|
||||||
if (extension?.resolvedSettings) {
|
|
||||||
for (const setting of extension.resolvedSettings) {
|
|
||||||
if (setting.value) {
|
|
||||||
env[setting.envVar] = setting.value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return env;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -99,16 +99,50 @@ describe('isHeadlessMode', () => {
|
|||||||
expect(isHeadlessMode({ prompt: true })).toBe(true);
|
expect(isHeadlessMode({ prompt: true })).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return false if query is provided but it is still a TTY', () => {
|
it('should return true if query is provided', () => {
|
||||||
// Note: per current logic, query alone doesn't force headless if TTY
|
expect(isHeadlessMode({ query: 'test query' })).toBe(true);
|
||||||
// This matches the existing behavior in packages/cli/src/config/config.ts
|
});
|
||||||
expect(isHeadlessMode({ query: 'test query' })).toBe(false);
|
|
||||||
|
it('should return true if -p or --prompt is in process.argv as a fallback', () => {
|
||||||
|
const originalArgv = process.argv;
|
||||||
|
process.argv = ['node', 'index.js', '-p', 'hello'];
|
||||||
|
try {
|
||||||
|
expect(isHeadlessMode()).toBe(true);
|
||||||
|
} finally {
|
||||||
|
process.argv = originalArgv;
|
||||||
|
}
|
||||||
|
|
||||||
|
process.argv = ['node', 'index.js', '--prompt', 'hello'];
|
||||||
|
try {
|
||||||
|
expect(isHeadlessMode()).toBe(true);
|
||||||
|
} finally {
|
||||||
|
process.argv = originalArgv;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false if -y or --yolo is in process.argv as a fallback', () => {
|
||||||
|
const originalArgv = process.argv;
|
||||||
|
process.argv = ['node', 'index.js', '-y'];
|
||||||
|
try {
|
||||||
|
expect(isHeadlessMode()).toBe(false);
|
||||||
|
} finally {
|
||||||
|
process.argv = originalArgv;
|
||||||
|
}
|
||||||
|
|
||||||
|
process.argv = ['node', 'index.js', '--yolo'];
|
||||||
|
try {
|
||||||
|
expect(isHeadlessMode()).toBe(false);
|
||||||
|
} finally {
|
||||||
|
process.argv = originalArgv;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle undefined process.stdout gracefully', () => {
|
it('should handle undefined process.stdout gracefully', () => {
|
||||||
const originalStdout = process.stdout;
|
const originalStdout = process.stdout;
|
||||||
// @ts-expect-error - testing edge case
|
Object.defineProperty(process, 'stdout', {
|
||||||
delete process.stdout;
|
value: undefined,
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
expect(isHeadlessMode()).toBe(false);
|
expect(isHeadlessMode()).toBe(false);
|
||||||
@@ -122,8 +156,10 @@ describe('isHeadlessMode', () => {
|
|||||||
|
|
||||||
it('should handle undefined process.stdin gracefully', () => {
|
it('should handle undefined process.stdin gracefully', () => {
|
||||||
const originalStdin = process.stdin;
|
const originalStdin = process.stdin;
|
||||||
// @ts-expect-error - testing edge case
|
Object.defineProperty(process, 'stdin', {
|
||||||
delete process.stdin;
|
value: undefined,
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
expect(isHeadlessMode()).toBe(false);
|
expect(isHeadlessMode()).toBe(false);
|
||||||
|
|||||||
@@ -28,18 +28,22 @@ export interface HeadlessModeOptions {
|
|||||||
* @returns true if the environment is considered headless.
|
* @returns true if the environment is considered headless.
|
||||||
*/
|
*/
|
||||||
export function isHeadlessMode(options?: HeadlessModeOptions): boolean {
|
export function isHeadlessMode(options?: HeadlessModeOptions): boolean {
|
||||||
if (process.env['GEMINI_CLI_INTEGRATION_TEST'] === 'true') {
|
if (process.env['GEMINI_CLI_INTEGRATION_TEST'] !== 'true') {
|
||||||
return (
|
const isCI =
|
||||||
!!options?.prompt ||
|
process.env['CI'] === 'true' || process.env['GITHUB_ACTIONS'] === 'true';
|
||||||
(!!process.stdin && !process.stdin.isTTY) ||
|
if (isCI) {
|
||||||
(!!process.stdout && !process.stdout.isTTY)
|
return true;
|
||||||
);
|
}
|
||||||
}
|
}
|
||||||
return (
|
|
||||||
process.env['CI'] === 'true' ||
|
const isNotTTY =
|
||||||
process.env['GITHUB_ACTIONS'] === 'true' ||
|
|
||||||
!!options?.prompt ||
|
|
||||||
(!!process.stdin && !process.stdin.isTTY) ||
|
(!!process.stdin && !process.stdin.isTTY) ||
|
||||||
(!!process.stdout && !process.stdout.isTTY)
|
(!!process.stdout && !process.stdout.isTTY);
|
||||||
);
|
|
||||||
|
if (isNotTTY || !!options?.prompt || !!options?.query) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: check process.argv for flags that imply headless mode.
|
||||||
|
return process.argv.some((arg) => arg === '-p' || arg === '--prompt');
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@google/gemini-cli-test-utils",
|
"name": "@google/gemini-cli-test-utils",
|
||||||
"version": "0.29.0-nightly.20260203.71f46f116",
|
"version": "0.29.4",
|
||||||
"private": true,
|
"private": true,
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
|||||||
@@ -49,6 +49,9 @@ async function main() {
|
|||||||
define: {
|
define: {
|
||||||
'import.meta.url': 'import_meta.url',
|
'import.meta.url': 'import_meta.url',
|
||||||
},
|
},
|
||||||
|
alias: {
|
||||||
|
punycode: 'punycode/',
|
||||||
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
/* add to the end of plugins array */
|
/* add to the end of plugins array */
|
||||||
esbuildProblemMatcherPlugin,
|
esbuildProblemMatcherPlugin,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"name": "gemini-cli-vscode-ide-companion",
|
"name": "gemini-cli-vscode-ide-companion",
|
||||||
"displayName": "Gemini CLI Companion",
|
"displayName": "Gemini CLI Companion",
|
||||||
"description": "Enable Gemini CLI with direct access to your IDE workspace.",
|
"description": "Enable Gemini CLI with direct access to your IDE workspace.",
|
||||||
"version": "0.29.0-nightly.20260203.71f46f116",
|
"version": "0.29.4",
|
||||||
"publisher": "google",
|
"publisher": "google",
|
||||||
"icon": "assets/icon.png",
|
"icon": "assets/icon.png",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|||||||
Reference in New Issue
Block a user