Duck.ai を匿名LLMとして利用する試行錯誤の記録
1 概要
ローカルLLM環境を構築していたが,性能面やコスト面を考えると
「他人のGPUを使った方が合理的ではないか」という疑問が出てきた.
そこで以下を調査した.
- 匿名性の高いAIサービス
- APIとして利用可能か
- ローカルツール(Open WebUI / Emacs)から接続できるか
その過程で Duck.ai とその OpenAI互換プロキシ を発見したため,
試行錯誤の記録をまとめる.
—
2 背景:ローカルLLMの限界
ローカルLLMの構築は以下の理由で限界を感じ始めていた.
- VRAM不足
- 推論速度が遅い
- 高性能モデルが使えない
特に大規模モデルは
- 70B
- 120B
などになり,一般PCでは実用的ではない.
そのため
「ローカルLLMを頑張るより,外部GPUを借りた方が合理的では?」
という方向に思考が変わった.
—
3 匿名性の高いAIサービスの探索
通常のAIサービスは
- OpenAI
- Anthropic
- Google
などであり,すべて
- アカウント必須
- 課金
- トラッキング
が存在する.
そこで以下の条件で探索した.
- アカウント不要
- 匿名利用可能
- API的に叩ける
調査対象として挙がったのは以下.
- Duck.ai
- HuggingChat
- Poe
- Perplexity
- OpenRouter
この中で最も匿名性を強調しているのが
Duck.ai
だった.
—
4 Duck.aiとは
Duck.aiは
DuckDuckGo が提供する AI チャット機能.
特徴
- アカウント不要
- ブラウザのみで利用可能
- プライバシー重視
内部では以下のモデルを切り替えている.
- GPT 系
- Claude 系
- Mistral 系
ただし問題がある.
公式APIが存在しない
つまり
- プログラムから利用できない
- 自動化できない
—
5 Duck.ai プロキシの発見
調査中に GitHub で以下を発見した.
Duck.ai を
OpenAI API互換に変換するプロキシ
これにより
Duck.ai → OpenAI API
として扱えるようになる.
つまり
- Open WebUI
- curl
- 各種AIツール
から利用可能になる.
—
6 DockerでDuck.aiプロキシを起動
Dockerで以下を実行.
docker run -d -p 3333:3000 --name duckai amirkabiri/duckai
これにより
が OpenAI API互換エンドポイントとして動作する.
—
7 APIの動作確認
まずモデル一覧を取得.
Invoke-RestMethod http://localhost:3333/v1/models
結果
- gpt-4o-mini
- gpt-5-mini
- claude-3-5-haiku
などが確認できた.
—
8 Open WebUIから接続を試みる
Open WebUI から
OpenAI互換APIとして接続.
しかし以下のエラーが発生.
DuckAI API error: 400 Bad Request Each message must have a valid role
原因として考えられるもの
- Open WebUIのメッセージ形式
- Duck.aiプロキシの実装不足
- tool / function calling
など.
—
9 curlでは成功する
Open WebUIは失敗するが,
curlでは成功した.
curl -X POST http://192.168.10.247:3333/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"hello"}]}'
レスポンス
{
"choices":[
{
"message":{
"content":"Hello! How can I assist you today?"
}
}
]
}
つまり
API自体は正常
—
10 Open WebUI対応を断念
Open WebUI側の要求が
- tool
- function
- message構造
など複雑なため,
Duck.aiプロキシが完全互換ではない可能性が高い.
そのため
Open WebUI接続は断念
—
11 方針転換:Emacsから直接叩く
次の発想.
「curlで成功するなら
Emacsから叩けばいいのでは?」
既存の環境では
- Emacs
- ローカルLLM
- curl
を使った自作ツールが存在している.
そのため
Duck.ai を
Emacsツールとして統合
する方針に変更.
—
12 EmacsからDuck.ai接続
curlを直接実行.
curl -X POST http://192.168.10.247:3333/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"hello"}]}'
成功.
—
13 Emacs Lispから非同期実行
Emacsから
- 非同期
- 別バッファ表示
- ESC ESCで停止
というUIを実装.
これにより
Emacs内AIツール
としてDuck.aiを利用可能になった.
—
14 結論
今回の試行錯誤で分かったこと.
- Duck.ai は匿名AIとして優秀
- 公式APIは存在しない
- プロキシを使えばOpenAI互換APIになる
- Open WebUIとは相性が悪い
- curlなら問題なく動作
- Emacsから直接叩くのが最もシンプル
結果として
Emacs AI環境にDuck.aiを統合
する方向になった.
—
15 実装
以下を実装.
- EmacsメニューUI
- 翻訳
- 要約
- 校正
- キーワード抽出
- デバッグ支援
これにより
軽量AIアシスタント環境
をEmacs内で実現できる.
実際のスクリプトはこんな感じ.
(require 'json)
;; duckai 設定
(defvar my-duckai-endpoint "http://192.168.x.x:3333/v1/chat/completions")
(defvar my-duckai-model "default")
(defvar my-duckai-buffer "*duckai*")
(defvar my-duckai-process nil)
;; ESC ×2 で強制kill
(defun my-duckai-stop ()
"Stop the DuckAI process if it's running."
(interactive)
(when (process-live-p my-duckai-process)
(kill-process my-duckai-process)))
(global-set-key (kbd "ESC ESC") #'my-duckai-stop)
(defun my-duckai-submit (prompt action)
"Submit PROMPT to DuckAI with ACTION."
(let* ((buf (get-buffer-create my-duckai-buffer))
(encoded-prompt (json-encode prompt))
(json-request (format
"{\"model\":\"%s\",\"messages\":[{\"role\":\"user\",\"content\":%s}]}"
my-duckai-model
encoded-prompt))
(cmd (list "curl" "-s"
"-X" "POST"
my-duckai-endpoint
"-H" "Content-Type: application/json; charset=utf-8"
"-d" json-request)))
(with-current-buffer buf
(erase-buffer)
(insert "waiting...\n"))
(display-buffer buf)
(setq my-duckai-process
(make-process
:name "duckai"
:buffer buf
:command cmd
:noquery t
:filter
(lambda (proc output)
(with-current-buffer (process-buffer proc)
(goto-char (point-max))
(insert output)))
:sentinel
(lambda (proc _event)
(when (eq (process-status proc) 'exit)
(with-current-buffer (process-buffer proc)
(goto-char (point-min))
(when (re-search-forward "\"content\":\"\\(.*?\\)\"" nil t)
(let ((text (match-string 1)))
(setq text (replace-regexp-in-string "\\\\n" "\n" text))
(setq text (replace-regexp-in-string "\\\\\"" "\"" text))
(setq text (replace-regexp-in-string "\\\\" "" text))
(erase-buffer)
(insert text))))))))))
;; プロンプト生成
(defun my-duckai-action (action)
"Perform ACTION on the selected text."
(let ((start (region-beginning))
(end (region-end))
(prompt (buffer-substring-no-properties (region-beginning) (region-end)))
(action-command ""))
(cond
((string= action "p") (setq action-command prompt)) ;; send as is
((string= action "j") (setq action-command (format "次の文章を自然な日本語に翻訳してください:\n\n: %s" prompt)))
((string= action "e") (setq action-command (format "Translate the following text into natural English:\n\n: %s" prompt)))
((string= action "s") (setq action-command (format "次の文章を10行以内で簡潔に要約してください.各行は1文のみ.\n\n: %s" prompt)))
((string= action "f") (setq action-command (format "次の文章を校正してください.誤字脱字や不自然な表現のみ修正してください.\n\n: %s" prompt)))
((string= action "k") (setq action-command (format "次の文章の内容を箇条書きで整理してください.最大10項目.\n\n: %s" prompt)))
((string= action "w") (setq action-command (format "次の文章から重要なキーワードを20個以内で列挙してください.説明は不要です.\n\n: %s" prompt)))
((string= action "d") (setq action-command (format "次のコードまたはエラーメッセージの原因を簡潔に説明し,修正例を示してください.\n\n: %s" prompt)))
)
(when action-command
(my-duckai-submit action-command action))))
(defun my-duckai-menu ()
"Open the DuckAI submenu."
(interactive)
(let ((key (read-key
(format "[%s] p=そのまま送る j=日本語 e=English s=要約 f=校正 k=箇条書き w=キーワード抽出 d=デバッグ:"
my-duckai-model))))
(cond
((eq key ?p) (my-duckai-action "p"))
((eq key ?j) (my-duckai-action "j"))
((eq key ?e) (my-duckai-action "e"))
((eq key ?s) (my-duckai-action "s"))
((eq key ?f) (my-duckai-action "f"))
((eq key ?k) (my-duckai-action "k"))
((eq key ?w) (my-duckai-action "w"))
((eq key ?d) (my-duckai-action "d"))
(t (message "Invalid action!")))))
;; キーバインド
(global-set-key (kbd "C-c d") #'my-duckai-menu)