Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions config/gni/devtools_grd_files.gni
Original file line number Diff line number Diff line change
Expand Up @@ -654,6 +654,8 @@ grd_files_bundled_sources = [
"front_end/panels/ai_chat/ui/HelpDialog.js",
"front_end/panels/ai_chat/ui/PromptEditDialog.js",
"front_end/panels/ai_chat/ui/SettingsDialog.js",
"front_end/panels/ai_chat/ui/OnboardingDialog.js",
"front_end/panels/ai_chat/ui/onboardingStyles.js",
"front_end/panels/ai_chat/ui/mcp/MCPConnectionsDialog.js",
"front_end/panels/ai_chat/ui/mcp/MCPConnectorsCatalogDialog.js",
"front_end/panels/ai_chat/ui/EvaluationDialog.js",
Expand Down Expand Up @@ -687,6 +689,7 @@ grd_files_bundled_sources = [
"front_end/panels/ai_chat/LLM/LLMProviderRegistry.js",
"front_end/panels/ai_chat/LLM/LLMErrorHandler.js",
"front_end/panels/ai_chat/LLM/LLMResponseParser.js",
"front_end/panels/ai_chat/LLM/FuzzyModelMatcher.js",
"front_end/panels/ai_chat/LLM/OpenAIProvider.js",
"front_end/panels/ai_chat/LLM/LiteLLMProvider.js",
"front_end/panels/ai_chat/LLM/GroqProvider.js",
Expand Down
2 changes: 2 additions & 0 deletions config/gni/devtools_image_files.gni
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ devtools_image_files = [
"touchCursor.png",
"gdp-logo-light.png",
"gdp-logo-dark.png",
"browser-operator-logo.png",
"demo.gif",
]

devtools_svg_sources = [
Expand Down
Binary file added front_end/Images/browser-operator-logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added front_end/Images/demo.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
21 changes: 21 additions & 0 deletions front_end/panels/ai_chat/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ devtools_module("ai_chat") {
"ui/ToolDescriptionFormatter.ts",
"ui/HelpDialog.ts",
"ui/SettingsDialog.ts",
"ui/OnboardingDialog.ts",
"ui/onboardingStyles.ts",
"ui/settings/types.ts",
"ui/settings/constants.ts",
"ui/settings/i18n-strings.ts",
Expand All @@ -60,6 +62,7 @@ devtools_module("ai_chat") {
"ui/settings/advanced/VectorDBSettings.ts",
"ui/settings/advanced/TracingSettings.ts",
"ui/settings/advanced/EvaluationSettings.ts",
"ui/settings/advanced/MemorySettings.ts",
"ui/PromptEditDialog.ts",
"ui/EvaluationDialog.ts",
"ui/WebAppCodeViewer.ts",
Expand All @@ -75,6 +78,11 @@ devtools_module("ai_chat") {
"persistence/ConversationTypes.ts",
"persistence/ConversationStorageManager.ts",
"persistence/ConversationManager.ts",
"memory/types.ts",
"memory/MemoryModule.ts",
"memory/MemoryBlockManager.ts",
"memory/MemoryAgentConfig.ts",
"memory/index.ts",
"core/Graph.ts",
"core/State.ts",
"core/Types.ts",
Expand Down Expand Up @@ -103,6 +111,7 @@ devtools_module("ai_chat") {
"LLM/LLMProviderRegistry.ts",
"LLM/LLMErrorHandler.ts",
"LLM/LLMResponseParser.ts",
"LLM/FuzzyModelMatcher.ts",
"LLM/OpenAIProvider.ts",
"LLM/LiteLLMProvider.ts",
"LLM/GroqProvider.ts",
Expand Down Expand Up @@ -135,6 +144,9 @@ devtools_module("ai_chat") {
"tools/DeleteFileTool.ts",
"tools/ReadFileTool.ts",
"tools/ListFilesTool.ts",
"memory/SearchMemoryTool.ts",
"memory/UpdateMemoryTool.ts",
"memory/ListMemoryBlocksTool.ts",
"tools/UpdateTodoTool.ts",
"tools/ExecuteCodeTool.ts",
"tools/SequentialThinkingTool.ts",
Expand Down Expand Up @@ -278,6 +290,11 @@ _ai_chat_sources = [
"ui/mcp/MCPConnectorsCatalogDialog.ts",
"ai_chat_impl.ts",
"models/ChatTypes.ts",
"memory/types.ts",
"memory/MemoryModule.ts",
"memory/MemoryBlockManager.ts",
"memory/MemoryAgentConfig.ts",
"memory/index.ts",
"core/Graph.ts",
"core/State.ts",
"core/Types.ts",
Expand Down Expand Up @@ -306,6 +323,7 @@ _ai_chat_sources = [
"LLM/LLMProviderRegistry.ts",
"LLM/LLMErrorHandler.ts",
"LLM/LLMResponseParser.ts",
"LLM/FuzzyModelMatcher.ts",
"LLM/OpenAIProvider.ts",
"LLM/LiteLLMProvider.ts",
"LLM/GroqProvider.ts",
Expand Down Expand Up @@ -338,6 +356,9 @@ _ai_chat_sources = [
"tools/DeleteFileTool.ts",
"tools/ReadFileTool.ts",
"tools/ListFilesTool.ts",
"memory/SearchMemoryTool.ts",
"memory/UpdateMemoryTool.ts",
"memory/ListMemoryBlocksTool.ts",
"tools/UpdateTodoTool.ts",
"tools/ExecuteCodeTool.ts",
"tools/SequentialThinkingTool.ts",
Expand Down
185 changes: 185 additions & 0 deletions front_end/panels/ai_chat/LLM/FuzzyModelMatcher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
// Copyright 2025 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

/**
* Fuzzy model name matcher for finding the closest available model
* when an exact match isn't found.
*/

/**
* Calculate Levenshtein distance between two strings
*/
function levenshteinDistance(a: string, b: string): number {
const matrix: number[][] = [];

// Initialize matrix
for (let i = 0; i <= b.length; i++) {
matrix[i] = [i];
}
for (let j = 0; j <= a.length; j++) {
matrix[0][j] = j;
}

// Fill matrix
for (let i = 1; i <= b.length; i++) {
for (let j = 1; j <= a.length; j++) {
if (b.charAt(i - 1) === a.charAt(j - 1)) {
matrix[i][j] = matrix[i - 1][j - 1];
} else {
matrix[i][j] = Math.min(
matrix[i - 1][j - 1] + 1, // substitution
matrix[i][j - 1] + 1, // insertion
matrix[i - 1][j] + 1 // deletion
);
}
}
}

return matrix[b.length][a.length];
}

/**
* Calculate similarity score between two strings (0-1)
*/
function similarity(a: string, b: string): number {
const distance = levenshteinDistance(a, b);
const maxLen = Math.max(a.length, b.length);
return maxLen === 0 ? 1 : 1 - distance / maxLen;
}

/**
* Normalize model name for comparison by removing dates, versions, and separators
*/
function normalizeModelName(name: string): string {
return name
.toLowerCase()
.replace(/[-_]/g, '') // Remove separators
.replace(/\d{4}-?\d{2}-?\d{2}$/g, '') // Remove date suffixes (2025-04-14 or 20250514)
.replace(/\d{8}$/g, '') // Remove date suffixes without dashes
.trim();
}

/**
* Check if target is a prefix of candidate (case-insensitive)
*/
function isPrefixMatch(target: string, candidate: string): boolean {
const normalizedTarget = target.toLowerCase().replace(/[._]/g, '-');
const normalizedCandidate = candidate.toLowerCase().replace(/[._]/g, '-');
return normalizedCandidate.startsWith(normalizedTarget);
}

/**
* Find the closest matching model from available options
*
* Matching strategy (in priority order):
* 1. Exact match - return immediately
* 2. Prefix match - if target is prefix of an available model
* 3. Normalized match - strip dates/versions and compare base names
* 4. Levenshtein similarity - if similarity > threshold, return best match
*
* @param targetModel - The model name to find a match for
* @param availableModels - Array of available model names
* @param threshold - Minimum similarity score (0-1) for fuzzy matching (default: 0.5)
* @returns The closest matching model name, or null if no good match found
*/
export function findClosestModel(
targetModel: string,
availableModels: string[],
threshold: number = 0.5
): string | null {
if (!targetModel || availableModels.length === 0) {
return null;
}

// 1. Exact match
if (availableModels.includes(targetModel)) {
return targetModel;
}

// 2. Prefix match - find models where target is a prefix
const prefixMatches = availableModels.filter(model => isPrefixMatch(targetModel, model));
if (prefixMatches.length > 0) {
// Return the shortest prefix match (most specific)
return prefixMatches.sort((a, b) => a.length - b.length)[0];
}

// 3. Normalized match - compare base names without dates/versions
const normalizedTarget = normalizeModelName(targetModel);
for (const model of availableModels) {
if (normalizeModelName(model) === normalizedTarget) {
return model;
}
}

// 4. Levenshtein similarity on normalized names
let bestMatch: string | null = null;
let bestScore = 0;

for (const model of availableModels) {
const score = similarity(normalizedTarget, normalizeModelName(model));
if (score > bestScore && score >= threshold) {
bestScore = score;
bestMatch = model;
}
}

return bestMatch;
}

/**
* Find closest model with detailed match info for logging
*/
export interface FuzzyMatchResult {
match: string | null;
matchType: 'exact' | 'prefix' | 'normalized' | 'similarity' | 'none';
score: number;
}

export function findClosestModelWithInfo(
targetModel: string,
availableModels: string[],
threshold: number = 0.5
): FuzzyMatchResult {
if (!targetModel || availableModels.length === 0) {
return { match: null, matchType: 'none', score: 0 };
}

// 1. Exact match
if (availableModels.includes(targetModel)) {
return { match: targetModel, matchType: 'exact', score: 1 };
}

// 2. Prefix match
const prefixMatches = availableModels.filter(model => isPrefixMatch(targetModel, model));
if (prefixMatches.length > 0) {
const match = prefixMatches.sort((a, b) => a.length - b.length)[0];
return { match, matchType: 'prefix', score: targetModel.length / match.length };
}

// 3. Normalized match
const normalizedTarget = normalizeModelName(targetModel);
for (const model of availableModels) {
if (normalizeModelName(model) === normalizedTarget) {
return { match: model, matchType: 'normalized', score: 1 };
}
}

// 4. Levenshtein similarity
let bestMatch: string | null = null;
let bestScore = 0;

for (const model of availableModels) {
const score = similarity(normalizedTarget, normalizeModelName(model));
if (score > bestScore && score >= threshold) {
bestScore = score;
bestMatch = model;
}
}

if (bestMatch) {
return { match: bestMatch, matchType: 'similarity', score: bestScore };
}

return { match: null, matchType: 'none', score: 0 };
}
Loading