@oh-my-pi/pi-coding-agent 16.1.11 → 16.1.12

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.
@@ -1,5 +1,5 @@
1
- import type { Context, Message, Tool } from "@oh-my-pi/pi-ai";
2
- import { toolWireSchema } from "@oh-my-pi/pi-ai/utils/schema";
1
+ import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
2
+ import type { AssistantMessage, Context, ImageContent, Message, TextContent } from "@oh-my-pi/pi-ai";
3
3
  import type { SessionContext } from "../session/session-context";
4
4
  import { compileSecretRegex } from "./regex";
5
5
 
@@ -15,6 +15,9 @@ export interface SecretEntry {
15
15
  flags?: string;
16
16
  }
17
17
 
18
+ export type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue | undefined };
19
+ export type JsonRecord = { [key: string]: JsonValue | undefined };
20
+
18
21
  // ═══════════════════════════════════════════════════════════════════════════
19
22
  // Deterministic replacement generation
20
23
  // ═══════════════════════════════════════════════════════════════════════════
@@ -85,26 +88,34 @@ export class SecretObfuscator {
85
88
 
86
89
  constructor(entries: SecretEntry[]) {
87
90
  let index = 0;
91
+ let hasRealSec = false;
88
92
  for (const entry of entries) {
89
93
  const mode = entry.mode ?? "obfuscate";
90
94
 
91
95
  if (entry.type === "plain") {
92
96
  if (mode === "obfuscate") {
97
+ if (entry.content.length < 8) {
98
+ // Tone down short plain secret obfuscation to avoid false matches on small words like "esp"
99
+ continue;
100
+ }
93
101
  const placeholder = buildPlaceholder(index);
94
102
  this.#plainMappings.set(entry.content, index);
95
103
  this.#obfuscateMappings.set(index, { secret: entry.content, placeholder });
96
104
  this.#deobfuscateMap.set(placeholder, entry.content);
97
105
  index++;
106
+ hasRealSec = true;
98
107
  } else {
99
108
  // replace mode
100
109
  const replacement = entry.replacement ?? generateDeterministicReplacement(entry.content);
101
110
  this.#replaceMappings.set(entry.content, replacement);
111
+ hasRealSec = true;
102
112
  }
103
113
  } else {
104
114
  // regex type — compiled here, matches discovered during obfuscate()
105
115
  try {
106
116
  const regex = compileSecretRegex(entry.content, entry.flags);
107
117
  this.#regexEntries.push({ regex, mode, replacement: entry.replacement });
118
+ hasRealSec = true;
108
119
  } catch {
109
120
  // Invalid regex — skip silently (validation happens at load time)
110
121
  }
@@ -112,7 +123,7 @@ export class SecretObfuscator {
112
123
  }
113
124
 
114
125
  this.#nextIndex = index;
115
- this.#hasAny = entries.length > 0;
126
+ this.#hasAny = hasRealSec;
116
127
  }
117
128
 
118
129
  hasSecrets(): boolean {
@@ -154,6 +165,10 @@ export class SecretObfuscator {
154
165
  const replacement = entry.replacement ?? generateDeterministicReplacement(matchValue);
155
166
  result = replaceAll(result, matchValue, replacement);
156
167
  } else {
168
+ if (matchValue.length < 8) {
169
+ // Tone down short regex match obfuscation to avoid false matches on small words/fragments
170
+ continue;
171
+ }
157
172
  // obfuscate mode — get or create stable index
158
173
  let index = this.#findObfuscateIndex(matchValue);
159
174
  if (index === undefined) {
@@ -174,30 +189,13 @@ export class SecretObfuscator {
174
189
  /** Deobfuscate obfuscate-mode placeholders back to original secrets. Replace-mode is NOT reversed. */
175
190
  deobfuscate(text: string): string {
176
191
  if (!this.#hasAny || !text.includes("#")) return text;
177
- return text.replace(PLACEHOLDER_RE, match => {
178
- return this.#deobfuscateMap.get(match) ?? match;
179
- });
180
- }
181
-
182
- /** Deep-walk an object, deobfuscating all string values. */
183
- deobfuscateObject<T>(obj: T): T {
184
- if (!this.#hasAny) return obj;
185
- return deepWalkStrings(obj, s => this.deobfuscate(s));
186
- }
187
-
188
- /** Deep-walk an object, obfuscating all string values. */
189
- obfuscateObject<T>(obj: T): T {
190
- if (!this.#hasAny) return obj;
191
- return deepWalkStrings(obj, s => this.obfuscate(s));
192
+ return text.replace(PLACEHOLDER_RE, match => this.#deobfuscateMap.get(match) ?? match);
192
193
  }
193
-
194
194
  /** Find the obfuscate index for a known secret value. */
195
195
  #findObfuscateIndex(secret: string): number | undefined {
196
- // Check plain mappings first
197
196
  const plainIndex = this.#plainMappings.get(secret);
198
197
  if (plainIndex !== undefined) return plainIndex;
199
198
 
200
- // Check regex-discovered mappings
201
199
  for (const [index, mapping] of this.#obfuscateMappings) {
202
200
  if (mapping.secret === secret) return index;
203
201
  }
@@ -205,47 +203,200 @@ export class SecretObfuscator {
205
203
  }
206
204
  }
207
205
 
206
+ // ═══════════════════════════════════════════════════════════════════════════
207
+ // Display restore (inbound, persisted/provider → local display)
208
+ // ═══════════════════════════════════════════════════════════════════════════
209
+
210
+ /**
211
+ * Restore secret placeholders for local display. Only message kinds the model
212
+ * itself authored from obfuscated context carry placeholders — assistant
213
+ * content and the LLM-written branch/compaction summaries. User, developer, and
214
+ * tool-result messages are persisted with their literal text, so a literal
215
+ * `#ABCD#` the operator typed must survive untouched; those roles are never
216
+ * walked.
217
+ */
208
218
  export function deobfuscateSessionContext(
209
219
  sessionContext: SessionContext,
210
220
  obfuscator: SecretObfuscator | undefined,
211
221
  ): SessionContext {
212
222
  if (!obfuscator?.hasSecrets()) return sessionContext;
213
- const messages = obfuscator.deobfuscateObject(sessionContext.messages);
223
+ const messages = deobfuscateAgentMessages(obfuscator, sessionContext.messages);
214
224
  return messages === sessionContext.messages ? sessionContext : { ...sessionContext, messages };
215
225
  }
216
226
 
227
+ export function deobfuscateAgentMessages(obfuscator: SecretObfuscator, messages: AgentMessage[]): AgentMessage[] {
228
+ let changed = false;
229
+ const result = messages.map((message): AgentMessage => {
230
+ switch (message.role) {
231
+ case "assistant": {
232
+ const content = deobfuscateAssistantContent(obfuscator, message.content);
233
+ if (content === message.content) return message;
234
+ changed = true;
235
+ return { ...message, content };
236
+ }
237
+ case "branchSummary": {
238
+ const summary = obfuscator.deobfuscate(message.summary);
239
+ if (summary === message.summary) return message;
240
+ changed = true;
241
+ return { ...message, summary };
242
+ }
243
+ case "compactionSummary": {
244
+ const summary = obfuscator.deobfuscate(message.summary);
245
+ const shortSummary =
246
+ message.shortSummary === undefined ? undefined : obfuscator.deobfuscate(message.shortSummary);
247
+ const blocks = message.blocks === undefined ? undefined : deobfuscateTextBlocks(obfuscator, message.blocks);
248
+ if (summary === message.summary && shortSummary === message.shortSummary && blocks === message.blocks) {
249
+ return message;
250
+ }
251
+ changed = true;
252
+ return { ...message, summary, shortSummary, blocks };
253
+ }
254
+ default:
255
+ return message;
256
+ }
257
+ });
258
+ return changed ? result : messages;
259
+ }
260
+
261
+ /**
262
+ * Restore placeholders in assistant content: visible text, thinking text, and
263
+ * tool-call arguments/intent/rawBlock. Signatures and redacted-thinking bytes
264
+ * are opaque provider-replay data and pass through byte-identical.
265
+ */
266
+ export function deobfuscateAssistantContent(
267
+ obfuscator: SecretObfuscator,
268
+ content: AssistantMessage["content"],
269
+ ): AssistantMessage["content"] {
270
+ if (!obfuscator.hasSecrets()) return content;
271
+ let changed = false;
272
+ const result = content.map((block): AssistantMessage["content"][number] => {
273
+ if (block.type === "text") {
274
+ const text = obfuscator.deobfuscate(block.text);
275
+ if (text === block.text) return block;
276
+ changed = true;
277
+ return { ...block, text };
278
+ }
279
+ if (block.type === "thinking") {
280
+ const thinking = obfuscator.deobfuscate(block.thinking);
281
+ if (thinking === block.thinking) return block;
282
+ changed = true;
283
+ return { ...block, thinking };
284
+ }
285
+ if (block.type === "toolCall") {
286
+ const args = deobfuscateToolArguments(obfuscator, block.arguments);
287
+ const intent = block.intent === undefined ? undefined : obfuscator.deobfuscate(block.intent);
288
+ const rawBlock = block.rawBlock === undefined ? undefined : obfuscator.deobfuscate(block.rawBlock);
289
+ if (args === block.arguments && intent === block.intent && rawBlock === block.rawBlock) return block;
290
+ changed = true;
291
+ return { ...block, arguments: args, intent, rawBlock };
292
+ }
293
+ return block;
294
+ });
295
+ return changed ? result : content;
296
+ }
297
+
298
+ /**
299
+ * Restore placeholders inside a tool call's arguments. Arguments are arbitrary
300
+ * model-authored JSON, so tool-call arguments are the ONLY place a recursive
301
+ * JSON walk runs.
302
+ */
303
+ export function deobfuscateToolArguments(
304
+ obfuscator: SecretObfuscator,
305
+ args: Record<string, unknown>,
306
+ ): Record<string, unknown> {
307
+ if (!obfuscator.hasSecrets()) return args;
308
+ return mapJsonStrings(args as JsonValue, s => obfuscator.deobfuscate(s)) as Record<string, unknown>;
309
+ }
310
+
311
+ /** Redact secrets inside a tool call's arguments (same JSON-walk exception as {@link deobfuscateToolArguments}). */
312
+ export function obfuscateToolArguments(
313
+ obfuscator: SecretObfuscator,
314
+ args: Record<string, unknown>,
315
+ ): Record<string, unknown> {
316
+ if (!obfuscator.hasSecrets()) return args;
317
+ return mapJsonStrings(args as JsonValue, s => obfuscator.obfuscate(s)) as Record<string, unknown>;
318
+ }
319
+
217
320
  // ═══════════════════════════════════════════════════════════════════════════
218
- // Message obfuscation (outbound to LLM)
321
+ // Outbound obfuscation (local provider)
219
322
  // ═══════════════════════════════════════════════════════════════════════════
220
323
 
221
- /** Obfuscate all string content in LLM messages (for outbound interception). */
324
+ type UserFacingMessage = Extract<Message, { role: "user" | "developer" | "toolResult" }>;
325
+
326
+ /** Obfuscate `text` blocks of a content array; image and other blocks pass through. */
327
+ function obfuscateTextBlocks(
328
+ obfuscator: SecretObfuscator,
329
+ content: (TextContent | ImageContent)[],
330
+ ): (TextContent | ImageContent)[] {
331
+ let changed = false;
332
+ const result = content.map((block): TextContent | ImageContent => {
333
+ if (block.type !== "text") return block;
334
+ const text = obfuscator.obfuscate(block.text);
335
+ if (text === block.text) return block;
336
+ changed = true;
337
+ return { ...block, text };
338
+ });
339
+ return changed ? result : content;
340
+ }
341
+
342
+ /** Restore placeholders in `text` blocks of a content array; image and other blocks pass through. */
343
+ function deobfuscateTextBlocks(
344
+ obfuscator: SecretObfuscator,
345
+ content: (TextContent | ImageContent)[],
346
+ ): (TextContent | ImageContent)[] {
347
+ let changed = false;
348
+ const result = content.map((block): TextContent | ImageContent => {
349
+ if (block.type !== "text") return block;
350
+ const text = obfuscator.deobfuscate(block.text);
351
+ if (text === block.text) return block;
352
+ changed = true;
353
+ return { ...block, text };
354
+ });
355
+ return changed ? result : content;
356
+ }
357
+
358
+ /**
359
+ * Redact secrets from outbound messages. Opt-in by origin: only user messages,
360
+ * tool results, and user-authored developer messages (e.g. `@file` mentions)
361
+ * can carry operator secrets. System prompts, tool schemas, and assistant
362
+ * output are author-controlled or model-generated and pass through untouched.
363
+ * Within a targeted message only `text` blocks are rewritten — inline image
364
+ * bytes are never walked.
365
+ */
222
366
  export function obfuscateMessages(obfuscator: SecretObfuscator, messages: Message[]): Message[] {
223
- return obfuscator.obfuscateObject(messages);
367
+ if (!obfuscator.hasSecrets()) return messages;
368
+ let changed = false;
369
+ const result = messages.map((message): Message => {
370
+ if (
371
+ message.role !== "user" &&
372
+ message.role !== "toolResult" &&
373
+ !(message.role === "developer" && message.attribution === "user")
374
+ ) {
375
+ return message;
376
+ }
377
+ const target = message as UserFacingMessage;
378
+ if (typeof target.content === "string") {
379
+ const content = obfuscator.obfuscate(target.content);
380
+ if (content === target.content) return message;
381
+ changed = true;
382
+ return { ...target, content } as Message;
383
+ }
384
+ const content = obfuscateTextBlocks(obfuscator, target.content);
385
+ if (content === target.content) return message;
386
+ changed = true;
387
+ return { ...target, content } as Message;
388
+ });
389
+ return changed ? result : messages;
224
390
  }
225
391
 
226
- /** Obfuscate provider request context without walking live tool schema instances. */
392
+ /**
393
+ * Redact outbound provider context. Only conversation messages are rewritten;
394
+ * the static system prompt and tool schemas pass through unchanged.
395
+ */
227
396
  export function obfuscateProviderContext(obfuscator: SecretObfuscator | undefined, context: Context): Context {
228
397
  if (!obfuscator?.hasSecrets()) return context;
229
- return {
230
- ...context,
231
- systemPrompt: obfuscator.obfuscateObject(context.systemPrompt),
232
- messages: obfuscator.obfuscateObject(context.messages),
233
- tools: obfuscateProviderTools(obfuscator, context.tools),
234
- };
235
- }
236
-
237
- /** Convert tool schemas to wire JSON Schema before obfuscating provider-visible strings. */
238
- export function obfuscateProviderTools(
239
- obfuscator: SecretObfuscator | undefined,
240
- tools: Tool[] | undefined,
241
- ): Tool[] | undefined {
242
- if (!tools || !obfuscator?.hasSecrets()) return tools;
243
- return tools.map(tool => ({
244
- ...tool,
245
- description: obfuscator.obfuscate(tool.description),
246
- parameters: obfuscator.obfuscateObject(toolWireSchema(tool)),
247
- customFormat: tool.customFormat ? obfuscator.obfuscateObject(tool.customFormat) : undefined,
248
- }));
398
+ const messages = obfuscateMessages(obfuscator, context.messages);
399
+ return messages === context.messages ? context : { ...context, messages };
249
400
  }
250
401
 
251
402
  // ═══════════════════════════════════════════════════════════════════════════
@@ -264,35 +415,33 @@ function replaceAll(text: string, search: string, replacement: string): string {
264
415
  return result;
265
416
  }
266
417
 
267
- /** Deep-walk an object, transforming all string values. */
268
- function deepWalkStrings<T>(obj: T, transform: (s: string) => string): T {
269
- if (typeof obj === "string") {
270
- return transform(obj) as unknown as T;
271
- }
272
- if (Array.isArray(obj)) {
418
+ /**
419
+ * Map every string in arbitrary JSON. Used ONLY for tool-call arguments, whose
420
+ * shape is model-authored and not known ahead of time. No other caller may walk
421
+ * untyped data: every message/content path is handled by a typed transformer.
422
+ */
423
+ function mapJsonStrings(value: JsonValue, fn: (s: string) => string): JsonValue {
424
+ if (typeof value === "string") return fn(value);
425
+ if (Array.isArray(value)) {
273
426
  let changed = false;
274
- const result = obj.map(item => {
275
- const transformed = deepWalkStrings(item, transform);
276
- if (transformed !== item) changed = true;
277
- return transformed;
427
+ const out = value.map(item => {
428
+ const next = mapJsonStrings(item, fn);
429
+ if (next !== item) changed = true;
430
+ return next;
278
431
  });
279
- return (changed ? result : obj) as unknown as T;
432
+ return changed ? out : value;
280
433
  }
281
- if (obj !== null && typeof obj === "object" && isPlainRecord(obj)) {
434
+ if (value !== null && typeof value === "object") {
282
435
  let changed = false;
283
- const result: Record<string, unknown> = {};
284
- for (const key of Object.keys(obj)) {
285
- const value = (obj as Record<string, unknown>)[key];
286
- const transformed = deepWalkStrings(value, transform);
287
- if (transformed !== value) changed = true;
288
- result[key] = transformed;
436
+ const out: JsonRecord = {};
437
+ for (const key of Object.keys(value)) {
438
+ const item = value[key];
439
+ if (item === undefined) continue;
440
+ const next = mapJsonStrings(item, fn);
441
+ if (next !== item) changed = true;
442
+ out[key] = next;
289
443
  }
290
- return (changed ? result : obj) as T;
444
+ return changed ? out : value;
291
445
  }
292
- return obj;
293
- }
294
-
295
- function isPlainRecord(obj: object): obj is Record<string, unknown> {
296
- const prototype = Object.getPrototypeOf(obj);
297
- return prototype === Object.prototype || prototype === null;
446
+ return value;
298
447
  }
@@ -234,9 +234,10 @@ import ttsrInterruptTemplate from "../prompts/system/ttsr-interrupt.md" with { t
234
234
  import ttsrToolReminderTemplate from "../prompts/system/ttsr-tool-reminder.md" with { type: "text" };
235
235
  import unexpectedStopRetryTemplate from "../prompts/system/unexpected-stop-retry.md" with { type: "text" };
236
236
  import {
237
+ deobfuscateAssistantContent,
237
238
  deobfuscateSessionContext,
239
+ obfuscateMessages,
238
240
  obfuscateProviderContext,
239
- obfuscateProviderTools,
240
241
  type SecretObfuscator,
241
242
  } from "../secrets/obfuscator";
242
243
  import { invalidateHostMetadata } from "../ssh/connection-manager";
@@ -2500,7 +2501,7 @@ export class AgentSession {
2500
2501
  const obfuscator = this.#obfuscator;
2501
2502
  if (obfuscator && event.type === "message_end" && event.message.role === "assistant") {
2502
2503
  const message = event.message;
2503
- const deobfuscatedContent = obfuscator.deobfuscateObject(message.content);
2504
+ const deobfuscatedContent = deobfuscateAssistantContent(obfuscator, message.content);
2504
2505
  if (deobfuscatedContent !== message.content) {
2505
2506
  displayEvent = { ...event, message: { ...message, content: deobfuscatedContent } };
2506
2507
  }
@@ -5232,28 +5233,17 @@ export class AgentSession {
5232
5233
  return deobfuscateSessionContext(this.sessionManager.buildSessionContext({ transcript: true }), this.#obfuscator);
5233
5234
  }
5234
5235
 
5235
- #obfuscateForProvider<T>(value: T): T {
5236
- if (!this.#obfuscator?.hasSecrets()) return value;
5237
- return this.#obfuscator.obfuscateObject(value);
5238
- }
5239
-
5240
5236
  #obfuscateTextForProvider(text: string | undefined): string | undefined {
5241
5237
  if (!text || !this.#obfuscator?.hasSecrets()) return text;
5242
5238
  return this.#obfuscator.obfuscate(text);
5243
5239
  }
5244
5240
 
5245
5241
  #obfuscatePreparationForProvider(preparation: CompactionPreparation): CompactionPreparation {
5246
- if (!this.#obfuscator?.hasSecrets()) return preparation;
5247
- if (!preparation.previousSummary && !preparation.previousPreserveData) return preparation;
5248
- return {
5249
- ...preparation,
5250
- previousSummary: preparation.previousSummary
5251
- ? this.#obfuscator.obfuscate(preparation.previousSummary)
5252
- : preparation.previousSummary,
5253
- previousPreserveData: preparation.previousPreserveData
5254
- ? this.#obfuscator.obfuscateObject(preparation.previousPreserveData)
5255
- : preparation.previousPreserveData,
5256
- };
5242
+ if (!this.#obfuscator?.hasSecrets() || !preparation.previousSummary) return preparation;
5243
+ // `previousPreserveData` is opaque provider-replay state (e.g. OpenAI remote-compaction
5244
+ // `encrypted_content`); rewriting it would corrupt replay, so only the plaintext summary
5245
+ // is redacted.
5246
+ return { ...preparation, previousSummary: this.#obfuscator.obfuscate(preparation.previousSummary) };
5257
5247
  }
5258
5248
 
5259
5249
  #deobfuscateFromProvider(text: string): string {
@@ -5270,7 +5260,8 @@ export class AgentSession {
5270
5260
  }
5271
5261
 
5272
5262
  #convertToLlmForSideRequest(messages: AgentMessage[]): Message[] {
5273
- return this.#obfuscateForProvider(convertToLlm(messages));
5263
+ const converted = convertToLlm(messages);
5264
+ return this.#obfuscator?.hasSecrets() ? obfuscateMessages(this.#obfuscator, converted) : converted;
5274
5265
  }
5275
5266
 
5276
5267
  /** Convert session messages using the same pre-LLM pipeline as the active session. */
@@ -7865,8 +7856,8 @@ export class AgentSession {
7865
7856
  compactionAbortController.signal,
7866
7857
  {
7867
7858
  promptOverride: this.#obfuscateTextForProvider(compactionPrep.hookPrompt),
7868
- extraContext: this.#obfuscateForProvider(compactionPrep.hookContext),
7869
- remoteInstructions: this.#obfuscateForProvider(this.#baseSystemPrompt.join("\n\n")),
7859
+ extraContext: compactionPrep.hookContext,
7860
+ remoteInstructions: this.#baseSystemPrompt.join("\n\n"),
7870
7861
  convertToLlm: messages => this.#convertToLlmForSideRequest(messages),
7871
7862
  },
7872
7863
  );
@@ -8060,11 +8051,10 @@ export class AgentSession {
8060
8051
  model,
8061
8052
  this.#modelRegistry.resolver(model, this.sessionId),
8062
8053
  {
8063
- systemPrompt: this.#obfuscateForProvider(this.#baseSystemPrompt),
8064
- tools: obfuscateProviderTools(
8065
- this.#obfuscator,
8066
- this.#pruneToolDescriptions ? stripToolDescriptions(this.agent.state.tools) : this.agent.state.tools,
8067
- ),
8054
+ systemPrompt: this.#baseSystemPrompt,
8055
+ tools: this.#pruneToolDescriptions
8056
+ ? stripToolDescriptions(this.agent.state.tools)
8057
+ : this.agent.state.tools,
8068
8058
  customInstructions: this.#obfuscateTextForProvider(customInstructions),
8069
8059
  convertToLlm: messages => this.#convertToLlmForSideRequest(messages),
8070
8060
  initiatorOverride: "agent",
@@ -9752,8 +9742,8 @@ export class AgentSession {
9752
9742
  autoCompactionSignal,
9753
9743
  {
9754
9744
  promptOverride: this.#obfuscateTextForProvider(compactionPrep.hookPrompt),
9755
- extraContext: this.#obfuscateForProvider(compactionPrep.hookContext),
9756
- remoteInstructions: this.#obfuscateForProvider(this.#baseSystemPrompt.join("\n\n")),
9745
+ extraContext: compactionPrep.hookContext,
9746
+ remoteInstructions: this.#baseSystemPrompt.join("\n\n"),
9757
9747
  metadata: this.agent.metadataForProvider(candidate.provider),
9758
9748
  initiatorOverride: "agent",
9759
9749
  convertToLlm: messages => this.#convertToLlmForSideRequest(messages),
@@ -11379,7 +11369,7 @@ export class AgentSession {
11379
11369
  }
11380
11370
  if (event.type === "done") {
11381
11371
  assistantMessage = this.#obfuscator?.hasSecrets()
11382
- ? { ...event.message, content: this.#obfuscator.deobfuscateObject(event.message.content) }
11372
+ ? { ...event.message, content: deobfuscateAssistantContent(this.#obfuscator, event.message.content) }
11383
11373
  : event.message;
11384
11374
  break;
11385
11375
  }
@@ -9,10 +9,9 @@ export const SUPPORTED_INPUT_IMAGE_MIME_TYPES = SUPPORTED_IMAGE_MIME_TYPES;
9
9
 
10
10
  /**
11
11
  * Ollama and its local-backend family decode image input through llama.cpp /
12
- * `stb_image`, which is compiled without WebP support. The first-party Codex
13
- * Responses backend also rejects WebP `input_image.image_url` data URLs. Detect
14
- * those models so the resize pipeline encodes to PNG/JPEG instead — the
15
- * automatic equivalent of `OMP_NO_WEBP=1`.
12
+ * `stb_image`, which is compiled without WebP support, so a WebP upload fails
13
+ * with an opaque HTTP 400. Detect those models so the resize pipeline encodes
14
+ * to PNG/JPEG instead — the automatic equivalent of `OMP_NO_WEBP=1`.
16
15
  */
17
16
  export function modelLacksWebpSupport(
18
17
  model: Pick<Model, "provider" | "api" | "imageInputDecoder"> | undefined,
@@ -20,8 +19,6 @@ export function modelLacksWebpSupport(
20
19
  if (!model) return false;
21
20
  return (
22
21
  model.imageInputDecoder === "stb" ||
23
- model.api === "openai-codex-responses" ||
24
- model.provider === "openai-codex" ||
25
22
  model.provider === "ollama" ||
26
23
  model.provider === "ollama-cloud" ||
27
24
  model.provider === "llama.cpp" ||