📝

さくらのAI Engine (HTTP ストリームモード)をPostmanでテストしてレスポンスをシンプルに表記する。

に公開

https://knowledge.sakura.ad.jp/46949/
先日さくらインターネットから『さくらのAI Emngine』というサービスがリリースされました。

オープンウェイト/オープンソース型LLMモデルをさくらインターネットのデータセンターでホスティングしているデータ安全性の高いサービスです。

オープンウェイト/オープンソース型LLMモデルによるローカルLLMは、利用が安いが精度が課題というのは痛切でしたが、gpt-oss-120bのリリースにより少し状況が変わってきているのではないかと思います。1200億パラメータを持ち1世代前のChatGPTと同程度の能力を保有しているといわれています。

さっそくやってみる

1. curlコマンドのテスト

公式マニュアルにその手順がありますので単純なチャットをこのドキュメントをもとに実行しておきます。
https://manual.sakura.ad.jp/cloud/ai-engine/02-howto.html

curl --location 'https://api.ai.sakura.ad.jp/v1/chat/completions' \
  --header 'Accept: application/json' \
  --header 'Authorization: Bearer <TOKEN>' \
  --header 'Content-Type: application/json' \
  --data '{
    "model": "gpt-oss-120b",
    "messages": [
      { "role": "system", "content": "こんにちは!" }
    ],
    "temperature": 0.7,
    "max_tokens": 200,
    "stream": false
  }'
{
  "id": "chatcmpl-6a63359f24f845d886d2e0ceff4f390b",
  "object": "chat.completion",
  "created": 1759295412,
  "model": "gpt-oss-120b",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": "こんにちは!今日はどんなことをお手伝いできますか?",
        "refusal": null,
        "annotations": null,
        "audio": null,
        "function_call": null,
        "tool_calls": [],
        "reasoning_content": "The user gave a system message: \"こんにちは!\" in Japanese, meaning \"Hello!\". There's no instruction to do anything else. The system says: \"You are ChatGPT...\". The developer message is empty. We should respond appropriately, greeting them. Probably respond in Japanese."
      },
      "logprobs": null,
      "finish_reason": "stop",
      "stop_reason": null,
      "token_ids": null
    }
  ],
  "service_tier": null,
  "system_fingerprint": null,
  "usage": {
    "prompt_tokens": 71,
    "completion_tokens": 80,
    "total_tokens": 151,
    "prompt_tokens_details": null
  },
  "prompt_logprobs": null,
  "prompt_token_ids": null,
  "kv_transfer_params": null
}

"stream": falseと指定すると一度に回答が戻ってきます。さくらのAI EngineはHTTPストリームにも対応しているため"stream": trueで実行すると以下になります。

HTTP ストリームの出力(一部)
data: {"id":"chatcmpl-39400a40a8954432bbcc6897030f9903","object":"chat.completion.chunk","created":1759295465,"model":"gpt-oss-120b","choices":[{"index":0,"delta":{"role":"assistant","content":""},"logprobs":null,"finish_reason":null}],"usage":{"prompt_tokens":71,"total_tokens":71,"completion_tokens":0},"prompt_token_ids":null}

data: {"id":"chatcmpl-39400a40a8954432bbcc6897030f9903","object":"chat.completion.chunk","created":1759295465,"model":"gpt-oss-120b","choices":[{"index":0,"delta":{"reasoning_content":""},"logprobs":null,"finish_reason":null,"token_ids":null}],"usage":{"prompt_tokens":71,"total_tokens":74,"completion_tokens":3}}

data: {"id":"chatcmpl-39400a40a8954432bbcc6897030f9903","object":"chat.completion.chunk","created":1759295465,"model":"gpt-oss-120b","choices":[{"index":0,"delta":{"reasoning_content":"The"},"logprobs":null,"finish_reason":null,"token_ids":null}],"usage":{"prompt_tokens":71,"total_tokens":75,"completion_tokens":4}}

data: {"id":"chatcmpl-39400a40a8954432bbcc6897030f9903","object":"chat.completion.chunk","created":1759295465,"model":"gpt-oss-120b","choices":[{"index":0,"delta":{"reasoning_content":" user"},"logprobs":null,"finish_reason":null,"token_ids":null}],"usage":{"prompt_tokens":71,"total_tokens":76,"completion_tokens":5}}

data: {"id":"chatcmpl-39400a40a8954432bbcc6897030f9903","object":"chat.completion.chunk","created":1759295465,"model":"gpt-oss-120b","choices":[{"index":0,"delta":{"reasoning_content":" says"},"logprobs":null,"finish_reason":null,"token_ids":null}],"usage":{"prompt_tokens":71,"total_tokens":77,"completion_tokens":6}}

data: {"id":"chatcmpl-39400a40a8954432bbcc6897030f9903","object":"chat.completion.chunk","created":1759295465,"model":"gpt-oss-120b","choices":[{"index":0,"delta":{"reasoning_content":" \""},"logprobs":null,"finish_reason":null,"token_ids":null}],"usage":{"prompt_tokens":71,"total_tokens":78,"completion_tokens":7}}

data: {"id":"chatcmpl-39400a40a8954432bbcc6897030f9903","object":"chat.completion.chunk","created":1759295465,"model":"gpt-oss-120b","choices":[{"index":0,"delta":{"reasoning_content":"こんにちは"},"logprobs":null,"finish_reason":null,"token_ids":null}],"usage":{"prompt_tokens":71,"total_tokens":79,"completion_tokens":8}}
<snip>

HTTP Streamモードでは画面に順次回答が戻ってくるため、長い処理を行う場合はユーザの待ち時間が少なく、間に介在するプロキシの負担も軽くなるため、生成AIの活用ではよくつかわれるモードです。

2. curlコマンドのPostmanへのインポート

HTTP Streamモードではレスポンスが順次戻ってくるため必要なレスポンスを確認するために手間が発生します。Postmanではレスポンスをスクリプトで処理してコンソールに出力させることができます。この機能を用いることでHTTP Stream型のレスポンスから必要なブブのみを取り出すことができるためデバッグに便利です。
https://zenn.dev/kameoncloud/articles/1cfc13d988aeaf

Postmanにはcurlをそのままインポートできる機能がありますのでまずはその機能を用いてコレクションを作成します。
インポートボタンをクリックして上記のcurlコマンドを貼り付けます。

インポートが完了したら実行すると以下の通りレスポンスが順次戻っています。

検索ができるためcurlよりは便利ですが、スクリプトを使うとさらに必要な部分を自動で抜き出す処理を書いておくことができます。

3. Post-response スクリプト

Post-responseタブに以下のスクリプトを挿入します。

// 取得したレスポンスを文字列化
let raw = pm.response.text();

// SSEの "data:" 行ごとに分割
let lines = raw.split("\n").filter(l => l.startsWith("data: "));

// [DONE] を除外
lines = lines.filter(l => !l.includes("[DONE]"));

// JSONだけ抽出してパース
let chunks = lines.map(l => {
    try {
        return JSON.parse(l.replace("data: ", "").trim());
    } catch (e) {
        return null;
    }
}).filter(x => x);

// コンテンツ部分を抽出(delta.content があれば結合)
let text = "";
chunks.forEach(c => {
    let delta = c?.choices?.[0]?.delta;
    if (delta && delta.content) {
        text += delta.content;
    }
});

// Postman のコンソールに出力
console.log("=== 整形結果 ===");
console.log(text);

// 例: Tests の変数に保存しておくこともできる
pm.environment.set("last_ai_output", text);

再度コレクションを実行するとコンソールに以下の様に表示されます。

さくらインターネット株式会社

Discussion