2026 年 9 月 1 日に、Chrome 拡張機能 web-digest v1.1.0 をリリースしました。
web-digest は、現在開いている Web ページから主要な内容を抽出し、生成 AI で要約して Chrome の Side Panel に表示する拡張機能です。初期版では、要約先をさくらの AI Engine に固定していました。初期版の構成や、見出し・段落を使った長文分割については既に Qiita にまとめています[1]。ブログでは、本文抽出、長文分割、AI API への接続を分離し、将来ほかの生成 AI API も選択できるようにする方針を書いていました[2]。
v1.1.0 では、その接続部分を実際に拡張しました。主な変更は次の 3 点です[3]。
| 項目 | v1.0 系 | v1.1.0 |
|---|---|---|
| AI provider | Sakura AI Engine | Sakura AI Engine / OpenAI / Claude |
| credential / model | Sakura 用のみ | provider ごとに独立 |
| 要約言語 | 原文と同じ言語 | 原文と同じ言語 / 日本語 |
| 1 request の material budget | 40,000 文字 | 200,000 文字 |
| 長文処理 | chunk 分割後に統合 | 同じ構造を維持 |
今回の実装で中心に置いたのは、3 社の API を一つの wire protocol に寄せることではありません。アプリケーション内部では provider-neutral な request / result を使い、provider 固有の差を adapter に閉じ込める構成にしました。
動作環境と v1.1.0 で維持した前提
v1.1.0 の前提は次のとおりです。
- Google Chrome 116 以上
- Manifest V3
- build step なし
- provider は Sakura AI Engine、OpenAI、Claude の 3 種類
- API credential は利用者自身が用意する BYOK
- 1 run では選択した 1 provider だけを使用する
- provider failure 時に別 provider へ自動 fallback しない
- ページ本文はツールバーの操作を起点に
activeTabとchrome.scriptingで取得する - 要約結果は Side Panel に表示する
- 独自 backend は持たない
つまり、v1.1.0 は本文抽出や Side Panel を作り直したバージョンではありません。初期版の Chrome 側の構造を維持し、AI provider との境界と出力言語の制御を拡張しています。
単一 provider の実装を dispatcher と adapter に分離する
初期版では、Service Worker から Sakura AI Engine 固有の処理へ直接つなげても構造上の問題はありませんでした。接続先が 1 つだけだったためです。
3 provider 対応では、上位処理が API ごとの request body や response shape を知り始めると、本文抽出や長文処理まで provider 条件分岐に引きずられます。そこで、Service Worker から見える契約を次の 5 項目に固定しました。
provider
model
credential
instruction
content
src/engine/dispatcher.js は provider identifier から adapter を選ぶだけです。
const ADAPTERS = {
[Provider.SAKURA]: callSakura,
[Provider.OPENAI]: callOpenAI,
[Provider.ANTHROPIC]: callClaude,
};
export async function callProvider(
{ provider, model, credential, instruction, content },
options = {},
) {
const call = ADAPTERS[provider];
if (!call) return { ok: false, kind: ErrorKind.INTERNAL_ERROR };
return call(
{ model, credential, instruction, content },
options,
);
}
この層には fallback も retry もありません。選択された provider が失敗した場合、その failure をそのまま上位へ返します。
この構成にすると、長文処理側は「Sakura ならこの request」「Claude なら別の request」と判断する必要がありません。page、chunk、integrate のどの段階でも、同じ logical request を dispatcher に渡せます。
3 社の native API は adapter 内で個別に扱う
共通化したのはアプリケーション内部の契約であり、各 provider の HTTP API ではありません。
v1.1.0 では次の API を使っています。
| Provider | API | instruction | material |
|---|---|---|---|
| Sakura AI Engine | Chat Completions API |
system message |
user message |
| OpenAI | Responses API | instructions |
input |
| Claude | Messages API | top-level system
|
user message |
Sakura AI Engine は OpenAI 互換の Chat Completions endpoint を提供しており、web-digest では https://api.ai.sakura.ad.jp/v1/chat/completions を使用しています[4]。
const messages = [
{ role: "system", content: instruction },
{ role: "user", content },
];
body: JSON.stringify({ model, messages })
OpenAI では Responses API を直接使用します。Responses API の instructions は system / developer instruction、input はモデルへの入力として定義されています[5]。
body: JSON.stringify({
model,
instructions: instruction,
input: content,
store: false,
})
Claude では Anthropic の Messages API を直接使用します。Messages API は POST /v1/messages で、messages と top-level system を分けて指定できます[6]。
body: JSON.stringify({
model,
system: instruction,
max_tokens: 32768,
messages: [{ role: "user", content }],
})
3 社を同じ API として扱うために互換 layer を追加するのではなく、adapter ごとに native API を組み立てています。このほうが API 固有の failure mapping や response parsing を各 adapter に閉じ込められます。
上位処理に返す結果は provider-neutral にします。
{ ok: true, summary: "..." }
{ ok: false, kind: "...", detail: "..." }
これにより、Service Worker は HTTP status や provider 固有の response body を解釈しません。
credential と model は provider ごとに分離する
multi-provider 化で互換性に影響しやすいのが設定です。
初期版では Sakura AI Engine 用として次の key を使っていました。
apiToken
model
v1.1.0 では、この既存 key を Sakura 用としてそのまま残し、OpenAI と Claude の key を追加しました。
export const STORAGE_KEY_PROVIDER = "provider";
export const STORAGE_KEY_TOKEN = "apiToken";
export const STORAGE_KEY_MODEL = "model";
export const STORAGE_KEY_OPENAI_KEY = "openaiApiKey";
export const STORAGE_KEY_OPENAI_MODEL = "openaiModel";
export const STORAGE_KEY_ANTHROPIC_KEY = "anthropicApiKey";
export const STORAGE_KEY_ANTHROPIC_MODEL = "anthropicModel";
provider selection 自体は provider に保存します。
保存済みの provider が存在しない場合は Sakura と解釈します。
export function resolveProvider(value) {
return typeof value === "string" && KNOWN_PROVIDERS.has(value)
? value
: Provider.SAKURA;
}
この方法なら、v1.0 系から更新した利用者に migration script は不要です。従来の apiToken と model をそのまま読み、provider 未設定なら従来どおり Sakura AI Engine を使います。
また、provider を切り替えても別 provider の credential と model は削除しません。OpenAI を設定したあと Sakura に戻り、再び OpenAI を選んでも、OpenAI の設定は独立して残ります。
multi-provider 化では「新しい正規化された key へ全設定を移す」方法もありますが、今回のように既存 key の意味を維持できる場合は、保存済みデータを動かさないほうが互換性の条件を減らせます。
OpenAI と Claude の host permission は optional にする
AI provider を増やすと、Chrome 拡張側では通信先の host permission も増えます。
Sakura AI Engine は初期版からの default provider なので required permission のまま維持しました。一方、OpenAI と Claude は、選択しない利用者には不要です。そのため optional_host_permissions に分けています。
{
"host_permissions": [
"https://api.ai.sakura.ad.jp/*"
],
"optional_host_permissions": [
"https://api.openai.com/*",
"https://api.anthropic.com/*"
]
}
Chrome Extensions では optional_host_permissions を runtime に利用者へ要求できます[7]。
web-digest では、Settings で OpenAI または Claude を選んだ操作を起点に chrome.permissions.request() を呼びます。
export async function requestProviderPermission(
provider,
permissionsApi = chrome.permissions,
) {
if (!needsOptionalPermission(provider)) return true;
return await permissionsApi.request({
origins: [PROVIDER_HOST_PERMISSION[provider]],
});
}
permission が許可された場合だけ provider selection を保存します。拒否された場合は、それまで選択されていた provider を維持します。
また、Chrome 側で permission が後から revoke された場合に備え、Settings には Grant or restore permission を用意しています。run 開始時には permission prompt を出さず、既に permission があるかだけを確認します。
ここでは「設定画面を開いた時点で OpenAI と Claude の permission をまとめて要求する」という方式を採っていません。利用する provider の origin だけを、その provider を選んだ時点で要求します。
日本語要約は別の翻訳処理にしない
v1.1.0 では Summarize in Japanese を追加しました。
実装上は、原文の言語で要約したあと、その要約を日本語へ翻訳する二段階処理ではありません。
run の開始時に設定値を 1 回読み、trusted instruction に次の control を追加します。
LANGUAGE MODE: source
または、
LANGUAGE MODE: japanese
コードでは次のようにしています。
export const LanguageMode = {
SOURCE: "source",
JAPANESE: "japanese",
};
export function composeInstruction(baseInstruction, japaneseSummary) {
const mode = japaneseSummary
? LanguageMode.JAPANESE
: LanguageMode.SOURCE;
return `${baseInstruction}\n\nLANGUAGE MODE: ${mode}`;
}
source なら原文の言語で要約し、japanese なら入力言語に関係なく日本語の要約を直接生成します。
この方法では translation 用の追加 request が発生しません。
長いページでは、この設定の読み方も重要です。web-digest は長文を chunk に分割し、各 chunk の要約を最後に integrate します。
page
↓
chunk 1 ─┐
chunk 2 ─┼─> integrate
chunk 3 ─┘
japaneseSummary を request ごとに読み直すと、処理途中で設定を変更した場合に、同じ run の chunk ごとに出力言語が変わる可能性があります。
v1.1.0 では provider、credential、model、日本語要約設定を run の開始時に 1 回だけ読みます。その run の page / chunk / integrate すべてに同じ LANGUAGE MODE を使います。
設定値を「毎回最新にする」ことよりも、「一つの処理単位の中では固定する」ことを優先した例です。
TASK とページ本文を同じ instruction として扱わない
長文処理では、モデルに現在の request が何を意味するか伝える必要があります。
web-digest では次の 3 種類を使います。
TASK: page
TASK: chunk
TASK: integrate
TASK は Web ページ由来のデータではなく、拡張機能自身が決める制御情報です。そのため v1.1.0 の最終構造では TASK を trusted instruction 側へ置き、ページタイトルと本文だけを material 側へ置いています。
export function composeRequest(instruction, material, task = "page") {
const body = material.title
? `TITLE: ${material.title}\n\nBODY:\n${material.text}`
: `BODY:\n${material.text}`;
return {
instruction: `${instruction}\n\nTASK: ${task}`,
content: body,
};
}
各 adapter は、この 2 つを provider 固有の別フィールドへ割り当てます。
Sakura
instruction -> system message
content -> user message
OpenAI
instruction -> instructions
content -> input
Claude
instruction -> system
content -> user message
ページ本文に TASK: integrate や LANGUAGE MODE: source と書かれていても、それは content 内の文字列です。worker が決めた TASK や LANGUAGE MODE と同じ制御面には入りません。
provider を共通化するときは、HTTP request の形だけでなく、どの値をアプリケーションが決め、どの値を外部入力として扱うかも共通契約に含める必要があります。
1 request の material budget を 200,000 文字へ広げる
初期版では、1 request に渡す material budget を 40,000 文字としていました[1]。
v1.1.0 では次の値に変更しています。
export const MAX_REQUEST_MATERIAL_CHARS = 200000;
処理経路は次のようになります。
material <= 200000
↓
page request
material > 200000
↓
structural chunking
↓
chunk requests
↓
integrate request
200,000 という値は provider の context window ではありません。web-digest が one-request path と staged summarization のどちらを選ぶか判断するためのローカルな文字数予算です。
provider ごとの tokenizer を Chrome 拡張へ持ち込んでいないため、共通の前処理では token count ではなく character count を使います。実際の provider が入力を拒否した場合は、その provider adapter が error response を too-much-text などの共通 failure へ変換します。
これによって「200,000 文字なら必ず 1 request で成功する」とは扱いません。ローカルな分割判断と provider 側の実際の制限は別の層です。
Manifest V3 で長い AI request を待つ
生成 AI API は、入力サイズやモデルによって応答まで 30 秒を超える場合があります。一方、Chrome の extension Service Worker は通常 30 秒の inactivity で終了し、fetch() response が 30 秒を超えて到着しない場合も termination condition に含まれます[8]。
web-digest の provider request timeout は 120 秒です。
export const REQUEST_TIMEOUT_MS = 120000;
ここで 120 秒待つ実装だけを置いても、Service Worker の lifecycle と整合しません。
現在の実装では、provider summarization を実行している間だけ 25 秒周期で trivial runtime API を呼びます。Chrome の移行ガイドにも、長時間 operation の間に chrome.runtime.getPlatformInfo() を 25 秒周期で呼び、完了時に interval を解除する例があります[9]。
export const SERVICE_WORKER_KEEPALIVE_INTERVAL_MS = 25000;
export async function keepServiceWorkerAlive(
operation,
{
runtimeApi = chrome.runtime,
setIntervalImpl = setInterval,
clearIntervalImpl = clearInterval,
} = {},
) {
const keepAlive = setIntervalImpl(() => {
runtimeApi.getPlatformInfo().catch(() => {});
}, SERVICE_WORKER_KEEPALIVE_INTERVAL_MS);
try {
return await operation();
} finally {
clearIntervalImpl(keepAlive);
}
}
適用範囲は summarization operation だけです。
const answer = await keepServiceWorkerAlive(() =>
summarizeMaterial(
shaped.material,
instruction,
providerCall,
),
);
この 25 秒と、provider request の 120 秒は別の値です。
| 値 | 意味 |
|---|---|
| 30 秒 | Chrome Service Worker の通常の inactivity threshold |
| 25 秒 | summarization 実行中の lifetime reset 間隔 |
| 120 秒 | 1 provider request の timeout |
25 秒の pulse は provider request を retry しません。timeout も延長しません。成功、provider failure、timeout、exception のいずれでも finally で interval を解除します。
Service Worker を常時起動しておくための heartbeat ではなく、利用者が開始した長時間処理の期間だけ lifecycle を維持するための処理です。
v1.1.0 で変えなかったもの
v1.1.0 の変更点を整理すると、変更しなかった境界も明確になります。
本文抽出は従来どおり Chrome 側で行います。ページを開いただけでは実行せず、ツールバーの操作を起点に activeTab と chrome.scripting で対象ページを読みます。
長文では、見出しや段落を残した material を chunk に分割し、部分要約を最後に統合する構造を維持しました。provider が 3 種類になっても、この処理は provider identifier を見ません。
結果表示も Side Panel のままです。独自 backend を追加せず、選択した provider へ利用者自身の credential で直接 request を送ります。
さらに、1 run では 1 provider だけを使います。Sakura が失敗したら OpenAI を試す、といった自動 fallback は行いません。long page で複数 request が必要になった場合も、その run の provider は固定です。
v1.1.0 で増やしたのは「別の AI を選ぶ経路」と「同じ要約 pipeline から日本語を直接出す制御」です。Chrome 側の本文処理や UI まで provider ごとに分岐させないことが、変更範囲を限定する条件になりました。
まとめ
web-digest v1.1.0 では、Sakura AI Engine 固定だった初期版を Sakura AI Engine、OpenAI、Claude の 3 provider 対応へ拡張し、日本語要約を追加しました。
実装で共通化したのは、3 社の API protocol そのものではありません。
Service Worker から provider へ渡す provider、model、credential、instruction、content を共通契約にし、Sakura の Chat Completions API、OpenAI の Responses API、Claude の Messages API への変換は adapter に分離しました。
設定では、既存の apiToken と model を Sakura 用として維持し、provider 未設定時も Sakura に解決することで migration を不要にしました。OpenAI と Claude の host permission は optional とし、選択した provider の origin だけを runtime に要求します。
日本語要約は translation request を追加せず、run-level の LANGUAGE MODE として実装しました。長文の chunk / integrate を含め、一つの run では出力言語と provider を固定します。
さらに、1 request の material budget を 200,000 文字へ拡張し、長時間の AI request では Manifest V3 Service Worker の lifecycle と 120 秒の provider timeout を別々に扱いました。
単一 provider の実装を multi-provider 化するとき、共通化する境界を wire protocol に置くと、provider 差分が上位へ漏れます。web-digest では、アプリケーションが必要とする logical request / result を共通化し、各 API の違いは adapter に残すことで、本文抽出、長文処理、UI、run state を維持したまま接続先を増やしました。
参考文献
- id774, さくらの AI Engine で長い Web ページを要約する Chrome 拡張 web-digest を作った(2026-08-16). https://qiita.com/ynakayama/items/6b74c7012f665172520f
- id774, 長い Web ページを構造に沿って要約する Chrome 拡張 web-digest を作った(2026-08-17). https://blog.id774.net/entry/2026/08/17/5517/
- id774, web-digest Repository Version History(2026-08-31). https://github.com/id774/web-digest/blob/master/doc/VERSIONS
- さくらインターネット, 利用手順(2026-05-20). https://manual.sakura.ad.jp/cloud/ai-engine/02-howto.html
- OpenAI, Create a model response(2026-09-01). https://developers.openai.com/api/reference/cli/resources/responses/methods/create
- Anthropic, Create a Message(2026-09-01). https://platform.claude.com/docs/en/api/messages/create
- Chrome for Developers, Declare permissions(2024-02-05). https://developer.chrome.com/docs/extensions/develop/concepts/declare-permissions
- Chrome for Developers, The extension service worker lifecycle(2023-05-02). https://developer.chrome.com/docs/extensions/develop/concepts/service-workers/lifecycle
- Chrome for Developers, Migrate to a service worker(2023-03-09). https://developer.chrome.com/docs/extensions/develop/migrate/to-service-workers