@bitkyc08/opencodex 2.54.0 → 2.55.0
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/gui/dist/assets/{index-CkvITofZ.js → index-VuoiWj9J.js} +10 -10
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/anthropic-image-codec.ts +57 -0
- package/src/adapters/anthropic-image-normalize.ts +28 -1
- package/src/adapters/anthropic.ts +68 -6
- package/src/adapters/base.ts +8 -0
- package/src/adapters/coding-agent/protocol.ts +41 -16
- package/src/adapters/cursor/cursor-errors.ts +1 -1
- package/src/adapters/cursor/live-transport.ts +5 -1
- package/src/adapters/cursor/native-exec-fs.ts +10 -10
- package/src/adapters/cursor/native-exec-network.ts +2 -2
- package/src/adapters/cursor/native-exec-shell.ts +13 -12
- package/src/adapters/cursor/native-exec.ts +51 -10
- package/src/adapters/cursor/policy-error.ts +75 -0
- package/src/adapters/cursor/protobuf-request.ts +105 -1
- package/src/adapters/devin/cloud-direct/catalog.ts +34 -2
- package/src/adapters/devin/live-models.ts +33 -2
- package/src/adapters/google-wire-compiler.ts +8 -0
- package/src/adapters/google.ts +46 -0
- package/src/adapters/input-media-guard.ts +45 -0
- package/src/adapters/kiro/adapter.ts +8 -0
- package/src/adapters/kiro/payload.ts +28 -6
- package/src/adapters/kiro-events.ts +25 -6
- package/src/adapters/kiro-images.ts +30 -0
- package/src/adapters/kiro-retry.ts +8 -0
- package/src/adapters/openai-chat.ts +33 -4
- package/src/adapters/openai-responses.ts +26 -0
- package/src/adapters/registry.ts +4 -0
- package/src/bridge.ts +163 -116
- package/src/chat/image-parts.ts +151 -0
- package/src/chat/inbound.ts +70 -33
- package/src/cli/connect.ts +30 -9
- package/src/cli/dispatch.ts +7 -3
- package/src/cli/index.ts +3 -0
- package/src/cli/runtime-api.ts +25 -0
- package/src/cli/status.ts +21 -19
- package/src/cli/system-restart-client.ts +25 -0
- package/src/clients/config-export.ts +14 -4
- package/src/codex/app-server-processes.ts +25 -0
- package/src/codex/auth-context.ts +8 -0
- package/src/codex/autostart-health.ts +36 -2
- package/src/codex/catalog/provider-fetch.ts +41 -0
- package/src/codex/catalog-auto-refresh.ts +182 -0
- package/src/codex/catalog-refresh-status.ts +93 -0
- package/src/codex/history-provider.ts +55 -0
- package/src/codex/model-entitlements.ts +78 -0
- package/src/codex/native-profile-processes.ts +114 -15
- package/src/codex/prompt-text-probe.ts +274 -41
- package/src/codex/routing-adoption.ts +189 -0
- package/src/codex/routing.ts +520 -48
- package/src/codex/runtime.ts +249 -7
- package/src/combos/failover.ts +45 -0
- package/src/config.ts +124 -4
- package/src/generated/compatibility-version.json +110 -74
- package/src/generated/model-metadata.ts +1 -0
- package/src/lib/request-execution-budget.ts +202 -0
- package/src/lib/upstream-retry.ts +95 -8
- package/src/lib/workflow-budget.ts +172 -0
- package/src/oauth/devin.ts +57 -12
- package/src/providers/quota.ts +37 -6
- package/src/providers/registry.ts +53 -6
- package/src/responses/input-media.ts +65 -0
- package/src/responses/parser-content.ts +42 -0
- package/src/responses/schema.ts +12 -2
- package/src/server/audio-live.ts +1 -2
- package/src/server/audio-transcriptions.ts +1 -2
- package/src/server/auth-cors.ts +1 -1
- package/src/server/background-lifecycle.ts +18 -0
- package/src/server/chat-completions.ts +23 -8
- package/src/server/chat-native.ts +17 -17
- package/src/server/index.ts +24 -0
- package/src/server/management/request-history-routes.ts +5 -0
- package/src/server/request-log.ts +8 -2
- package/src/server/responses/compact.ts +51 -3
- package/src/server/responses/core.ts +238 -28
- package/src/server/search.ts +7 -9
- package/src/types/config.ts +51 -6
- package/src/usage/log.ts +37 -0
- package/src/vision/eligibility.ts +37 -4
- package/src/vision/index.ts +1 -0
- package/src/vision/plan.ts +45 -10
- package/src/web-search/alpha-search.ts +324 -0
- package/src/web-search/index.ts +13 -22
- package/src/web-search/passthrough-bridge.ts +195 -22
- 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 =
|
|
77
|
-
const cacheWrite =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =>
|
|
790
|
-
|
|
791
|
-
|
|
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);
|
package/src/adapters/registry.ts
CHANGED
|
@@ -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"]>>) => {
|