電子趣味の部屋

電子系のガジェットやアプリ開発等の趣味の話題を書いてます

SSH接続許可設定 公開鍵認証+特定のIPアドレスからのアクセスはパスワード認証を許可する方法

何回も使用する設定ですが、設定の度に調べてしまうので、メモも兼ねてここに書きます。

SSH接続許可設定でログインの認証方式を公開鍵認証(パスワード認証は無効)にして、特定のIPアドレスからのアクセスはパスワード認証を許可する方法です。

/etc/ssh/sshd_config に書く内容

PasswordAuthentication no
Match Address xxx.xxx.xxx.xxx,xxx.xxx.xxx.xxx  # 許可するIPアドレス(カンマで複数指定可能)
PasswordAuthentication yes

少し解説

この設定は、「原則としてパスワード認証によるSSHログインを禁止するが、指定した特定のIPアドレスからのみ例外的にパスワード認証を許可する」という意味になります。

具体的には以下の3ステップで動作しています。

  1. PasswordAuthentication noすべての接続に対して、いったんパスワード認証を不許可(禁止)にします。
  2. Match Address xxx.xxx.xxx.xxx,xxx.xxx.xxx.xxx これ以降の設定を適用する特定の接続元IPアドレスを指定します。
  3. PasswordAuthentication yes上の「Match」で指定したIPアドレスからの接続に限り、設定を上書きしてパスワード認証を許可します。

⚠️ 注意点OpenSSHの仕様上、Match 構文の効力はファイルの末尾(または次の Match 構文が現れるまで)まで続きます。
そのため、この設定は必ず sshd_config ファイルの一番最後(末尾)に記述する必要があります。

DOS/V機のこと (WindowsXP以前辺りのこと)

自分が所有した過去のレトロPCをいろいろ書いてきましたが、今回はDOS/V機(PC-AT互換機)のことを書きたいと思います。

初めてDOS/V機を購入したのは、USBの搭載が始まったWindows95 OSR2の頃でした。
今までPC-9801BXをメモリ最大(14.6MB)とCバスのグラフィックボードを使用して無理やり動かしてましたが、さすがにきつくなってきてPCを買い替えることにしました。
当時はまだPC-9821の選択肢があったのですが、価格面で選択しませんでした。
まだ大学生の身でバイトをして多少の余裕はありましたが、安くすませるために自作することにしました。
現在では有名メーカー製でも安いものもありますが、当時はまだまだメーカー製と自作の価格差がありました。

初めて自作したPCはマザーボードはASUSTeKのSocket7のもの、CPUはPentium(クロック数は忘れた)、メモリ32MB、GPUはS3 ViRGEの構成でした。
あと、当時はサウンドはまだ別に必要でSoundBlasterの何かにしました。後に拡張端子にYAMAHAのXG音源DBX50Gを取り付けました。
その後CPUはMMXの登場でどうしてもこの機能が欲しくなり、Intelは高価でしたので、Cyrix 6x86に交換しました、その後はAMD K6-II,K6-III,Athlonとこの頃からAMD派になりました。

GPUもその後、S3 ViRGE/DX、nVidia RIVA TNTに交換しました。

その後、WindowsXPの時代には社会人になって金銭的にも余裕が出て毎年のようにCPUとGPUは変えてました。
RIVA TNTが初めてのnVidiaで、その後もnvidiaを選択することが多かったのですが、RADEONの方もまだATIの頃にビデオキャプチャとテレビチューナーの付いたATI ALL-IN-WONDER RADEONにしたこともあります。
性能的にはミドルスペックで、最新モデルではなく1世代前の現行品モデルを選び、交換前のパーツは売却してましたが、それでも結構な金額を使ってたと思います。
CPUは価格に対する性能の点で基本的にAMDを選択してました。AMDの性能が低迷している時期はIntelを選択してしまいましたが、Ryzen以降はまたAMDに戻りました。

一度自作に手を出すとなかなか抜け出せなかったのですが、パーツを追うのに疲れたのと設置スペースの問題から5年ほど前に辞めてしまいました。
blog-e.uosoft.net

最近はメモリやSSD以外のパーツを買い替えることはないのですが、ミニPCやゲーミングノートにして場所や消費電力やファンの音が軽減され、気持ち的にもスッキリしてます。

ChatGPT互換API(OpenAI互換API)の解説

先日公開したVSCodeプラグイン「LLM Code」は、LM StudioとOllamaで動作確認をしていますが、ChatGPT互換のAPIを使っているので、他の環境でも動くと思いますと書きました。
blog-e.uosoft.net


この「ChatGPT互換」「OpenAI互換API」という言葉は、ローカルLLMを触っているとよく目にします。
今回は、OpenAI互換APIとは何なのか、なぜ色々なツールで同じように使えるのかをまとめてみます。

OpenAI互換APIとは


OpenAI互換APIとは、OpenAIがChatGPTのモデルを提供するために用意したAPIと''同じ形式でリクエストを受け付けて、同じ形式でレスポンスを返すAPI''のことです。

OpenAIのAPIは早い時期から広く使われていたため、多くのアプリやライブラリがこの形式に対応しました。
そこで、後から登場したLLMの実行環境やサービスも、自前の独自形式ではなくOpenAIと同じ形式のAPIを用意するようになりました。
そうすれば、既存のアプリやライブラリをそのまま使ってもらえるからです。

言ってみれば、LLMの世界の「共通コンセント」のようなものです。
コンセントの形が同じなら、どのメーカーの家電でも挿せるのと同じで、APIの形式が同じなら、接続先を変えるだけで同じアプリが使えます。

対応している主な環境


ローカルでLLMを動かす環境の多くが、OpenAI互換APIのサーバ機能を持っています。

環境 デフォルトの接続先(ベースURL)
LM Studio http://localhost:1234/v1
Ollama http://localhost:11434/v1
llama.cpp(llama-server) http://localhost:8080/v1
vLLM http://localhost:8000/v1


ポート番号は設定で変更できます。
また、クラウドのLLMサービスでも、OpenAI互換のエンドポイントを用意しているところが多くあります。

基本のしくみ


OpenAI互換APIでよく使われるのは、次の2つのエンドポイントです。

エンドポイント 用途
GET /v1/models 使用できるモデルの一覧を取得
POST /v1/chat/completions チャット形式で文章を生成


中心になるのは「/v1/chat/completions」で、LLMとの会話はほぼこれ1つで行います。

リクエストの形式


リクエストはJSONで送ります。
最低限必要なのは、使用するモデル名(model)と会話の内容(messages)です。

{
  "model": "モデル名",
  "messages": [
    { "role": "system", "content": "あなたはMSXに詳しいアシスタントです。" },
    { "role": "user", "content": "MSX2の画面モードを教えてください。" }
  ],
  "temperature": 0.7
}


messagesは会話の履歴を配列で並べたもので、roleで誰の発言かを指定します。

role 意味
system AIへの前提条件や役割の指示
user ユーザーの発言
assistant AIの過去の返答


ここで大事なのは、''APIは会話を覚えていない''ということです。
2回目以降の質問では、それまでのuserとassistantのやり取りを全部messagesに入れて送り直します。
チャットアプリが会話の流れを覚えているように見えるのは、アプリ側が毎回履歴をまとめて送っているからです。

例えば、1回目に「MSX2の画面モードを教えてください」と聞き、AIが答えた後に「その中でスプライトが使えるのはどれ?」と続けて質問する場合は、次のように送ります。

{
  "model": "モデル名",
  "messages": [
    { "role": "system", "content": "あなたはMSXに詳しいアシスタントです。" },
    { "role": "user", "content": "MSX2の画面モードを教えてください。" },
    { "role": "assistant", "content": "MSX2ではSCREEN 0〜8が使えます。SCREEN 0は..." },
    { "role": "user", "content": "その中でスプライトが使えるのはどれ?" }
  ]
}


1回目の質問、1回目のAIの返答(assistant)、2回目の質問を順番に並べています。
「その中で」が何を指しているのか、AIは送られてきた履歴を読んで判断しています。
もしassistantの行を入れずに2回目の質問だけを送ると、AIは何の話かわからなくなります。

3回目、4回目と続く場合も同じで、それまでのやり取りの後ろに新しい質問を追加していきます。
例えば、2回目の返答の後に「SCREEN 5でスプライトを表示するサンプルを書いて」と頼み、さらに「スプライトをカーソルキーで動かせるようにして」と続ける場合、4回目の質問では次のようになります。

{
  "model": "モデル名",
  "messages": [
    { "role": "system", "content": "あなたはMSXに詳しいアシスタントです。" },
    { "role": "user", "content": "MSX2の画面モードを教えてください。" },
    { "role": "assistant", "content": "MSX2ではSCREEN 0〜8が使えます。SCREEN 0は..." },
    { "role": "user", "content": "その中でスプライトが使えるのはどれ?" },
    { "role": "assistant", "content": "スプライトが使えるのはSCREEN 1〜8です。..." },
    { "role": "user", "content": "SCREEN 5でスプライトを表示するサンプルを書いて。" },
    { "role": "assistant", "content": "10 SCREEN 5,2\n20 SPRITE$(0)=...\n..." },
    { "role": "user", "content": "スプライトをカーソルキーで動かせるようにして。" }
  ]
}


userとassistantが交互に並び、最後が今回の質問になっています。
4回目の「スプライトを」がどのプログラムのことなのか、AIは3回目の返答に入っているサンプルプログラムを読んで判断します。
systemの指示は先頭に1回書いておけば、会話が続いても毎回同じものを送り続けます。

このように、やり取りが増えるごとにmessagesがどんどん長くなっていきます。
そのため、会話が長くなるほど送るデータ量と処理時間が増え、モデルが扱える長さ(コンテキスト長)を超えるとエラーになったり、古い部分を忘れたりします。
チャットアプリの中には、古い履歴を削ったり要約したりして送る工夫をしているものもあります。

temperatureは返答のランダムさで、低いほど毎回同じような答えに、高いほどバリエーションのある答えになります。
プログラムを生成させる場合は低めにすることが多いです。

レスポンスの形式


レスポンスもJSONで返ってきます。

{
  "id": "chatcmpl-xxxx",
  "object": "chat.completion",
  "model": "モデル名",
  "choices": [
    {
      "index": 0,
      "message": { "role": "assistant", "content": "MSX2の画面モードは..." },
      "finish_reason": "stop"
    }
  ],
  "usage": { "prompt_tokens": 35, "completion_tokens": 120, "total_tokens": 155 }
}


AIの返答は「choices[0].message.content」に入っています。
usageには使用したトークン数が入っていて、クラウドサービスではこれが料金の計算に使われます。

ストリーミング


ChatGPTのように文字が少しずつ表示されるのは、ストリーミングという方式を使っているからです。
リクエストに「"stream": true」を付けると、返答が一度にではなく、少しずつ送られてきます。

{
  "model": "モデル名",
  "messages": [
    { "role": "user", "content": "MSX2の画面モードを教えてください。" }
  ],
  "stream": true
}


すると、次のようなデータが順番に届きます。

data: {"choices":[{"delta":{"content":"MSX"}}]}
data: {"choices":[{"delta":{"content":"2の"}}]}
data: {"choices":[{"delta":{"content":"画面"}}]}
data: [DONE]


これはServer-Sent Events(SSE)という形式で、「data: 」で始まる行ごとに続きの文字がdeltaとして届きます。
最後に「data: [DONE]」が来たら終了です。
ローカルLLMは生成に時間がかかることが多いので、ストリーミングで少しずつ表示すると体感的な待ち時間がかなり短くなります。

実際に使ってみる

curlで試す


LM Studioでサーバを起動した状態なら、次のコマンドで試せます。

curl http://localhost:1234/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{
    "model": "モデル名",
    "messages": [{ "role": "user", "content": "こんにちは" }]
  }'


Ollamaの場合は、URLを「http://localhost:11434/v1/chat/completions」に変えるだけです。

Pythonで使う


OpenAIの公式Pythonライブラリも、接続先を変えればそのまま使えます。

from openai import OpenAI
 
client = OpenAI(
    base_url="http://localhost:1234/v1",
    api_key="dummy"  # ローカルではキーは使われないが、空だとエラーになるため適当な文字列を入れる
)
 
response = client.chat.completions.create(
    model="モデル名",
    messages=[{"role": "user", "content": "こんにちは"}]
)
 
print(response.choices[0].message.content)


base_urlを書き換えるだけで、OpenAI、LM Studio、Ollamaなどを切り替えられます。
これがOpenAI互換APIの一番のメリットです。

JavaScriptで使う


ライブラリを使わず、fetchだけでも呼び出せます。

const res = await fetch("http://localhost:1234/v1/chat/completions", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    model: "モデル名",
    messages: [{ role: "user", content: "こんにちは" }]
  })
});
 
const data = await res.json();
console.log(data.choices[0].message.content);

ストリーミングで受け取る


"stream": trueを使う場合の例です。

curlでは、-Nオプションを付けると届いた分からすぐに表示されます。

curl -N http://localhost:1234/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{
    "model": "モデル名",
    "messages": [{ "role": "user", "content": "こんにちは" }],
    "stream": true
  }'


実行すると「data: {...}」の行が次々に表示され、最後に「data: [DONE]」が出て終わります。

Pythonのopenaiライブラリでは、stream=Trueを付けると、届いた分をforループで順番に受け取れます。

from openai import OpenAI
 
client = OpenAI(base_url="http://localhost:1234/v1", api_key="dummy")
 
stream = client.chat.completions.create(
    model="モデル名",
    messages=[{"role": "user", "content": "こんにちは"}],
    stream=True
)
 
for chunk in stream:
    text = chunk.choices[0].delta.content
    if text:
        print(text, end="", flush=True)
print()


JavaScriptのfetchでは、レスポンスを少しずつ読み込み、「data: 」の行を自分で分解します。

const res = await fetch("http://localhost:1234/v1/chat/completions", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    model: "モデル名",
    messages: [{ role: "user", content: "こんにちは" }],
    stream: true
  })
});
 
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
let answer = "";
 
while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  buffer += decoder.decode(value, { stream: true });
 
  // 改行ごとに分解し、最後の途中の行はバッファに残す
  const lines = buffer.split("\n");
  buffer = lines.pop();
 
  for (const line of lines) {
    if (!line.startsWith("data: ")) continue;
    const data = line.slice(6).trim();
    if (data === "[DONE]") continue;
    const json = JSON.parse(data);
    const text = json.choices[0]?.delta?.content;
    if (text) {
      answer += text;
      console.log(text); // 画面に追記する処理に置き換える
    }
  }
}


データは行の途中で区切られて届くことがあるので、改行までをバッファにためてから処理しているのがポイントです。
ストリーミングの場合、2回目以降の質問に備えて、受け取った文字をつなげた全文(ここではanswer)をassistantの発言として履歴に保存しておきます。

注意点:完全に同じではない


「互換」とはいえ、OpenAIのAPIと完全に同じ動作をするとは限りません。

  • 対応していないパラメータは無視されたり、エラーになったりする
  • ツール呼び出し(Function Calling)や画像入力は、環境やモデルによって対応状況が違う
  • モデル名は環境ごとに違うので、/v1/modelsで確認して指定する必要がある
  • 扱える文章の長さ(コンテキスト長)はモデルやサーバの設定で決まる
  • エラー時のレスポンスの形式が環境ごとに少しずつ違う


また、OpenAI自身は現在、Chat Completions以外に新しい形式のAPIも提供しています。
ただ、一般的に「OpenAI互換」と言った場合は、今回紹介した/v1/chat/completionsの形式を指していることがほとんどです。

アプリを作る側としては、基本的なチャット機能だけを使うようにしておくと、多くの環境で動かしやすくなります。

まとめ

  • OpenAI互換APIは、OpenAIのAPIと同じ形式で使えるAPIのこと
  • LM Studio、Ollama、llama.cppなど、多くのローカルLLM環境が対応している
  • 中心になるのは/v1/chat/completionsで、modelとmessagesを送ると返答が返ってくる
  • APIは会話を覚えていないので、履歴は毎回まとめて送る
  • 接続先のURLを変えるだけで、同じプログラムを色々な環境で使える
  • ただし、細かい機能の対応状況は環境によって違う


この共通形式のおかげで、LLM Codeのようなツールも、特定の環境に縛られずに作ることができます。
ローカルLLMを使ったツールを作ってみたい方は、まずcurlで試してみると、しくみがよくわかると思います。

ローカル環境のLLMサーバに接続できるVSCodeのプラグイン LLM Code

CODEXやClaudeCodeと同じ使用感でローカル環境のLLMサーバに接続できるVSCodeのプラグイン LLM Code を作りました
以下のページからダウンロードできます。
LLM Code – UO Soft

ローカル環境のLLMサーバに接続できるVSCodeのプラグインはいくつかあるのですが、いまいち操作がシンプルで手軽に扱えるものが無いと感じました。
そこでバイブコーディングを目的としたシンプルなものでほしいと思い、自分で作ってみました。
チャットしながら作っていく以外の機能はありませんが、使えるものができたので公開します。

LM StudioとOllamaでしか試してませんが、ChatGPT互換のAPIを使用してるので対応する環境は多いと思います。

LLM Code – UO Soft

ClockworkPi PicoCalc用MSXライクBASICインタプリタを作りました

ClockworkPi PicoCalcには、公式のBASICインタプリタとしてPicoMiteが用意されていますが、昔ながらの行番号付きBASICの方が、ポケコンの雰囲気を味わえて面白いのではないかと思いました。

行番号付きBASICといえば、使い慣れたMSX BASICと思いました。そこで、文法やコマンドをできるだけMSX BASICに合わせる方針で、AIの力も借りながらBASICインタプリタを作ってみました。

PicoCalc BASIC – UO Soft

ダウンロードはこちらからできます。

https://www.uosoft.net/files/picocalc-basic.zip

開発を進めるうちにRaspberry Pi Picoではメモリが足りなくなってしまったため、Pico 2専用となりました。
無線機能は使用しませんが、Pico 2 Wでも動作します。

ZIPファイルを解凍すると、distフォルダ内に「picocalc_basic.uf2」がありますので、これをRaspberry Pi Pico 2に書き込んでください。

また、docsフォルダにはMarkdown形式のドキュメントが収録されており、リファレンスマニュアルもこちらにあります。

[j:plain]

MSX実機と同じようなスクリーンエディタを搭載しています。

ファイルの保存先はSDカードです。

MSX2相当の画面モードに対応しており、グラフィック命令やスプライト機能も使用できます。

完全互換ではありませんが、VRAMをシミュレートしているため、VPOKEやVPEEKも使用できます。
文字パターンの変更にも対応しています。

サウンド機能については、PSGに加えてMSX-MUSIC(YM2413)もシミュレートしており、PLAYやCALL MUSICなどのサウンド関連命令も使用できます。

さらに、MSX実機にはない便利な機能として「LOADER」コマンドを用意しました。これにより、ファイルを簡単に選択して実行できます。

MSX BASICとの完全互換を目指したものではありませんが、MSX BASICに近い感覚で使えるBASICインタプリタ環境になったと思います。

ChatGPT GPT-6 AstraでダンジョンRPGを作ってみました

どうもGPT-6 Astraがすごいと聞いて、試しにWebで動作するダンジョンRPGを作ってみました。
作成方法はVisual Studio Codeで任意のフォルダを開いて、プラグインCodexを使用して作成しました。

プロンプトは特に凝ってるわけではなく、簡単なものにしてみました。

WEBで動作するウィザードリィ#1を作ってください。
限りなく原作に近づけてください。
日本語版でお願いします。

実行した結果全文です。

もしかしたらウィザードリィ#1をそのまま再現できるかもと思いましたが、やはりそうはいきませんでした。

特に往復したやり取りもしていない状態で、1回で出来上がったものがこちらです。

https://www.uosoft.net/blog-parts/dungeon_rpg/

ちゃんとダンジョンRPGになってます

戦闘はモンスターのグラフィックではないですが、割と安定して動作してます。

プランがPlusで100%の状態から実行した使用状況です。

1回のやり取りで5時間制限の19%使用してます。
本格的に何か作ろうと思ったらPlusではすぐに上限に達してしまうと思うので実用的ではないと思いますが、本当に作りたいものがあればPro(5倍16,800円、20倍30,000円)でも十分に元を取れるくらいの効果はあると思いました。

ウィズ (魔法使いウィズ)の話

今回は少しマイナーなゲームの話です。

小さい頃に近所に個人経営のゲームセンターがあったのですが、すぐ隣の八百屋の店頭に1台だけゲームが置いてありました。
そこにあったのがウィズでした。

なぜかこのゲームが好きで、友達とゲームセンターに行っても1人だけ外に出てWIZで遊んでました。
当時は小学生でそんなに小遣いがあるわけでもなく、遊べる頻度も少ないので2面くらいしか行った記憶がありません。
もしファミコンで発売されていたら絶対に入手してたと思います。

その後、MSX2を入手しました。
blog-e.uosoft.net

この時に魔法使いWIZという名前で移植されていることを知り探したところ、中古ショップで手に入れることができました。
当時はMSX1時代のソフトはコナミ以外は中古で安く買えた時代でこれとロードランナーをそれぞれ1,000円くらいで買ったと思います。
ロードランナーもファミコンで持ってましたが、MSX版の方がオリジナルって感じで好きでした。

その当時もMSXで遊べただけで満足で、あまりやり込むこともなく相変わらず初めのさわりの部分くらいでゲームオーバーになってました。
いまでこそMSX版も好きですが、当時はアーケードと比較するとやはり劣るのであまり満足してませんでした。

現在でもMSX版は再入手してたまに遊んでます。

PS4でアーケードアーカイブスの発売時にはファミ通のサイトか何かで事前に情報を入手して、発売日に購入しました。
Switchを入手後、こちらでもアーケードアーカイブスにあることを見て購入しました。
最近Switch2版のアーケードアーカイブス2も発売されたので、こちらも購入してしまいました。
巻き戻し機能やタイムアタック等が追加されてます。

現在でも1面しかまともに遊んでませんが、たまに遊んでます。