@bitkyc08/opencodex 2.54.0-preview.20260914 → 2.55.0-preview.20260914

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 (86) hide show
  1. package/gui/dist/assets/{index-B4VYfZcY.js → index-DH2PUHqr.js} +10 -10
  2. package/gui/dist/index.html +1 -1
  3. package/package.json +1 -1
  4. package/src/adapters/anthropic-image-codec.ts +57 -0
  5. package/src/adapters/anthropic-image-normalize.ts +28 -1
  6. package/src/adapters/anthropic.ts +68 -6
  7. package/src/adapters/base.ts +8 -0
  8. package/src/adapters/coding-agent/protocol.ts +41 -16
  9. package/src/adapters/cursor/cursor-errors.ts +1 -1
  10. package/src/adapters/cursor/live-transport.ts +5 -1
  11. package/src/adapters/cursor/native-exec-fs.ts +10 -10
  12. package/src/adapters/cursor/native-exec-network.ts +2 -2
  13. package/src/adapters/cursor/native-exec-shell.ts +13 -12
  14. package/src/adapters/cursor/native-exec.ts +51 -10
  15. package/src/adapters/cursor/policy-error.ts +75 -0
  16. package/src/adapters/cursor/protobuf-request.ts +105 -1
  17. package/src/adapters/devin/cloud-direct/catalog.ts +34 -2
  18. package/src/adapters/devin/live-models.ts +33 -2
  19. package/src/adapters/google-wire-compiler.ts +8 -0
  20. package/src/adapters/google.ts +46 -0
  21. package/src/adapters/input-media-guard.ts +45 -0
  22. package/src/adapters/kiro/adapter.ts +8 -0
  23. package/src/adapters/kiro/payload.ts +28 -6
  24. package/src/adapters/kiro-events.ts +25 -6
  25. package/src/adapters/kiro-images.ts +30 -0
  26. package/src/adapters/kiro-retry.ts +8 -0
  27. package/src/adapters/openai-chat.ts +33 -4
  28. package/src/adapters/openai-responses.ts +26 -0
  29. package/src/adapters/registry.ts +4 -0
  30. package/src/bridge.ts +163 -116
  31. package/src/chat/image-parts.ts +151 -0
  32. package/src/chat/inbound.ts +70 -33
  33. package/src/cli/connect.ts +30 -9
  34. package/src/cli/dispatch.ts +7 -3
  35. package/src/cli/index.ts +3 -0
  36. package/src/cli/runtime-api.ts +25 -0
  37. package/src/cli/status.ts +21 -19
  38. package/src/cli/system-restart-client.ts +25 -0
  39. package/src/clients/config-export.ts +14 -4
  40. package/src/codex/app-server-processes.ts +25 -0
  41. package/src/codex/auth-context.ts +8 -0
  42. package/src/codex/autostart-health.ts +36 -2
  43. package/src/codex/catalog/provider-fetch.ts +41 -0
  44. package/src/codex/catalog-auto-refresh.ts +182 -0
  45. package/src/codex/catalog-refresh-status.ts +93 -0
  46. package/src/codex/history-provider.ts +55 -0
  47. package/src/codex/model-entitlements.ts +78 -0
  48. package/src/codex/native-profile-processes.ts +114 -15
  49. package/src/codex/prompt-text-probe.ts +274 -41
  50. package/src/codex/routing-adoption.ts +189 -0
  51. package/src/codex/routing.ts +520 -48
  52. package/src/codex/runtime.ts +249 -7
  53. package/src/combos/failover.ts +45 -0
  54. package/src/config.ts +124 -4
  55. package/src/generated/compatibility-version.json +110 -74
  56. package/src/generated/model-metadata.ts +1 -0
  57. package/src/lib/request-execution-budget.ts +202 -0
  58. package/src/lib/upstream-retry.ts +95 -8
  59. package/src/lib/workflow-budget.ts +172 -0
  60. package/src/oauth/devin.ts +57 -12
  61. package/src/providers/quota.ts +37 -6
  62. package/src/providers/registry.ts +53 -6
  63. package/src/responses/input-media.ts +65 -0
  64. package/src/responses/parser-content.ts +42 -0
  65. package/src/responses/schema.ts +12 -2
  66. package/src/server/audio-live.ts +1 -2
  67. package/src/server/audio-transcriptions.ts +1 -2
  68. package/src/server/auth-cors.ts +1 -1
  69. package/src/server/background-lifecycle.ts +18 -0
  70. package/src/server/chat-completions.ts +23 -8
  71. package/src/server/chat-native.ts +17 -17
  72. package/src/server/index.ts +24 -0
  73. package/src/server/management/request-history-routes.ts +5 -0
  74. package/src/server/request-log.ts +8 -2
  75. package/src/server/responses/compact.ts +51 -3
  76. package/src/server/responses/core.ts +238 -28
  77. package/src/server/search.ts +7 -9
  78. package/src/types/config.ts +51 -6
  79. package/src/usage/log.ts +37 -0
  80. package/src/vision/eligibility.ts +37 -4
  81. package/src/vision/index.ts +1 -0
  82. package/src/vision/plan.ts +45 -10
  83. package/src/web-search/alpha-search.ts +324 -0
  84. package/src/web-search/index.ts +13 -22
  85. package/src/web-search/passthrough-bridge.ts +195 -22
  86. package/src/web-search/sidecar-providers.ts +22 -0
@@ -66,6 +66,24 @@ function tokenCount(eventType: string, obj: Record<string, unknown>, key: string
66
66
  return value;
67
67
  }
68
68
 
69
+ /**
70
+ * A cache counter Kiro did not report, kept as unknown rather than zero (#4546).
71
+ *
72
+ * `OcxUsage` omits cache fields it has no reading for, and `cacheHitRate` is null when
73
+ * unobserved -- the convention everywhere except here. Coercing an absent counter to 0 makes
74
+ * "the provider said nothing" indistinguishable from "nothing was cached", which is the
75
+ * difference between a routing change that preserved the prompt cache and one that destroyed
76
+ * it. A malformed value is still a malformed event; only absence is unknown.
77
+ */
78
+ function optionalTokenCount(
79
+ eventType: string,
80
+ obj: Record<string, unknown>,
81
+ key: string,
82
+ ): number | undefined {
83
+ if (obj[key] === undefined) return undefined;
84
+ return tokenCount(eventType, obj, key, true);
85
+ }
86
+
69
87
  function parseTokenUsage(eventType: string, value: unknown): OcxUsage | undefined {
70
88
  if (value === undefined || value === null) return undefined;
71
89
  if (typeof value !== "object" || Array.isArray(value)) {
@@ -73,19 +91,20 @@ function parseTokenUsage(eventType: string, value: unknown): OcxUsage | undefine
73
91
  }
74
92
  const usage = value as Record<string, unknown>;
75
93
  const uncached = tokenCount(eventType, usage, "uncachedInputTokens", true);
76
- const cacheRead = tokenCount(eventType, usage, "cacheReadInputTokens", false);
77
- const cacheWrite = tokenCount(eventType, usage, "cacheWriteInputTokens", false);
94
+ const cacheRead = optionalTokenCount(eventType, usage, "cacheReadInputTokens");
95
+ const cacheWrite = optionalTokenCount(eventType, usage, "cacheWriteInputTokens");
78
96
  const outputTokens = tokenCount(eventType, usage, "outputTokens", true);
79
97
  const totalTokens = tokenCount(eventType, usage, "totalTokens", true);
80
- const inputTokens = uncached + cacheRead + cacheWrite;
98
+ // An unreported counter contributes nothing to the total, which is a different statement
99
+ // from claiming it was measured as zero.
100
+ const inputTokens = uncached + (cacheRead ?? 0) + (cacheWrite ?? 0);
81
101
  if (!Number.isSafeInteger(inputTokens)) return malformed(eventType, "input token usage overflowed");
82
102
  return {
83
103
  inputTokens,
84
104
  outputTokens,
85
105
  totalTokens,
86
- cachedInputTokens: cacheRead,
87
- cacheReadInputTokens: cacheRead,
88
- cacheCreationInputTokens: cacheWrite,
106
+ ...(cacheRead !== undefined ? { cachedInputTokens: cacheRead, cacheReadInputTokens: cacheRead } : {}),
107
+ ...(cacheWrite !== undefined ? { cacheCreationInputTokens: cacheWrite } : {}),
89
108
  };
90
109
  }
91
110
 
@@ -35,6 +35,36 @@ export function extractKiroImages(content: string | OcxContentPart[]): KiroImage
35
35
  return out;
36
36
  }
37
37
 
38
+ /**
39
+ * Count images Kiro cannot inline, so the loss is never silent.
40
+ *
41
+ * Kiro's wire carries base64 bytes only, so a remote reference genuinely cannot be
42
+ * sent, and this proxy does not fetch one on a request path. Such a part used to be
43
+ * dropped with neither bytes nor any trace that an attachment existed. Counting them
44
+ * lets the payload builder attach a bounded marker instead.
45
+ *
46
+ * The count is all that crosses: a remote image URL can carry a signed token, so the
47
+ * URL itself is never echoed into prose.
48
+ */
49
+ export function countKiroUninlinableImages(content: string | OcxContentPart[]): number {
50
+ if (typeof content === "string") return 0;
51
+ let count = 0;
52
+ for (const p of content) {
53
+ if (p.type !== "image") continue;
54
+ // Keyed on the scheme, not on parse success: a malformed data URL also fails
55
+ // parseDataUrlImage, and labelling that "remote reference" would misstate the cause.
56
+ if (!p.imageUrl.startsWith("data:")) count++;
57
+ }
58
+ return count;
59
+ }
60
+
61
+ /** Bounded, content-free marker for images Kiro could not inline. */
62
+ export function kiroUninlinableImageMarker(count: number): string {
63
+ if (count <= 0) return "";
64
+ if (count === 1) return "[image omitted: remote image references are not supported by this provider]";
65
+ return "[" + String(count) + " images omitted: remote image references are not supported by this provider]";
66
+ }
67
+
38
68
  /**
39
69
  * Conservative POLICY caps for the CodeWhisperer GenerateAssistantResponse payload,
40
70
  * whose limits are undocumented. Derived from adjacent AWS surfaces
@@ -5,6 +5,7 @@ import { readBoundedResponseBody } from "../lib/bounded-body";
5
5
  import { resolveClientRetryAfter } from "../lib/retry-after";
6
6
  import { parseRetryAfterMs } from "../combos";
7
7
  import {
8
+ SendBudgetExhaustedError,
8
9
  abortError,
9
10
  cancelResponseBodyBestEffort,
10
11
  fetchWithAttemptDeadline,
@@ -162,6 +163,13 @@ async function fetchWithResetRecovery(
162
163
  let lastError: unknown;
163
164
  for (let attempt = 0; attempt < RESET_ATTEMPTS; attempt++) {
164
165
  if (ctx.abortSignal?.aborted) throw abortError(ctx.abortSignal);
166
+ // Every physical send is admitted, not just the adapter entry. Kiro nests a throttle loop
167
+ // over this ladder and can run the ladder twice per throttle round, so counting one entry
168
+ // as one send hid up to eighteen upstream requests from the per-request cap (#4546).
169
+ const decision = ctx.sendBudget?.reserveDispatch({ sendClass: "transient", targetKey: url });
170
+ if (decision && (!decision.allowed || !decision.permit.use())) {
171
+ throw new SendBudgetExhaustedError(url);
172
+ }
165
173
  try {
166
174
  const headers = new Headers(request.headers);
167
175
  const recovered = attempt > 0;
@@ -108,6 +108,17 @@ function openAIChatTransport(provider: OcxProviderConfig): {
108
108
  return { url, headers, hasCredential };
109
109
  }
110
110
 
111
+ /**
112
+ * The translated Chat route has no video mapping: this adapter does not implement one,
113
+ * and the marker records that fact so the payload is not dropped in silence.
114
+ *
115
+ * The wording is deliberately about opencodex's own translation, not the provider or
116
+ * model. An earlier revision said "unsupported by this provider", which attributed an
117
+ * opencodex mapping limit to upstream capability the proxy has not established. Native
118
+ * Chat passthrough and Google inline video are unaffected by this route.
119
+ */
120
+ const VIDEO_UNSUPPORTED_MARKER = "[video omitted: the translated Chat route has no video mapping]";
121
+
111
122
  /**
112
123
  * Build a provider request from an inbound Chat Completions body without translating it
113
124
  * through the Responses contract. This is deliberately a whitelist: Chat-only caller
@@ -784,11 +795,29 @@ function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProviderCon
784
795
  } else if (typeof msg.content === "string") {
785
796
  chatMsg = { role: "user", content: msg.content };
786
797
  } else if (!hasImages) {
787
- chatMsg = { role: "user", content: parts!.map(p => (p as OcxTextContent).text).join("") };
798
+ // A video part has no `text`, so joining it produced "" and the whole message
799
+ // was dropped: a video-only or text-plus-video turn vanished silently. OpenAI's
800
+ // Chat Completions wire has no video content part, so state the omission
801
+ // instead of losing it. Scoped to this adapter's wire, not a claim about video
802
+ // support in general — native Chat passthrough and Google inline video are
803
+ // unaffected.
804
+ chatMsg = {
805
+ role: "user",
806
+ content: parts!.map(p => (p.type === "video"
807
+ ? VIDEO_UNSUPPORTED_MARKER
808
+ : (p as OcxTextContent).text)).join(""),
809
+ };
788
810
  } else {
789
- const chatParts = parts!.map(p => p.type === "image"
790
- ? { type: "image_url", image_url: { url: p.imageUrl, ...(p.detail ? { detail: p.detail } : {}) } }
791
- : { type: "text", text: (p as OcxTextContent).text });
811
+ const chatParts = parts!.map(p => {
812
+ if (p.type === "image") {
813
+ return { type: "image_url", image_url: { url: p.imageUrl, ...(p.detail ? { detail: p.detail } : {}) } };
814
+ }
815
+ // Previously this produced { type: "text", text: undefined } for a video
816
+ // part — a malformed part, worse than a drop because it can fail upstream
817
+ // schema validation.
818
+ if (p.type === "video") return { type: "text", text: VIDEO_UNSUPPORTED_MARKER };
819
+ return { type: "text", text: (p as OcxTextContent).text };
820
+ });
792
821
  chatMsg = { role: "user", content: chatParts };
793
822
  }
794
823
  if (pendingToolCalls.length > 0) deferredBarrierMessages.push(chatMsg);
@@ -1285,6 +1285,31 @@ function stripUnsupportedForwardParams(body: unknown): unknown {
1285
1285
  return rest;
1286
1286
  }
1287
1287
 
1288
+ /** Sampling controls the canonical ChatGPT backend rejects; other forward gateways accept them. */
1289
+ const CANONICAL_FORWARD_UNSUPPORTED_SAMPLING = ["temperature", "top_p", "stop", "user"] as const;
1290
+
1291
+ /**
1292
+ * Remove sampling controls only the canonical ChatGPT backend rejects.
1293
+ *
1294
+ * A translated Chat turn used to lose these at the Chat ingress for every provider on
1295
+ * the `openai-responses` adapter, which silently discarded caller intent on generic
1296
+ * key gateways that accept them. Deciding at the ingress was also unsound for combo
1297
+ * and policy routes, whose concrete child is chosen later — so the decision belongs
1298
+ * here, on the provider that actually receives the body.
1299
+ *
1300
+ * Returns a copy and never mutates, so `parsed._rawBody` stays caller-owned, and
1301
+ * no-ops when the body carries none of these keys.
1302
+ */
1303
+ export function stripCanonicalForwardSamplingParams(body: unknown): unknown {
1304
+ if (!isPlainObject(body)) return body;
1305
+ if (!CANONICAL_FORWARD_UNSUPPORTED_SAMPLING.some(key => Object.prototype.hasOwnProperty.call(body, key))) {
1306
+ return body;
1307
+ }
1308
+ const next: Record<string, unknown> = { ...body };
1309
+ for (const key of CANONICAL_FORWARD_UNSUPPORTED_SAMPLING) delete next[key];
1310
+ return next;
1311
+ }
1312
+
1288
1313
  /** Return the lossless text represented by one system message, or null when it is multimodal. */
1289
1314
  function canonicalForwardSystemText(item: Record<string, unknown>): string | null {
1290
1315
  const content = item.content;
@@ -2254,6 +2279,7 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
2254
2279
  // Only the canonical ChatGPT backend rejects the retired field; a self-hosted or
2255
2280
  // third-party forward gateway may still accept it, so this must not be widened.
2256
2281
  if (isCanonicalOpenAiForwardProvider(provider)) {
2282
+ outBody = stripCanonicalForwardSamplingParams(outBody);
2257
2283
  outBody = stripDeprecatedPromptCacheRetention(outBody, parsed.modelId);
2258
2284
  outBody = stripCanonicalForwardPromptCacheOptions(outBody);
2259
2285
  outBody = normalizeCanonicalForwardPromptEnvelope(outBody);
@@ -15,6 +15,7 @@ import { createOllamaNativeAdapter } from "./ollama-native";
15
15
  import { createResponsesPassthroughAdapter } from "./openai-responses";
16
16
  import type { OcxProviderConfig } from "../types";
17
17
  import { createAdapterTierMetadata } from "../providers/fastwire";
18
+ import { withInputMediaGuard } from "./input-media-guard";
18
19
 
19
20
  export type AdapterCacheRetention = "none" | "short" | "long";
20
21
 
@@ -180,6 +181,9 @@ export function createRegisteredAdapter(
180
181
  const definition = getAdapterDefinition(provider.adapter);
181
182
  if (!definition) throw new Error(`Unknown adapter: ${provider.adapter}`);
182
183
  const adapter = definition.create(provider, context);
184
+ if (effectiveAdapterContract(provider.adapter).wire !== "openai-responses") {
185
+ withInputMediaGuard(adapter);
186
+ }
183
187
  const buildRequest = adapter.buildRequest.bind(adapter);
184
188
  adapter.buildRequest = (parsed, incoming) => {
185
189
  const attachTierMetadata = (request: Awaited<ReturnType<ProviderAdapter["buildRequest"]>>) => {