@bitkyc08/opencodex 2.6.2 → 2.6.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.
@@ -16,7 +16,7 @@
16
16
  } catch (e) {}
17
17
  })();
18
18
  </script>
19
- <script type="module" crossorigin src="/assets/index-LK87QnT7.js"></script>
19
+ <script type="module" crossorigin src="/assets/index-DrtXeL9W.js"></script>
20
20
  <link rel="stylesheet" crossorigin href="/assets/index-BwvDb198.css">
21
21
  </head>
22
22
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bitkyc08/opencodex",
3
- "version": "2.6.2",
3
+ "version": "2.6.3",
4
4
  "description": "Universal provider proxy for OpenAI Codex — use any LLM with Codex CLI/App/SDK",
5
5
  "type": "module",
6
6
  "main": "./bin/package-main.mjs",
@@ -36,7 +36,7 @@
36
36
  "dev:proxy": "bun run src/cli.ts start",
37
37
  "dev:gui": "cd gui && bun run dev",
38
38
  "start": "bun run src/cli.ts start",
39
- "test": "bun test tests",
39
+ "test": "bun test ./tests/",
40
40
  "typecheck": "bun x tsc --noEmit",
41
41
  "privacy:scan": "bun scripts/privacy-scan.ts",
42
42
  "generate:jawcode-metadata": "bun scripts/generate-jawcode-metadata.ts",
@@ -13,6 +13,22 @@ import type {
13
13
  import { isAllowedToolChoice, namespacedToolName, toolAllowedByChoice } from "../types";
14
14
  import { contentPartsToText, parseDataUrl } from "./image";
15
15
 
16
+ /**
17
+ * Inline image parts (Gemini `inline_data`) extracted from tool-result content. Only base64 data URLs
18
+ * can be inlined; a remote URL has no mime type we can supply, so it is skipped here (the textual
19
+ * result already carries an "[image]" marker via contentPartsToText).
20
+ */
21
+ function toolResultImageParts(content: string | OcxContentPart[]): unknown[] {
22
+ if (typeof content === "string") return [];
23
+ const parts: unknown[] = [];
24
+ for (const p of content) {
25
+ if (p.type !== "image") continue;
26
+ const data = parseDataUrl(p.imageUrl);
27
+ if (data) parts.push({ inline_data: { mime_type: data.mediaType, data: data.base64 } });
28
+ }
29
+ return parts;
30
+ }
31
+
16
32
  function messagesToGeminiFormat(parsed: OcxParsedRequest): { systemInstruction?: unknown; contents: unknown[] } {
17
33
  const systemInstruction = parsed.context.systemPrompt?.length
18
34
  ? { parts: [{ text: parsed.context.systemPrompt.join("\n\n") }] }
@@ -54,10 +70,15 @@ function messagesToGeminiFormat(parsed: OcxParsedRequest): { systemInstruction?:
54
70
  break;
55
71
  }
56
72
  case "toolResult": {
57
- contents.push({
58
- role: "user",
59
- parts: [{ functionResponse: { name: namespacedToolName(msg.toolNamespace, msg.toolName), response: { result: contentPartsToText(msg.content) } } }],
60
- });
73
+ // The functionResponse part carries the textual result. Gemini cannot embed images inside a
74
+ // functionResponse, but it does accept sibling inline_data parts in the same user turn, so
75
+ // tool-result screenshots (e.g. Computer Use) ride along as inline_data instead of being
76
+ // flattened to a "[image]" marker the model can't actually see.
77
+ const parts: unknown[] = [
78
+ { functionResponse: { name: namespacedToolName(msg.toolNamespace, msg.toolName), response: { result: contentPartsToText(msg.content) } } },
79
+ ];
80
+ for (const part of toolResultImageParts(msg.content)) parts.push(part);
81
+ contents.push({ role: "user", parts });
61
82
  break;
62
83
  }
63
84
  }
@@ -1,4 +1,5 @@
1
1
  import type { OcxParsedRequest } from "../types";
2
+ import { namespacedToolName } from "../types";
2
3
 
3
4
  const MAX_KIRO_TOOL_DESCRIPTION = 1024;
4
5
 
@@ -14,13 +15,49 @@ function sanitizeKiroSchema(value: unknown): unknown {
14
15
  return out;
15
16
  }
16
17
 
18
+ function ensureRootObjectType(schema: unknown): Record<string, unknown> {
19
+ const obj = schema && typeof schema === "object" && !Array.isArray(schema)
20
+ ? schema as Record<string, unknown>
21
+ : {};
22
+ // Bedrock rejects oneOf/allOf/anyOf at the root ("input_schema does not support oneOf, allOf, or
23
+ // anyOf at the top level"). Flatten them into a single object schema by merging every variant's
24
+ // properties so the model can still supply any valid argument. Required is merged only for allOf
25
+ // (AND semantics); anyOf/oneOf (OR) leave required off so a valid single-branch call passes.
26
+ const composition = obj.oneOf ?? obj.anyOf ?? obj.allOf;
27
+ if (Array.isArray(composition)) {
28
+ const merged: Record<string, unknown> = { type: "object" };
29
+ const props: Record<string, unknown> = {};
30
+ const required = new Set<string>();
31
+ for (const variant of composition) {
32
+ if (!variant || typeof variant !== "object" || Array.isArray(variant)) continue;
33
+ const v = variant as Record<string, unknown>;
34
+ if (v.properties && typeof v.properties === "object") {
35
+ Object.assign(props, sanitizeKiroSchema(v.properties) as Record<string, unknown>);
36
+ }
37
+ if (obj.allOf !== undefined && Array.isArray(v.required)) {
38
+ for (const r of v.required) if (typeof r === "string") required.add(r);
39
+ }
40
+ }
41
+ if (Object.keys(props).length > 0) merged.properties = props;
42
+ if (required.size > 0) merged.required = [...required];
43
+ return merged;
44
+ }
45
+ const t = obj.type;
46
+ if (t === "object") return obj;
47
+ if (Array.isArray(t) && t.includes("object")) return { ...obj, type: "object" };
48
+ return { ...obj, type: "object" };
49
+ }
50
+
17
51
  export function convertKiroToolContext(parsed: OcxParsedRequest): { tools: unknown[]; systemAdditions: string[] } {
18
52
  const tools = parsed.context.tools ?? [];
19
53
  const systemAdditions: string[] = [];
20
54
  return {
21
55
  tools: tools.map(t => {
22
56
  const description = t.description || `Tool: ${t.name}`;
23
- const toolName = t.name.slice(0, 64);
57
+ // Send the full namespaced wire name (e.g. mcp__chrome-devtools__navigate_page) so Kiro echoes
58
+ // it back unchanged; the bridge's toolNsMap is keyed by this name and restores the MCP namespace
59
+ // Codex routes by. Truncating here breaks long MCP/computer-use round trips.
60
+ const toolName = namespacedToolName(t.namespace, t.name);
24
61
  const kiroDescription = description.length > MAX_KIRO_TOOL_DESCRIPTION
25
62
  ? `Tool documentation moved to the system prompt: ${toolName}.`
26
63
  : description;
@@ -31,7 +68,7 @@ export function convertKiroToolContext(parsed: OcxParsedRequest): { tools: unkno
31
68
  toolSpecification: {
32
69
  name: toolName,
33
70
  description: kiroDescription,
34
- inputSchema: { json: sanitizeKiroSchema(t.parameters ?? {}) as Record<string, unknown> },
71
+ inputSchema: { json: ensureRootObjectType(sanitizeKiroSchema(t.parameters ?? {})) },
35
72
  },
36
73
  };
37
74
  }),
@@ -10,6 +10,7 @@ import { appendFallbackText, toolCallFallbackText, toolResultFallbackText } from
10
10
  import { KiroThinkingParser } from "./kiro-thinking";
11
11
  import { isCompleteKiroToolInput, kiroTruncationErrorMessage } from "./kiro-truncation";
12
12
  import { fallbackToolUseId, fingerprint, invocationId, mapModelId, normalizeToolId, osTag, stableConversationId } from "./kiro-wire";
13
+ import { namespacedToolName } from "../types";
13
14
  import type {
14
15
  AdapterEvent,
15
16
  OcxAssistantMessage,
@@ -55,7 +56,6 @@ interface KiroHistoryEntry {
55
56
  userInputMessage?: KiroUserInputMessage;
56
57
  assistantResponseMessage?: { content: string; toolUses?: KiroToolUse[] };
57
58
  }
58
-
59
59
  function userContentText(content: string | OcxContentPart[]): string {
60
60
  if (typeof content === "string") return content;
61
61
  return content.map(p => (p.type === "text" ? p.text : "")).filter(Boolean).join("\n");
@@ -72,15 +72,12 @@ function usageContentText(content: string | OcxContentPart[]): string {
72
72
  .filter(Boolean)
73
73
  .join("\n");
74
74
  }
75
-
76
75
  function serializeForUsage(value: unknown): string {
77
76
  try { return JSON.stringify(value); } catch { return String(value); }
78
77
  }
79
-
80
78
  function currentTurnUsageMessages(messages: OcxMessage[]): OcxMessage[] {
81
79
  return messages.slice(messages.map(m => m.role).lastIndexOf("assistant") + 1).filter(m => m.role !== "assistant");
82
80
  }
83
-
84
81
  function currentTurnPayloadMessages(messages: OcxMessage[]): OcxMessage[] {
85
82
  const roles = messages.map(m => m.role);
86
83
  const lastAssistant = roles.lastIndexOf("assistant");
@@ -216,12 +213,15 @@ export function buildKiroPayload(parsed: OcxParsedRequest, profileArn: string |
216
213
  const history: KiroHistoryEntry[] = [];
217
214
  const fallbackEntries = new WeakSet<KiroHistoryEntry>();
218
215
  let pending: KiroToolResult[] = [];
216
+ let pendingImages: KiroImage[] = [];
219
217
  let lastRole = "";
220
218
  const attachPending = (entry: KiroHistoryEntry): void => {
221
219
  if (pending.length === 0) return;
222
220
  const uim = entry.userInputMessage!;
223
221
  uim.userInputMessageContext = { ...(uim.userInputMessageContext ?? {}), toolResults: pending };
222
+ if (pendingImages.length > 0) uim.images = [...(uim.images ?? []), ...pendingImages];
224
223
  pending = [];
224
+ pendingImages = [];
225
225
  };
226
226
  const pushUserEntry = (entry: KiroHistoryEntry): void => {
227
227
  if (pending.length === 0 && lastRole === "user") {
@@ -253,7 +253,7 @@ export function buildKiroPayload(parsed: OcxParsedRequest, profileArn: string |
253
253
  ? toolCalls.map(tc => {
254
254
  const toolUseId = normalizeToolId(tc.id);
255
255
  structuredToolIds.add(toolUseId);
256
- return { name: tc.name, input: (tc.arguments ?? {}) as Record<string, unknown>, toolUseId };
256
+ return { name: namespacedToolName(tc.namespace, tc.name), input: (tc.arguments ?? {}) as Record<string, unknown>, toolUseId };
257
257
  })
258
258
  : [];
259
259
  if (kiroTools.length === 0) {
@@ -267,6 +267,7 @@ export function buildKiroPayload(parsed: OcxParsedRequest, profileArn: string |
267
267
  } else if (msg.role === "toolResult") {
268
268
  const tr = msg as OcxToolResultMessage;
269
269
  const text = userContentText(tr.content);
270
+ const images = extractKiroImages(tr.content);
270
271
  const toolUseId = normalizeToolId(tr.toolCallId);
271
272
  if (kiroTools.length > 0 && structuredToolIds.has(toolUseId)) {
272
273
  pending.push({
@@ -274,9 +275,10 @@ export function buildKiroPayload(parsed: OcxParsedRequest, profileArn: string |
274
275
  status: tr.isError ? "error" : "success",
275
276
  toolUseId,
276
277
  });
278
+ pendingImages.push(...images);
277
279
  } else {
278
280
  if (pending.length > 0) pushUserEntry(mkUser("(tool results)"));
279
- const fallback = mkUser(toolResultFallbackText(tr));
281
+ const fallback = mkUser(toolResultFallbackText(tr), images);
280
282
  fallbackEntries.add(fallback);
281
283
  pushUserEntry(fallback);
282
284
  }
@@ -440,9 +442,13 @@ export async function* parseKiroStream(
440
442
  yield contentEvent;
441
443
  }
442
444
  if (open) {
443
- open = null;
444
- yield { type: "error", message: kiroTruncationErrorMessage("stream ended before tool stop") };
445
- return;
445
+ const input = open.chunks.join("");
446
+ if (!isCompleteKiroToolInput(input)) {
447
+ open = null;
448
+ yield { type: "error", message: kiroTruncationErrorMessage("stream ended before tool stop") };
449
+ return;
450
+ }
451
+ yield* flushTool();
446
452
  }
447
453
  const outputTokens = estimateTokens(outputChars, modelId);
448
454
  const usage: OcxUsage = { inputTokens, outputTokens, estimated: true };
@@ -56,7 +56,7 @@ const functionCallItemSchema = z.object({
56
56
  const functionCallOutputItemSchema = z.object({
57
57
  type: z.literal("function_call_output"),
58
58
  call_id: z.string().min(1),
59
- output: z.union([z.string(), z.array(outputContentBlockSchema)]).optional(),
59
+ output: z.union([z.string(), z.array(z.union([outputContentBlockSchema, inputImageBlockSchema]))]).optional(),
60
60
  });
61
61
  const customToolCallItemSchema = z.object({
62
62
  type: z.literal("custom_tool_call"),