Next.js ✕ LM Studio ✕ gpt-oss-20b

# gpt-oss-20b をLM studioで動かしてそれをnext.jsを用いてブラウザでchat botっぽく表示する方法です。

## 結果

↑こんな感じになると思います。

## やりかた

### LM-studio & gpt-oss-20b
なんか最近(8/8現在)でたgpt-oss-20bです。
LM studioの導入はこのお方
の記事を、例えば参考にしてください。

こことか見ておく。

### Approuter

pnp install
pnpm add vercel/ai openai

みたいなのを打っといてください。

Create-next-appとターミナルで打って、いい感じに質問に答えておきましょう。

app/api/chat/route.ts

にディレクトリを作りつつ、以下のコードをコードを貼ってください。

/* app/api/chat/route.ts */

import { NextResponse } from 'next/server'; 
export async function POST(req: Request) { 
const { messages } = await req.json(); 

const response = await fetch(process.env.AI_API_URL!, { 
method: 'POST', 
headers: { 'Content-Type': 'application/json' }, 
body: JSON.stringify({ model: 'openai/gpt-oss-20b', messages }), 
}); 

if (!response.ok) { 
return new NextResponse('Error contacting LM Studio API', { status: 500 }); 
} 

const data = await response.json(); 

// LM Studioのレスポンスから必要な部分だけを抽出して返す 
const message = data.choices?.[0]?.message; 

if (!message) { 
return new NextResponse('Invalid response format from LM Studio API', { status: 500 }); 
} 

// 必要最低限の形で返す 
return NextResponse.json({ 
choices: [ 
{ 
message, 
}, 
], 
}); 
}


### 実際の表示画面部分
これをapp/src にpage.tsxとして置いといてください。(とりあえず)

'use client'; 

import { useState } from 'react'; 
import ReactMarkdown from 'react-markdown'; 

type Message = { 
role: 'user' | 'assistant'; 
content: string; 
id: string; 
}; 

export default function Chat() { 
const [messages, setMessages] = useState<Message[]>([]); 
const [input, setInput] = useState(''); 

const handleSubmit = async (e: React.FormEvent) => { 
e.preventDefault(); 
if (!input.trim()) return; 

const userMessage: Message = { 
role: 'user', 
content: input, 
id: crypto.randomUUID(), 
}; 

// 表示用にユーザーメッセージを先に追加 
setMessages(prev => [...prev, userMessage]); 
setInput(''); 

try { 
const res = await fetch('/api/chat', { 
method: 'POST', 
headers: { 'Content-Type': 'application/json' }, 
body: JSON.stringify({ messages: [...messages, userMessage] }), 
}); 

const data = await res.json(); 

const aiContent = data?.choices?.[0]?.message?.content; 

if (aiContent) { 
const aiMessage: Message = { 
role: 'assistant', 
content: aiContent, 
id: crypto.randomUUID(), 
}; 

setMessages(prev => [...prev, aiMessage]); 
} 
} catch (err) { 
console.error('API error:', err); 
} 
}; 

return ( 
<main> 
<div> {messages.map((m) => ( 
<div key={m.id}> 
<strong>{m.role === 'user' ? '👤 User' : '🤖 AI'}:</strong> 
<ReactMarkdown>{m.content}</ReactMarkdown> 
</div> ))} 
</div> 

<form onSubmit={handleSubmit}> 
<input type="text" 
value={input} 
onChange={e => setInput(e.target.value)} 
placeholder="質問を入力..." 
/> 
<button type="submit">送信</button> 
</form> </main> ); 
}

### `.env.local` 設定

env.localファイルを作って、

AI_API_URL=http://localhost:1234/v1/chat/completions
OPENAI_API_KEY=lm-studio
# - `AI_API_URL`: LM Studio のチャットエンドポイント(デフォルト) 
# - `OPENAI_API_KEY`: 実際には使われないが、ライブラリ上必須なのでダミーを入れる

あとは、

 npm run dev 

でも打って、でてきた

 ▲ Next.js 15.4.6 (Turbopack)
- Local: http://localhost:3000
- Network: http://192.168.21.9:3000
- Environments: .env.local

http://localhost:3000

みたいなのに飛べばいいと思います。
### 参考

Vercel AI SDK を使って Next.js アプリに AI 機能を追加する

## おまけ(## Google APIでの検索を入れたい場合

## Google APIでの検索を入れたい場合
https://programmablesearchengine.google.com/controlpanel/all
からGoogle CSXの番号を拾ってくる。

Google Custom APIを検索して、登録する。
API key
このAPI keyCSXコードを.env.localっていうmy app直下につけたす。
## 📎 参考
- (https://ai-sdk.dev/providers/ai-sdk-providers/openai)[Vercelai/sdkの説明]
- [https://github.com/vercel/ai](https://github.com/vercel/ai)
- [https://lmstudio.ai](https://lmstudio.ai)
# Google Custom Search API
# GOOGLE_API_KEY=あなたのAPIキー
# GOOGLE_CX=あなたの検索エンジンID
GOOGLE_API_KEY=AIUEO_WADDLE_DEE
GOOGLE_CX=WADDLE_DEE

そしたら、src/app/api/chat/route.tsの中身を

import { NextResponse } from 'next/server';

export async function POST(req: Request) {
    const { messages } = await req.json();

    if (!Array.isArray(messages) || messages.length === 0) {
        return new NextResponse('Invalid messages', { status: 400 });
    }

    // 最新のユーザーメッセージを取得
    const lastUserMessage = [...messages].reverse().find(m => m.role === 'user');
    if (!lastUserMessage) {
        return new NextResponse('No user message found', { status: 400 });
    }

    // Google検索API呼び出し
    const searchUrl = `https://www.googleapis.com/customsearch/v1?q=${encodeURIComponent(
        lastUserMessage.content
    )}&key=${process.env.GOOGLE_API_KEY}&cx=${process.env.GOOGLE_CX}`;

    const searchRes = await fetch(searchUrl);
    if (!searchRes.ok) {
        console.error('Google Search API error:', await searchRes.text());
        return new NextResponse('Google Search API error', { status: 500 });
    }

    const searchData = await searchRes.json();
    const searchText =
        searchData.items?.map((item: any) => `${item.title}: ${item.snippet}`).join('\n') ||
        '検索結果なし';

    // LM Studioに渡すmessages配列を構築
    const lmMessages = [
        { role: 'system', content: '以下の検索結果を参考にして質問に答えてください。' },
        ...messages,
        { role: 'user', content: `検索結果:\n${searchText}` },
    ];

    // LM Studio APIへ問い合わせ
    const lmRes = await fetch(process.env.AI_API_URL!, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
            model: 'openai/gpt-oss-20b', // ここはLM Studioで使うモデル名に置き換えてください
            messages: lmMessages,
        }),
    });

    if (!lmRes.ok) {
        console.error('LM Studio API error:', await lmRes.text());
        return new NextResponse('Error contacting LM Studio API', { status: 500 });
    }

    const data = await lmRes.json();
    const message = data.choices?.[0]?.message;

    if (!message) {
        return new NextResponse('Invalid response from LM Studio', { status: 500 });
    }

    return NextResponse.json({
        choices: [{ message }],
    });
}

にしてください。以上です。心配だったら、
src/app/example/page.tsx

'use client';
import React, { useState } from 'react';

export default function Example() {
    const [value, setValue] = useState('');

    const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
        setValue(e.target.value);
    };

    return (
        <div>
            <input type="text" value={value} onChange={handleChange} />
            <p>入力された値: {value}</p>
        </div>
    );
}

src/app/page.tsxを

'use client';

import { useState } from 'react';
import ReactMarkdown from 'react-markdown';

type Message = {
    role: 'user' | 'assistant' | 'system';
    content: string;
    id: string;
};

export default function Chat() {
    const [messages, setMessages] = useState<Message[]>([
        {
            role: 'system',
            content: '僕は、親切なAIアシスタントだよ。検索結果を参考にして回答するよ。',
            id: crypto.randomUUID(),
        },
    ]);
    const [input, setInput] = useState('');

    const handleSubmit = async (e: React.FormEvent) => {
        e.preventDefault();
        if (!input.trim()) return;

        const userMessage: Message = {
            role: 'user',
            content: input,
            id: crypto.randomUUID(),
        };

        // ユーザーメッセージを追加
        const updatedMessages = [...messages, userMessage];
        setMessages(updatedMessages);
        setInput('');

        try {
            const res = await fetch('/api/chat', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ messages: updatedMessages }),
            });

            const data = await res.json();
            const aiContent = data?.choices?.[0]?.message?.content;

            if (aiContent) {
                const aiMessage: Message = {
                    role: 'assistant',
                    content: aiContent,
                    id: crypto.randomUUID(),
                };
                setMessages(prev => [...prev, aiMessage]);
            }
        } catch (err) {
            console.error('API error:', err);
        }
    };

    return (
        <main style={{ maxWidth: 600, margin: 'auto', padding: 20 }}>
            <div style={{ marginBottom: 20 }}>
                {messages.map(m => (
                    <div key={m.id} style={{ marginBottom: 10 }}>
                        <strong>{m.role === 'user' ? '👤 User' : m.role === 'assistant' ? '🤖 AI' : '⚙️ System'}:</strong>
                        <ReactMarkdown>{m.content}</ReactMarkdown>
                    </div>
                ))}
            </div>

            <form onSubmit={handleSubmit}>
                <input
                    type="text"
                    value={input}
                    onChange={e => setInput(e.target.value)}
                    placeholder="質問を入力..."
                    style={{ width: '80%', padding: '8px' }}
                />
                <button type="submit" style={{ padding: '8px 16px', marginLeft: 8 }}>
                    送信
                </button>
            </form>
        </main>
    );
}

src/app/layout.tsxを

export default function RootLayout({
                                       children,
                                   }: Readonly<{
    children: React.ReactNode;
}>) {
    return (
        <html lang="en">
        <body> {/*  class名を削除 */}
        {children}
        </body>
        </html>
    );
}

.env.localのまとめ↓

# LM Studioのローカルサーバーアドレスを指定
# 通常は "http://localhost:1234/v1" です(LM studioの設定画面を見てください)
# OPENAI_API_BASE_URL="http://localhost:1234/v1"
AI_API_URL=http://localhost:3141592/v1/chat/completions


# LM StudioAPIキーを要求しませんが、ライブラリ側で必須の場合があるため
# ダミーの文字列を入れておきます
OPENAI_API_KEY="lm-studio"


# Google Custom Search API
# GOOGLE_API_KEY=あなたのAPIキー
# GOOGLE_CX=あなたの検索エンジンID
GOOGLE_API_KEY=AIUEO_WADDLE_DEE
GOOGLE_CX=WADDLE_DEE

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です