bolt.newをローカル環境で動作させる際、APIエンドポイントの変更や機能拡張が必要になる場合があります。本記事では、OpenAI APIへの切り替え、AI SDKのエラー対処、およびファイルダウンロード機能の実装方法について解説します。
AIプロバイダーの切り替え設定
まず、AnthropicからOpenAIへプロバイダーを変更するための設定を行います。@ai-sdk/openaiパッケージをインストールします。
pnpm install @ai-sdk/openai
次に、LLMモデルの設定ファイルを作成します。環境変数を活用した柔軟な設定が可能です。
// app/lib/.server/llm/provider.ts
import { createOpenAI } from '@ai-sdk/openai';
import { createAnthropic } from '@ai-sdk/anthropic';
type ProviderConfig = {
baseURL: string;
apiKey: string;
modelName: string;
};
export function initializeOpenAIProvider(config: ProviderConfig) {
const openai = createOpenAI({
baseURL: config.baseURL,
apiKey: config.apiKey,
});
return openai(config.modelName);
}
export function initializeAnthropicProvider(config: ProviderConfig) {
const anthropic = createAnthropic({
baseURL: config.baseURL,
apiKey: config.apiKey,
});
return anthropic(config.modelName);
}
ストリーミング処理のロジックを更新し、プロバイダーを切り替えます。
// app/lib/.server/llm/text-stream.ts
import { streamText as vercelStreamText, convertToCoreMessages } from 'ai';
import { initializeOpenAIProvider } from '~/lib/.server/llm/provider';
import { getSystemPrompt } from './system-prompt';
import { TOKEN_LIMIT } from './limits';
export async function streamText(messages: ChatMessage[], env: Env, options?: StreamOptions) {
const modelConfig = {
baseURL: env.OPENAI_BASE_URL || 'https://api.openai.com/v1',
apiKey: env.OPENAI_API_KEY,
modelName: env.OPENAI_MODEL || 'gpt-4o',
};
return vercelStreamText({
model: initializeOpenAIProvider(modelConfig),
system: getSystemPrompt(),
maxTokens: TOKEN_LIMIT,
messages: convertToCoreMessages(messages),
...options,
});
}
トークン制限の調整
OpenAIのモデル仕様に合わせて、最大トークン数を適切な値に設定します。
// app/lib/.server/llm/limits.ts
// OpenAI GPT-4oの制限に基づいて設定
export const TOKEN_LIMIT = 4096;
// レスポンスセグメント数の制限
export const MAX_SEGMENTS = 2;
AI SDKの互換性対策
AI SDKで未対応のチャンクタイプが送信される場合、エラーが発生します。これを回避するため、カスタムストリームラッパーを実装します。
// app/lib/.server/llm/safe-stream.ts
import { createAsyncIterableStream } from 'ai';
export function createSafeIterableStream(sourceStream: AsyncIterable<any>) {
return createAsyncIterableStream(sourceStream, {
transform(chunk, controller) {
if (!chunk || typeof chunk !== 'object') {
return;
}
switch (chunk.type) {
case 'object':
controller.enqueue(chunk.object);
break;
case 'text-delta':
case 'finish':
// これらのタイプは正常に処理
break;
case 'error':
controller.error(chunk.error);
break;
default:
// 未知のチャンクタイプはログに記録して無視
console.warn(`未対応のチャンクタイプを検知: ${JSON.stringify(chunk)}`);
// 例外を投げずに処理を継続
}
}
});
}
プロジェクトファイルのダウンロード機能
作業中のプロジェクトをZIP形式でダウンロードする機能を追加します。必要なパッケージをインストールします。
pnpm add jszip file-saver
ダウンロードボタンコンポーネントを作成します。
// app/components/workbench/DownloadButton.tsx
import { useCallback } from 'react';
import JSZip from 'jszip';
import { saveAs } from 'file-saver';
import { useWorkbenchStore } from '~/stores/workbench';
export function ProjectDownloadButton() {
const fileStore = useWorkbenchStore();
const handleDownload = useCallback(async () => {
try {
const projectFiles = fileStore.getAllFiles();
const archive = new JSZip();
const projectFolder = archive.folder('project');
if (!projectFolder) {
throw new Error('ZIPフォルダの作成に失敗しました');
}
Object.entries(projectFiles).forEach(([path, file]) => {
if (file?.type === 'file' && file.content) {
const relativePath = path.replace(/^\/home\/project\//, '');
projectFolder.file(relativePath, file.content);
}
});
const zipContent = await archive.generateAsync({
type: 'blob',
compression: 'DEFLATE',
compressionOptions: { level: 6 }
});
saveAs(zipContent, `bolt-project-${Date.now()}.zip`);
} catch (error) {
console.error('ダウンロード処理でエラーが発生:', error);
}
}, [fileStore]);
return (
<button
onClick={handleDownload}
className="flex items-center gap-2 rounded-md bg-blue-600 px-4 py-2 text-white hover:bg-blue-700"
>
<svg className="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M9 19l3 3m0 0l3-3m-3 3V10" />
</svg>
プロジェクトをダウンロード
</button>
);
}
このコンポーネントをワークベンチに統合します。
// app/components/workbench/WorkbenchLayout.tsx
import { ProjectDownloadButton } from './DownloadButton';
export const WorkbenchLayout = ({ isStreaming }: WorkbenchProps) => {
return (
<div className="flex h-full flex-col">
<div className="flex items-center justify-between border-b px-4 py-2">
<h2 className="text-lg font-semibold">ワークベンチ</h2>
<div className="flex items-center gap-2">
{!isStreaming && <ProjectDownloadButton />}
<!-- その他のコントロール -->
</div>
</div>
<!-- エディタ領域 -->
</div>
);
};
以上の設定により、bolt.newをOpenAI APIで動作させ、プロジェクトファイルをダウンロードできるようになります。環境変数を適切に設定することで、異なるモデルやエンドポイントにも柔軟に対応可能です。