OllamaはローカルでHTTPサーバーとして動くため、JavaScriptからは http://localhost:11434/api/chat へJSONをPOSTするだけで呼び出せます。Node.js標準の fetch だけで完結し、専用のnpmパッケージは必要ありません。ただしこのエンドポイントは既定でストリーミング(逐次受信)応答を返すため、まとめて1つのJSONを受け取りたい場合は "stream": false を明示します。
この記事では、Node.jsからchat APIを呼ぶ最小構成を、非ストリーム版とストリーム版の両方で示します。どちらも実行できる完全なコードで、外部ライブラリには依存しません。あわせて、ストリームで実際に届く行単位JSONの形と、つながらない・遅いときの切り分け手順を扱います。
Ollamaには、Ollama独自の native API(/api/…)と、OpenAI互換エンドポイント(/v1/…)の2系統があります。本記事が扱うのは native API の /api/chat です。インストールとCLIでの実行は「Ollamaの使い方」で解説しているため、ここでは済んでいる前提で進めます。
情報確認日:2026年7月28日(日本時間)
結論:POST /api/chat に投げ、stream の既定値だけ間違えない
この記事の結論
- 接続先は既定で
http://127.0.0.1:11434、chatはPOST /api/chat - chat APIでは
modelとmessagesが必須。会話履歴はmessages配列で渡す streamの既定値はtrue。1回で受け取るならfalseを明示する- ストリームは改行区切りJSON(NDJSON)。1行ずつ
JSON.parseする - 最終行は
done: trueで、所要時間とトークン数の統計が付く - 時間の単位はすべてナノ秒。ミリ秒と読み違えない
Ollama APIの接続先と前提
OllamaはCLIツールであると同時に、常駐するHTTPサーバーでもあります。ollama run でターミナルから会話しているとき、裏で動いているのはこのサーバーです。JavaScriptから使う場合も、追加の設定なしに同じサーバーへリクエストを送ります。
ローカルのエンドポイントを確認する
公式ドキュメントでは、インストール後のAPIは既定で http://localhost:11434/api に提供されると記載されています。サーバーは既定で 127.0.0.1 のポート 11434 にバインドされ、他のマシンから接続させたい場合のみ OLLAMA_HOST 環境変数でバインドアドレスを変更します。
OLLAMA_HOST を 0.0.0.0 に変えただけで、ポート11434をインターネットへ直接公開しないでください。ローカルのAPIには認証が不要です。別端末から使う場合は、信頼できるプライベートネットワークやファイアウォールで接続元を限定し、外部公開が必要なら認証とTLSを備えたリバースプロキシなどを設計してください。
まず、サーバーが応答するかをターミナルで確認します。
curl http://localhost:11434/api/tags
ローカルに取得済みのモデル一覧がJSONで返れば、APIは動いています。ここで接続が拒否される場合は、コードを書く前にサービスの起動から解決してください。
native APIとOpenAI互換エンドポイントの違いは次のとおりです。
| 項目 | native API | OpenAI互換 |
|---|---|---|
| パス | POST /api/chat |
POST /v1/chat/completions |
| 向いている場面 | Ollama固有の項目をそのまま使う | 既存のOpenAI向けコードを流用する |
| APIキー | 不要 | 形式上必要(公式例では ollama を指定し、値は無視される) |
| 本記事 | 扱う | 扱わない |
すでにOpenAI向けのコードがあり、接続先を差し替えるだけで済ませたい場合は /v1/ を選ぶ判断もあります。クラウドAPI側の書き方は「OpenAI APIをJavaScriptで使う方法」を参照してください。新規にOllama前提で書くなら、統計値や keep_alive などをそのまま扱えるnative APIが素直です。
CLIでモデル取得済みか確認する
APIはモデルを自動では取得しません。リクエストで指定した名前のモデルがローカルに無ければエラーになります。GET /api/tags は、ローカルで利用できるモデルの一覧を返します。
// list-models.mjs
const BASE_URL = process.env.OLLAMA_BASE_URL ?? "http://127.0.0.1:11434";
const res = await fetch(`${BASE_URL}/api/tags`);
const data = await res.json();
for (const m of data.models) {
console.log(m.name, m.size);
}
この記事のモデル名はすべて例示です。llama3.2 と書いてある箇所は、あなたが導入済みの任意のモデルで置き換え可能です。/api/tags が返す name の値をそのままコピーするのが確実で、タグを省略すると :latest の扱いで食い違うことがあります。
なお、環境変数名に OLLAMA_HOST をそのまま流用しないでください。Ollama本体の OLLAMA_HOST は 0.0.0.0:11434 のようなバインドアドレス形式で、URLとして連結すると壊れます。アプリ側の接続先はこの記事のように別名(例:OLLAMA_BASE_URL)で持つほうが安全です。
JavaScriptから最小リクエストを送る
ここからが本題です。Node.js 18以降には fetch が標準搭載されているため、依存パッケージなしで書けます。
fetch でchat APIを呼ぶ
まず非ストリーム版です。応答が生成し終わるまで待ち、1つのJSONとして受け取ります。
// chat-once.mjs
// 実行: node chat-once.mjs
const BASE_URL = process.env.OLLAMA_BASE_URL ?? "http://127.0.0.1:11434";
const MODEL = process.env.OLLAMA_MODEL ?? "llama3.2";
async function chatOnce(messages) {
const res = await fetch(`${BASE_URL}/api/chat`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
model: MODEL,
messages,
stream: false
})
});
if (!res.ok) {
// エラー時は { "error": "..." } 形式のJSONが返る
throw new Error(`Ollama ${res.status}: ${await res.text()}`);
}
return res.json();
}
const result = await chatOnce([
{ role: "user", content: "空が青い理由を3行で説明してください。" }
]);
console.log(result.message.content);
ポイントは "stream": false です。この1行を省いたまま応答全体へ res.json() を使うと、複数のJSONオブジェクトを1つのJSONとして解析しようとして失敗します。既定のストリーミング応答は「JSONの配列」ではなく、JSONオブジェクトが改行で区切られたNDJSONだからです。
応答本文と終了理由を読む
返ってくるJSONは、本文と実行統計が同じ階層に並んだ形です。本文は message.content にあります。
console.log("本文:", result.message.content);
console.log("終了理由:", result.done_reason);
console.log("入力トークン:", result.prompt_eval_count);
console.log("生成トークン:", result.eval_count);
console.log("生成時間(ミリ秒):", result.eval_duration / 1e6);
主なフィールドの意味は次のとおりです。
| フィールド | 単位・型 | 意味 |
|---|---|---|
message.role |
文字列 | 応答側は assistant |
message.content |
文字列 | 本文。ストリームでは断片が入る |
done |
真偽値 | 最終応答で true |
done_reason |
文字列 | 終了理由。例:stop / load / unload |
total_duration |
ナノ秒 | リクエスト全体の所要時間 |
load_duration |
ナノ秒 | モデル読み込みに要した時間 |
prompt_eval_count |
整数 | 入力側のトークン数 |
eval_count |
整数 | 生成したトークン数 |
eval_duration |
ナノ秒 | 生成に要した時間 |
公式ドキュメントは、所要時間の値がナノ秒で返ると説明しています。秒に直すなら 1e9 で、ミリ秒なら 1e6 で割ります。load_duration の比率が大きい場合は、生成だけでなくモデルの読み込みに時間がかかっています。初回だけ遅いときは、load_duration と eval_duration を分けて確認します。
system・履歴・生成設定を渡す
chat APIは会話履歴を保存しません。前のやり取りを踏まえた応答が欲しければ、毎回 messages に履歴を全部積んで送ります。
messagesを組み立てる
role に指定できるのは system、user、assistant、tool の4つです。system は振る舞いの指示、user は利用者の発言、assistant はモデルの過去の応答を表します。
// chat-history.mjs
const BASE_URL = process.env.OLLAMA_BASE_URL ?? "http://127.0.0.1:11434";
const MODEL = process.env.OLLAMA_MODEL ?? "llama3.2";
const history = [
{ role: "system", content: "あなたは簡潔に答える技術アシスタントです。" }
];
async function ask(text) {
history.push({ role: "user", content: text });
const res = await fetch(`${BASE_URL}/api/chat`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ model: MODEL, messages: history, stream: false })
});
if (!res.ok) throw new Error(`Ollama ${res.status}: ${await res.text()}`);
const data = await res.json();
// 次の質問で文脈が続くよう、応答も履歴に戻す
history.push(data.message);
return data.message.content;
}
console.log(await ask("Node.jsのfetchはどのバージョンから標準ですか。"));
console.log(await ask("それ以前のバージョンではどうしますか。"));
2回目の質問が「それ以前」だけで通じるのは、履歴を送り直しているからです。逆に言えば、履歴を積み続けると入力トークンが増え、応答は遅くなります。長い会話では古いやり取りを要約に置き換えるなど、上限を決めて運用します。
設定値をコードから分離する
生成の細かい調整は options に入れます。ここには temperature のようなモデル側のパラメータを渡せます。あわせて覚えておきたいのが keep_alive と format です。
| 項目 | 役割 | 備考 |
|---|---|---|
options |
生成パラメータをまとめて指定 | temperature など |
keep_alive |
リクエスト後にモデルをメモリへ保持する時間 | 既定は 5m |
format |
応答形式の指定 | json またはJSONスキーマ |
think |
思考モデルで思考を有効にするか | 真偽値、または low / medium / high / max |
これらをリクエスト生成のコードに直接書くと、調整のたびにロジックへ触れることになります。設定は1か所へ寄せ、環境変数で上書きできる形にしておくと運用が楽です。
// config.mjs
export const config = {
baseUrl: process.env.OLLAMA_BASE_URL ?? "http://127.0.0.1:11434",
model: process.env.OLLAMA_MODEL ?? "llama3.2",
keepAlive: process.env.OLLAMA_KEEP_ALIVE ?? "5m",
options: {
temperature: Number(process.env.OLLAMA_TEMPERATURE ?? 0.7)
}
};
export function buildBody(messages, { stream }) {
return JSON.stringify({
model: config.model,
messages,
stream,
keep_alive: config.keepAlive,
options: config.options
});
}
keep_alive を長くするとモデルが常駐して2回目以降が速くなり、短くすればメモリが早く空きます。開発機で複数のモデルを行き来するなら短め、単一モデルを叩き続けるなら長めが目安です。
ストリーミング応答を処理する
体感速度を上げるなら、生成の途中経過を順次表示します。ここがnative APIで最も間違えやすい部分です。
行単位のJSONを順番に読む
公式ドキュメントによれば、ストリーミング応答は改行区切りJSON(NDJSON、コンテンツタイプは application/x-ndjson)で返されます。Server-Sent Events(サーバー送信イベント)ではないため、data: の接頭辞はありません。1行が1つの独立したJSONです。
実際に届く内容は次のような形です。値や断片の切れ方は、実行やモデルによって変わります。
{"model":"llama3.2","created_at":"2026-07-28T02:14:07.412183Z","message":{"role":"assistant","content":"空"},"done":false}
{"model":"llama3.2","created_at":"2026-07-28T02:14:07.438902Z","message":{"role":"assistant","content":"が"},"done":false}
{"model":"llama3.2","created_at":"2026-07-28T02:14:07.465120Z","message":{"role":"assistant","content":"青"},"done":false}
{"model":"llama3.2","created_at":"2026-07-28T02:14:07.491744Z","message":{"role":"assistant","content":"く"},"done":false}
{"model":"llama3.2","created_at":"2026-07-28T02:14:07.518233Z","message":{"role":"assistant","content":"見える"},"done":false}
(中略)
{"model":"llama3.2","created_at":"2026-07-28T02:14:09.902551Z","message":{"role":"assistant","content":""},"done":true,"done_reason":"stop","total_duration":2691042917,"load_duration":31204583,"prompt_eval_count":29,"prompt_eval_duration":248113000,"eval_count":118,"eval_duration":2405219000}
読み取りのポイントは3つです。第一に、本文は message.content の断片を連結します。第二に、done: true のイベントに統計値が付きます。最後の content が空でも、そこだけを異常と判断しません。第三に、ネットワークから届くチャンクの区切りとNDJSONの行区切りは一致しません。1回の受信に複数行が入ることも、行の途中で切れることもあるため、バッファを持って改行で分割し、ストリーム終了時に残りも処理します。
// chat-stream.mjs
// 実行: node chat-stream.mjs
const BASE_URL = process.env.OLLAMA_BASE_URL ?? "http://127.0.0.1:11434";
const MODEL = process.env.OLLAMA_MODEL ?? "llama3.2";
async function chatStream(messages, onDelta, { signal, onEvent = () => {} } = {}) {
const res = await fetch(`${BASE_URL}/api/chat`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ model: MODEL, messages, stream: true }),
signal
});
if (!res.ok) {
throw new Error(`Ollama ${res.status}: ${await res.text()}`);
}
if (!res.body) throw new Error("Ollama response has no body");
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
let content = "";
let final = null;
const handleLine = (line) => {
if (line === "") return;
const event = JSON.parse(line);
onEvent(event);
// 途中で失敗した場合、statusは200のままエラー行が流れてくる
if (event.error) throw new Error(`stream error: ${event.error}`);
const delta = event.message?.content ?? "";
if (delta) {
content += delta;
onDelta(delta);
}
if (event.done) final = event;
};
while (true) {
const { value, done } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
let nl;
while ((nl = buffer.indexOf("\n")) >= 0) {
const line = buffer.slice(0, nl).trim();
buffer = buffer.slice(nl + 1);
handleLine(line);
}
}
// TextDecoderと改行なしで残った最終行を処理する
buffer += decoder.decode();
handleLine(buffer.trim());
if (!final) throw new Error("Ollama stream ended before done: true");
return { content, final };
}
const { content, final } = await chatStream(
[{ role: "user", content: "空が青い理由を3行で説明してください。" }],
(delta) => process.stdout.write(delta)
);
console.log("\n---");
console.log("終了理由:", final?.done_reason);
console.log("生成トークン:", final?.eval_count);
event.error の判定を入れているのには理由があります。公式ドキュメントは、ストリームの途中でエラーが起きた場合、応答はすでに始まっているためHTTPステータスは変わらず、error プロパティを持つ行がNDJSONとして流れると説明しています。res.ok のチェックだけでは、途中で壊れた応答を成功として扱ってしまいます。
中断・タイムアウトを扱う
ローカルLLMの応答時間は、モデルとマシンによって大きく変わります。利用者が画面を閉じたときなどにクライアント側のリクエストを中断できるよう、AbortController(中断コントローラー)で signal を渡せる形にしてあります。
ここで注意したいのが、タイムアウトの取り方です。AbortSignal.timeout(60_000) のような全体時間の上限は、正常に長く生成しているだけの応答まで打ち切ります。実用的には「一定時間、新しい行が届かなければ異常」とみなす無通信タイムアウトのほうが合います。
// idle-timeout.mjs
export function createIdleTimeout(ms) {
const controller = new AbortController();
let timer;
const bump = () => {
clearTimeout(timer);
timer = setTimeout(() => controller.abort(), ms);
};
bump();
return { signal: controller.signal, bump, clear: () => clearTimeout(timer) };
}
// 使い方: 1行受け取るたびに bump() を呼ぶ
const idle = createIdleTimeout(20_000);
try {
await chatStream(
messages,
(delta) => process.stdout.write(delta),
{ signal: idle.signal, onEvent: idle.bump }
);
} catch (err) {
if (err.name === "AbortError") console.error("応答が途絶えました");
else throw err;
} finally {
idle.clear();
}
中断すると fetch は AbortError を投げます。これは異常ではなく想定内の経路なので、通常のエラーと分けて扱ってください。finally でタイマーを解放しないと、処理が終わってもプロセスが終了しない点にも注意します。
接続できない・応答が遅い時の対処
症状を「つながらない」とひとまとめにせず、どの層で止まっているかを順に確かめます。
サービス・ポート・モデル名を切り分ける
Ollamaは失敗時、{"error": "..."} の形でJSONを返します。ステータスコードと本文を両方ログに出しておくと、どの層で失敗したかを切り分けやすくなります。
| 症状 | 確認すること | 対処 |
|---|---|---|
| fetch failed / 接続拒否 | サーバーが起動しているか | curl http://localhost:11434/api/tags で応答を確認する |
| 400 Bad Request | リクエストJSONの形 | model の指定漏れ、messages の構造、Content-Type を見直す |
| 404 Not Found | モデル名またはパス | /api/tags の name と一致させる。未取得なら ollama pull |
| 429 Too Many Requests | 利用中のエンドポイントでレート制限が発生していないか | エラー本文を確認し、再試行間隔と頻度を調整する |
| 500 Internal Server Error | サーバー側の処理 | Ollamaのログと空きメモリを確認する |
| 503 Service Unavailable | ローカルサーバーのキュー上限 | 同時実行数を下げる。必要なら OLLAMA_MAX_QUEUE の設計を見直す |
| 応答は返るが本文が空 | stream の値と読み取り方 |
非ストリームなら message.content、ストリームなら断片の連結を確認する |
別のマシンで動くOllamaへ接続する場合は、サーバー側が既定で 127.0.0.1 にバインドされている点を確認します。外部から届かせるにはサーバー側の設定も必要ですが、OLLAMA_HOST を変更する前に、接続元の制限・認証・暗号化を含む公開範囲を決めてください。クライアント側のURLだけを書き換えても接続できません。
同時実行数とメモリ不足を確認する
「動くけれど遅い」場合は、時間の内訳を先に見ます。load_duration が大きければモデルの読み込み待ち、eval_duration が大きければ生成そのものです。読み込みが毎回発生しているなら、keep_alive が短すぎるか、リクエストのたびに別のモデルを指定している可能性があります。
メモリへ現在載っているモデルは GET /api/ps で確認できます。
curl http://localhost:11434/api/ps
返るJSONには、モデル名、サイズ、VRAM上のサイズ、メモリから降ろされる予定時刻(expires_at)が含まれます。想定より多くのモデルが載っていれば、それだけメモリを圧迫しています。
並列数は、メモリに合わせて測定しながら決めます。Ollamaは同一モデルの並列処理に対応しますが、並列数に応じてコンテキスト用メモリも増えます。メモリが足りなければリクエストはキューへ回り、上限を超えると拒否されます。まず直列で所要時間とメモリ使用量を測り、その後に少しずつ増やしてください。
よくある失敗
stream の既定値を false だと思い込む
クラウドAPIの多くは非ストリームが既定ですが、Ollamaのchatは既定でストリーミングします。stream を書かずに res.json() を呼ぶと解析に失敗し、原因がAPI側にあるように見えてしまいます。1回で受け取るなら必ず明示します。
受信の区切りを行の区切りと同一視する
reader.read() が返すデータが、ちょうど1行に対応する保証はありません。複数行がまとまって届くことも、日本語の途中で切れることもあります。バッファに貯めて改行で分割し、TextDecoder には { stream: true } を渡してください。これを省くと、マルチバイト文字が壊れます。
統計値の単位をミリ秒と読む
total_duration が 2691042917 と出たとき、これは約2.7秒であって約2691秒でも約31日でもありません。ナノ秒です。ログやダッシュボードへ出すときは、記録の時点で秒かミリ秒へ変換しておくと誤読を防げます。
よくある質問
npmのollamaパッケージは必要ですか?
必須ではありません。公式にはJavaScript向けライブラリが提供されていますが、この記事のとおり標準の fetch だけで /api/chat を呼べます。依存を増やしたくない場合や、リクエスト内容を完全に把握したい場合は、素のfetchで十分です。
ブラウザのJavaScriptから直接呼べますか?
公式ドキュメントでは、Ollamaは既定で 127.0.0.1 と 0.0.0.0 からのクロスオリジンリクエストを許可し、追加のオリジンは OLLAMA_ORIGINS で設定すると説明されています。ただし、ブラウザから直接呼ぶ構成では、利用者の端末でOllamaが動いていることや、許可オリジンの管理が必要です。一般公開するWebアプリでは、認証・入力検証・接続先制限を備えた自分のバックエンドを経由させる設計を検討してください。
OpenAI互換の /v1 とどちらを使うべきですか?
すでにOpenAI向けのコードがあり、接続先の差し替えだけで動かしたいなら /v1/chat/completions が近道です。新規に書くなら、keep_alive や各種統計値をそのまま扱えるnative APIを勧めます。両者は同じサーバーが提供しているので、後から切り替えることもできます。
モデルはリクエスト時に自動で取得されますか?
されません。未取得のモデル名を指定するとエラーになるため、事前に ollama pull で取得しておきます。アプリ起動時に /api/tags を確認し、必要なモデルが無ければ分かりやすいメッセージを出す作りにしておくと、利用者が原因にたどり着きやすくなります。
まとめ
OllamaのnativeなchatAPIは、POST /api/chat にJSONを送るだけの素直な設計です。JavaScriptから使うために特別なライブラリは要らず、Node.js標準の fetch で非ストリームとストリームの両方を実装できます。
つまずきどころは仕様そのものではなく、既定値と単位に集中しています。stream の既定は true、ストリームは改行区切りJSON、所要時間はナノ秒、途中のエラーはステータス200のまま本文で届く。この4点を押さえておけば、残りは通常のHTTPクライアントの実装と変わりません。まずは非ストリーム版を動かして応答の形を確認し、体感速度が必要になった段階でストリーム版へ移行するのが安全な順序です。
公式情報
本記事の仕様は、以下の公式ドキュメントを2026年7月28日(日本時間)に確認した内容に基づいています。OllamaのAPIは厳密にはバージョン管理されていないため、実装前に最新版を確認してください。