mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-07-23 08:10:57 -07:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a9653ca0a7 | |||
| 415070e504 | |||
| ca6cc1ecd3 |
@@ -39,18 +39,22 @@ runs:
|
||||
if: "inputs.dry-run != 'true'"
|
||||
env:
|
||||
GH_TOKEN: '${{ inputs.github-token }}'
|
||||
INPUTS_BRANCH_NAME: ${{ inputs.branch-name }}
|
||||
INPUTS_PR_TITLE: ${{ inputs.pr-title }}
|
||||
INPUTS_PR_BODY: ${{ inputs.pr-body }}
|
||||
INPUTS_BASE_BRANCH: ${{ inputs.base-branch }}
|
||||
shell: 'bash'
|
||||
working-directory: '${{ inputs.working-directory }}'
|
||||
run: |
|
||||
set -e
|
||||
if ! git ls-remote --exit-code --heads origin "${{ inputs.branch-name }}"; then
|
||||
echo "::error::Branch '${{ inputs.branch-name }}' does not exist on the remote repository."
|
||||
if ! git ls-remote --exit-code --heads origin "${INPUTS_BRANCH_NAME}"; then
|
||||
echo "::error::Branch '${INPUTS_BRANCH_NAME}' does not exist on the remote repository."
|
||||
exit 1
|
||||
fi
|
||||
PR_URL=$(gh pr create \
|
||||
--title "${{ inputs.pr-title }}" \
|
||||
--body "${{ inputs.pr-body }}" \
|
||||
--base "${{ inputs.base-branch }}" \
|
||||
--head "${{ inputs.branch-name }}" \
|
||||
--title "${INPUTS_PR_TITLE}" \
|
||||
--body "${INPUTS_PR_BODY}" \
|
||||
--base "${INPUTS_BASE_BRANCH}" \
|
||||
--head "${INPUTS_BRANCH_NAME}" \
|
||||
--fill)
|
||||
gh pr merge "$PR_URL" --auto
|
||||
|
||||
@@ -30,16 +30,22 @@ runs:
|
||||
id: 'npm_auth_token'
|
||||
shell: 'bash'
|
||||
run: |
|
||||
AUTH_TOKEN="${{ inputs.github-token }}"
|
||||
PACKAGE_NAME="${{ inputs.package-name }}"
|
||||
AUTH_TOKEN="${INPUTS_GITHUB_TOKEN}"
|
||||
PACKAGE_NAME="${INPUTS_PACKAGE_NAME}"
|
||||
PRIVATE_REPO="@google-gemini/"
|
||||
if [[ "$PACKAGE_NAME" == "$PRIVATE_REPO"* ]]; then
|
||||
AUTH_TOKEN="${{ inputs.github-token }}"
|
||||
AUTH_TOKEN="${INPUTS_GITHUB_TOKEN}"
|
||||
elif [[ "$PACKAGE_NAME" == "@google/gemini-cli" ]]; then
|
||||
AUTH_TOKEN="${{ inputs.wombat-token-cli }}"
|
||||
AUTH_TOKEN="${INPUTS_WOMBAT_TOKEN_CLI}"
|
||||
elif [[ "$PACKAGE_NAME" == "@google/gemini-cli-core" ]]; then
|
||||
AUTH_TOKEN="${{ inputs.wombat-token-core }}"
|
||||
AUTH_TOKEN="${INPUTS_WOMBAT_TOKEN_CORE}"
|
||||
elif [[ "$PACKAGE_NAME" == "@google/gemini-cli-a2a-server" ]]; then
|
||||
AUTH_TOKEN="${{ inputs.wombat-token-a2a-server }}"
|
||||
AUTH_TOKEN="${INPUTS_WOMBAT_TOKEN_A2A_SERVER}"
|
||||
fi
|
||||
echo "auth-token=$AUTH_TOKEN" >> $GITHUB_OUTPUT
|
||||
env:
|
||||
INPUTS_GITHUB_TOKEN: ${{ inputs.github-token }}
|
||||
INPUTS_PACKAGE_NAME: ${{ inputs.package-name }}
|
||||
INPUTS_WOMBAT_TOKEN_CLI: ${{ inputs.wombat-token-cli }}
|
||||
INPUTS_WOMBAT_TOKEN_CORE: ${{ inputs.wombat-token-core }}
|
||||
INPUTS_WOMBAT_TOKEN_A2A_SERVER: ${{ inputs.wombat-token-a2a-server }}
|
||||
|
||||
@@ -90,15 +90,19 @@ runs:
|
||||
id: 'release_branch'
|
||||
shell: 'bash'
|
||||
run: |
|
||||
BRANCH_NAME="release/${{ inputs.release-tag }}"
|
||||
BRANCH_NAME="release/${INPUTS_RELEASE_TAG}"
|
||||
git switch -c "${BRANCH_NAME}"
|
||||
echo "BRANCH_NAME=${BRANCH_NAME}" >> "${GITHUB_OUTPUT}"
|
||||
env:
|
||||
INPUTS_RELEASE_TAG: ${{ inputs.release-tag }}
|
||||
|
||||
- name: '⬆️ Update package versions'
|
||||
working-directory: '${{ inputs.working-directory }}'
|
||||
shell: 'bash'
|
||||
run: |
|
||||
npm run release:version "${{ inputs.release-version }}"
|
||||
npm run release:version "${INPUTS_RELEASE_VERSION}"
|
||||
env:
|
||||
INPUTS_RELEASE_VERSION: ${{ inputs.release-version }}
|
||||
|
||||
- name: '💾 Commit and Conditionally Push package versions'
|
||||
working-directory: '${{ inputs.working-directory }}'
|
||||
@@ -160,23 +164,30 @@ runs:
|
||||
working-directory: '${{ inputs.working-directory }}'
|
||||
env:
|
||||
NODE_AUTH_TOKEN: '${{ steps.core-token.outputs.auth-token }}'
|
||||
INPUTS_DRY_RUN: ${{ inputs.dry-run }}
|
||||
INPUTS_CORE_PACKAGE_NAME: ${{ inputs.core-package-name }}
|
||||
shell: 'bash'
|
||||
run: |
|
||||
npm publish \
|
||||
--dry-run="${{ inputs.dry-run }}" \
|
||||
--workspace="${{ inputs.core-package-name }}" \
|
||||
--dry-run="${INPUTS_DRY_RUN}" \
|
||||
--workspace="${INPUTS_CORE_PACKAGE_NAME}" \
|
||||
--no-tag
|
||||
npm dist-tag rm ${{ inputs.core-package-name }} false --silent
|
||||
npm dist-tag rm ${INPUTS_CORE_PACKAGE_NAME} false --silent
|
||||
|
||||
- name: '🔗 Install latest core package'
|
||||
working-directory: '${{ inputs.working-directory }}'
|
||||
if: "${{ inputs.dry-run != 'true' }}"
|
||||
shell: 'bash'
|
||||
run: |
|
||||
npm install "${{ inputs.core-package-name }}@${{ inputs.release-version }}" \
|
||||
--workspace="${{ inputs.cli-package-name }}" \
|
||||
--workspace="${{ inputs.a2a-package-name }}" \
|
||||
npm install "${INPUTS_CORE_PACKAGE_NAME}@${INPUTS_RELEASE_VERSION}" \
|
||||
--workspace="${INPUTS_CLI_PACKAGE_NAME}" \
|
||||
--workspace="${INPUTS_A2A_PACKAGE_NAME}" \
|
||||
--save-exact
|
||||
env:
|
||||
INPUTS_CORE_PACKAGE_NAME: ${{ inputs.core-package-name }}
|
||||
INPUTS_RELEASE_VERSION: ${{ inputs.release-version }}
|
||||
INPUTS_CLI_PACKAGE_NAME: ${{ inputs.cli-package-name }}
|
||||
INPUTS_A2A_PACKAGE_NAME: ${{ inputs.a2a-package-name }}
|
||||
|
||||
- name: 'Get CLI Token'
|
||||
uses: './.github/actions/npm-auth-token'
|
||||
@@ -192,13 +203,15 @@ runs:
|
||||
working-directory: '${{ inputs.working-directory }}'
|
||||
env:
|
||||
NODE_AUTH_TOKEN: '${{ steps.cli-token.outputs.auth-token }}'
|
||||
INPUTS_DRY_RUN: ${{ inputs.dry-run }}
|
||||
INPUTS_CLI_PACKAGE_NAME: ${{ inputs.cli-package-name }}
|
||||
shell: 'bash'
|
||||
run: |
|
||||
npm publish \
|
||||
--dry-run="${{ inputs.dry-run }}" \
|
||||
--workspace="${{ inputs.cli-package-name }}" \
|
||||
--dry-run="${INPUTS_DRY_RUN}" \
|
||||
--workspace="${INPUTS_CLI_PACKAGE_NAME}" \
|
||||
--no-tag
|
||||
npm dist-tag rm ${{ inputs.cli-package-name }} false --silent
|
||||
npm dist-tag rm ${INPUTS_CLI_PACKAGE_NAME} false --silent
|
||||
|
||||
- name: 'Get a2a-server Token'
|
||||
uses: './.github/actions/npm-auth-token'
|
||||
@@ -214,14 +227,16 @@ runs:
|
||||
working-directory: '${{ inputs.working-directory }}'
|
||||
env:
|
||||
NODE_AUTH_TOKEN: '${{ steps.a2a-token.outputs.auth-token }}'
|
||||
INPUTS_DRY_RUN: ${{ inputs.dry-run }}
|
||||
INPUTS_A2A_PACKAGE_NAME: ${{ inputs.a2a-package-name }}
|
||||
shell: 'bash'
|
||||
# Tag staging for initial release
|
||||
run: |
|
||||
npm publish \
|
||||
--dry-run="${{ inputs.dry-run }}" \
|
||||
--workspace="${{ inputs.a2a-package-name }}" \
|
||||
--dry-run="${INPUTS_DRY_RUN}" \
|
||||
--workspace="${INPUTS_A2A_PACKAGE_NAME}" \
|
||||
--no-tag
|
||||
npm dist-tag rm ${{ inputs.a2a-package-name }} false --silent
|
||||
npm dist-tag rm ${INPUTS_A2A_PACKAGE_NAME} false --silent
|
||||
|
||||
- name: '🔬 Verify NPM release by version'
|
||||
uses: './.github/actions/verify-release'
|
||||
@@ -255,13 +270,16 @@ runs:
|
||||
if: "${{ inputs.dry-run != 'true' && inputs.skip-github-release != 'true' && inputs.npm-tag != 'dev' && inputs.npm-registry-url != 'https://npm.pkg.github.com/' }}"
|
||||
env:
|
||||
GITHUB_TOKEN: '${{ inputs.github-token }}'
|
||||
INPUTS_RELEASE_TAG: ${{ inputs.release-tag }}
|
||||
STEPS_RELEASE_BRANCH_OUTPUTS_BRANCH_NAME: ${{ steps.release_branch.outputs.BRANCH_NAME }}
|
||||
INPUTS_PREVIOUS_TAG: ${{ inputs.previous-tag }}
|
||||
shell: 'bash'
|
||||
run: |
|
||||
gh release create "${{ inputs.release-tag }}" \
|
||||
gh release create "${INPUTS_RELEASE_TAG}" \
|
||||
bundle/gemini.js \
|
||||
--target "${{ steps.release_branch.outputs.BRANCH_NAME }}" \
|
||||
--title "Release ${{ inputs.release-tag }}" \
|
||||
--notes-start-tag "${{ inputs.previous-tag }}" \
|
||||
--target "${STEPS_RELEASE_BRANCH_OUTPUTS_BRANCH_NAME}" \
|
||||
--title "Release ${INPUTS_RELEASE_TAG}" \
|
||||
--notes-start-tag "${INPUTS_PREVIOUS_TAG}" \
|
||||
--generate-notes \
|
||||
${{ inputs.npm-tag != 'latest' && '--prerelease' || '' }}
|
||||
|
||||
@@ -271,5 +289,8 @@ runs:
|
||||
continue-on-error: true
|
||||
shell: 'bash'
|
||||
run: |
|
||||
echo "Cleaning up release branch ${{ steps.release_branch.outputs.BRANCH_NAME }}..."
|
||||
git push origin --delete "${{ steps.release_branch.outputs.BRANCH_NAME }}"
|
||||
echo "Cleaning up release branch ${STEPS_RELEASE_BRANCH_OUTPUTS_BRANCH_NAME}..."
|
||||
git push origin --delete "${STEPS_RELEASE_BRANCH_OUTPUTS_BRANCH_NAME}"
|
||||
|
||||
env:
|
||||
STEPS_RELEASE_BRANCH_OUTPUTS_BRANCH_NAME: ${{ steps.release_branch.outputs.BRANCH_NAME }}
|
||||
|
||||
@@ -52,8 +52,10 @@ runs:
|
||||
id: 'branch_name'
|
||||
shell: 'bash'
|
||||
run: |
|
||||
REF_NAME="${{ inputs.ref-name }}"
|
||||
REF_NAME="${INPUTS_REF_NAME}"
|
||||
echo "name=${REF_NAME%/merge}" >> $GITHUB_OUTPUT
|
||||
env:
|
||||
INPUTS_REF_NAME: ${{ inputs.ref-name }}
|
||||
- name: 'Build and Push the Docker Image'
|
||||
uses: 'docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83' # ratchet:docker/build-push-action@v6
|
||||
with:
|
||||
|
||||
@@ -56,8 +56,8 @@ runs:
|
||||
id: 'image_tag'
|
||||
shell: 'bash'
|
||||
run: |-
|
||||
SHELL_TAG_NAME="${{ inputs.github-ref-name }}"
|
||||
FINAL_TAG="${{ inputs.github-sha }}"
|
||||
SHELL_TAG_NAME="${INPUTS_GITHUB_REF_NAME}"
|
||||
FINAL_TAG="${INPUTS_GITHUB_SHA}"
|
||||
if [[ "$SHELL_TAG_NAME" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?$ ]]; then
|
||||
echo "Release detected."
|
||||
FINAL_TAG="${SHELL_TAG_NAME#v}"
|
||||
@@ -66,22 +66,28 @@ runs:
|
||||
fi
|
||||
echo "Determined image tag: $FINAL_TAG"
|
||||
echo "FINAL_TAG=$FINAL_TAG" >> $GITHUB_OUTPUT
|
||||
env:
|
||||
INPUTS_GITHUB_REF_NAME: ${{ inputs.github-ref-name }}
|
||||
INPUTS_GITHUB_SHA: ${{ inputs.github-sha }}
|
||||
- name: 'build'
|
||||
id: 'docker_build'
|
||||
shell: 'bash'
|
||||
env:
|
||||
GEMINI_SANDBOX_IMAGE_TAG: '${{ steps.image_tag.outputs.FINAL_TAG }}'
|
||||
GEMINI_SANDBOX: 'docker'
|
||||
STEPS_IMAGE_TAG_OUTPUTS_FINAL_TAG: ${{ steps.image_tag.outputs.FINAL_TAG }}
|
||||
run: |-
|
||||
npm run build:sandbox -- \
|
||||
--image google/gemini-cli-sandbox:${{ steps.image_tag.outputs.FINAL_TAG }} \
|
||||
--image google/gemini-cli-sandbox:${STEPS_IMAGE_TAG_OUTPUTS_FINAL_TAG} \
|
||||
--output-file final_image_uri.txt
|
||||
echo "uri=$(cat final_image_uri.txt)" >> $GITHUB_OUTPUT
|
||||
- name: 'publish'
|
||||
shell: 'bash'
|
||||
if: "${{ inputs.dry-run != 'true' }}"
|
||||
run: |-
|
||||
docker push "${{ steps.docker_build.outputs.uri }}"
|
||||
docker push "${STEPS_DOCKER_BUILD_OUTPUTS_URI}"
|
||||
env:
|
||||
STEPS_DOCKER_BUILD_OUTPUTS_URI: ${{ steps.docker_build.outputs.uri }}
|
||||
- name: 'Create issue on failure'
|
||||
if: |-
|
||||
${{ failure() }}
|
||||
|
||||
@@ -18,5 +18,7 @@ runs:
|
||||
shell: 'bash'
|
||||
run: |-
|
||||
echo ""@google-gemini:registry=https://npm.pkg.github.com"" > ~/.npmrc
|
||||
echo ""//npm.pkg.github.com/:_authToken=${{ inputs.github-token }}"" >> ~/.npmrc
|
||||
echo ""//npm.pkg.github.com/:_authToken=${INPUTS_GITHUB_TOKEN}"" >> ~/.npmrc
|
||||
echo ""@google:registry=https://wombat-dressing-room.appspot.com"" >> ~/.npmrc
|
||||
env:
|
||||
INPUTS_GITHUB_TOKEN: ${{ inputs.github-token }}
|
||||
|
||||
@@ -71,10 +71,13 @@ runs:
|
||||
${{ inputs.dry-run != 'true' }}
|
||||
env:
|
||||
NODE_AUTH_TOKEN: '${{ steps.core-token.outputs.auth-token }}'
|
||||
INPUTS_CORE_PACKAGE_NAME: ${{ inputs.core-package-name }}
|
||||
INPUTS_VERSION: ${{ inputs.version }}
|
||||
INPUTS_CHANNEL: ${{ inputs.channel }}
|
||||
shell: 'bash'
|
||||
working-directory: '${{ inputs.working-directory }}'
|
||||
run: |
|
||||
npm dist-tag add ${{ inputs.core-package-name }}@${{ inputs.version }} ${{ inputs.channel }}
|
||||
npm dist-tag add ${INPUTS_CORE_PACKAGE_NAME}@${INPUTS_VERSION} ${INPUTS_CHANNEL}
|
||||
|
||||
- name: 'Get cli Token'
|
||||
uses: './.github/actions/npm-auth-token'
|
||||
@@ -91,10 +94,13 @@ runs:
|
||||
${{ inputs.dry-run != 'true' }}
|
||||
env:
|
||||
NODE_AUTH_TOKEN: '${{ steps.cli-token.outputs.auth-token }}'
|
||||
INPUTS_CLI_PACKAGE_NAME: ${{ inputs.cli-package-name }}
|
||||
INPUTS_VERSION: ${{ inputs.version }}
|
||||
INPUTS_CHANNEL: ${{ inputs.channel }}
|
||||
shell: 'bash'
|
||||
working-directory: '${{ inputs.working-directory }}'
|
||||
run: |
|
||||
npm dist-tag add ${{ inputs.cli-package-name }}@${{ inputs.version }} ${{ inputs.channel }}
|
||||
npm dist-tag add ${INPUTS_CLI_PACKAGE_NAME}@${INPUTS_VERSION} ${INPUTS_CHANNEL}
|
||||
|
||||
- name: 'Get a2a Token'
|
||||
uses: './.github/actions/npm-auth-token'
|
||||
@@ -111,10 +117,13 @@ runs:
|
||||
${{ inputs.dry-run == 'false' }}
|
||||
env:
|
||||
NODE_AUTH_TOKEN: '${{ steps.a2a-token.outputs.auth-token }}'
|
||||
INPUTS_A2A_PACKAGE_NAME: ${{ inputs.a2a-package-name }}
|
||||
INPUTS_VERSION: ${{ inputs.version }}
|
||||
INPUTS_CHANNEL: ${{ inputs.channel }}
|
||||
shell: 'bash'
|
||||
working-directory: '${{ inputs.working-directory }}'
|
||||
run: |
|
||||
npm dist-tag add ${{ inputs.a2a-package-name }}@${{ inputs.version }} ${{ inputs.channel }}
|
||||
npm dist-tag add ${INPUTS_A2A_PACKAGE_NAME}@${INPUTS_VERSION} ${INPUTS_CHANNEL}
|
||||
|
||||
- name: 'Log dry run'
|
||||
if: |-
|
||||
@@ -122,4 +131,15 @@ runs:
|
||||
shell: 'bash'
|
||||
working-directory: '${{ inputs.working-directory }}'
|
||||
run: |
|
||||
echo "Dry run: Would have added tag '${{ inputs.channel }}' to version '${{ inputs.version }}' for ${{ inputs.cli-package-name }}, ${{ inputs.core-package-name }}, and ${{ inputs.a2a-package-name }}."
|
||||
echo "Dry run: Would have added tag '${INPUTS_CHANNEL}' to version '${INPUTS_VERSION}' for ${INPUTS_CLI_PACKAGE_NAME}, ${INPUTS_CORE_PACKAGE_NAME}, and ${INPUTS_A2A_PACKAGE_NAME}."
|
||||
|
||||
env:
|
||||
INPUTS_CHANNEL: ${{ inputs.channel }}
|
||||
|
||||
INPUTS_VERSION: ${{ inputs.version }}
|
||||
|
||||
INPUTS_CLI_PACKAGE_NAME: ${{ inputs.cli-package-name }}
|
||||
|
||||
INPUTS_CORE_PACKAGE_NAME: ${{ inputs.core-package-name }}
|
||||
|
||||
INPUTS_A2A_PACKAGE_NAME: ${{ inputs.a2a-package-name }}
|
||||
|
||||
@@ -64,10 +64,13 @@ runs:
|
||||
working-directory: '${{ inputs.working-directory }}'
|
||||
run: |-
|
||||
gemini_version=$(gemini --version)
|
||||
if [ "$gemini_version" != "${{ inputs.expected-version }}" ]; then
|
||||
echo "❌ NPM Version mismatch: Got $gemini_version from ${{ inputs.npm-package }}, expected ${{ inputs.expected-version }}"
|
||||
if [ "$gemini_version" != "${INPUTS_EXPECTED_VERSION}" ]; then
|
||||
echo "❌ NPM Version mismatch: Got $gemini_version from ${INPUTS_NPM_PACKAGE}, expected ${INPUTS_EXPECTED_VERSION}"
|
||||
exit 1
|
||||
fi
|
||||
env:
|
||||
INPUTS_EXPECTED_VERSION: ${{ inputs.expected-version }}
|
||||
INPUTS_NPM_PACKAGE: ${{ inputs.npm-package }}
|
||||
|
||||
- name: 'Clear npm cache'
|
||||
shell: 'bash'
|
||||
@@ -77,11 +80,14 @@ runs:
|
||||
shell: 'bash'
|
||||
working-directory: '${{ inputs.working-directory }}'
|
||||
run: |-
|
||||
gemini_version=$(npx --prefer-online "${{ inputs.npm-package}}" --version)
|
||||
if [ "$gemini_version" != "${{ inputs.expected-version }}" ]; then
|
||||
echo "❌ NPX Run Version mismatch: Got $gemini_version from ${{ inputs.npm-package }}, expected ${{ inputs.expected-version }}"
|
||||
gemini_version=$(npx --prefer-online "${INPUTS_NPM_PACKAGE}" --version)
|
||||
if [ "$gemini_version" != "${INPUTS_EXPECTED_VERSION}" ]; then
|
||||
echo "❌ NPX Run Version mismatch: Got $gemini_version from ${INPUTS_NPM_PACKAGE}, expected ${INPUTS_EXPECTED_VERSION}"
|
||||
exit 1
|
||||
fi
|
||||
env:
|
||||
INPUTS_NPM_PACKAGE: ${{ inputs.npm-package }}
|
||||
INPUTS_EXPECTED_VERSION: ${{ inputs.expected-version }}
|
||||
|
||||
- name: 'Install dependencies for integration tests'
|
||||
shell: 'bash'
|
||||
|
||||
@@ -53,7 +53,7 @@ jobs:
|
||||
REPO_NAME: '${{ github.event.inputs.repo_name }}'
|
||||
run: |
|
||||
mkdir -p ./pr
|
||||
echo '${{ env.REPO_NAME }}' > ./pr/repo_name
|
||||
echo '${REPO_NAME}' > ./pr/repo_name
|
||||
- uses: 'actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02' # ratchet:actions/upload-artifact@v4
|
||||
with:
|
||||
name: 'repo_name'
|
||||
@@ -288,12 +288,15 @@ jobs:
|
||||
steps:
|
||||
- name: 'Check E2E test results'
|
||||
run: |
|
||||
if [[ ${{ needs.e2e_linux.result }} != 'success' || \
|
||||
${{ needs.e2e_mac.result }} != 'success' ]]; then
|
||||
if [[ ${NEEDS_E2E_LINUX_RESULT} != 'success' || \
|
||||
${NEEDS_E2E_MAC_RESULT} != 'success' ]]; then
|
||||
echo "One or more E2E jobs failed."
|
||||
exit 1
|
||||
fi
|
||||
echo "All required E2E jobs passed!"
|
||||
env:
|
||||
NEEDS_E2E_LINUX_RESULT: ${{ needs.e2e_linux.result }}
|
||||
NEEDS_E2E_MAC_RESULT: ${{ needs.e2e_mac.result }}
|
||||
|
||||
set_workflow_status:
|
||||
runs-on: 'gemini-cli-ubuntu-16-core'
|
||||
|
||||
@@ -380,13 +380,20 @@ jobs:
|
||||
steps:
|
||||
- name: 'Check all job results'
|
||||
run: |
|
||||
if [[ (${{ needs.lint.result }} != 'success' && ${{ needs.lint.result }} != 'skipped') || \
|
||||
(${{ needs.link_checker.result }} != 'success' && ${{ needs.link_checker.result }} != 'skipped') || \
|
||||
(${{ needs.test_linux.result }} != 'success' && ${{ needs.test_linux.result }} != 'skipped') || \
|
||||
(${{ needs.test_mac.result }} != 'success' && ${{ needs.test_mac.result }} != 'skipped') || \
|
||||
(${{ needs.codeql.result }} != 'success' && ${{ needs.codeql.result }} != 'skipped') || \
|
||||
(${{ needs.bundle_size.result }} != 'success' && ${{ needs.bundle_size.result }} != 'skipped') ]]; then
|
||||
if [[ (${NEEDS_LINT_RESULT} != 'success' && ${NEEDS_LINT_RESULT} != 'skipped') || \
|
||||
(${NEEDS_LINK_CHECKER_RESULT} != 'success' && ${NEEDS_LINK_CHECKER_RESULT} != 'skipped') || \
|
||||
(${NEEDS_TEST_LINUX_RESULT} != 'success' && ${NEEDS_TEST_LINUX_RESULT} != 'skipped') || \
|
||||
(${NEEDS_TEST_MAC_RESULT} != 'success' && ${NEEDS_TEST_MAC_RESULT} != 'skipped') || \
|
||||
(${NEEDS_CODEQL_RESULT} != 'success' && ${NEEDS_CODEQL_RESULT} != 'skipped') || \
|
||||
(${NEEDS_BUNDLE_SIZE_RESULT} != 'success' && ${NEEDS_BUNDLE_SIZE_RESULT} != 'skipped') ]]; then
|
||||
echo "One or more CI jobs failed."
|
||||
exit 1
|
||||
fi
|
||||
echo "All CI jobs passed!"
|
||||
env:
|
||||
NEEDS_LINT_RESULT: ${{ needs.lint.result }}
|
||||
NEEDS_LINK_CHECKER_RESULT: ${{ needs.link_checker.result }}
|
||||
NEEDS_TEST_LINUX_RESULT: ${{ needs.test_linux.result }}
|
||||
NEEDS_TEST_MAC_RESULT: ${{ needs.test_mac.result }}
|
||||
NEEDS_CODEQL_RESULT: ${{ needs.codeql.result }}
|
||||
NEEDS_BUNDLE_SIZE_RESULT: ${{ needs.bundle_size.result }}
|
||||
|
||||
@@ -68,10 +68,10 @@ jobs:
|
||||
VERBOSE: 'true'
|
||||
shell: 'bash'
|
||||
run: |
|
||||
if [[ "${{ env.IS_DOCKER }}" == "true" ]]; then
|
||||
npm run deflake:test:integration:sandbox:docker -- --runs="${{ env.RUNS }}" -- --testNamePattern "'${{ env.TEST_NAME_PATTERN }}'"
|
||||
if [[ "${IS_DOCKER}" == "true" ]]; then
|
||||
npm run deflake:test:integration:sandbox:docker -- --runs="${RUNS}" -- --testNamePattern "'${TEST_NAME_PATTERN}'"
|
||||
else
|
||||
npm run deflake:test:integration:sandbox:none -- --runs="${{ env.RUNS }}" -- --testNamePattern "'${{ env.TEST_NAME_PATTERN }}'"
|
||||
npm run deflake:test:integration:sandbox:none -- --runs="${RUNS}" -- --testNamePattern "'${TEST_NAME_PATTERN}'"
|
||||
fi
|
||||
|
||||
deflake_e2e_mac:
|
||||
@@ -109,7 +109,7 @@ jobs:
|
||||
TEST_NAME_PATTERN: '${{ github.event.inputs.test_name_pattern }}'
|
||||
VERBOSE: 'true'
|
||||
run: |
|
||||
npm run deflake:test:integration:sandbox:none -- --runs="${{ env.RUNS }}" -- --testNamePattern "'${{ env.TEST_NAME_PATTERN }}'"
|
||||
npm run deflake:test:integration:sandbox:none -- --runs="${RUNS}" -- --testNamePattern "'${TEST_NAME_PATTERN}'"
|
||||
|
||||
deflake_e2e_windows:
|
||||
name: 'Slow E2E - Win'
|
||||
@@ -167,4 +167,4 @@ jobs:
|
||||
TEST_NAME_PATTERN: '${{ github.event.inputs.test_name_pattern }}'
|
||||
shell: 'pwsh'
|
||||
run: |
|
||||
npm run deflake:test:integration:sandbox:none -- --runs="${{ env.RUNS }}" -- --testNamePattern "'${{ env.TEST_NAME_PATTERN }}'"
|
||||
npm run deflake:test:integration:sandbox:none -- --runs="$env:RUNS" -- --testNamePattern "'$env:TEST_NAME_PATTERN'"
|
||||
|
||||
@@ -44,5 +44,5 @@ jobs:
|
||||
- name: 'Run evaluation'
|
||||
working-directory: '/app'
|
||||
run: |
|
||||
poetry run exp_run --experiment-mode=on-demand --branch-or-commit=${{ github.ref_name }} --model-name=gemini-2.5-pro --dataset=swebench_verified --concurrency=15
|
||||
poetry run exp_run --experiment-mode=on-demand --branch-or-commit=${GITHUB_REF_NAME} --model-name=gemini-2.5-pro --dataset=swebench_verified --concurrency=15
|
||||
poetry run python agent_prototypes/scripts/parse_gcli_logs_experiment.py --experiment_dir=experiments/adhoc/gcli_temp_exp --gcs-bucket="${EVAL_GCS_BUCKET}" --gcs-path=gh_action_artifacts
|
||||
|
||||
@@ -118,6 +118,7 @@ jobs:
|
||||
ORIGINAL_RELEASE_VERSION: '${{ steps.patch_version.outputs.RELEASE_VERSION }}'
|
||||
ORIGINAL_RELEASE_TAG: '${{ steps.patch_version.outputs.RELEASE_TAG }}'
|
||||
ORIGINAL_PREVIOUS_TAG: '${{ steps.patch_version.outputs.PREVIOUS_TAG }}'
|
||||
VARS_CLI_PACKAGE_NAME: ${{vars.CLI_PACKAGE_NAME}}
|
||||
run: |
|
||||
echo "🔍 Verifying no concurrent patch releases have occurred..."
|
||||
|
||||
@@ -129,7 +130,7 @@ jobs:
|
||||
|
||||
# Re-run the same version calculation script
|
||||
echo "Re-calculating version to check for changes..."
|
||||
CURRENT_PATCH_JSON=$(node scripts/get-release-version.js --cli-package-name="${{vars.CLI_PACKAGE_NAME}}" --type=patch --patch-from="${CHANNEL}")
|
||||
CURRENT_PATCH_JSON=$(node scripts/get-release-version.js --cli-package-name="${VARS_CLI_PACKAGE_NAME}" --type=patch --patch-from="${CHANNEL}")
|
||||
CURRENT_RELEASE_VERSION=$(echo "${CURRENT_PATCH_JSON}" | jq -r .releaseVersion)
|
||||
CURRENT_RELEASE_TAG=$(echo "${CURRENT_PATCH_JSON}" | jq -r .releaseTag)
|
||||
CURRENT_PREVIOUS_TAG=$(echo "${CURRENT_PATCH_JSON}" | jq -r .previousReleaseTag)
|
||||
@@ -162,10 +163,15 @@ jobs:
|
||||
- name: 'Print Calculated Version'
|
||||
run: |-
|
||||
echo "Patch Release Summary:"
|
||||
echo " Release Version: ${{ steps.patch_version.outputs.RELEASE_VERSION }}"
|
||||
echo " Release Tag: ${{ steps.patch_version.outputs.RELEASE_TAG }}"
|
||||
echo " NPM Tag: ${{ steps.patch_version.outputs.NPM_TAG }}"
|
||||
echo " Previous Tag: ${{ steps.patch_version.outputs.PREVIOUS_TAG }}"
|
||||
echo " Release Version: ${STEPS_PATCH_VERSION_OUTPUTS_RELEASE_VERSION}"
|
||||
echo " Release Tag: ${STEPS_PATCH_VERSION_OUTPUTS_RELEASE_TAG}"
|
||||
echo " NPM Tag: ${STEPS_PATCH_VERSION_OUTPUTS_NPM_TAG}"
|
||||
echo " Previous Tag: ${STEPS_PATCH_VERSION_OUTPUTS_PREVIOUS_TAG}"
|
||||
env:
|
||||
STEPS_PATCH_VERSION_OUTPUTS_RELEASE_VERSION: ${{ steps.patch_version.outputs.RELEASE_VERSION }}
|
||||
STEPS_PATCH_VERSION_OUTPUTS_RELEASE_TAG: ${{ steps.patch_version.outputs.RELEASE_TAG }}
|
||||
STEPS_PATCH_VERSION_OUTPUTS_NPM_TAG: ${{ steps.patch_version.outputs.NPM_TAG }}
|
||||
STEPS_PATCH_VERSION_OUTPUTS_PREVIOUS_TAG: ${{ steps.patch_version.outputs.PREVIOUS_TAG }}
|
||||
|
||||
- name: 'Run Tests'
|
||||
if: "${{github.event.inputs.force_skip_tests != 'true'}}"
|
||||
|
||||
@@ -360,23 +360,28 @@ jobs:
|
||||
- name: 'Create and switch to a new branch'
|
||||
id: 'release_branch'
|
||||
run: |
|
||||
BRANCH_NAME="chore/nightly-version-bump-${{ needs.calculate-versions.outputs.NEXT_NIGHTLY_VERSION }}"
|
||||
BRANCH_NAME="chore/nightly-version-bump-${NEEDS_CALCULATE_VERSIONS_OUTPUTS_NEXT_NIGHTLY_VERSION}"
|
||||
git switch -c "${BRANCH_NAME}"
|
||||
echo "BRANCH_NAME=${BRANCH_NAME}" >> "${GITHUB_OUTPUT}"
|
||||
env:
|
||||
NEEDS_CALCULATE_VERSIONS_OUTPUTS_NEXT_NIGHTLY_VERSION: ${{ needs.calculate-versions.outputs.NEXT_NIGHTLY_VERSION }}
|
||||
|
||||
- name: 'Update package versions'
|
||||
run: 'npm run release:version "${{ needs.calculate-versions.outputs.NEXT_NIGHTLY_VERSION }}"'
|
||||
run: 'npm run release:version "${NEEDS_CALCULATE_VERSIONS_OUTPUTS_NEXT_NIGHTLY_VERSION}"'
|
||||
env:
|
||||
NEEDS_CALCULATE_VERSIONS_OUTPUTS_NEXT_NIGHTLY_VERSION: ${{ needs.calculate-versions.outputs.NEXT_NIGHTLY_VERSION }}
|
||||
|
||||
- name: 'Commit and Push package versions'
|
||||
env:
|
||||
BRANCH_NAME: '${{ steps.release_branch.outputs.BRANCH_NAME }}'
|
||||
DRY_RUN: '${{ github.event.inputs.dry_run }}'
|
||||
NEEDS_CALCULATE_VERSIONS_OUTPUTS_NEXT_NIGHTLY_VERSION: ${{ needs.calculate-versions.outputs.NEXT_NIGHTLY_VERSION }}
|
||||
run: |-
|
||||
git add package.json packages/*/package.json
|
||||
if [ -f package-lock.json ]; then
|
||||
git add package-lock.json
|
||||
fi
|
||||
git commit -m "chore(release): bump version to ${{ needs.calculate-versions.outputs.NEXT_NIGHTLY_VERSION }}"
|
||||
git commit -m "chore(release): bump version to ${NEEDS_CALCULATE_VERSIONS_OUTPUTS_NEXT_NIGHTLY_VERSION}"
|
||||
if [[ "${DRY_RUN}" == "false" ]]; then
|
||||
echo "Pushing release branch to remote..."
|
||||
git push --set-upstream origin "${BRANCH_NAME}"
|
||||
|
||||
@@ -23,8 +23,8 @@ jobs:
|
||||
HEAD_SHA: '${{ github.event.inputs.head_sha || github.event.pull_request.head.sha }}'
|
||||
run: |
|
||||
mkdir -p ./pr
|
||||
echo '${{ env.REPO_NAME }}' > ./pr/repo_name
|
||||
echo '${{ env.HEAD_SHA }}' > ./pr/head_sha
|
||||
echo '${REPO_NAME}' > ./pr/repo_name
|
||||
echo '${HEAD_SHA}' > ./pr/head_sha
|
||||
- uses: 'actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02' # ratchet:actions/upload-artifact@v4
|
||||
with:
|
||||
name: 'repo_name'
|
||||
|
||||
@@ -57,8 +57,12 @@ import { AgentTerminateMode } from './types.js';
|
||||
import type { AnyDeclarativeTool, AnyToolInvocation } from '../tools/tools.js';
|
||||
import { CompressionStatus } from '../core/turn.js';
|
||||
import { ChatCompressionService } from '../services/chatCompressionService.js';
|
||||
import type { ModelConfigKey } from '../services/modelConfigService.js';
|
||||
import type {
|
||||
ModelConfigKey,
|
||||
ResolvedModelConfig,
|
||||
} from '../services/modelConfigService.js';
|
||||
import { getModelConfigAlias } from './registry.js';
|
||||
import type { ModelRouterService } from '../routing/modelRouterService.js';
|
||||
|
||||
const {
|
||||
mockSendMessageStream,
|
||||
@@ -1192,6 +1196,101 @@ describe('LocalAgentExecutor', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Model Routing', () => {
|
||||
it('should use model routing when the agent model is "auto"', async () => {
|
||||
const definition = createTestDefinition();
|
||||
definition.modelConfig.model = 'auto';
|
||||
|
||||
const mockRouter = {
|
||||
route: vi.fn().mockResolvedValue({
|
||||
model: 'routed-model',
|
||||
metadata: { source: 'test', reasoning: 'test' },
|
||||
}),
|
||||
};
|
||||
vi.spyOn(mockConfig, 'getModelRouterService').mockReturnValue(
|
||||
mockRouter as unknown as ModelRouterService,
|
||||
);
|
||||
|
||||
// Mock resolved config to return 'auto'
|
||||
vi.spyOn(
|
||||
mockConfig.modelConfigService,
|
||||
'getResolvedConfig',
|
||||
).mockReturnValue({
|
||||
model: 'auto',
|
||||
generateContentConfig: {},
|
||||
} as unknown as ResolvedModelConfig);
|
||||
|
||||
const executor = await LocalAgentExecutor.create(
|
||||
definition,
|
||||
mockConfig,
|
||||
onActivity,
|
||||
);
|
||||
|
||||
mockModelResponse([
|
||||
{
|
||||
name: TASK_COMPLETE_TOOL_NAME,
|
||||
args: { finalResult: 'done' },
|
||||
id: 'call1',
|
||||
},
|
||||
]);
|
||||
|
||||
await executor.run({ goal: 'test' }, signal);
|
||||
|
||||
expect(mockRouter.route).toHaveBeenCalled();
|
||||
expect(mockSendMessageStream).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ model: 'routed-model' }),
|
||||
expect.any(Array),
|
||||
expect.any(String),
|
||||
expect.any(AbortSignal),
|
||||
);
|
||||
});
|
||||
|
||||
it('should NOT use model routing when the agent model is NOT "auto"', async () => {
|
||||
const definition = createTestDefinition();
|
||||
definition.modelConfig.model = 'concrete-model';
|
||||
|
||||
const mockRouter = {
|
||||
route: vi.fn(),
|
||||
};
|
||||
vi.spyOn(mockConfig, 'getModelRouterService').mockReturnValue(
|
||||
mockRouter as unknown as ModelRouterService,
|
||||
);
|
||||
|
||||
// Mock resolved config to return 'concrete-model'
|
||||
vi.spyOn(
|
||||
mockConfig.modelConfigService,
|
||||
'getResolvedConfig',
|
||||
).mockReturnValue({
|
||||
model: 'concrete-model',
|
||||
generateContentConfig: {},
|
||||
} as unknown as ResolvedModelConfig);
|
||||
|
||||
const executor = await LocalAgentExecutor.create(
|
||||
definition,
|
||||
mockConfig,
|
||||
onActivity,
|
||||
);
|
||||
|
||||
mockModelResponse([
|
||||
{
|
||||
name: TASK_COMPLETE_TOOL_NAME,
|
||||
args: { finalResult: 'done' },
|
||||
id: 'call1',
|
||||
},
|
||||
]);
|
||||
|
||||
await executor.run({ goal: 'test' }, signal);
|
||||
|
||||
expect(mockRouter.route).not.toHaveBeenCalled();
|
||||
expect(mockSendMessageStream).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ model: 'concrete-model' }),
|
||||
expect.any(Array),
|
||||
expect.any(String),
|
||||
expect.any(AbortSignal),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('run (Termination Conditions)', () => {
|
||||
const mockWorkResponse = (id: string) => {
|
||||
mockModelResponse([{ name: LS_TOOL_NAME, args: { path: '.' }, id }]);
|
||||
|
||||
@@ -40,6 +40,8 @@ import type {
|
||||
} from './types.js';
|
||||
import { AgentTerminateMode } from './types.js';
|
||||
import { templateString } from './utils.js';
|
||||
import { isAutoModel } from '../config/models.js';
|
||||
import type { RoutingContext } from '../routing/routingStrategy.js';
|
||||
import { parseThought } from '../utils/thoughtUtils.js';
|
||||
import { type z } from 'zod';
|
||||
import { zodToJsonSchema } from 'zod-to-json-schema';
|
||||
@@ -589,9 +591,34 @@ export class LocalAgentExecutor<TOutput extends z.ZodTypeAny> {
|
||||
signal: AbortSignal,
|
||||
promptId: string,
|
||||
): Promise<{ functionCalls: FunctionCall[]; textResponse: string }> {
|
||||
const modelConfigAlias = getModelConfigAlias(this.definition);
|
||||
|
||||
// Resolve the model config early to get the concrete model string (which may be `auto`).
|
||||
const resolvedConfig =
|
||||
this.runtimeContext.modelConfigService.getResolvedConfig({
|
||||
model: modelConfigAlias,
|
||||
overrideScope: this.definition.name,
|
||||
});
|
||||
const requestedModel = resolvedConfig.model;
|
||||
|
||||
let modelToUse: string;
|
||||
if (isAutoModel(requestedModel)) {
|
||||
const routingContext: RoutingContext = {
|
||||
history: chat.getHistory(/*curated=*/ true),
|
||||
request: message.parts || [],
|
||||
signal,
|
||||
requestedModel,
|
||||
};
|
||||
const router = this.runtimeContext.getModelRouterService();
|
||||
const decision = await router.route(routingContext);
|
||||
modelToUse = decision.model;
|
||||
} else {
|
||||
modelToUse = requestedModel;
|
||||
}
|
||||
|
||||
const responseStream = await chat.sendMessageStream(
|
||||
{
|
||||
model: getModelConfigAlias(this.definition),
|
||||
model: modelToUse,
|
||||
overrideScope: this.definition.name,
|
||||
},
|
||||
message.parts || [],
|
||||
|
||||
@@ -244,6 +244,42 @@ describe('AgentRegistry', () => {
|
||||
});
|
||||
|
||||
describe('registration logic', () => {
|
||||
it('should register runtime overrides when the model is "auto"', async () => {
|
||||
const autoAgent: LocalAgentDefinition = {
|
||||
...MOCK_AGENT_V1,
|
||||
name: 'AutoAgent',
|
||||
modelConfig: { ...MOCK_AGENT_V1.modelConfig, model: 'auto' },
|
||||
};
|
||||
|
||||
const registerOverrideSpy = vi.spyOn(
|
||||
mockConfig.modelConfigService,
|
||||
'registerRuntimeModelOverride',
|
||||
);
|
||||
|
||||
await registry.testRegisterAgent(autoAgent);
|
||||
|
||||
// Should register two overrides: one for the alias and one for the agent name (scope)
|
||||
expect(registerOverrideSpy).toHaveBeenCalledTimes(2);
|
||||
|
||||
// Check alias override
|
||||
expect(registerOverrideSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
match: { model: getModelConfigAlias(autoAgent) },
|
||||
modelConfig: expect.objectContaining({ model: 'auto' }),
|
||||
}),
|
||||
);
|
||||
|
||||
// Check scope override
|
||||
expect(registerOverrideSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
match: { overrideScope: autoAgent.name },
|
||||
modelConfig: expect.objectContaining({
|
||||
generateContentConfig: expect.any(Object),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should register a valid agent definition', async () => {
|
||||
await registry.testRegisterAgent(MOCK_AGENT_V1);
|
||||
expect(registry.getDefinition('MockAgent')).toEqual(MOCK_AGENT_V1);
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
GEMINI_MODEL_ALIAS_AUTO,
|
||||
PREVIEW_GEMINI_FLASH_MODEL,
|
||||
isPreviewModel,
|
||||
isAutoModel,
|
||||
} from '../config/models.js';
|
||||
import type { ModelConfigAlias } from '../services/modelConfigService.js';
|
||||
|
||||
@@ -206,24 +207,46 @@ export class AgentRegistry {
|
||||
model = this.config.getModel();
|
||||
}
|
||||
|
||||
const runtimeAlias: ModelConfigAlias = {
|
||||
modelConfig: {
|
||||
model,
|
||||
generateContentConfig: {
|
||||
temperature: modelConfig.temp,
|
||||
topP: modelConfig.top_p,
|
||||
thinkingConfig: {
|
||||
includeThoughts: true,
|
||||
thinkingBudget: modelConfig.thinkingBudget ?? -1,
|
||||
},
|
||||
},
|
||||
const generateContentConfig = {
|
||||
temperature: modelConfig.temp,
|
||||
topP: modelConfig.top_p,
|
||||
thinkingConfig: {
|
||||
includeThoughts: true,
|
||||
thinkingBudget: modelConfig.thinkingBudget ?? -1,
|
||||
},
|
||||
};
|
||||
|
||||
this.config.modelConfigService.registerRuntimeModelConfig(
|
||||
getModelConfigAlias(definition),
|
||||
runtimeAlias,
|
||||
);
|
||||
if (isAutoModel(model)) {
|
||||
this.config.modelConfigService.registerRuntimeModelOverride({
|
||||
match: {
|
||||
model: getModelConfigAlias(definition),
|
||||
},
|
||||
modelConfig: {
|
||||
model,
|
||||
generateContentConfig,
|
||||
},
|
||||
});
|
||||
this.config.modelConfigService.registerRuntimeModelOverride({
|
||||
match: {
|
||||
overrideScope: definition.name,
|
||||
},
|
||||
modelConfig: {
|
||||
generateContentConfig,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
const runtimeAlias: ModelConfigAlias = {
|
||||
modelConfig: {
|
||||
model,
|
||||
generateContentConfig,
|
||||
},
|
||||
};
|
||||
|
||||
this.config.modelConfigService.registerRuntimeModelConfig(
|
||||
getModelConfigAlias(definition),
|
||||
runtimeAlias,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -148,6 +148,20 @@ export function isGemini2Model(model: string): boolean {
|
||||
return /^gemini-2(\.|$)/.test(model);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the model is an auto model.
|
||||
*
|
||||
* @param model The model name to check.
|
||||
* @returns True if the model is an auto model.
|
||||
*/
|
||||
export function isAutoModel(model: string): boolean {
|
||||
return (
|
||||
model === GEMINI_MODEL_ALIAS_AUTO ||
|
||||
model === PREVIEW_GEMINI_MODEL_AUTO ||
|
||||
model === DEFAULT_GEMINI_MODEL_AUTO
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the model supports multimodal function responses (multimodal data nested within function response).
|
||||
* This is supported in Gemini 3.
|
||||
|
||||
@@ -605,6 +605,7 @@ export class GeminiClient {
|
||||
history: this.getChat().getHistory(/*curated=*/ true),
|
||||
request,
|
||||
signal,
|
||||
requestedModel: this.config.getModel(),
|
||||
};
|
||||
|
||||
let modelToUse: string;
|
||||
|
||||
@@ -35,6 +35,8 @@ export interface RoutingContext {
|
||||
request: PartListUnion;
|
||||
/** An abort signal to cancel an LLM call during routing. */
|
||||
signal: AbortSignal;
|
||||
/** The model string requested for this turn, if any. */
|
||||
requestedModel?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -281,4 +281,30 @@ describe('ClassifierStrategy', () => {
|
||||
);
|
||||
consoleWarnSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should respect requestedModel from context in resolveClassifierModel', async () => {
|
||||
const requestedModel = DEFAULT_GEMINI_MODEL; // Pro model
|
||||
const mockApiResponse = {
|
||||
reasoning: 'Choice is flash',
|
||||
model_choice: 'flash',
|
||||
};
|
||||
vi.mocked(mockBaseLlmClient.generateJson).mockResolvedValue(
|
||||
mockApiResponse,
|
||||
);
|
||||
|
||||
const contextWithRequestedModel = {
|
||||
...mockContext,
|
||||
requestedModel,
|
||||
} as RoutingContext;
|
||||
|
||||
const decision = await strategy.route(
|
||||
contextWithRequestedModel,
|
||||
mockConfig,
|
||||
mockBaseLlmClient,
|
||||
);
|
||||
|
||||
expect(decision).not.toBeNull();
|
||||
// Since requestedModel is Pro, and choice is flash, it should resolve to Flash
|
||||
expect(decision?.model).toBe(DEFAULT_GEMINI_FLASH_MODEL);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -168,7 +168,7 @@ export class ClassifierStrategy implements RoutingStrategy {
|
||||
const reasoning = routerResponse.reasoning;
|
||||
const latencyMs = Date.now() - startTime;
|
||||
const selectedModel = resolveClassifierModel(
|
||||
config.getModel(),
|
||||
context.requestedModel || config.getModel(),
|
||||
routerResponse.model_choice,
|
||||
config.getPreviewFeatures(),
|
||||
);
|
||||
|
||||
@@ -108,4 +108,25 @@ describe('FallbackStrategy', () => {
|
||||
// Important: check that it queried snapshot with the RESOLVED model, not 'auto'
|
||||
expect(mockService.snapshot).toHaveBeenCalledWith(DEFAULT_GEMINI_MODEL);
|
||||
});
|
||||
|
||||
it('should respect requestedModel from context', async () => {
|
||||
const requestedModel = 'requested-model';
|
||||
const configModel = 'config-model';
|
||||
vi.mocked(mockConfig.getModel).mockReturnValue(configModel);
|
||||
vi.mocked(mockService.snapshot).mockReturnValue({ available: true });
|
||||
|
||||
const contextWithRequestedModel = {
|
||||
requestedModel,
|
||||
} as RoutingContext;
|
||||
|
||||
const decision = await strategy.route(
|
||||
contextWithRequestedModel,
|
||||
mockConfig,
|
||||
mockClient,
|
||||
);
|
||||
|
||||
expect(decision).toBeNull();
|
||||
// Should check availability of the requested model from context
|
||||
expect(mockService.snapshot).toHaveBeenCalledWith(requestedModel);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,11 +18,11 @@ export class FallbackStrategy implements RoutingStrategy {
|
||||
readonly name = 'fallback';
|
||||
|
||||
async route(
|
||||
_context: RoutingContext,
|
||||
context: RoutingContext,
|
||||
config: Config,
|
||||
_baseLlmClient: BaseLlmClient,
|
||||
): Promise<RoutingDecision | null> {
|
||||
const requestedModel = config.getModel();
|
||||
const requestedModel = context.requestedModel || config.getModel();
|
||||
const resolvedModel = resolveModel(
|
||||
requestedModel,
|
||||
config.getPreviewFeatures(),
|
||||
|
||||
@@ -56,4 +56,25 @@ describe('OverrideStrategy', () => {
|
||||
expect(decision).not.toBeNull();
|
||||
expect(decision?.model).toBe(overrideModel);
|
||||
});
|
||||
|
||||
it('should respect requestedModel from context', async () => {
|
||||
const requestedModel = 'requested-model';
|
||||
const configModel = 'config-model';
|
||||
const mockConfig = {
|
||||
getModel: () => configModel,
|
||||
getPreviewFeatures: () => false,
|
||||
} as Config;
|
||||
const contextWithRequestedModel = {
|
||||
requestedModel,
|
||||
} as RoutingContext;
|
||||
|
||||
const decision = await strategy.route(
|
||||
contextWithRequestedModel,
|
||||
mockConfig,
|
||||
mockClient,
|
||||
);
|
||||
|
||||
expect(decision).not.toBeNull();
|
||||
expect(decision?.model).toBe(requestedModel);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -24,11 +24,11 @@ export class OverrideStrategy implements RoutingStrategy {
|
||||
readonly name = 'override';
|
||||
|
||||
async route(
|
||||
_context: RoutingContext,
|
||||
context: RoutingContext,
|
||||
config: Config,
|
||||
_baseLlmClient: BaseLlmClient,
|
||||
): Promise<RoutingDecision | null> {
|
||||
const overrideModel = config.getModel();
|
||||
const overrideModel = context.requestedModel || config.getModel();
|
||||
|
||||
// If the model is 'auto' we should pass to the next strategy.
|
||||
if (
|
||||
|
||||
@@ -577,6 +577,81 @@ describe('ModelConfigService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('runtime overrides', () => {
|
||||
it('should resolve a simple runtime-registered override', () => {
|
||||
const config: ModelConfigServiceConfig = {
|
||||
aliases: {},
|
||||
overrides: [],
|
||||
};
|
||||
const service = new ModelConfigService(config);
|
||||
|
||||
service.registerRuntimeModelOverride({
|
||||
match: { model: 'gemini-pro' },
|
||||
modelConfig: {
|
||||
generateContentConfig: {
|
||||
temperature: 0.99,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const resolved = service.getResolvedConfig({ model: 'gemini-pro' });
|
||||
|
||||
expect(resolved.model).toBe('gemini-pro');
|
||||
expect(resolved.generateContentConfig.temperature).toBe(0.99);
|
||||
});
|
||||
|
||||
it('should prioritize runtime overrides over default overrides when they have the same specificity', () => {
|
||||
const config: ModelConfigServiceConfig = {
|
||||
aliases: {},
|
||||
overrides: [
|
||||
{
|
||||
match: { model: 'gemini-pro' },
|
||||
modelConfig: { generateContentConfig: { temperature: 0.1 } },
|
||||
},
|
||||
],
|
||||
};
|
||||
const service = new ModelConfigService(config);
|
||||
|
||||
service.registerRuntimeModelOverride({
|
||||
match: { model: 'gemini-pro' },
|
||||
modelConfig: { generateContentConfig: { temperature: 0.9 } },
|
||||
});
|
||||
|
||||
const resolved = service.getResolvedConfig({ model: 'gemini-pro' });
|
||||
|
||||
// Runtime overrides are appended after overrides/customOverrides, so they should win.
|
||||
expect(resolved.generateContentConfig.temperature).toBe(0.9);
|
||||
});
|
||||
|
||||
it('should still respect specificity with runtime overrides', () => {
|
||||
const config: ModelConfigServiceConfig = {
|
||||
aliases: {},
|
||||
overrides: [],
|
||||
};
|
||||
const service = new ModelConfigService(config);
|
||||
|
||||
// Register a more specific runtime override
|
||||
service.registerRuntimeModelOverride({
|
||||
match: { model: 'gemini-pro', overrideScope: 'my-agent' },
|
||||
modelConfig: { generateContentConfig: { temperature: 0.1 } },
|
||||
});
|
||||
|
||||
// Register a less specific runtime override later
|
||||
service.registerRuntimeModelOverride({
|
||||
match: { model: 'gemini-pro' },
|
||||
modelConfig: { generateContentConfig: { temperature: 0.9 } },
|
||||
});
|
||||
|
||||
const resolved = service.getResolvedConfig({
|
||||
model: 'gemini-pro',
|
||||
overrideScope: 'my-agent',
|
||||
});
|
||||
|
||||
// Specificity should win over order
|
||||
expect(resolved.generateContentConfig.temperature).toBe(0.1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('custom aliases', () => {
|
||||
it('should resolve a custom alias', () => {
|
||||
const config: ModelConfigServiceConfig = {
|
||||
|
||||
@@ -65,6 +65,7 @@ export interface _ResolvedModelConfig {
|
||||
|
||||
export class ModelConfigService {
|
||||
private readonly runtimeAliases: Record<string, ModelConfigAlias> = {};
|
||||
private readonly runtimeOverrides: ModelConfigOverride[] = [];
|
||||
|
||||
// TODO(12597): Process config to build a typed alias hierarchy.
|
||||
constructor(private readonly config: ModelConfigServiceConfig) {}
|
||||
@@ -73,6 +74,10 @@ export class ModelConfigService {
|
||||
this.runtimeAliases[aliasName] = alias;
|
||||
}
|
||||
|
||||
registerRuntimeModelOverride(override: ModelConfigOverride): void {
|
||||
this.runtimeOverrides.push(override);
|
||||
}
|
||||
|
||||
private resolveAlias(
|
||||
aliasName: string,
|
||||
aliases: Record<string, ModelConfigAlias>,
|
||||
@@ -123,7 +128,11 @@ export class ModelConfigService {
|
||||
...customAliases,
|
||||
...this.runtimeAliases,
|
||||
};
|
||||
const allOverrides = [...overrides, ...customOverrides];
|
||||
const allOverrides = [
|
||||
...overrides,
|
||||
...customOverrides,
|
||||
...this.runtimeOverrides,
|
||||
];
|
||||
let baseModel: string | undefined = context.model;
|
||||
let resolvedConfig: GenerateContentConfig = {};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user