Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
12 changes: 12 additions & 0 deletions docker/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
# Multi-stage build for Chrome DevTools Frontend
FROM --platform=linux/amd64 ubuntu:22.04 AS builder

# Build arguments for API keys (optional, defaults to empty)
ARG OPENAI_API_KEY=""
ARG OPENROUTER_API_KEY=""
ARG GROQ_API_KEY=""
ARG LITELLM_API_KEY=""

# Set environment variables for build process
ENV OPENAI_API_KEY=${OPENAI_API_KEY}
ENV OPENROUTER_API_KEY=${OPENROUTER_API_KEY}
ENV GROQ_API_KEY=${GROQ_API_KEY}
ENV LITELLM_API_KEY=${LITELLM_API_KEY}

# Install required packages
RUN apt-get update && apt-get install -y \
curl \
Expand Down
17 changes: 14 additions & 3 deletions docker/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,29 @@ services:
build:
context: ..
dockerfile: docker/Dockerfile
args:
# API keys passed from host environment variables
# These can be set via .env file or exported in shell
OPENAI_API_KEY: ${OPENAI_API_KEY:-}
OPENROUTER_API_KEY: ${OPENROUTER_API_KEY:-}
GROQ_API_KEY: ${GROQ_API_KEY:-}
LITELLM_API_KEY: ${LITELLM_API_KEY:-}
image: devtools-frontend:latest
container_name: devtools-frontend
ports:
- "8000:8000"
restart: unless-stopped
volumes:
# volumes:
# For development: mount the built files directly (optional)
# Uncomment the line below to use local files instead of container files
# - ../out/Default/gen/front_end:/usr/share/nginx/html:ro
# Uncomment the lines below to use local files instead of container files
# volumes:
# - ../out/Default/gen/front_end:/usr/share/nginx/html:ro
environment:
- NGINX_HOST=localhost
- NGINX_PORT=8000
# Optional: Pass API keys to runtime for debugging (not required for functionality)
- OPENAI_API_KEY=${OPENAI_API_KEY:-}
- OPENROUTER_API_KEY=${OPENROUTER_API_KEY:-}
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8000/"]
interval: 30s
Expand Down
2 changes: 2 additions & 0 deletions front_end/panels/ai_chat/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ devtools_module("ai_chat") {
"core/Types.ts",
"core/AgentService.ts",
"core/Constants.ts",
"core/EnvironmentConfig.ts",
"core/GraphConfigs.ts",
"core/ConfigurableGraph.ts",
"core/BaseOrchestratorAgent.ts",
Expand Down Expand Up @@ -133,6 +134,7 @@ _ai_chat_sources = [
"core/Types.ts",
"core/AgentService.ts",
"core/Constants.ts",
"core/EnvironmentConfig.ts",
"core/GraphConfigs.ts",
"core/ConfigurableGraph.ts",
"core/BaseOrchestratorAgent.ts",
Expand Down
42 changes: 24 additions & 18 deletions front_end/panels/ai_chat/LLM/GroqProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { LLMBaseProvider } from './LLMProvider.js';
import { LLMRetryManager } from './LLMErrorHandler.js';
import { LLMResponseParser } from './LLMResponseParser.js';
import { createLogger } from '../core/Logger.js';
import { getEnvironmentConfig } from '../core/EnvironmentConfig.js';

const logger = createLogger('GroqProvider');

Expand Down Expand Up @@ -38,10 +39,29 @@ export class GroqProvider extends LLMBaseProvider {

readonly name: LLMProvider = 'groq';

private readonly envConfig = getEnvironmentConfig();

constructor(private readonly apiKey: string) {
super();
}

/**
* Get the API key with fallback hierarchy:
* 1. Constructor parameter (for backward compatibility)
* 2. localStorage (user-configured)
* 3. Build-time environment config
* 4. Empty string
*/
private getApiKey(): string {
// Constructor parameter (highest priority for backward compatibility)
if (this.getApiKey() && this.getApiKey().trim() !== '') {
return this.getApiKey().trim();
}

// Use environment config which handles localStorage -> build-time -> empty fallback
return this.envConfig.getApiKey('groq');
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix infinite recursion in getApiKey().

this.getApiKey() calls itself repeatedly. Use this.apiKey.

   private getApiKey(): string {
     // Constructor parameter (highest priority for backward compatibility)
-    if (this.getApiKey() && this.getApiKey().trim() !== '') {
-      return this.getApiKey().trim();
+    if (this.apiKey && this.apiKey.trim() !== '') {
+      return this.apiKey.trim();
     }
     
     // Use environment config which handles localStorage -> build-time -> empty fallback
-    return this.envConfig.getApiKey('groq');
+    return this.envConfig.getApiKey('groq')?.trim() ?? '';
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private getApiKey(): string {
// Constructor parameter (highest priority for backward compatibility)
if (this.getApiKey() && this.getApiKey().trim() !== '') {
return this.getApiKey().trim();
}
// Use environment config which handles localStorage -> build-time -> empty fallback
return this.envConfig.getApiKey('groq');
}
private getApiKey(): string {
// Constructor parameter (highest priority for backward compatibility)
if (this.apiKey && this.apiKey.trim() !== '') {
return this.apiKey.trim();
}
// Use environment config which handles localStorage -> build-time -> empty fallback
return this.envConfig.getApiKey('groq')?.trim() ?? '';
}
🧰 Tools
🪛 ESLint

[error] 60-60: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)

🤖 Prompt for AI Agents
In front_end/panels/ai_chat/LLM/GroqProvider.ts around lines 55 to 63,
getApiKey() currently calls this.getApiKey() causing infinite recursion; replace
those self-calls with the instance field this.apiKey (guarding for
null/undefined), e.g. check if this.apiKey is a non-empty string, return
this.apiKey.trim(), otherwise return this.envConfig.getApiKey('groq'); keep the
same priority order and trimming behavior.


/**
* Get the chat completions endpoint URL
*/
Expand Down Expand Up @@ -92,7 +112,7 @@ export class GroqProvider extends LLMBaseProvider {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.apiKey}`,
'Authorization': `Bearer ${this.getApiKey()}`,
},
Comment on lines 114 to 116
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Unquote Authorization header and guard against empty key.

  • Style: no quotes needed around Authorization.
  • Behavior: avoid sending an empty Bearer header.
-          'Authorization': `Bearer ${this.getApiKey()}`,
+          Authorization: (() => {
+            const k = this.getApiKey();
+            if (!k) throw new Error('Groq API key is missing. Configure it in Settings.');
+            return `Bearer ${k}`;
+          })(),
-          'Authorization': `Bearer ${this.getApiKey()}`,
+          Authorization: (() => {
+            const k = this.getApiKey();
+            if (!k) throw new Error('Groq API key is missing. Configure it in Settings.');
+            return `Bearer ${k}`;
+          })(),

Also applies to: 281-283

🧰 Tools
🪛 ESLint

[error] 115-115: Unnecessarily quoted property 'Authorization' found.

(@stylistic/quote-props)

🤖 Prompt for AI Agents
In front_end/panels/ai_chat/LLM/GroqProvider.ts around lines 114-116 (and
similarly at 281-283), the Authorization header is written with unnecessary
quotes around the header name and may send an empty Bearer token; change it to
use an unquoted header name and build the header only after retrieving and
trimming the API key into a local variable (e.g., const key =
this.getApiKey()?.trim()), then if key is falsy either throw a clear error or
omit the Authorization header instead of sending "Bearer " with an empty token.
Ensure the header includes `Bearer ${key}` only when key is non-empty.

body: JSON.stringify(payloadBody),
});
Expand Down Expand Up @@ -259,7 +279,7 @@ export class GroqProvider extends LLMBaseProvider {
const response = await fetch(this.getModelsEndpoint(), {
method: 'GET',
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Authorization': `Bearer ${this.getApiKey()}`,
},
});

Expand Down Expand Up @@ -432,29 +452,15 @@ export class GroqProvider extends LLMBaseProvider {
* Validate that required credentials are available for Groq
*/
validateCredentials(): {isValid: boolean, message: string, missingItems?: string[]} {
const storageKeys = this.getCredentialStorageKeys();
const apiKey = localStorage.getItem(storageKeys.apiKey!);

if (!apiKey) {
return {
isValid: false,
message: 'Groq API key is required. Please add your API key in Settings.',
missingItems: ['API Key']
};
}

return {
isValid: true,
message: 'Groq credentials are configured correctly.'
};
return this.envConfig.validateCredentials('groq');
}

/**
* Get the storage keys this provider uses for credentials
*/
getCredentialStorageKeys(): {apiKey: string} {
return {
apiKey: 'ai_chat_groq_api_key'
apiKey: this.envConfig.getStorageKey('groq')
};
}
}
24 changes: 22 additions & 2 deletions front_end/panels/ai_chat/LLM/OpenAIProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { LLMBaseProvider } from './LLMProvider.js';
import { LLMRetryManager } from './LLMErrorHandler.js';
import { LLMResponseParser } from './LLMResponseParser.js';
import { createLogger } from '../core/Logger.js';
import { getEnvironmentConfig } from '../core/EnvironmentConfig.js';

const logger = createLogger('OpenAIProvider');

Expand Down Expand Up @@ -42,10 +43,29 @@ export class OpenAIProvider extends LLMBaseProvider {

readonly name: LLMProvider = 'openai';

private readonly envConfig = getEnvironmentConfig();

constructor(private readonly apiKey: string) {
super();
}

/**
* Get the API key with fallback hierarchy:
* 1. Constructor parameter (for backward compatibility)
* 2. localStorage (user-configured)
* 3. Build-time environment config
* 4. Empty string
*/
private getApiKey(): string {
// Constructor parameter (highest priority for backward compatibility)
if (this.apiKey && this.apiKey.trim() !== '') {
return this.apiKey.trim();
}

// Use environment config which handles localStorage -> build-time -> empty fallback
return this.envConfig.getApiKey('openai');
}

/**
* Determines the model family based on the model name
*/
Expand Down Expand Up @@ -280,7 +300,7 @@ export class OpenAIProvider extends LLMBaseProvider {
metadata: {
provider: 'openai',
errorType: 'api_error',
hasApiKey: !!this.apiKey
hasApiKey: !!this.getApiKey()
}
}, context.traceId);
}
Expand All @@ -299,7 +319,7 @@ export class OpenAIProvider extends LLMBaseProvider {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${this.apiKey}`,
Authorization: `Bearer ${this.getApiKey()}`,
},
Comment on lines 321 to 323
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Avoid sending empty Authorization header.

If no key is present, fail fast with a clear error instead of Authorization: Bearer .

-          Authorization: `Bearer ${this.getApiKey()}`,
+          ...(this.getApiKey()
+            ? { Authorization: `Bearer ${this.getApiKey()}` }
+            : (() => { throw new Error('OpenAI API key is missing. Configure it in Settings.'); })()),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
'Content-Type': 'application/json',
Authorization: `Bearer ${this.apiKey}`,
Authorization: `Bearer ${this.getApiKey()}`,
},
'Content-Type': 'application/json',
...(this.getApiKey()
? { Authorization: `Bearer ${this.getApiKey()}` }
: (() => { throw new Error('OpenAI API key is missing. Configure it in Settings.'); })()),
},
🤖 Prompt for AI Agents
In front_end/panels/ai_chat/LLM/OpenAIProvider.ts around lines 321 to 323, the
code unconditionally sets Authorization: `Bearer ${this.getApiKey()}` which can
create an empty Authorization header; change this to fail fast when no API key
is present by checking this.getApiKey() before building headers and throw a
clear error (or return early) if it's missing, otherwise include the
Authorization header with the key; ensure headers only include Authorization
when a non-empty key exists.

body: JSON.stringify(payloadBody),
});
Expand Down
67 changes: 39 additions & 28 deletions front_end/panels/ai_chat/LLM/OpenRouterProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { LLMBaseProvider } from './LLMProvider.js';
import { LLMRetryManager } from './LLMErrorHandler.js';
import { LLMResponseParser } from './LLMResponseParser.js';
import { createLogger } from '../core/Logger.js';
import { getEnvironmentConfig } from '../core/EnvironmentConfig.js';

const logger = createLogger('OpenRouterProvider');

Expand Down Expand Up @@ -59,10 +60,29 @@ export class OpenRouterProvider extends LLMBaseProvider {
private visionModelsCacheExpiry: number = 0;
private static readonly CACHE_DURATION_MS = 30 * 60 * 1000; // 30 minutes

private readonly envConfig = getEnvironmentConfig();

constructor(private readonly apiKey: string) {
super();
}

/**
* Get the API key with fallback hierarchy:
* 1. Constructor parameter (for backward compatibility)
* 2. localStorage (user-configured)
* 3. Build-time environment config
* 4. Empty string
*/
private getApiKey(): string {
// Constructor parameter (highest priority for backward compatibility)
if (this.apiKey && this.apiKey.trim() !== '') {
return this.apiKey.trim();
}

// Use environment config which handles localStorage -> build-time -> empty fallback
return this.envConfig.getApiKey('openrouter');
}

/**
* Check if a model doesn't support temperature parameter
* OpenAI's GPT-5, O3, and O4 models accessed through OpenRouter don't support temperature
Expand Down Expand Up @@ -144,7 +164,7 @@ export class OpenRouterProvider extends LLMBaseProvider {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.apiKey}`,
'Authorization': `Bearer ${this.getApiKey()}`,
'HTTP-Referer': 'https://browseroperator.io', // Site URL for rankings on openrouter.ai
'X-Title': 'Browser Operator', // Site title for rankings on openrouter.ai
},
Comment on lines 166 to 170
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Avoid empty Bearer token; unquote Authorization header.

-        headers: {
-          'Content-Type': 'application/json',
-          'Authorization': `Bearer ${this.getApiKey()}`,
-          'HTTP-Referer': 'https://browseroperator.io', // Site URL for rankings on openrouter.ai
-          'X-Title': 'Browser Operator', // Site title for rankings on openrouter.ai
-        },
+        headers: {
+          'Content-Type': 'application/json',
+          Authorization: (() => {
+            const k = this.getApiKey()?.trim();
+            if (!k) throw new Error('OpenRouter API key is missing. Configure it in Settings.');
+            return `Bearer ${k}`;
+          })(),
+          'HTTP-Referer': 'https://browseroperator.io',
+          'X-Title': 'Browser Operator',
+        },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.apiKey}`,
'Authorization': `Bearer ${this.getApiKey()}`,
'HTTP-Referer': 'https://browseroperator.io', // Site URL for rankings on openrouter.ai
'X-Title': 'Browser Operator', // Site title for rankings on openrouter.ai
},
headers: {
'Content-Type': 'application/json',
Authorization: (() => {
const k = this.getApiKey()?.trim();
if (!k) throw new Error('OpenRouter API key is missing. Configure it in Settings.');
return `Bearer ${k}`;
})(),
'HTTP-Referer': 'https://browseroperator.io',
'X-Title': 'Browser Operator',
},
🧰 Tools
🪛 ESLint

[error] 167-167: Unnecessarily quoted property 'Authorization' found.

(@stylistic/quote-props)

🤖 Prompt for AI Agents
In front_end/panels/ai_chat/LLM/OpenRouterProvider.ts around lines 166-170, the
Authorization header currently risks sending an empty "Bearer " token and may be
improperly quoted; update the code to first read const apiKey =
this.getApiKey(), then only add the Authorization header when apiKey is truthy
(omit the header entirely if empty) and set it unquoted as Authorization:
`Bearer ${apiKey}` so no quoted string or empty bearer token is sent.

Expand Down Expand Up @@ -324,7 +344,7 @@ export class OpenRouterProvider extends LLMBaseProvider {
const response = await fetch(this.getToolSupportingModelsEndpoint(), {
method: 'GET',
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Authorization': `Bearer ${this.getApiKey()}`,
},
});
Comment on lines 344 to 349
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Same Authorization fix for fetchModels().

-        headers: {
-          'Authorization': `Bearer ${this.getApiKey()}`,
-        },
+        headers: {
+          Authorization: (() => {
+            const k = this.getApiKey()?.trim();
+            if (!k) throw new Error('OpenRouter API key is missing. Configure it in Settings.');
+            return `Bearer ${k}`;
+          })(),
+        },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const response = await fetch(this.getToolSupportingModelsEndpoint(), {
method: 'GET',
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Authorization': `Bearer ${this.getApiKey()}`,
},
});
const response = await fetch(this.getToolSupportingModelsEndpoint(), {
method: 'GET',
headers: {
Authorization: (() => {
const k = this.getApiKey()?.trim();
if (!k) throw new Error('OpenRouter API key is missing. Configure it in Settings.');
return `Bearer ${k}`;
})(),
},
});
🧰 Tools
🪛 ESLint

[error] 347-347: Unnecessarily quoted property 'Authorization' found.

(@stylistic/quote-props)

🤖 Prompt for AI Agents
In front_end/panels/ai_chat/LLM/OpenRouterProvider.ts around lines 344 to 349,
ensure fetchModels() uses the same Authorization header format as other calls:
include an Authorization header set to `Bearer ${this.getApiKey()}` when calling
the models endpoint; update the fetch call to add the headers object
(Authorization: `Bearer ${this.getApiKey()}`) so the API key is sent correctly
with the request.


Expand Down Expand Up @@ -358,7 +378,7 @@ export class OpenRouterProvider extends LLMBaseProvider {
const response = await fetch(this.getVisionModelsEndpoint(), {
method: 'GET',
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Authorization': `Bearer ${this.getApiKey()}`,
},
});

Expand Down Expand Up @@ -621,15 +641,21 @@ export class OpenRouterProvider extends LLMBaseProvider {
logger.debug('=== VALIDATING OPENROUTER CREDENTIALS ===');
logger.debug('Timestamp:', new Date().toISOString());

const storageKeys = this.getCredentialStorageKeys();
logger.debug('Storage keys:', storageKeys);
// Use the new environment config for validation
const validationResult = this.envConfig.validateCredentials('openrouter');

// Enhanced logging for debugging
const apiKey = this.getApiKey();
const source = this.envConfig.getApiKeySource('openrouter');
const buildInfo = this.envConfig.getBuildInfo();

const apiKey = localStorage.getItem(storageKeys.apiKey!);
logger.debug('API key check:');
logger.debug('- Storage key used:', storageKeys.apiKey);
logger.debug('- API key exists:', !!apiKey);
logger.debug('- API key length:', apiKey?.length || 0);
logger.debug('- API key prefix:', apiKey?.substring(0, 8) + '...' || 'none');
logger.debug('- API key source:', source);
logger.debug('- Build config available:', buildInfo.hasBuildConfig);
logger.debug('- Build time:', buildInfo.buildTime);

// Also check OAuth-related storage for debugging
const authMethod = localStorage.getItem('openrouter_auth_method');
Expand All @@ -638,37 +664,22 @@ export class OpenRouterProvider extends LLMBaseProvider {
logger.debug('- Auth method:', authMethod);
logger.debug('- OAuth token exists:', !!oauthToken);

// Check all OpenRouter-related localStorage keys
const allKeys = Object.keys(localStorage);
const openRouterKeys = allKeys.filter(key => key.includes('openrouter') || key.includes('ai_chat'));
logger.debug('All OpenRouter-related storage keys:');
openRouterKeys.forEach(key => {
const value = localStorage.getItem(key);
logger.debug(`- ${key}:`, value?.substring(0, 50) + (value && value.length > 50 ? '...' : '') || 'null');
});

if (!apiKey) {
if (validationResult.isValid) {
logger.info('✅ OpenRouter credentials validation passed');
} else {
logger.warn('❌ OpenRouter API key missing');
return {
isValid: false,
message: 'OpenRouter API key is required. Please add your API key in Settings.',
missingItems: ['API Key']
};
}

logger.info('✅ OpenRouter credentials validation passed');
return {
isValid: true,
message: 'OpenRouter credentials are configured correctly.'
};
return validationResult;
}

/**
* Get the storage keys this provider uses for credentials
*/
getCredentialStorageKeys(): {apiKey: string} {
const storageKey = this.envConfig.getStorageKey('openrouter');
const keys = {
apiKey: 'ai_chat_openrouter_api_key'
apiKey: storageKey
};
logger.debug('OpenRouter credential storage keys:', keys);
return keys;
Expand Down
Loading