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 ID | Workers PagesのAccount ID |
| API Token | AI 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;
}