@weiping/magpie-cf-jev 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +85 -2
  2. package/index.mjs +188 -0
  3. package/package.json +26 -4
package/README.md CHANGED
@@ -1,3 +1,86 @@
1
- # Temporary Holding Version
1
+ # magpie-cf-jev
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Cloudflare Workers AI 的 Jev / Clef 作为 **magpie 决策模型供应商**(provider id `cf-jev`)的一个插件。
4
+
5
+ 它存在的唯一理由:**绕过 magpie 上游的一个信封 bug**。
6
+
7
+ ## 问题
8
+
9
+ Workers AI 跑 `typesafe/jev` 时,回答套了两层信封:
10
+
11
+ ```json
12
+ {"result": {"state":"Completed", "result": {"model":"jev-1.13.0","answers":{…},"usage":{…}}}, "success": true, …}
13
+ ```
14
+
15
+ magpie 的 `provider.DecideAnswer`(`internal/provider/decide_wire.go`)只拆**一层** `result`,于是交给调用方的是 `{"state":"Completed","result":{…}}` —— 顶层没有 `answers`。后果:
16
+
17
+ - magpie 自己的分类器(`readJev`)读不到答案,`not a System One answer`;
18
+ - `/v1/systemone` 的客户端(例如 mbridge 的分类器)读不到 `answers`,落回默认档。
19
+
20
+ Clef 只有一层信封,所以只测 Clef 时看不出来。
21
+
22
+ ## 这插件怎么修的
23
+
24
+ magpie 的插件可以声明自己是决策供应商:`config` 钩子里 `decide: true`,模型的 `decides: true`,于是 magpie 把该 provider 的 `Decide` 指向 `plugin://cf-jev/v1`,所有 System One 请求都过插件的 `fetch`。
25
+
26
+ 插件在这个 `fetch` 里调 Cloudflare,**把两层信封拆干净**,只把 `{"model":…,"answers":…,"usage":…}` 交回 magpie。magpie 对 System One 路径不再拆信封(`DecideAnswer` 原样返回),所以答案带着顶层 `answers` 到达 magpie 自己和它的所有客户端。
27
+
28
+ 实测(本机,2026-10-25):
29
+
30
+ | 模型 | 插件直连 | 经 magpie `/v1/systemone` |
31
+ | --- | --- | --- |
32
+ | `cf-jev/typesafe/jev` | 200 · 1.0s | 200 · 1.2s · `keys=[model,answers,usage]` |
33
+ | `cf-jev/@cf/cloudflare/clef` | 200 · 0.5s | 200 · 0.5s |
34
+ | `cf-jev/@cf/cloudflare/clef-flash` | 200 · 0.4s | 200 · 0.5s |
35
+
36
+ ## 用法
37
+
38
+ ```sh
39
+ # 1. 装插件(本地目录,不走 npm)
40
+ magpie plugin add ~/workspace/dev/cf-jev-plugin
41
+
42
+ # 2. 登录,粘贴 Cloudflare API token
43
+ # 只做 Workers AI 的 token 可能列不出账号,那就按 "<account id>:<token>" 粘
44
+ magpie plugin login cf-jev
45
+
46
+ # 3. 确认三个决策模型都在,且 decide 探针通过
47
+ magpie provider models cf-jev
48
+ magpie provider test cf-jev # ✓ decide … typesafe/jev
49
+
50
+ # 4. 把分类组指到插件(order:快的在前)
51
+ magpie group set mb-classifier \
52
+ models=cf-jev/typesafe/jev,cf-jev/@cf/cloudflare/clef,cf-jev/@cf/cloudflare/clef-flash,typesafe/jev-latest \
53
+ routing=order
54
+ ```
55
+
56
+ > `magpie group mb-classifier` 对决策组总是显示 `not served now, skipped` / `no member ready` —— 那是 magpie CLI 的显示假象(它拿「给 agent 的模型目录」查成员,而决策模型本来就不在里面),不代表插件不能用。真要看可用性:`magpie provider test cf-jev` 或直接发一个 `/v1/systemone`。
57
+
58
+ ## 回滚
59
+
60
+ ```sh
61
+ magpie group set mb-classifier \
62
+ models=cloudflare-jev/typesafe/jev,cloudflare-jev/@cf/cloudflare/clef,cloudflare-jev/@cf/cloudflare/clef-flash,typesafe/jev-latest \
63
+ routing=order
64
+ magpie plugin rm cf-jev # 或 magpie plugin off cf-jev
65
+ ```
66
+
67
+ ## 什么时候不再需要它
68
+
69
+ 上游 `yetone/magpie` 把 `DecideAnswer` 改成「还剩一层 `result` 就继续拆」之后:
70
+
71
+ - magpie 自带的 `cloudflare-jev` 供应商会直接可用,这个插件就可以删掉;
72
+ - 补丁已经写好并验证过(`TestCloudflareAnswerPeelsTheEnvelopes` 在旧代码上红、新代码上绿),
73
+ 只是还没有 PR 权限能推上去。
74
+
75
+ ## 文件
76
+
77
+ | 文件 | 作用 |
78
+ | --- | --- |
79
+ | `index.mjs` | 插件本体:`config` 声明决策供应商,`auth.loader` 返回自带 `fetch` 的实现,`peel()` 拆信封 |
80
+ | `smoke.mjs` | 不经 magpie、直接按 host 的方式调 `fetch` 的自检(token 从 magpie 的 `providers.json` 读,不落盘) |
81
+ | `peel.test.mjs` | `peel()` 的单元测试:两层信封、一层信封、错误体、已经扁平的答案 |
82
+
83
+ ```sh
84
+ node --test peel.test.mjs # 单元测试
85
+ node smoke.mjs # 打真实 Cloudflare
86
+ ```
package/index.mjs ADDED
@@ -0,0 +1,188 @@
1
+ // cf-jev: Cloudflare Workers AI as a magpie decision provider.
2
+ //
3
+ // magpie can serve a plugin's decision API (its config hook's `decide`, a
4
+ // model's `decides`), asked at the provider's own base /systemone through
5
+ // the plugin's fetch. That fetch is where this plugin belongs: Workers AI
6
+ // answers a Jev in two envelopes — Cloudflare's own {"result": …} and the
7
+ // Jev's {"state": …,"result": …} — and magpie unwraps one of them
8
+ // (provider.DecideAnswer), so a Jev asked directly comes back with no
9
+ // answers at the top and every System One client refuses it (magpie's own
10
+ // classifier included). Here the envelopes are peeled before the answer
11
+ // leaves the plugin, so magpie and its clients see the System One answer
12
+ // as TypeSafe's own API and a Clef give it: `answers` at the top.
13
+ //
14
+ // The upstream fix (peel while another envelope is there) has been sent
15
+ // to yetone/magpie; until it ships, this plugin is the way to route a
16
+ // group's classifier through Workers AI.
17
+
18
+ const PROVIDER = "cf-jev";
19
+ const API = "https://api.cloudflare.com/client/v4";
20
+
21
+ // MODELS are the decision models Workers AI serves, and how each is asked:
22
+ // a Jev is a run of typesafe/jev whose question is the run's input, while
23
+ // a Clef is asked as it is, at its own path, naming itself.
24
+ const MODELS = [
25
+ { id: "typesafe/jev", name: "Jev (Workers AI)", clef: "" },
26
+ { id: "@cf/cloudflare/clef", name: "Clef", clef: "clef" },
27
+ { id: "@cf/cloudflare/clef-flash", name: "Clef Flash", clef: "clef-flash" },
28
+ ];
29
+
30
+ // modelOf is the model a System One question names. A probe asks with no
31
+ // model, or with the Jev's own name (jev-latest, jev-preview, as
32
+ // TypeSafe's API calls it): that is the Jev Workers AI runs.
33
+ const modelOf = (id) => {
34
+ const said = String(id ?? "").trim();
35
+ const hit = MODELS.find((m) => m.id === said);
36
+ if (hit) return hit;
37
+ return /^(|(typesafe\/)?jev(-latest|-preview)?)$/.test(said) ? MODELS[0] : undefined;
38
+ };
39
+
40
+ // peel is an answer out of whatever envelopes it came in: Cloudflare's own
41
+ // {"result": …}, the Jev's {"state": …,"result": …}, or neither. It stops
42
+ // as soon as the body has answers of its own, so a failure's errors and an
43
+ // already-flat answer are left as they came.
44
+ export function peel(body) {
45
+ let cur = body;
46
+ for (let i = 0; i < 4; i++) {
47
+ if (!cur || typeof cur !== "object") return cur;
48
+ if (cur.answers) return cur;
49
+ if (!cur.result || typeof cur.result !== "object") return cur;
50
+ cur = cur.result;
51
+ }
52
+ return cur;
53
+ }
54
+
55
+ // account is the Cloudflare account the token works in: the one the key
56
+ // names (a key pasted as "<account id>:<token>" says so), else the first
57
+ // the token can list, as magpie's built-in provider looks it up.
58
+ const accounts = new Map();
59
+ async function account(token, named) {
60
+ if (named) return named;
61
+ if (accounts.has(token)) return accounts.get(token);
62
+ const res = await fetch(`${API}/accounts`, {
63
+ headers: { Authorization: `Bearer ${token}` },
64
+ });
65
+ const body = await res.json().catch(() => null);
66
+ if (!res.ok || !Array.isArray(body?.result) || !body.result[0]?.id) {
67
+ const why = body?.errors?.[0]?.message ?? `${res.status} ${res.statusText}`;
68
+ throw new Error(
69
+ `Cloudflare listed no account for this token: ${why}. ` +
70
+ `A token made for Workers AI alone may not list accounts — sign in with "<account id>:<token>" then.`,
71
+ );
72
+ }
73
+ accounts.set(token, body.result[0].id);
74
+ return body.result[0].id;
75
+ }
76
+
77
+ // ask posts a System One question to Workers AI the way it takes it, and
78
+ // gives back the answer as System One gives it: answers at the top.
79
+ export async function ask({ token, named, model, question, signal }) {
80
+ // magpie probes a decision API while it lists the provider, before anyone
81
+ // has signed in: told so plainly rather than as a failed account lookup
82
+ if (!token) return { status: 401, body: "cf-jev is not signed in: magpie plugin login cf-jev" };
83
+ const m = modelOf(model);
84
+ if (!m) throw new Error(`${PROVIDER} does not serve ${model}`); const acct = await account(token, named);
85
+ const url = m.clef
86
+ ? `${API}/accounts/${acct}/ai/run/${m.id}`
87
+ : `${API}/accounts/${acct}/ai/run`;
88
+ const body = m.clef
89
+ ? { model: m.clef, state: question.state, questions: question.questions }
90
+ : { model: m.id, input: { state: question.state, questions: question.questions } };
91
+ const res = await fetch(url, {
92
+ method: "POST",
93
+ headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
94
+ body: JSON.stringify(body),
95
+ signal,
96
+ });
97
+ const raw = await res.text();
98
+ let parsed;
99
+ try {
100
+ parsed = JSON.parse(raw);
101
+ } catch {
102
+ return { status: 502, body: raw || `${res.status} ${res.statusText}` };
103
+ }
104
+ if (!res.ok || parsed.success === false) {
105
+ const why =
106
+ parsed?.errors?.map?.((e) => e?.message).filter(Boolean).join("; ") ||
107
+ `${res.status} ${res.statusText}`;
108
+ return { status: res.ok ? 502 : res.status, body: why };
109
+ }
110
+ return { status: 200, body: JSON.stringify(peel(parsed)) };
111
+ }
112
+
113
+ export async function CfJevPlugin() {
114
+ return {
115
+ config: async (cfg) => {
116
+ cfg.provider ??= {};
117
+ const was = cfg.provider[PROVIDER] ?? {};
118
+ cfg.provider[PROVIDER] = {
119
+ name: "Cloudflare Workers AI · Jev (cf-jev)",
120
+ ...was,
121
+ // magpie's own field: this provider serves a decision API
122
+ // (System One), asked at its base /systemone through this fetch
123
+ decide: true,
124
+ models: {
125
+ ...Object.fromEntries(MODELS.map((m) => [m.id, { name: m.name, decides: true }])),
126
+ ...(was.models ?? {}),
127
+ },
128
+ };
129
+ },
130
+ auth: {
131
+ provider: PROVIDER,
132
+ methods: [
133
+ {
134
+ type: "api",
135
+ label: "Cloudflare API token (Workers AI)",
136
+ placeholder: "token, or <account id>:<token>",
137
+ },
138
+ ],
139
+ async loader(getAuth) {
140
+ const auth = (await getAuth()) ?? {};
141
+ const raw = String(auth.key ?? auth.apiKey ?? "").trim();
142
+ // a key pasted as "<account id>:<token>" names the account itself,
143
+ // which a token made for Workers AI alone can't be asked for
144
+ const cut = raw.indexOf(":");
145
+ const named = cut > 0 ? raw.slice(0, cut).trim() : "";
146
+ const token = cut > 0 ? raw.slice(cut + 1).trim() : raw;
147
+ return {
148
+ apiKey: token,
149
+ // where magpie thinks the provider's requests go: this fetch is
150
+ // what answers them, so it is told a Cloudflare-shaped base and
151
+ // never asked to reach it
152
+ baseURL: API,
153
+ async fetch(input, init) {
154
+ const url = typeof input === "string" ? input : (input?.url ?? String(input));
155
+ const path = (() => {
156
+ try {
157
+ return new URL(url).pathname;
158
+ } catch {
159
+ return url;
160
+ }
161
+ })();
162
+ if (!path.endsWith("/systemone")) {
163
+ return new Response(`cf-jev serves System One only: ${path}`, { status: 404 });
164
+ }
165
+ let question;
166
+ try {
167
+ question = JSON.parse(String(init?.body ?? "{}"));
168
+ } catch {
169
+ return new Response("cf-jev got no System One question", { status: 400 });
170
+ }
171
+ const out = await ask({
172
+ token,
173
+ named,
174
+ model: question.model,
175
+ question,
176
+ signal: init?.signal,
177
+ });
178
+ if (out.status !== 200) return new Response(out.body, { status: out.status });
179
+ return new Response(out.body, {
180
+ status: 200,
181
+ headers: { "Content-Type": "application/json" },
182
+ });
183
+ },
184
+ };
185
+ },
186
+ },
187
+ };
188
+ }
package/package.json CHANGED
@@ -1,6 +1,28 @@
1
1
  {
2
2
  "name": "@weiping/magpie-cf-jev",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Cloudflare Workers AI (Jev and Clef) as a magpie decision provider, with the answers read out of Cloudflare's envelope",
5
+ "type": "module",
6
+ "main": "./index.mjs",
7
+ "exports": "./index.mjs",
8
+ "files": ["index.mjs", "README.md"],
9
+ "keywords": [
10
+ "magpie",
11
+ "magpie-plugin",
12
+ "opencode",
13
+ "opencode-plugin",
14
+ "cloudflare",
15
+ "workers-ai",
16
+ "jev",
17
+ "clef",
18
+ "system-one"
19
+ ],
20
+ "author": "weiping",
21
+ "license": "MIT",
22
+ "publishConfig": {
23
+ "access": "public"
24
+ },
25
+ "scripts": {
26
+ "test": "node --test peel.test.mjs"
27
+ }
28
+ }