-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Add dynamic OpenAI model fetching with vision-only filtering #173
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
86bf9e8
15372ae
149470f
dfe459a
16d0b05
4850710
0e4f1b3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -12,19 +12,23 @@ import { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| isComputerToolUseContentBlock, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| isImageContentBlock, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } from '@bytebot/shared'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { DEFAULT_MODEL } from './openai.constants'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { DEFAULT_MODEL, OPENAI_MODELS } from './openai.constants'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { Message, Role } from '@prisma/client'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { openaiTools } from './openai.tools'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| BytebotAgentService, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| BytebotAgentInterrupt, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| BytebotAgentResponse, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| BytebotAgentModel, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } from '../agent/agent.types'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @Injectable() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export class OpenAIService implements BytebotAgentService { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private readonly openai: OpenAI; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private readonly logger = new Logger(OpenAIService.name); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private cachedModels: BytebotAgentModel[] | null = null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private modelsCacheTime: number = 0; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private readonly CACHE_DURATION = 3600000; // 1 hour in milliseconds | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| constructor(private readonly configService: ConfigService) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const apiKey = this.configService.get<string>('OPENAI_API_KEY'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -40,6 +44,105 @@ export class OpenAIService implements BytebotAgentService { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Fetch available models from OpenAI API and cache them | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async getAvailableModels(): Promise<BytebotAgentModel[]> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Return cached models if still valid | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const now = Date.now(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.cachedModels && | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| now - this.modelsCacheTime < this.CACHE_DURATION | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return this.cachedModels; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const apiKey = this.configService.get<string>('OPENAI_API_KEY'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!apiKey) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.logger.warn('OPENAI_API_KEY not set, returning hardcoded models'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return OPENAI_MODELS; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Fetch models from OpenAI API | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const modelsList = await this.openai.models.list(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const models = modelsList.data; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Filter for relevant chat models that support vision (images/screenshots) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Exclude O1 and O3 models as they don't support image inputs | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const availableModels: BytebotAgentModel[] = models | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .filter( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| (model) => | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| model.id.startsWith('gpt-') && | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| !model.id.startsWith('gpt-3.5') && // Exclude GPT-3.5 (no vision support) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| !model.id.includes('instruct'), // Exclude instruct models | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+75
to
+78
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| (model) => | |
| model.id.startsWith('gpt-') && | |
| !model.id.startsWith('gpt-3.5') && // Exclude GPT-3.5 (no vision support) | |
| !model.id.includes('instruct'), // Exclude instruct models | |
| (model) => { | |
| // Only include known vision-capable models | |
| const id = model.id; | |
| // Add to this list as new vision-capable models are released | |
| return ( | |
| ( | |
| id.includes('gpt-4o') || | |
| id.includes('gpt-4-turbo') || | |
| id.includes('gpt-4-vision') || | |
| id.includes('gpt-4v') | |
| ) && | |
| !id.includes('instruct') | |
| ); | |
| }, |
Copilot
AI
Dec 3, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The sort priority includes 'gpt-4.1' at priority 1 and 'gpt-5' at priority 3, but according to the filter logic (line 76), only models starting with 'gpt-' and excluding 'gpt-3.5' are included. Since GPT-4.1 and GPT-5 are hypothetical future models that may not exist yet, consider whether these priority cases are necessary. If they are intended for future-proofing, a comment explaining this would be helpful.
Copilot
AI
Dec 3, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] The formatModelTitle method transforms model IDs like "gpt-4o-mini" to "GPT 4o Mini". However, this transformation logic may not handle all edge cases correctly. For example, "gpt-4o-2024-05-13" would become "GPT 4o 2024 05 13" with spaces between date components. Consider adding test cases or documentation for expected behavior with dated model IDs.
| // Convert model IDs like "gpt-4o-mini" to "GPT-4o Mini" | |
| return modelId | |
| .split('-') | |
| .map((part) => { | |
| if (part === 'gpt') return 'GPT'; | |
| if (part.match(/^\d/)) return part; // Keep numbers as-is | |
| return part.charAt(0).toUpperCase() + part.slice(1); | |
| }) | |
| .join('-') | |
| .replace(/-/g, ' '); | |
| // Convert model IDs like "gpt-4o-mini" to "GPT 4o Mini" | |
| // If the model ID ends with a date (e.g., "2024-05-13"), keep the date together as a single part | |
| const parts = modelId.split('-'); | |
| // Check if the last three parts form a date (YYYY-MM-DD) | |
| const len = parts.length; | |
| let formattedParts: string[]; | |
| if ( | |
| len >= 3 && | |
| /^\d{4}$/.test(parts[len - 3]) && | |
| /^\d{2}$/.test(parts[len - 2]) && | |
| /^\d{2}$/.test(parts[len - 1]) | |
| ) { | |
| // Group the last three parts as a date | |
| const datePart = `${parts[len - 3]}-${parts[len - 2]}-${parts[len - 1]}`; | |
| formattedParts = parts.slice(0, len - 3).concat([datePart]); | |
| } else { | |
| formattedParts = parts; | |
| } | |
| return formattedParts | |
| .map((part) => { | |
| if (part === 'gpt') return 'GPT'; | |
| if (part.match(/^\d/)) return part; // Keep numbers as-is | |
| // If part is a date (YYYY-MM-DD), keep as-is | |
| if (/^\d{4}-\d{2}-\d{2}$/.test(part)) return part; | |
| return part.charAt(0).toUpperCase() + part.slice(1); | |
| }) | |
| .join(' '); |
Copilot
AI
Dec 3, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The getContextWindow method includes logic for 'o1' and 'o3' models (lines 140-141), but these models are filtered out in getAvailableModels (line 76) because they don't start with 'gpt-'. Since these models are intentionally excluded from the available models list, this dead code should be removed to avoid confusion.
| if (modelId.includes('o1')) return 128000; | |
| if (modelId.includes('o3')) return 200000; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The comment states "Exclude O1 and O3 models as they don't support image inputs", but the filtering logic below only checks for models starting with 'gpt-'. O1 and O3 models (which would have IDs like 'o1-...' or 'o3-...') are already implicitly excluded by the first filter condition
model.id.startsWith('gpt-'). The comment should be clarified to explain that O1/O3 models are excluded because they don't start with 'gpt-', or the comment should be removed if it's redundant.