converse-mcp-server 2.22.1 → 2.22.3

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.
package/.env.example CHANGED
@@ -77,6 +77,10 @@ OPENROUTER_REFERER=https://github.com/FallDownTheSystem/converse
77
77
  # WARNING: Interactive policies may cause hangs in server/headless mode
78
78
  # CODEX_APPROVAL_POLICY=never
79
79
 
80
+ # Default Codex model (default: gpt-5.5)
81
+ # Options: gpt-5.5, gpt-5.3-codex, gpt-5-codex
82
+ # CODEX_MODEL=gpt-5.5
83
+
80
84
  # ============================================
81
85
  # Server Configuration
82
86
  # ============================================
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "converse-mcp-server",
3
- "version": "2.22.1",
3
+ "version": "2.22.3",
4
4
  "description": "Converse MCP Server - Converse with other LLMs with chat and consensus tools",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -94,14 +94,14 @@
94
94
  ".env.example"
95
95
  ],
96
96
  "dependencies": {
97
- "@anthropic-ai/claude-agent-sdk": "^0.2.110",
98
- "@anthropic-ai/sdk": "^0.86.1",
97
+ "@anthropic-ai/claude-agent-sdk": "^0.2.118",
98
+ "@anthropic-ai/sdk": "^0.90.0",
99
99
  "@github/copilot-sdk": "^0.2.2",
100
100
  "@google/genai": "^1.50.1",
101
- "@mistralai/mistralai": "^2.2.0",
101
+ "@mistralai/mistralai": "^2.2.1",
102
102
  "@modelcontextprotocol/sdk": "^1.29.0",
103
- "@openai/codex-sdk": "^0.118.0",
104
- "ai": "^6.0.164",
103
+ "@openai/codex-sdk": "^0.123.0",
104
+ "ai": "^6.0.168",
105
105
  "ai-sdk-provider-gemini-cli": "^2.0.1",
106
106
  "cors": "^2.8.6",
107
107
  "dotenv": "^17.4.2",
@@ -110,14 +110,14 @@
110
110
  "nanoid": "^5.1.9",
111
111
  "openai": "^6.34.0",
112
112
  "p-limit": "^7.3.0",
113
- "vite": "^8.0.8"
113
+ "vite": "^8.0.10"
114
114
  },
115
115
  "devDependencies": {
116
- "@vitest/coverage-v8": "^4.1.4",
116
+ "@vitest/coverage-v8": "^4.1.5",
117
117
  "cross-env": "^10.1.0",
118
- "eslint": "^10.2.0",
118
+ "eslint": "^10.2.1",
119
119
  "prettier": "^3.8.3",
120
120
  "rimraf": "^6.1.3",
121
- "vitest": "^4.1.4"
121
+ "vitest": "^4.1.5"
122
122
  }
123
123
  }
package/src/config.js CHANGED
@@ -276,6 +276,12 @@ const CONFIG_SCHEMA = {
276
276
  description:
277
277
  'Approval policy (never | untrusted | on-failure | on-request)',
278
278
  },
279
+ CODEX_MODEL: {
280
+ type: 'string',
281
+ default: 'gpt-5.5',
282
+ description:
283
+ 'Default Codex model (e.g., gpt-5.5, gpt-5.3-codex, gpt-5-codex)',
284
+ },
279
285
 
280
286
  // Copilot configuration
281
287
  COPILOT_TOOL_ACCESS: {
@@ -21,22 +21,24 @@ import { normalizeExtendedPath } from '../utils/pathUtils.js';
21
21
  const SUPPORTED_MODELS = {
22
22
  codex: {
23
23
  modelName: 'codex',
24
- friendlyName: 'OpenAI Codex (GPT-5.3)',
24
+ friendlyName: 'OpenAI Codex (GPT-5.5)',
25
25
  contextWindow: 400000,
26
26
  maxOutputTokens: 128000,
27
27
  supportsStreaming: true,
28
- supportsImages: false, // Codex doesn't support images
28
+ supportsImages: true, // Codex SDK 0.118+ supports images via --image (local_image input)
29
29
  supportsTemperature: false, // Codex manages temperature internally
30
30
  supportsWebSearch: false, // Codex accesses files directly, not web
31
31
  timeout: 600000, // 10 minutes
32
32
  description:
33
- 'OpenAI Codex agentic coding assistant with local file access and tool execution (GPT-5.3-Codex)',
33
+ 'OpenAI Codex agentic coding assistant with local file access and tool execution (GPT-5.5)',
34
34
  aliases: [
35
35
  'gpt-5-codex',
36
36
  'gpt5-codex',
37
37
  'gpt-5.2-codex',
38
38
  'gpt-5.3-codex',
39
39
  'gpt5.3-codex',
40
+ 'gpt-5.5',
41
+ 'gpt5.5',
40
42
  ],
41
43
  },
42
44
  };
@@ -91,14 +93,19 @@ async function getCodexSDK() {
91
93
  }
92
94
 
93
95
  /**
94
- * Convert message array to single prompt for Codex
95
- * Codex expects single prompts, not message history
96
+ * Convert message array to Codex SDK Input (string | UserInput[])
97
+ * Codex expects single prompts (new thread) or incremental input (resumed thread);
98
+ * history is managed SDK-side.
96
99
  *
97
- * Strategy:
98
- * - For new threads: Extract last user message only
99
- * - For resumed threads: Same - Codex maintains history internally
100
+ * Returns a plain string when the last user message is text-only, or an array
101
+ * of { type: 'text' | 'local_image' } parts when images are present. The SDK
102
+ * passes local_image paths to the CLI via --image.
103
+ *
104
+ * Images must be on-disk files — Converse stores the original path in
105
+ * metadata.path (chat.js / consensus.js set includeMetadata: true). Images
106
+ * without a path (e.g. pasted base64 with no metadata) are skipped.
100
107
  */
101
- function convertMessagesToPrompt(messages) {
108
+ function convertMessagesToCodexInput(messages) {
102
109
  if (!Array.isArray(messages)) {
103
110
  throw new CodexProviderError(
104
111
  'Messages must be an array',
@@ -113,7 +120,6 @@ function convertMessagesToPrompt(messages) {
113
120
  );
114
121
  }
115
122
 
116
- // Find last user message
117
123
  const lastUserMessage = messages.filter((m) => m.role === 'user').pop();
118
124
 
119
125
  if (!lastUserMessage) {
@@ -123,28 +129,46 @@ function convertMessagesToPrompt(messages) {
123
129
  );
124
130
  }
125
131
 
126
- // Extract text content from message
127
132
  if (typeof lastUserMessage.content === 'string') {
128
133
  return lastUserMessage.content;
129
134
  }
130
135
 
131
- // Handle array content (multimodal format)
132
136
  if (Array.isArray(lastUserMessage.content)) {
133
- const textParts = lastUserMessage.content
134
- .filter((item) => item.type === 'text')
135
- .map((item) => item.text);
137
+ const parts = [];
138
+ let droppedImages = 0;
139
+ for (const item of lastUserMessage.content) {
140
+ if (item.type === 'text' && item.text) {
141
+ parts.push({ type: 'text', text: item.text });
142
+ } else if (item.type === 'image') {
143
+ const imagePath = item.metadata?.path || item.metadata?.originalPath;
144
+ if (imagePath) {
145
+ parts.push({ type: 'local_image', path: imagePath });
146
+ } else {
147
+ droppedImages += 1;
148
+ }
149
+ }
150
+ }
136
151
 
137
- // Log warning if images present (Codex doesn't support images)
138
- const hasImages = lastUserMessage.content.some(
139
- (item) => item.type === 'image',
140
- );
141
- if (hasImages) {
152
+ if (droppedImages > 0) {
142
153
  debugLog(
143
- '[Codex] Warning: Images in message will be ignored (Codex does not support multimodal input)',
154
+ `[Codex] Skipped ${droppedImages} image(s) without a file path Codex requires on-disk images`,
144
155
  );
145
156
  }
146
157
 
147
- return textParts.join('\n');
158
+ if (parts.length === 0) {
159
+ throw new CodexProviderError(
160
+ 'Message contained no usable text or image parts',
161
+ ErrorCodes.INVALID_MESSAGES,
162
+ );
163
+ }
164
+
165
+ // Collapse to plain string when there are no images — keeps the non-image
166
+ // path identical to the legacy behavior and slightly simpler for the SDK.
167
+ if (parts.every((p) => p.type === 'text')) {
168
+ return parts.map((p) => p.text).join('\n');
169
+ }
170
+
171
+ return parts;
148
172
  }
149
173
 
150
174
  throw new CodexProviderError(
@@ -153,6 +177,18 @@ function convertMessagesToPrompt(messages) {
153
177
  );
154
178
  }
155
179
 
180
+ /**
181
+ * Extract the combined text from a Codex SDK Input for prompt-based checks
182
+ * like $imagegen detection.
183
+ */
184
+ function extractPromptText(input) {
185
+ if (typeof input === 'string') return input;
186
+ return input
187
+ .filter((p) => p.type === 'text')
188
+ .map((p) => p.text)
189
+ .join('\n\n');
190
+ }
191
+
156
192
  /**
157
193
  * Get thread ID from continuation metadata
158
194
  * Codex thread IDs are stored in continuation store for resumption
@@ -185,12 +221,14 @@ function mapReasoningEffort(effort) {
185
221
  }
186
222
 
187
223
  /**
188
- * Create stream generator for Codex streaming responses
189
- * Yields raw Codex SDK events that will be normalized by ProviderStreamNormalizer
224
+ * Create stream generator for Codex streaming responses.
225
+ * `input` is the Codex SDK Input (string | UserInput[]) strings for plain
226
+ * text turns, arrays when images are attached.
227
+ * Yields raw Codex SDK events that will be normalized by ProviderStreamNormalizer.
190
228
  */
191
- async function* createStreamingGenerator(thread, prompt, signal) {
229
+ async function* createStreamingGenerator(thread, input, signal) {
192
230
  try {
193
- const { events } = await thread.runStreamed(prompt, { signal });
231
+ const { events } = await thread.runStreamed(input, { signal });
194
232
 
195
233
  for await (const event of events) {
196
234
  // Check for cancellation
@@ -256,8 +294,9 @@ export const codexProvider = {
256
294
  // Get Codex SDK
257
295
  const Codex = await getCodexSDK();
258
296
 
259
- // Convert messages to prompt
260
- const prompt = convertMessagesToPrompt(messages);
297
+ // Convert messages to Codex SDK input (string or structured parts with images)
298
+ const input = convertMessagesToCodexInput(messages);
299
+ const promptText = extractPromptText(input);
261
300
 
262
301
  // Get thread ID if resuming conversation
263
302
  const threadId =
@@ -289,7 +328,7 @@ export const codexProvider = {
289
328
  // into image generation via $imagegen — otherwise Codex can't save the
290
329
  // generated file. Leave higher modes (workspace-write, danger-full-access)
291
330
  // alone so an explicit user choice is never downgraded or escalated.
292
- const wantsImageGen = /\$imagegen\b/i.test(prompt);
331
+ const wantsImageGen = /\$imagegen\b/i.test(promptText);
293
332
  const sandboxMode =
294
333
  wantsImageGen && configuredSandboxMode === 'read-only'
295
334
  ? 'workspace-write'
@@ -307,6 +346,7 @@ export const codexProvider = {
307
346
 
308
347
  // Create or resume thread
309
348
  const threadOptions = {
349
+ model: config.providers?.codexmodel,
310
350
  workingDirectory,
311
351
  sandboxMode,
312
352
  skipGitRepoCheck,
@@ -324,12 +364,12 @@ export const codexProvider = {
324
364
  // WORKAROUND: SDK's thread.run() hangs due to missing break after turn.completed
325
365
  // Always use streaming internally, consume synchronously when stream=false
326
366
  if (stream) {
327
- return createStreamingGenerator(thread, prompt, signal);
367
+ return createStreamingGenerator(thread, input, signal);
328
368
  }
329
369
 
330
370
  // Synchronous mode: consume streaming internally and return complete response
331
371
  const startTime = Date.now();
332
- const generator = createStreamingGenerator(thread, prompt, signal);
372
+ const generator = createStreamingGenerator(thread, input, signal);
333
373
 
334
374
  let content = '';
335
375
  let usage = null;