クラウドインフラ構築記

現在AWSの構築支援に携わっております。今注視しているのは、GKE、BigQuery、Google Dataflowなどサービスを展開しているGoolge Cloud Platformです。

Cloudflare OSを試してみました。#AIエージェントキャッチアップ

8/17お昼にCloudflare OSのキャッチアップの前に事前に試してみたことを記載しています

https://generative-agents.connpass.com/event/403576

概要

社内で使われていたAI生産性環境をCloudflareが2026年8月にOSS公開したもので、AIエージェント、社内システム、人を1つのセキュアワークスペースに統合し、AIワークロードを管理することを、従来のOSに見立てて表現したツールです。

ローカル環境構築

git clone https://github.com/cloudflare/cloudflare-os.git repo
cd repo
corepack enable
corepack pnpm install
corepack pnpm run-local

AI Provider

AI Modelの設定。Free planの場合、GPT-OSSが利用できる。ただし、利用制限あり

Cloudflare Account IDWorkers PagesのAccount ID
API TokenAI Gateway の認証トークン

Cloudflare AI Modelのkimi K3を使う場合は、Paid Planが必要

ツール

ツールが利用可能ですが、GitHub、AWS Knowledge MCPを試してみました。

GitHub MCP

GitHub OAuth Appで、Redirect URIを下記を設定する。

http://localhost:8787/gatekeeper/github/oauth

GitHub OAth App

packages/gatekeeper-github/.envに、GitHub OAuthで払い出した認証情報を記載して、立ち上げ直す
CLIENT_ID=xxxxx
CLIENT_SECRET=xxxxxxxx

MCP Server

streamable httpのURLを入力すれば使えるはずだが、400でうまくいかない

Workers AIの@cf/openai/gpt-oss-120bはOpenAI互換エンドポイントのスキーマ検証でcontentが常に文字列型であることを要求しており、nullを許容しない

onPayloadフックにcontent nullを空文字としてすることで回避可能ことができる

  diff --git a/packages/workshop-backend/src/ai-models.ts
  b/packages/workshop-backend/src/ai-models.ts
  index 7f7ea8d..1dc1bcb 100644
  --- a/packages/workshop-backend/src/ai-models.ts
  +++ b/packages/workshop-backend/src/ai-models.ts
  @@ -164,6 +164,28 @@ function workersAiCompat(catalog: Model<Api> |
  undefined): OpenAICompletionsComp
     };
   }

  +// Workers AI's OpenAI-compatible endpoint validates every message's
  `content` field against a
  +// schema that requires it to be a string. pi sends `content: null` for an
  assistant message that
  +// carries tool calls but no text (the OpenAI Chat Completions convention),
  which Workers AI
  +// rejects with a 400. Rewrite it to an empty string, matching what other
  strict OpenAI-compatible
  +// providers already need (see pi's own `requiresAssistantAfterToolResult`
  compat flag).
  +function fixWorkersAiNullAssistantContent(payload: unknown): unknown |
  undefined {
  +  if (typeof payload !== "object" || payload === null) return undefined;
  +  const { messages } = payload as { messages?: unknown };
  +  if (!Array.isArray(messages)) return undefined;
  +  let changed = false;
  +  const fixedMessages = messages.map((message) => {
  +    if (typeof message === "object" && message !== null &&
  +        (message as { role?: unknown }).role === "assistant" &&
  +        (message as { content?: unknown }).content === null) {
  +      changed = true;
  +      return { ...message, content: "" };
  +    }
  +    return message;
  +  });
  +  return changed ? { ...payload, messages: fixedMessages } : undefined;
  +}
  +
   // Build the pi model descriptor for reaching a provider's own native API
  through an AI Gateway
   // (the platform's or a user's). `gatewayUrl` is a gateway root
   // (https://gateway.ai.cloudflare.com/v1/{accountId}/{gateway}); each
  provider's native API is
  @@ -328,10 +350,26 @@ function makeHandle(args: HandleArgs): ModelHandle {
             await options.onResponse?.(response, responseModel);
           },
           // PDF attachments ride pi image parts and are rewritten here into
  the provider's native
  -        // document blocks (no-op for payloads without one; see
  chat-attachment-pdf.ts).
  +        // document blocks (no-op for payloads without one; see
  chat-attachment-pdf.ts). Workers AI
  +        // additionally needs its assistant `content: null` quirk fixed up
  (see
  +        // fixWorkersAiNullAssistantContent above).
           onPayload: async (payload, payloadModel) => {
             const replaced = await options.onPayload?.(payload, payloadModel);
  -          return bridgePdfAttachments(args.model.api, replaced ?? payload) ??
  replaced;
  +          let current = replaced ?? payload;
  +          let changed = replaced !== undefined;
  +          if (args.model.provider === "cloudflare-workers-ai") {
  +            const fixed = fixWorkersAiNullAssistantContent(current);
  +            if (fixed !== undefined) {
  +              current = fixed;
  +              changed = true;
  +            }
  +          }
  +          const bridged = bridgePdfAttachments(args.model.api, current);
  +          if (bridged !== undefined) {
  +            current = bridged;
  +            changed = true;
  +          }
  +          return changed ? current : undefined;
           },
           // NOTE(binding-transport): pi passes `options.fetch` into its SDK
  clients on all paths.
           // If Workers-binding-backed inference returns (upstream ask filed),
  inject a

日本語確定でプロンプトが送信されるのもキーバインド修正で回避できる

ChatInterface.tsx:3425のEnterキー処理を見ると、送信抑止の判定がe.nativeEvent.i
  sComposingのみです。

  // Enter sends message (unless Shift is held or IME composition is in
  progress)
  if (e.key === "Enter" && !e.shiftKey && !e.nativeEvent.isComposing) {
    e.preventDefault();
    if (!isAgentActive && !isBlocked) submitMessage();
    return;
  }

コメントは受け付けていません。