Merge
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
import Cookies from 'js-cookie';
|
||||
import type { ModelInfo, OllamaApiResponse, OllamaModel } from './types';
|
||||
import type { ProviderInfo } from '~/types/model';
|
||||
import type { ProviderInfo, IProviderSetting } from '~/types/model';
|
||||
import { createScopedLogger } from './logger';
|
||||
import { logStore } from '~/lib/stores/logs';
|
||||
|
||||
export const WORK_DIR_NAME = 'project';
|
||||
export const WORK_DIR = `/home/${WORK_DIR_NAME}`;
|
||||
@@ -10,6 +12,8 @@ export const PROVIDER_REGEX = /\[Provider: (.*?)\]\n\n/;
|
||||
export const DEFAULT_MODEL = 'claude-3-5-sonnet-latest';
|
||||
export const PROMPT_COOKIE_KEY = 'cachedPrompt';
|
||||
|
||||
const logger = createScopedLogger('Constants');
|
||||
|
||||
const PROVIDER_LIST: ProviderInfo[] = [
|
||||
{
|
||||
name: 'Anthropic',
|
||||
@@ -123,22 +127,24 @@ const PROVIDER_LIST: ProviderInfo[] = [
|
||||
name: 'Google',
|
||||
staticModels: [
|
||||
{ name: 'gemini-1.5-flash-latest', label: 'Gemini 1.5 Flash', provider: 'Google', maxTokenAllowed: 8192 },
|
||||
{ name: 'gemini-2.0-flash-exp', label: 'Gemini 2.0 Flash', provider: 'Google', maxTokenAllowed: 8192 },
|
||||
{ name: 'gemini-1.5-flash-002', label: 'Gemini 1.5 Flash-002', provider: 'Google', maxTokenAllowed: 8192 },
|
||||
{ name: 'gemini-1.5-flash-8b', label: 'Gemini 1.5 Flash-8b', provider: 'Google', maxTokenAllowed: 8192 },
|
||||
{ name: 'gemini-1.5-pro-latest', label: 'Gemini 1.5 Pro', provider: 'Google', maxTokenAllowed: 8192 },
|
||||
{ name: 'gemini-1.5-pro-002', label: 'Gemini 1.5 Pro-002', provider: 'Google', maxTokenAllowed: 8192 },
|
||||
{ name: 'gemini-exp-1121', label: 'Gemini exp-1121', provider: 'Google', maxTokenAllowed: 8192 },
|
||||
{ name: 'gemini-exp-1206', label: 'Gemini exp-1206', provider: 'Google', maxTokenAllowed: 8192 },
|
||||
],
|
||||
getApiKeyLink: 'https://aistudio.google.com/app/apikey',
|
||||
},
|
||||
{
|
||||
name: 'Groq',
|
||||
staticModels: [
|
||||
{ name: 'llama-3.1-70b-versatile', label: 'Llama 3.1 70b (Groq)', provider: 'Groq', maxTokenAllowed: 8000 },
|
||||
{ name: 'llama-3.1-8b-instant', label: 'Llama 3.1 8b (Groq)', provider: 'Groq', maxTokenAllowed: 8000 },
|
||||
{ name: 'llama-3.2-11b-vision-preview', label: 'Llama 3.2 11b (Groq)', provider: 'Groq', maxTokenAllowed: 8000 },
|
||||
{ name: 'llama-3.2-90b-vision-preview', label: 'Llama 3.2 90b (Groq)', provider: 'Groq', maxTokenAllowed: 8000 },
|
||||
{ name: 'llama-3.2-3b-preview', label: 'Llama 3.2 3b (Groq)', provider: 'Groq', maxTokenAllowed: 8000 },
|
||||
{ name: 'llama-3.2-1b-preview', label: 'Llama 3.2 1b (Groq)', provider: 'Groq', maxTokenAllowed: 8000 },
|
||||
{ name: 'llama-3.3-70b-versatile', label: 'Llama 3.3 70b (Groq)', provider: 'Groq', maxTokenAllowed: 8000 },
|
||||
],
|
||||
getApiKeyLink: 'https://console.groq.com/keys',
|
||||
},
|
||||
@@ -295,13 +301,16 @@ const staticModels: ModelInfo[] = PROVIDER_LIST.map((p) => p.staticModels).flat(
|
||||
|
||||
export let MODEL_LIST: ModelInfo[] = [...staticModels];
|
||||
|
||||
export async function getModelList(apiKeys: Record<string, string>) {
|
||||
export async function getModelList(
|
||||
apiKeys: Record<string, string>,
|
||||
providerSettings?: Record<string, IProviderSetting>,
|
||||
) {
|
||||
MODEL_LIST = [
|
||||
...(
|
||||
await Promise.all(
|
||||
PROVIDER_LIST.filter(
|
||||
(p): p is ProviderInfo & { getDynamicModels: () => Promise<ModelInfo[]> } => !!p.getDynamicModels,
|
||||
).map((p) => p.getDynamicModels(apiKeys)),
|
||||
).map((p) => p.getDynamicModels(apiKeys, providerSettings?.[p.name])),
|
||||
)
|
||||
).flat(),
|
||||
...staticModels,
|
||||
@@ -309,9 +318,9 @@ export async function getModelList(apiKeys: Record<string, string>) {
|
||||
return MODEL_LIST;
|
||||
}
|
||||
|
||||
async function getTogetherModels(apiKeys?: Record<string, string>): Promise<ModelInfo[]> {
|
||||
async function getTogetherModels(apiKeys?: Record<string, string>, settings?: IProviderSetting): Promise<ModelInfo[]> {
|
||||
try {
|
||||
const baseUrl = import.meta.env.TOGETHER_API_BASE_URL || '';
|
||||
const baseUrl = settings?.baseUrl || import.meta.env.TOGETHER_API_BASE_URL || '';
|
||||
const provider = 'Together';
|
||||
|
||||
if (!baseUrl) {
|
||||
@@ -350,8 +359,8 @@ async function getTogetherModels(apiKeys?: Record<string, string>): Promise<Mode
|
||||
}
|
||||
}
|
||||
|
||||
const getOllamaBaseUrl = () => {
|
||||
const defaultBaseUrl = import.meta.env.OLLAMA_API_BASE_URL || 'http://localhost:11434';
|
||||
const getOllamaBaseUrl = (settings?: IProviderSetting) => {
|
||||
const defaultBaseUrl = settings?.baseUrl || import.meta.env.OLLAMA_API_BASE_URL || 'http://localhost:11434';
|
||||
|
||||
// Check if we're in the browser
|
||||
if (typeof window !== 'undefined') {
|
||||
@@ -365,15 +374,9 @@ const getOllamaBaseUrl = () => {
|
||||
return isDocker ? defaultBaseUrl.replace('localhost', 'host.docker.internal') : defaultBaseUrl;
|
||||
};
|
||||
|
||||
async function getOllamaModels(): Promise<ModelInfo[]> {
|
||||
/*
|
||||
* if (typeof window === 'undefined') {
|
||||
* return [];
|
||||
* }
|
||||
*/
|
||||
|
||||
async function getOllamaModels(apiKeys?: Record<string, string>, settings?: IProviderSetting): Promise<ModelInfo[]> {
|
||||
try {
|
||||
const baseUrl = getOllamaBaseUrl();
|
||||
const baseUrl = getOllamaBaseUrl(settings);
|
||||
const response = await fetch(`${baseUrl}/api/tags`);
|
||||
const data = (await response.json()) as OllamaApiResponse;
|
||||
|
||||
@@ -383,26 +386,29 @@ async function getOllamaModels(): Promise<ModelInfo[]> {
|
||||
provider: 'Ollama',
|
||||
maxTokenAllowed: 8000,
|
||||
}));
|
||||
} catch (e) {
|
||||
console.error('Error getting Ollama models:', e);
|
||||
} catch (e: any) {
|
||||
logStore.logError('Failed to get Ollama models', e, { baseUrl: settings?.baseUrl });
|
||||
logger.warn('Failed to get Ollama models: ', e.message || '');
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async function getOpenAILikeModels(): Promise<ModelInfo[]> {
|
||||
async function getOpenAILikeModels(
|
||||
apiKeys?: Record<string, string>,
|
||||
settings?: IProviderSetting,
|
||||
): Promise<ModelInfo[]> {
|
||||
try {
|
||||
const baseUrl = import.meta.env.OPENAI_LIKE_API_BASE_URL || '';
|
||||
const baseUrl = settings?.baseUrl || import.meta.env.OPENAI_LIKE_API_BASE_URL || '';
|
||||
|
||||
if (!baseUrl) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let apiKey = import.meta.env.OPENAI_LIKE_API_KEY ?? '';
|
||||
let apiKey = '';
|
||||
|
||||
const apikeys = JSON.parse(Cookies.get('apiKeys') || '{}');
|
||||
|
||||
if (apikeys && apikeys.OpenAILike) {
|
||||
apiKey = apikeys.OpenAILike;
|
||||
if (apiKeys && apiKeys.OpenAILike) {
|
||||
apiKey = apiKeys.OpenAILike;
|
||||
}
|
||||
|
||||
const response = await fetch(`${baseUrl}/models`, {
|
||||
@@ -456,13 +462,9 @@ async function getOpenRouterModels(): Promise<ModelInfo[]> {
|
||||
}));
|
||||
}
|
||||
|
||||
async function getLMStudioModels(): Promise<ModelInfo[]> {
|
||||
if (typeof window === 'undefined') {
|
||||
return [];
|
||||
}
|
||||
|
||||
async function getLMStudioModels(_apiKeys?: Record<string, string>, settings?: IProviderSetting): Promise<ModelInfo[]> {
|
||||
try {
|
||||
const baseUrl = import.meta.env.LMSTUDIO_API_BASE_URL || 'http://localhost:1234';
|
||||
const baseUrl = settings?.baseUrl || import.meta.env.LMSTUDIO_API_BASE_URL || 'http://localhost:1234';
|
||||
const response = await fetch(`${baseUrl}/v1/models`);
|
||||
const data = (await response.json()) as any;
|
||||
|
||||
@@ -471,13 +473,15 @@ async function getLMStudioModels(): Promise<ModelInfo[]> {
|
||||
label: model.id,
|
||||
provider: 'LMStudio',
|
||||
}));
|
||||
} catch (e) {
|
||||
console.error('Error getting LMStudio models:', e);
|
||||
} catch (e: any) {
|
||||
logStore.logError('Failed to get LMStudio models', e, { baseUrl: settings?.baseUrl });
|
||||
logger.warn('Failed to get LMStudio models: ', e.message || '');
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async function initializeModelList(): Promise<ModelInfo[]> {
|
||||
async function initializeModelList(providerSettings?: Record<string, IProviderSetting>): Promise<ModelInfo[]> {
|
||||
let apiKeys: Record<string, string> = {};
|
||||
|
||||
try {
|
||||
@@ -491,14 +495,15 @@ async function initializeModelList(): Promise<ModelInfo[]> {
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.warn(`Failed to fetch apikeys from cookies:${error?.message}`);
|
||||
logStore.logError('Failed to fetch API keys from cookies', error);
|
||||
logger.warn(`Failed to fetch apikeys from cookies: ${error?.message}`);
|
||||
}
|
||||
MODEL_LIST = [
|
||||
...(
|
||||
await Promise.all(
|
||||
PROVIDER_LIST.filter(
|
||||
(p): p is ProviderInfo & { getDynamicModels: () => Promise<ModelInfo[]> } => !!p.getDynamicModels,
|
||||
).map((p) => p.getDynamicModels(apiKeys)),
|
||||
).map((p) => p.getDynamicModels(apiKeys, providerSettings?.[p.name])),
|
||||
)
|
||||
).flat(),
|
||||
...staticModels,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Message } from 'ai';
|
||||
import { generateId, detectProjectType } from './fileUtils';
|
||||
import { generateId } from './fileUtils';
|
||||
import { detectProjectCommands, createCommandsMessage } from './projectCommands';
|
||||
|
||||
export const createChatFromFolder = async (
|
||||
files: File[],
|
||||
@@ -8,17 +9,16 @@ export const createChatFromFolder = async (
|
||||
): Promise<Message[]> => {
|
||||
const fileArtifacts = await Promise.all(
|
||||
files.map(async (file) => {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
return new Promise<{ content: string; path: string }>((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = () => {
|
||||
const content = reader.result as string;
|
||||
const relativePath = file.webkitRelativePath.split('/').slice(1).join('/');
|
||||
resolve(
|
||||
`<boltAction type="file" filePath="${relativePath}">
|
||||
${content}
|
||||
</boltAction>`,
|
||||
);
|
||||
resolve({
|
||||
content,
|
||||
path: relativePath,
|
||||
});
|
||||
};
|
||||
reader.onerror = reject;
|
||||
reader.readAsText(file);
|
||||
@@ -26,38 +26,30 @@ ${content}
|
||||
}),
|
||||
);
|
||||
|
||||
const project = await detectProjectType(files);
|
||||
const setupCommand = project.setupCommand
|
||||
? `\n\n<boltAction type="shell">\n${project.setupCommand}\n</boltAction>`
|
||||
: '';
|
||||
const followupMessage = project.followupMessage ? `\n\n${project.followupMessage}` : '';
|
||||
const commands = await detectProjectCommands(fileArtifacts);
|
||||
const commandsMessage = createCommandsMessage(commands);
|
||||
|
||||
const binaryFilesMessage =
|
||||
binaryFiles.length > 0
|
||||
? `\n\nSkipped ${binaryFiles.length} binary files:\n${binaryFiles.map((f) => `- ${f}`).join('\n')}`
|
||||
: '';
|
||||
|
||||
const assistantMessages: Message[] = [
|
||||
{
|
||||
role: 'assistant',
|
||||
content: `I've imported the contents of the "${folderName}" folder.${binaryFilesMessage}
|
||||
const filesMessage: Message = {
|
||||
role: 'assistant',
|
||||
content: `I've imported the contents of the "${folderName}" folder.${binaryFilesMessage}
|
||||
|
||||
<boltArtifact id="imported-files" title="Imported Files">
|
||||
${fileArtifacts.join('\n\n')}
|
||||
${fileArtifacts
|
||||
.map(
|
||||
(file) => `<boltAction type="file" filePath="${file.path}">
|
||||
${file.content}
|
||||
</boltAction>`,
|
||||
)
|
||||
.join('\n\n')}
|
||||
</boltArtifact>`,
|
||||
id: generateId(),
|
||||
createdAt: new Date(),
|
||||
},
|
||||
{
|
||||
role: 'assistant',
|
||||
content: `
|
||||
<boltArtifact id="imported-files" title="Imported Files">
|
||||
${setupCommand}
|
||||
</boltArtifact>${followupMessage}`,
|
||||
id: generateId(),
|
||||
createdAt: new Date(),
|
||||
},
|
||||
];
|
||||
id: generateId(),
|
||||
createdAt: new Date(),
|
||||
};
|
||||
|
||||
const userMessage: Message = {
|
||||
role: 'user',
|
||||
@@ -66,5 +58,11 @@ ${setupCommand}
|
||||
createdAt: new Date(),
|
||||
};
|
||||
|
||||
return [userMessage, ...assistantMessages];
|
||||
const messages = [userMessage, filesMessage];
|
||||
|
||||
if (commandsMessage) {
|
||||
messages.push(commandsMessage);
|
||||
}
|
||||
|
||||
return messages;
|
||||
};
|
||||
|
||||
80
app/utils/projectCommands.ts
Normal file
80
app/utils/projectCommands.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import type { Message } from 'ai';
|
||||
import { generateId } from './fileUtils';
|
||||
|
||||
export interface ProjectCommands {
|
||||
type: string;
|
||||
setupCommand: string;
|
||||
followupMessage: string;
|
||||
}
|
||||
|
||||
interface FileContent {
|
||||
content: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
export async function detectProjectCommands(files: FileContent[]): Promise<ProjectCommands> {
|
||||
const hasFile = (name: string) => files.some((f) => f.path.endsWith(name));
|
||||
|
||||
if (hasFile('package.json')) {
|
||||
const packageJsonFile = files.find((f) => f.path.endsWith('package.json'));
|
||||
|
||||
if (!packageJsonFile) {
|
||||
return { type: '', setupCommand: '', followupMessage: '' };
|
||||
}
|
||||
|
||||
try {
|
||||
const packageJson = JSON.parse(packageJsonFile.content);
|
||||
const scripts = packageJson?.scripts || {};
|
||||
|
||||
// Check for preferred commands in priority order
|
||||
const preferredCommands = ['dev', 'start', 'preview'];
|
||||
const availableCommand = preferredCommands.find((cmd) => scripts[cmd]);
|
||||
|
||||
if (availableCommand) {
|
||||
return {
|
||||
type: 'Node.js',
|
||||
setupCommand: `npm install && npm run ${availableCommand}`,
|
||||
followupMessage: `Found "${availableCommand}" script in package.json. Running "npm run ${availableCommand}" after installation.`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'Node.js',
|
||||
setupCommand: 'npm install',
|
||||
followupMessage:
|
||||
'Would you like me to inspect package.json to determine the available scripts for running this project?',
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error parsing package.json:', error);
|
||||
return { type: '', setupCommand: '', followupMessage: '' };
|
||||
}
|
||||
}
|
||||
|
||||
if (hasFile('index.html')) {
|
||||
return {
|
||||
type: 'Static',
|
||||
setupCommand: 'npx --yes serve',
|
||||
followupMessage: '',
|
||||
};
|
||||
}
|
||||
|
||||
return { type: '', setupCommand: '', followupMessage: '' };
|
||||
}
|
||||
|
||||
export function createCommandsMessage(commands: ProjectCommands): Message | null {
|
||||
if (!commands.setupCommand) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
role: 'assistant',
|
||||
content: `
|
||||
<boltArtifact id="project-setup" title="Project Setup">
|
||||
<boltAction type="shell">
|
||||
${commands.setupCommand}
|
||||
</boltAction>
|
||||
</boltArtifact>${commands.followupMessage ? `\n\n${commands.followupMessage}` : ''}`,
|
||||
id: generateId(),
|
||||
createdAt: new Date(),
|
||||
};
|
||||
}
|
||||
49
app/utils/sampler.ts
Normal file
49
app/utils/sampler.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Creates a function that samples calls at regular intervals and captures trailing calls.
|
||||
* - Drops calls that occur between sampling intervals
|
||||
* - Takes one call per sampling interval if available
|
||||
* - Captures the last call if no call was made during the interval
|
||||
*
|
||||
* @param fn The function to sample
|
||||
* @param sampleInterval How often to sample calls (in ms)
|
||||
* @returns The sampled function
|
||||
*/
|
||||
export function createSampler<T extends (...args: any[]) => any>(fn: T, sampleInterval: number): T {
|
||||
let lastArgs: Parameters<T> | null = null;
|
||||
let lastTime = 0;
|
||||
let timeout: NodeJS.Timeout | null = null;
|
||||
|
||||
// Create a function with the same type as the input function
|
||||
const sampled = function (this: any, ...args: Parameters<T>) {
|
||||
const now = Date.now();
|
||||
lastArgs = args;
|
||||
|
||||
// If we're within the sample interval, just store the args
|
||||
if (now - lastTime < sampleInterval) {
|
||||
// Set up trailing call if not already set
|
||||
if (!timeout) {
|
||||
timeout = setTimeout(
|
||||
() => {
|
||||
timeout = null;
|
||||
lastTime = Date.now();
|
||||
|
||||
if (lastArgs) {
|
||||
fn.apply(this, lastArgs);
|
||||
lastArgs = null;
|
||||
}
|
||||
},
|
||||
sampleInterval - (now - lastTime),
|
||||
);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// If we're outside the interval, execute immediately
|
||||
lastTime = now;
|
||||
fn.apply(this, args);
|
||||
lastArgs = null;
|
||||
} as T;
|
||||
|
||||
return sampled;
|
||||
}
|
||||
@@ -26,12 +26,3 @@ export interface ModelInfo {
|
||||
provider: string;
|
||||
maxTokenAllowed: number;
|
||||
}
|
||||
|
||||
export interface ProviderInfo {
|
||||
staticModels: ModelInfo[];
|
||||
name: string;
|
||||
getDynamicModels?: () => Promise<ModelInfo[]>;
|
||||
getApiKeyLink?: string;
|
||||
labelForGetApiKey?: string;
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user