惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

M
MIT News - Artificial intelligence
雷峰网
雷峰网
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Last Week in AI
Last Week in AI
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
阮一峰的网络日志
阮一峰的网络日志
月光博客
月光博客
博客园 - Franky
腾讯CDC
T
Tailwind CSS Blog
Recent Announcements
Recent Announcements
V
V2EX
N
Netflix TechBlog - Medium
量子位
Jina AI
Jina AI
Y
Y Combinator Blog
The GitHub Blog
The GitHub Blog
G
Google Developers Blog
爱范儿
爱范儿
博客园 - 叶小钗
D
Docker
MongoDB | Blog
MongoDB | Blog
D
DataBreaches.Net
T
The Blog of Author Tim Ferriss

Recent Commits to openclaw:main

test: merge chat side-result checks · openclaw/openclaw@ddd2c2a test: merge cron history checks · openclaw/openclaw@f7eb746 test: merge responsive navigation shell checks · openclaw/openclaw@c2e4b47 docs(changelog): add codex oauth fixes · openclaw/openclaw@628e6cd test: merge navigation routing cases · openclaw/openclaw@5d8cecb Tests: mock channel registry bundled fallback · openclaw/openclaw@2b08233 Secrets: avoid broad web search discovery for single plugin config · openclaw/openclaw@a464f59 test: merge config view browser checks · openclaw/openclaw@20cf511 fix(status): align oauth health with runtime · openclaw/openclaw@eed7116 feat: add macOS screen snapshots for monitor preview (#67954) thanks … · openclaw/openclaw@f377db1 fix: report shared auth scopes in hello-ok (#67810) thanks @BunsDev · openclaw/openclaw@0b6c39b Auto-reply: avoid eager bundled route fallback · openclaw/openclaw@3ea1bf4 Tests: narrow session binding contract setup · openclaw/openclaw@54e4e16 fix(macOS): enable undo/redo in webchat composer text input (#34962) · openclaw/openclaw@00951dc Tests: speed up channel setup promotion · openclaw/openclaw@82b529a Docs: refresh agent instructions · openclaw/openclaw@5775fe2 fix(auth): serialize OAuth refresh across agents to fix #26322 (#67876) · openclaw/openclaw@8e79080 test: allow ollama public surface boundary test · openclaw/openclaw@7d4f1a6 Docs: add test performance guardrails · openclaw/openclaw@89706d3 Tests: restore context-engine usage proof · openclaw/openclaw@e4c4f95 Tests: slim context engine runtime coverage · openclaw/openclaw@74c198f ci: retry failed custom checkouts · openclaw/openclaw@0ee5baf test: trim duplicate provider auth onboarding cases · openclaw/openclaw@1ffc02e matrix: fix sessions_spawn --thread subagent session spawning (#67643) · openclaw/openclaw@1ce2596 test: reduce auth choice fixture churn · openclaw/openclaw@857b9cd test: mock health status config boundaries · openclaw/openclaw@9d5ab4a test: mock onboard config io boundary · openclaw/openclaw@299694d test: mock legacy state plugin boundaries · openclaw/openclaw@2713089 test: mock channel install boundaries · openclaw/openclaw@b945248 test: mock doctor preview channel boundaries · openclaw/openclaw@b1a3ad4
fix: support codex app-server image understanding · openc...
steipete · 2026-04-24 · via Recent Commits to openclaw:main

@@ -0,0 +1,262 @@

1+

import { describe, expect, it, vi } from "vitest";

2+

import { buildCodexMediaUnderstandingProvider } from "./media-understanding-provider.js";

3+

import type { CodexAppServerClient } from "./src/app-server/client.js";

4+

import type { CodexServerNotification, JsonValue } from "./src/app-server/protocol.js";

5+6+

function codexModel(inputModalities: string[] = ["text", "image"]) {

7+

return {

8+

id: "gpt-5.4",

9+

model: "gpt-5.4",

10+

upgrade: null,

11+

upgradeInfo: null,

12+

availabilityNux: null,

13+

displayName: "gpt-5.4",

14+

description: "GPT-5.4",

15+

hidden: false,

16+

supportedReasoningEfforts: [{ reasoningEffort: "low", description: "fast" }],

17+

defaultReasoningEffort: "low",

18+

inputModalities,

19+

supportsPersonality: false,

20+

additionalSpeedTiers: [],

21+

isDefault: true,

22+

};

23+

}

24+25+

function threadStartResult() {

26+

return {

27+

thread: {

28+

id: "thread-1",

29+

forkedFromId: null,

30+

preview: "",

31+

ephemeral: true,

32+

modelProvider: "openai",

33+

createdAt: 1,

34+

updatedAt: 1,

35+

status: { type: "idle" },

36+

path: null,

37+

cwd: "/tmp/openclaw-agent",

38+

cliVersion: "0.118.0",

39+

source: "unknown",

40+

agentNickname: null,

41+

agentRole: null,

42+

gitInfo: null,

43+

name: null,

44+

turns: [],

45+

},

46+

model: "gpt-5.4",

47+

modelProvider: "openai",

48+

serviceTier: null,

49+

cwd: "/tmp/openclaw-agent",

50+

instructionSources: [],

51+

approvalPolicy: "never",

52+

approvalsReviewer: "user",

53+

sandbox: { type: "dangerFullAccess" },

54+

permissionProfile: null,

55+

reasoningEffort: null,

56+

};

57+

}

58+59+

function turnStartResult(status = "inProgress", items: JsonValue[] = []) {

60+

return {

61+

turn: {

62+

id: "turn-1",

63+

status,

64+

items,

65+

error: null,

66+

startedAt: null,

67+

completedAt: null,

68+

durationMs: null,

69+

},

70+

};

71+

}

72+73+

function createFakeClient(options?: {

74+

inputModalities?: string[];

75+

completeWithItems?: boolean;

76+

notifyError?: string;

77+

}) {

78+

const notifications = new Set<(notification: CodexServerNotification) => void>();

79+

const requests: Array<{ method: string; params?: JsonValue }> = [];

80+

const request = vi.fn(async (method: string, params?: JsonValue) => {

81+

requests.push({ method, params });

82+

if (method === "model/list") {

83+

return {

84+

data: [codexModel(options?.inputModalities)],

85+

nextCursor: null,

86+

};

87+

}

88+

if (method === "thread/start") {

89+

return threadStartResult();

90+

}

91+

if (method === "turn/start") {

92+

if (options?.notifyError) {

93+

for (const notify of notifications) {

94+

notify({

95+

method: "error",

96+

params: {

97+

threadId: "thread-1",

98+

turnId: "turn-1",

99+

error: {

100+

message: options.notifyError,

101+

codexErrorInfo: null,

102+

additionalDetails: null,

103+

},

104+

willRetry: false,

105+

},

106+

});

107+

}

108+

} else if (!options?.completeWithItems) {

109+

for (const notify of notifications) {

110+

notify({

111+

method: "item/agentMessage/delta",

112+

params: {

113+

threadId: "thread-1",

114+

turnId: "turn-1",

115+

itemId: "msg-1",

116+

delta: "A red square.",

117+

},

118+

});

119+

notify({

120+

method: "turn/completed",

121+

params: {

122+

threadId: "thread-1",

123+

turnId: "turn-1",

124+

turn: turnStartResult("completed").turn,

125+

},

126+

});

127+

}

128+

}

129+

return turnStartResult(

130+

options?.completeWithItems ? "completed" : "inProgress",

131+

options?.completeWithItems

132+

? [

133+

{

134+

id: "msg-1",

135+

type: "agentMessage",

136+

text: "A blue circle.",

137+

phase: null,

138+

memoryCitation: null,

139+

},

140+

]

141+

: [],

142+

);

143+

}

144+

return {};

145+

});

146+147+

const client = {

148+

request,

149+

addNotificationHandler(handler: (notification: CodexServerNotification) => void) {

150+

notifications.add(handler);

151+

return () => notifications.delete(handler);

152+

},

153+

} as unknown as CodexAppServerClient;

154+155+

return { client, requests };

156+

}

157+158+

describe("codex media understanding provider", () => {

159+

it("runs image understanding through a bounded Codex app-server turn", async () => {

160+

const { client, requests } = createFakeClient();

161+

const provider = buildCodexMediaUnderstandingProvider({

162+

clientFactory: async () => client,

163+

});

164+165+

const result = await provider.describeImage?.({

166+

buffer: Buffer.from("image-bytes"),

167+

fileName: "image.png",

168+

mime: "image/png",

169+

provider: "codex",

170+

model: "gpt-5.4",

171+

prompt: "Describe briefly.",

172+

timeoutMs: 30_000,

173+

cfg: {},

174+

agentDir: "/tmp/openclaw-agent",

175+

});

176+177+

expect(result).toEqual({ text: "A red square.", model: "gpt-5.4" });

178+

expect(requests.map((entry) => entry.method)).toEqual([

179+

"model/list",

180+

"thread/start",

181+

"turn/start",

182+

]);

183+

expect(requests[1]?.params).toMatchObject({

184+

model: "gpt-5.4",

185+

modelProvider: "openai",

186+

approvalPolicy: "never",

187+

sandbox: "read-only",

188+

dynamicTools: [],

189+

ephemeral: true,

190+

persistExtendedHistory: false,

191+

});

192+

expect(requests[2]?.params).toMatchObject({

193+

threadId: "thread-1",

194+

approvalPolicy: "never",

195+

model: "gpt-5.4",

196+

input: [

197+

{ type: "text", text: "Describe briefly.", text_elements: [] },

198+

{ type: "image", url: "data:image/png;base64,aW1hZ2UtYnl0ZXM=" },

199+

],

200+

});

201+

});

202+203+

it("extracts text from terminal turn items", async () => {

204+

const { client } = createFakeClient({ completeWithItems: true });

205+

const provider = buildCodexMediaUnderstandingProvider({

206+

clientFactory: async () => client,

207+

});

208+209+

const result = await provider.describeImages?.({

210+

images: [{ buffer: Buffer.from("image-bytes"), fileName: "image.png", mime: "image/png" }],

211+

provider: "codex",

212+

model: "gpt-5.4",

213+

prompt: "Describe briefly.",

214+

timeoutMs: 30_000,

215+

cfg: {},

216+

agentDir: "/tmp/openclaw-agent",

217+

});

218+219+

expect(result).toEqual({ text: "A blue circle.", model: "gpt-5.4" });

220+

});

221+222+

it("rejects text-only Codex app-server models before starting a turn", async () => {

223+

const { client, requests } = createFakeClient({ inputModalities: ["text"] });

224+

const provider = buildCodexMediaUnderstandingProvider({

225+

clientFactory: async () => client,

226+

});

227+228+

await expect(

229+

provider.describeImage?.({

230+

buffer: Buffer.from("image-bytes"),

231+

fileName: "image.png",

232+

mime: "image/png",

233+

provider: "codex",

234+

model: "gpt-5.4",

235+

timeoutMs: 30_000,

236+

cfg: {},

237+

agentDir: "/tmp/openclaw-agent",

238+

}),

239+

).rejects.toThrow("Codex app-server model does not support images: gpt-5.4");

240+

expect(requests.map((entry) => entry.method)).toEqual(["model/list"]);

241+

});

242+243+

it("surfaces Codex app-server turn errors", async () => {

244+

const { client } = createFakeClient({ notifyError: "vision unavailable" });

245+

const provider = buildCodexMediaUnderstandingProvider({

246+

clientFactory: async () => client,

247+

});

248+249+

await expect(

250+

provider.describeImage?.({

251+

buffer: Buffer.from("image-bytes"),

252+

fileName: "image.png",

253+

mime: "image/png",

254+

provider: "codex",

255+

model: "gpt-5.4",

256+

timeoutMs: 30_000,

257+

cfg: {},

258+

agentDir: "/tmp/openclaw-agent",

259+

}),

260+

).rejects.toThrow("vision unavailable");

261+

});

262+

});