diff --git a/eslint.config.js b/eslint.config.js index 996d1fc0f20..696eb9f1577 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -12,7 +12,7 @@ import prettierConfig from 'eslint-config-prettier'; import importPlugin from 'eslint-plugin-import'; import vitest from '@vitest/eslint-plugin'; import globals from 'globals'; -import licenseHeader from 'eslint-plugin-license-header'; +import headers from 'eslint-plugin-headers'; import path from 'node:path'; import url from 'node:url'; @@ -209,19 +209,26 @@ export default tseslint.config( { files: ['./**/*.{tsx,ts,js}'], plugins: { - 'license-header': licenseHeader, + headers, import: importPlugin, }, rules: { - 'license-header/header': [ + 'headers/header-format': [ 'error', - [ - '/**', - ' * @license', - ' * Copyright 2025 Google LLC', - ' * SPDX-License-Identifier: Apache-2.0', - ' */', - ], + { + source: 'string', + content: [ + '@license', + 'Copyright (year) Google LLC', + 'SPDX-License-Identifier: Apache-2.0', + ].join('\n'), + patterns: { + year: { + pattern: '202[5-6]', + defaultValue: '2026', + }, + }, + }, ], 'import/enforce-node-protocol-usage': ['error', 'always'], }, diff --git a/package-lock.json b/package-lock.json index 09d38da588b..b34d8560584 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,8 +36,8 @@ "esbuild-plugin-wasm": "^1.1.0", "eslint": "^9.24.0", "eslint-config-prettier": "^10.1.2", + "eslint-plugin-headers": "^1.3.3", "eslint-plugin-import": "^2.31.0", - "eslint-plugin-license-header": "^0.8.0", "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^5.2.0", "glob": "^12.0.0", @@ -8865,6 +8865,19 @@ "ms": "^2.1.1" } }, + "node_modules/eslint-plugin-headers": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-headers/-/eslint-plugin-headers-1.3.3.tgz", + "integrity": "sha512-VzZY4+cGRoR5HpALLARH+ibIjB6a2w12/cFEayORHXMRHMzDnweSjpmvxyzX3rsSIVCg01zmvepB7Tnmaj4kGQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^16.0.0 || >= 18.0.0" + }, + "peerDependencies": { + "eslint": ">=7" + } + }, "node_modules/eslint-plugin-import": { "version": "2.32.0", "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", @@ -8919,16 +8932,6 @@ "semver": "bin/semver.js" } }, - "node_modules/eslint-plugin-license-header": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-license-header/-/eslint-plugin-license-header-0.8.0.tgz", - "integrity": "sha512-khTCz6G3JdoQfwrtY4XKl98KW4PpnWUKuFx8v+twIRhJADEyYglMDC0td8It75C1MZ88gcvMusWuUlJsos7gYg==", - "dev": true, - "license": "MIT", - "dependencies": { - "requireindex": "^1.2.0" - } - }, "node_modules/eslint-plugin-react": { "version": "7.37.5", "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", @@ -15069,16 +15072,6 @@ "dev": true, "license": "MIT" }, - "node_modules/requireindex": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/requireindex/-/requireindex-1.2.0.tgz", - "integrity": "sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.5" - } - }, "node_modules/requires-port": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", diff --git a/package.json b/package.json index 973f356e11e..44ac64b26d5 100644 --- a/package.json +++ b/package.json @@ -94,8 +94,8 @@ "esbuild-plugin-wasm": "^1.1.0", "eslint": "^9.24.0", "eslint-config-prettier": "^10.1.2", + "eslint-plugin-headers": "^1.3.3", "eslint-plugin-import": "^2.31.0", - "eslint-plugin-license-header": "^0.8.0", "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^5.2.0", "glob": "^12.0.0", diff --git a/packages/core/src/core/client.ts b/packages/core/src/core/client.ts index c1d7f7c9a1c..0fcc8b5ab17 100644 --- a/packages/core/src/core/client.ts +++ b/packages/core/src/core/client.ts @@ -57,6 +57,7 @@ import { applyModelSelection, createAvailabilityContextProvider, } from '../availability/policyHelpers.js'; +import { resolveModel } from '../config/models.js'; import type { RetryAvailabilityContext } from '../utils/retry.js'; const MAX_TURNS = 100; @@ -397,7 +398,7 @@ export class GeminiClient { // Availability logic: The configured model is the source of truth, // including any permanent fallbacks (config.setModel) or manual overrides. - return this.config.getActiveModel(); + return resolveModel(this.config.getActiveModel()); } async *sendMessageStream( diff --git a/packages/core/src/utils/tokenCalculation.test.ts b/packages/core/src/utils/tokenCalculation.test.ts index 7e1eae3e88d..c6e54bc8870 100644 --- a/packages/core/src/utils/tokenCalculation.test.ts +++ b/packages/core/src/utils/tokenCalculation.test.ts @@ -123,8 +123,26 @@ describe('calculateRequestTokenCount', () => { // Should fallback to estimation: // 'Hello': 5 chars * 0.25 = 1.25 - // inlineData: JSON.stringify length / 4 - expect(count).toBeGreaterThan(0); + // inlineData: 3000 + // Total: 3001.25 -> 3001 + expect(count).toBe(3001); expect(mockContentGenerator.countTokens).toHaveBeenCalled(); }); + + it('should use fixed estimate for images in fallback', async () => { + vi.mocked(mockContentGenerator.countTokens).mockRejectedValue( + new Error('API error'), + ); + const request = [ + { inlineData: { mimeType: 'image/png', data: 'large_data' } }, + ]; + + const count = await calculateRequestTokenCount( + request, + mockContentGenerator, + model, + ); + + expect(count).toBe(3000); + }); }); diff --git a/packages/core/src/utils/tokenCalculation.ts b/packages/core/src/utils/tokenCalculation.ts index 0359cb3e7c0..06292bb9254 100644 --- a/packages/core/src/utils/tokenCalculation.ts +++ b/packages/core/src/utils/tokenCalculation.ts @@ -6,6 +6,7 @@ import type { PartListUnion, Part } from '@google/genai'; import type { ContentGenerator } from '../core/contentGenerator.js'; +import { debugLogger } from './debugLogger.js'; // Token estimation constants // ASCII characters (0-127) are roughly 4 chars per token @@ -13,6 +14,8 @@ const ASCII_TOKENS_PER_CHAR = 0.25; // Non-ASCII characters (including CJK) are often 1-2 tokens per char. // We use 1.3 as a conservative estimate to avoid underestimation. const NON_ASCII_TOKENS_PER_CHAR = 1.3; +// Fixed token estimate for images +const IMAGE_TOKEN_ESTIMATE = 3000; /** * Estimates token count for parts synchronously using a heuristic. @@ -31,10 +34,21 @@ export function estimateTokenCountSync(parts: Part[]): number { } } } else { - // For non-text parts (functionCall, functionResponse, executableCode, etc.), - // we fallback to the JSON string length heuristic. - // Note: This is an approximation. - totalTokens += JSON.stringify(part).length / 4; + // For images, we use a fixed safe estimate (3,000 tokens) covering + // up to 4K resolution on Gemini 3. + // See: https://ai.google.dev/gemini-api/docs/vision#token_counting + const inlineData = 'inlineData' in part ? part.inlineData : undefined; + const fileData = 'fileData' in part ? part.fileData : undefined; + const mimeType = inlineData?.mimeType || fileData?.mimeType; + + if (mimeType?.startsWith('image/')) { + totalTokens += IMAGE_TOKEN_ESTIMATE; + } else { + // For other non-text parts (functionCall, functionResponse, etc.), + // we fallback to the JSON string length heuristic. + // Note: This is an approximation. + totalTokens += JSON.stringify(part).length / 4; + } } } return Math.floor(totalTokens); @@ -69,8 +83,9 @@ export async function calculateRequestTokenCount( contents: [{ role: 'user', parts }], }); return response.totalTokens ?? 0; - } catch { + } catch (error) { // Fallback to local estimation if the API call fails + debugLogger.debug('countTokens API failed:', error); return estimateTokenCountSync(parts); } } diff --git a/scripts/generate-git-commit-info.js b/scripts/generate-git-commit-info.js index 75c8bb97ba3..049c39c249b 100644 --- a/scripts/generate-git-commit-info.js +++ b/scripts/generate-git-commit-info.js @@ -57,7 +57,7 @@ try { const fileContent = `/** * @license - * Copyright ${new Date().getFullYear()} Google LLC + * Copyright ${new Date().getUTCFullYear()} Google LLC * SPDX-License-Identifier: Apache-2.0 */