Duck.ai を匿名LLMとして利用する試行錯誤の記録

ozy's labo.


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

これにより

http://localhost:3333/v1

が 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)



Date: 2026-03-06

Author: ozyukiwo

Created: 2026-03-11 水 08:08

Emacs 26.3 (Org mode 9.1.9)

Validate