@bitkyc08/opencodex 2.42.0 → 2.43.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/AGENTS_INSTALL.md +2 -2
- package/README.md +31 -0
- package/bin/ocx.mjs +10 -1
- package/gui/dist/assets/index-DS1NE4Jn.css +1 -0
- package/gui/dist/assets/index-Djowl68T.js +112 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic-image-codec.ts +304 -0
- package/src/adapters/anthropic-image-normalize.ts +8 -298
- package/src/adapters/anthropic.ts +30 -7
- package/src/adapters/command-code.ts +7 -5
- package/src/adapters/cursor/desktop-executor-contract.ts +15 -0
- package/src/adapters/cursor/images.ts +36 -6
- package/src/adapters/cursor/live-transport.ts +7 -2
- package/src/adapters/cursor/native-exec-desktop.ts +2 -15
- package/src/adapters/cursor/protobuf-request.ts +13 -3
- package/src/adapters/cursor/tool-definitions.ts +5 -670
- package/src/adapters/cursor/tool-guidance.ts +236 -0
- package/src/adapters/cursor/tool-naming.ts +252 -0
- package/src/adapters/cursor/tool-schemas.ts +195 -0
- package/src/adapters/cursor/types.ts +6 -3
- package/src/adapters/exec-tool-result-normalize.ts +1 -1
- package/src/adapters/google-errors.ts +9 -1
- package/src/adapters/google.ts +1 -0
- package/src/adapters/kiro-calibration.ts +181 -0
- package/src/adapters/kiro.ts +135 -3
- package/src/adapters/openai-responses.ts +216 -26
- package/src/adapters/responses-code-mode.ts +59 -0
- package/src/adapters/tool-catalog-nudge.ts +1 -1
- package/src/adapters/xai-schema-analysis.ts +86 -0
- package/src/adapters/xai-tool-schema.ts +2 -87
- package/src/adapters/xai-web-search.ts +1 -1
- package/src/bridge.ts +22 -10
- package/src/chat/inbound.ts +11 -3
- package/src/claude/inbound-content-options.ts +60 -0
- package/src/claude/inbound-model-options.ts +142 -0
- package/src/claude/inbound-records.ts +7 -0
- package/src/claude/inbound.ts +10 -202
- package/src/claude/model-info.ts +45 -0
- package/src/cli/account-auth.ts +21 -6
- package/src/cli/capabilities.ts +13 -2
- package/src/cli/claude.ts +232 -39
- package/src/cli/config-command.ts +9 -1
- package/src/cli/dispatch.ts +5 -1
- package/src/cli/doctor.ts +10 -0
- package/src/cli/effort.ts +372 -0
- package/src/cli/export-command.ts +3 -9
- package/src/cli/help.ts +1 -0
- package/src/cli/index.ts +13 -0
- package/src/cli/init.ts +4 -0
- package/src/cli/model-selection-guidance.ts +30 -0
- package/src/cli/models-runtime.ts +3 -2
- package/src/cli/models.ts +8 -3
- package/src/cli/opencode.ts +4 -1
- package/src/cli/provider-runtime.ts +65 -0
- package/src/cli/provider.ts +8 -0
- package/src/cli/registry.ts +16 -2
- package/src/cli/runtime-api.ts +3 -1
- package/src/cli/star-prompt.ts +22 -6
- package/src/cli/status-probes.ts +168 -0
- package/src/cli/status.ts +5 -168
- package/src/clients/config-export/constants.ts +69 -0
- package/src/clients/config-export/contracts.ts +154 -0
- package/src/clients/config-export/dsh.ts +132 -0
- package/src/clients/config-export/fast-models.ts +29 -0
- package/src/clients/config-export/mcode.ts +83 -0
- package/src/clients/config-export/model-metadata.ts +108 -0
- package/src/clients/config-export/omp.ts +104 -0
- package/src/clients/config-export/zcode.ts +92 -0
- package/src/clients/config-export.ts +18 -710
- package/src/codex/account-lifecycle.ts +20 -3
- package/src/codex/account-usability.ts +2 -0
- package/src/codex/auth-api.ts +170 -23
- package/src/codex/auth-context.ts +328 -24
- package/src/codex/catalog/effort.ts +30 -4
- package/src/codex/catalog/metadata.ts +39 -9
- package/src/codex/catalog/native-models.ts +53 -12
- package/src/codex/catalog/parsing.ts +119 -5
- package/src/codex/catalog/provider-fetch.ts +150 -20
- package/src/codex/catalog/reserve.ts +52 -0
- package/src/codex/catalog/sync.ts +89 -16
- package/src/codex/convergence-types.ts +1 -0
- package/src/codex/convergence.ts +2 -0
- package/src/codex/data/upstream-models.json +1 -1
- package/src/codex/forward-transport-headers.ts +25 -0
- package/src/codex/inject.ts +3 -28
- package/src/codex/legacy-config-keys.ts +68 -0
- package/src/codex/log-guard/inspect-schema.ts +137 -0
- package/src/codex/log-guard/inspect.ts +2 -134
- package/src/codex/loopback-target.ts +54 -0
- package/src/codex/main-account-cache.ts +63 -1
- package/src/codex/main-account-hard-lock.ts +52 -0
- package/src/codex/main-account.ts +3 -1
- package/src/codex/management-convergence.ts +3 -0
- package/src/codex/model-entitlements.ts +54 -4
- package/src/codex/project-config-warnings.ts +92 -2
- package/src/codex/prompt-layers/encoding.ts +80 -0
- package/src/codex/prompt-layers/paths.ts +54 -0
- package/src/codex/prompt-layers/revision.ts +55 -0
- package/src/codex/prompt-layers/toml-edit.ts +163 -0
- package/src/codex/prompt-layers/toml-read.ts +181 -0
- package/src/codex/prompt-layers.ts +14 -520
- package/src/codex/quota-auto-refresh-state.ts +16 -0
- package/src/codex/quota-auto-refresh.ts +219 -0
- package/src/codex/quota-types.ts +51 -0
- package/src/codex/quota.ts +252 -93
- package/src/codex/reserve-availability.ts +177 -0
- package/src/codex/routing.ts +28 -9
- package/src/codex/shim.ts +53 -11
- package/src/codex/subagent-model-fallback.ts +23 -3
- package/src/combos/failover.ts +125 -7
- package/src/combos/identifiers.ts +89 -0
- package/src/combos/index.ts +4 -0
- package/src/combos/resolve.ts +80 -9
- package/src/combos/types.ts +20 -93
- package/src/config/subagent-models.ts +24 -0
- package/src/config.ts +156 -13
- package/src/generated/compatibility-version.json +454 -166
- package/src/generated/model-metadata.ts +1 -1
- package/src/integrations/journal.ts +65 -4
- package/src/integrations/store.ts +5 -0
- package/src/lab/events/limits.ts +4 -0
- package/src/lib/destination-policy.ts +31 -2
- package/src/lib/errors.ts +39 -0
- package/src/lib/provider-outbound.ts +69 -3
- package/src/lib/proxy-env.ts +22 -0
- package/src/lib/redact-folding.ts +176 -0
- package/src/lib/redact.ts +2 -175
- package/src/lib/state-store-sweeper.ts +20 -6
- package/src/lib/token-estimate.ts +94 -27
- package/src/lib/windows-user-principal.ts +53 -5
- package/src/oauth/anthropic-routing.ts +99 -3
- package/src/oauth/generic-account-failover.ts +36 -13
- package/src/oauth/index.ts +127 -26
- package/src/oauth/login-cli.ts +5 -0
- package/src/oauth/meta-muse.ts +117 -15
- package/src/oauth/pool-settings-capability.ts +15 -4
- package/src/providers/api-keys.ts +8 -10
- package/src/providers/default-aliases.ts +39 -0
- package/src/providers/derive.ts +10 -2
- package/src/providers/fastwire.ts +36 -7
- package/src/providers/initial-model-selection-runtime.ts +90 -0
- package/src/providers/initial-model-selection.ts +120 -0
- package/src/providers/key-failover.ts +134 -54
- package/src/providers/key-store.ts +11 -1
- package/src/providers/label.ts +1 -1
- package/src/providers/model-discovery.ts +76 -0
- package/src/providers/model-rename-startup.ts +72 -8
- package/src/providers/openai-sidecar.ts +17 -5
- package/src/providers/openai-tiers-destination.ts +102 -0
- package/src/providers/openai-tiers.ts +2 -99
- package/src/providers/opencode-go-transport.ts +41 -0
- package/src/providers/quota-key-accounts.ts +141 -0
- package/src/providers/quota-types.ts +9 -0
- package/src/providers/quota.ts +535 -104
- package/src/providers/registry.ts +60 -17
- package/src/providers/xai-responses-opt-in.ts +31 -5
- package/src/quota/reset-activation.ts +81 -0
- package/src/quota/reset-detector.ts +305 -0
- package/src/quota/reset-notify-config.ts +162 -0
- package/src/quota/reset-observer.ts +125 -0
- package/src/quota/reset-poller.ts +160 -0
- package/src/quota/reset-seen-store.ts +385 -0
- package/src/quota/reset-sinks.ts +199 -0
- package/src/quota/window-mapping.ts +106 -0
- package/src/responses/apply-patch-envelope.ts +46 -0
- package/src/responses/code-mode-helper-compat.ts +39 -1
- package/src/responses/custom-tool-compat.ts +10 -4
- package/src/responses/hosted-tool-policy.ts +12 -4
- package/src/responses/parser-content.ts +133 -0
- package/src/responses/parser-text-format.ts +24 -0
- package/src/responses/parser-tools.ts +188 -0
- package/src/responses/parser.ts +3 -326
- package/src/responses/state.ts +124 -28
- package/src/router.ts +48 -13
- package/src/routing/analytics.ts +1 -0
- package/src/routing/capability.ts +17 -4
- package/src/server/auth-cors.ts +7 -1
- package/src/server/background-lifecycle.ts +23 -1
- package/src/server/chat-completions.ts +25 -3
- package/src/server/claude-messages.ts +62 -5
- package/src/server/effort-row.ts +1 -1
- package/src/server/fast-row.ts +295 -0
- package/src/server/gui-static.ts +30 -4
- package/src/server/index.ts +107 -21
- package/src/server/management/agent-settings-routes.ts +2 -2
- package/src/server/management/combo-routes.ts +37 -9
- package/src/server/management/config-routes.ts +93 -2
- package/src/server/management/integration-routes.ts +108 -0
- package/src/server/management/model-routes.ts +13 -3
- package/src/server/management/model-rows.ts +20 -1
- package/src/server/management/native-integration-routes.ts +4 -1
- package/src/server/management/oauth-account-routes.ts +38 -10
- package/src/server/management/provider-routes.ts +34 -3
- package/src/server/management/quota-reset-routes.ts +57 -0
- package/src/server/management/route-registry.ts +7 -4
- package/src/server/management/shared.ts +19 -5
- package/src/server/management/system-routes.ts +3 -2
- package/src/server/management-api.ts +14 -2
- package/src/server/ports.ts +12 -2
- package/src/server/relay-eager.ts +38 -23
- package/src/server/relay.ts +4 -0
- package/src/server/request-log.ts +6 -0
- package/src/server/responses/codex-ws-correlation.ts +65 -0
- package/src/server/responses/codex-ws-exchange.ts +261 -0
- package/src/server/responses/codex-ws-metadata.ts +134 -0
- package/src/server/responses/codex-ws-pool.ts +162 -0
- package/src/server/responses/codex-ws-request.ts +87 -0
- package/src/server/responses/codex-ws-session.ts +93 -0
- package/src/server/responses/codex-ws-wire.ts +144 -0
- package/src/server/responses/collaboration.ts +41 -1
- package/src/server/responses/compact.ts +105 -12
- package/src/server/responses/core.ts +476 -57
- package/src/server/responses/fetch-helpers.ts +10 -3
- package/src/server/responses/input-admission.ts +16 -9
- package/src/server/responses/responses-field-backfill.ts +1 -1
- package/src/server/responses/ws-upstream.ts +34 -318
- package/src/server/responses-custom-tool-repair.ts +20 -4
- package/src/server/responses-undeclared-tool-guard.ts +100 -8
- package/src/server/safe-response-headers.ts +23 -0
- package/src/server/search.ts +9 -0
- package/src/server/subagent-models-startup.ts +27 -0
- package/src/server/system-env-shell.ts +238 -0
- package/src/server/system-env.ts +7 -234
- package/src/server/ws-bridge.ts +3 -25
- package/src/server/xai-responses-startup.ts +21 -0
- package/src/service-manager-probe.ts +1 -1
- package/src/service.ts +55 -16
- package/src/types/config.ts +108 -12
- package/src/types/provider.ts +36 -7
- package/src/types/tools.ts +26 -1
- package/src/types.ts +2 -0
- package/src/update/notify.ts +8 -2
- package/src/usage/cost.ts +38 -28
- package/src/usage/expected-prices.ts +34 -15
- package/src/usage/log.ts +2 -0
- package/src/usage/model-identity.ts +26 -0
- package/src/usage/summary.ts +15 -1
- package/src/vision/describe.ts +6 -0
- package/src/vision/image-rewrite.ts +108 -0
- package/src/vision/index.ts +19 -306
- package/src/vision/plan.ts +205 -0
- package/src/web-search/executor.ts +6 -0
- package/src/web-search/index.ts +8 -1
- package/gui/dist/assets/index-BU1tE0sr.js +0 -112
- package/gui/dist/assets/index-DL9-iS6J.css +0 -1
package/src/vision/describe.ts
CHANGED
|
@@ -7,11 +7,14 @@ import { sidecarEnter } from "../lib/sidecar-tracker";
|
|
|
7
7
|
import { applyUpstreamRecoveryInit, fetchWithResetRetry } from "../lib/upstream-retry";
|
|
8
8
|
import { parseSidecarSSE } from "../web-search/parse";
|
|
9
9
|
import type { SidecarOutcomeRecorder } from "../web-search/executor";
|
|
10
|
+
import { NATIVE_RESERVE_MODEL } from "../codex/catalog/native-models";
|
|
10
11
|
|
|
11
12
|
export interface VisionSettings {
|
|
12
13
|
model: string;
|
|
13
14
|
reasoning: VisionReasoningEffort;
|
|
14
15
|
timeoutMs: number;
|
|
16
|
+
/** Effective Desktop authless compatibility does not grant auxiliary model use. */
|
|
17
|
+
reserveCompatibility?: boolean;
|
|
15
18
|
}
|
|
16
19
|
|
|
17
20
|
/** A description, or an `error` string when it couldn't run (caller injects a graceful marker). */
|
|
@@ -58,6 +61,9 @@ export async function describeImage(
|
|
|
58
61
|
abortSignal?: AbortSignal,
|
|
59
62
|
recordOutcome?: SidecarOutcomeRecorder,
|
|
60
63
|
): Promise<DescribeOutcome> {
|
|
64
|
+
if (settings.reserveCompatibility && settings.model === NATIVE_RESERVE_MODEL) {
|
|
65
|
+
return { text: "", error: "Luna Reserve compatibility is only available as a conversation model, not a vision helper. Choose another vision helper model." };
|
|
66
|
+
}
|
|
61
67
|
const invalid = validateImageUrl(imageUrl);
|
|
62
68
|
if (invalid) return { text: "", error: invalid };
|
|
63
69
|
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { OcxContentPart, OcxParsedRequest, OcxTextContent } from "../types";
|
|
2
|
+
import type { TranslatorBudget } from "../lib/translator-budget";
|
|
3
|
+
|
|
4
|
+
export const descriptionEncoder = new TextEncoder();
|
|
5
|
+
|
|
6
|
+
/** A user/developer/toolResult message can carry images (toolResult: e.g. Codex view_image output). */
|
|
7
|
+
export function carriesImages(role: string): boolean {
|
|
8
|
+
return role === "user" || role === "developer" || role === "toolResult";
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
const IMAGE_OMITTED_TEXT = "[image omitted: this model is text-only and the vision sidecar is unavailable (no ChatGPT login)]";
|
|
13
|
+
|
|
14
|
+
function isPlainRecord(value: unknown): value is Record<string, unknown> {
|
|
15
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Keep the native Responses passthrough body aligned with image replacements made in the parsed
|
|
20
|
+
* message graph. The passthrough adapter serializes `_rawBody`, while translated adapters serialize
|
|
21
|
+
* `context.messages`; updating only the latter would send the original pixels to a text-only
|
|
22
|
+
* Responses upstream even after the vision sidecar produced a caption.
|
|
23
|
+
*
|
|
24
|
+
* Rewrites only image-bearing user/developer messages and tool outputs. All other native Responses
|
|
25
|
+
* items (reasoning, calls, ids, compaction, and provider-specific metadata) remain byte-structurally
|
|
26
|
+
* untouched.
|
|
27
|
+
*/
|
|
28
|
+
export function syncRawBodyImageDescriptions(parsed: OcxParsedRequest, descriptions: readonly string[]): void {
|
|
29
|
+
const rawBody = parsed._rawBody;
|
|
30
|
+
if (!isPlainRecord(rawBody) || !Array.isArray(rawBody.input)) return;
|
|
31
|
+
|
|
32
|
+
let nextDescription = 0;
|
|
33
|
+
const rewriteImages = (value: unknown): unknown => {
|
|
34
|
+
if (Array.isArray(value)) {
|
|
35
|
+
let changed = false;
|
|
36
|
+
const rewritten = value.map(entry => {
|
|
37
|
+
const next = rewriteImages(entry);
|
|
38
|
+
if (next !== entry) changed = true;
|
|
39
|
+
return next;
|
|
40
|
+
});
|
|
41
|
+
return changed ? rewritten : value;
|
|
42
|
+
}
|
|
43
|
+
if (!isPlainRecord(value)) return value;
|
|
44
|
+
if (value.type === "input_image" && typeof value.image_url === "string") {
|
|
45
|
+
// Both message and tool-output parsers exclude empty URLs from caption jobs.
|
|
46
|
+
if (value.image_url.length === 0) {
|
|
47
|
+
const fileId = typeof value.file_id === "string" && value.file_id.length > 0 ? value.file_id : undefined;
|
|
48
|
+
return { type: "input_text", text: fileId ? `[image: ${fileId}]` : IMAGE_OMITTED_TEXT };
|
|
49
|
+
}
|
|
50
|
+
const description = descriptions[nextDescription++];
|
|
51
|
+
return { type: "input_text", text: description ?? IMAGE_OMITTED_TEXT };
|
|
52
|
+
}
|
|
53
|
+
return value;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
let changed = false;
|
|
57
|
+
const input = rawBody.input.map(item => {
|
|
58
|
+
if (!isPlainRecord(item)) return item;
|
|
59
|
+
const type = typeof item.type === "string" ? item.type : (typeof item.role === "string" ? "message" : "");
|
|
60
|
+
const role = typeof item.role === "string" ? item.role : "";
|
|
61
|
+
const isMessageContent = (
|
|
62
|
+
(type === "message" && (role === "user" || role === "developer"))
|
|
63
|
+
|| type === "agent_message"
|
|
64
|
+
);
|
|
65
|
+
const field = isMessageContent
|
|
66
|
+
? "content"
|
|
67
|
+
: (type === "function_call_output" || type === "custom_tool_call_output")
|
|
68
|
+
? "output"
|
|
69
|
+
: undefined;
|
|
70
|
+
if (!field) return item;
|
|
71
|
+
const rewritten = rewriteImages(item[field]);
|
|
72
|
+
if (rewritten === item[field]) return item;
|
|
73
|
+
changed = true;
|
|
74
|
+
return { ...item, [field]: rewritten };
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
if (changed) rawBody.input = input;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Fail-closed image strip for sidecar-covered models when NO sidecar plan exists (no forward
|
|
82
|
+
* provider / missing forwarded auth / sidecar disabled): the upstream is text-only, so forwarding
|
|
83
|
+
* raw images would 400 or silently confuse it. Replace each image with an explicit marker so the
|
|
84
|
+
* model (and the user, via its reply) knows the image was dropped rather than ignored.
|
|
85
|
+
*/
|
|
86
|
+
export function stripImagesInPlace(parsed: OcxParsedRequest, translatorBudget?: TranslatorBudget): boolean {
|
|
87
|
+
let stripped = false;
|
|
88
|
+
const descriptions: string[] = [];
|
|
89
|
+
for (const msg of parsed.context.messages) {
|
|
90
|
+
if (!carriesImages(msg.role) || !Array.isArray(msg.content)) continue;
|
|
91
|
+
const parts = msg.content as OcxContentPart[];
|
|
92
|
+
if (!parts.some(p => p.type === "image")) continue;
|
|
93
|
+
msg.content = parts.map(p => {
|
|
94
|
+
if (p.type !== "image") return p;
|
|
95
|
+
const replacement = { type: "text", text: IMAGE_OMITTED_TEXT } as OcxContentPart;
|
|
96
|
+
descriptions.push((replacement as OcxTextContent).text);
|
|
97
|
+
const reservation = translatorBudget?.reserveTransient(
|
|
98
|
+
descriptionEncoder.encode((replacement as OcxTextContent).text).byteLength,
|
|
99
|
+
{ kind: "request_copies" },
|
|
100
|
+
);
|
|
101
|
+
reservation?.commitRetained();
|
|
102
|
+
return replacement;
|
|
103
|
+
});
|
|
104
|
+
stripped = true;
|
|
105
|
+
}
|
|
106
|
+
syncRawBodyImageDescriptions(parsed, descriptions);
|
|
107
|
+
return stripped;
|
|
108
|
+
}
|
package/src/vision/index.ts
CHANGED
|
@@ -1,26 +1,16 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import type {
|
|
3
|
-
import type
|
|
4
|
-
import { describeImage, type DescribeOutcome, type VisionSettings } from "./describe";
|
|
2
|
+
import type { OcxContentPart, OcxMessage, OcxParsedRequest, OcxTextContent } from "../types";
|
|
3
|
+
import { describeImage, type DescribeOutcome } from "./describe";
|
|
5
4
|
import { describeImageAnthropic } from "./anthropic-describe";
|
|
6
5
|
import { describeImageRouted } from "./routed-describe";
|
|
7
|
-
import { isModelVisionSidecarConsumer as isModelTextOnly, modelAcceptsImageInput } from "./eligibility";
|
|
8
|
-
import { normalizeVisionReasoningForModel } from "./reasoning";
|
|
9
6
|
import type { CodexAuthContext } from "../codex/auth-context";
|
|
10
|
-
import { resolveSidecarAuth } from "../sidecar/auth";
|
|
11
|
-
import type { ResolvedOpenAiForwardSidecar } from "../providers/openai-sidecar";
|
|
12
7
|
import type { SidecarOutcomeRecorder } from "../web-search/executor";
|
|
13
8
|
import { enforceAppOwnedMemoryBudget } from "../lib/app-owned-memory";
|
|
14
9
|
import type { TranslatorBudget } from "../lib/translator-budget";
|
|
15
|
-
import {
|
|
16
|
-
|
|
17
|
-
MAX_VISION_TIMEOUT_MS,
|
|
18
|
-
MIN_VISION_TIMEOUT_MS,
|
|
19
|
-
} from "./timeout-bounds";
|
|
10
|
+
import type { VisionPlan } from "./plan";
|
|
11
|
+
import { carriesImages, descriptionEncoder, syncRawBodyImageDescriptions } from "./image-rewrite";
|
|
20
12
|
|
|
21
13
|
export { describeImage } from "./describe";
|
|
22
|
-
|
|
23
|
-
/** Backward-compatible request-time name for the shared vision-sidecar consumer predicate. */
|
|
24
14
|
export { isModelVisionSidecarConsumer as isModelTextOnly } from "./eligibility";
|
|
25
15
|
export { describeImageAnthropic, parseAnthropicVisionSSE } from "./anthropic-describe";
|
|
26
16
|
export {
|
|
@@ -33,19 +23,25 @@ export {
|
|
|
33
23
|
visionEligibleModelOptions,
|
|
34
24
|
} from "./eligibility";
|
|
35
25
|
export type { VisionCandidateModel, VisionModelOption, VisionSidecarBackend } from "./eligibility";
|
|
26
|
+
export { DEFAULT_VISION_TIMEOUT_MS, MAX_VISION_TIMEOUT_MS, MIN_VISION_TIMEOUT_MS } from "./timeout-bounds";
|
|
36
27
|
export {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
28
|
+
DEFAULT_MAX_DESCRIPTIONS_PER_TURN,
|
|
29
|
+
resolveMaxDescriptionsPerTurn,
|
|
30
|
+
isValidVisionTimeoutMs,
|
|
31
|
+
resolveVisionTimeoutMs,
|
|
32
|
+
findAnthropicVisionProvider,
|
|
33
|
+
resolveVisionBackend,
|
|
34
|
+
resolveOpenAiVisionModel,
|
|
35
|
+
resolveEffectiveVisionModel,
|
|
36
|
+
shouldResolveOpenAiVisionSidecar,
|
|
37
|
+
planVisionSidecar,
|
|
38
|
+
} from "./plan";
|
|
39
|
+
export type { AnthropicVisionProvider, VisionPlan } from "./plan";
|
|
40
|
+
export { stripImagesInPlace } from "./image-rewrite";
|
|
41
|
+
|
|
41
42
|
|
|
42
|
-
const DEFAULT_VISION_MODEL = "gpt-5.4-mini";
|
|
43
|
-
const DEFAULT_ANTHROPIC_VISION_MODEL = "claude-sonnet-5";
|
|
44
|
-
const DEFAULT_REASONING: VisionReasoningEffort = "low";
|
|
45
|
-
export const DEFAULT_MAX_DESCRIPTIONS_PER_TURN = 8;
|
|
46
43
|
const DESCRIPTION_CACHE_MAX_ENTRIES = 256;
|
|
47
44
|
export const VISION_DESCRIPTION_CACHE_MAX_BYTES = 1024 * 1024;
|
|
48
|
-
const descriptionEncoder = new TextEncoder();
|
|
49
45
|
/** Max images described in parallel — keeps first-token latency bounded without flooding the backend. */
|
|
50
46
|
const VISION_CONCURRENCY = 3;
|
|
51
47
|
/** Per-image description hard cap (chars) so multi-image turns can't blow the main model's context. */
|
|
@@ -160,25 +156,6 @@ export function evictOldestVisionDescriptionForBudget(): number {
|
|
|
160
156
|
return descriptionCache.evictOldest?.() ?? 0;
|
|
161
157
|
}
|
|
162
158
|
|
|
163
|
-
/** Runtime config is permissive: zero is intentional; malformed values fall back to the bounded default. */
|
|
164
|
-
export function resolveMaxDescriptionsPerTurn(value: unknown): number {
|
|
165
|
-
if (value === 0) return 0;
|
|
166
|
-
return typeof value === "number" && Number.isInteger(value) && value > 0
|
|
167
|
-
? value
|
|
168
|
-
: DEFAULT_MAX_DESCRIPTIONS_PER_TURN;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
export function isValidVisionTimeoutMs(value: unknown): value is number {
|
|
172
|
-
return typeof value === "number"
|
|
173
|
-
&& Number.isInteger(value)
|
|
174
|
-
&& value >= MIN_VISION_TIMEOUT_MS
|
|
175
|
-
&& value <= MAX_VISION_TIMEOUT_MS;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/** Runtime config is permissive: malformed or out-of-range values fall back to the default. */
|
|
179
|
-
export function resolveVisionTimeoutMs(value: unknown): number {
|
|
180
|
-
return isValidVisionTimeoutMs(value) ? value : DEFAULT_VISION_TIMEOUT_MS;
|
|
181
|
-
}
|
|
182
159
|
|
|
183
160
|
/** Run `worker` over `items` with bounded concurrency, preserving input order in the result array. */
|
|
184
161
|
async function runBounded<T, R>(items: T[], limit: number, worker: (item: T) => Promise<R>): Promise<R[]> {
|
|
@@ -198,176 +175,7 @@ function clamp(s: string, max: number): string {
|
|
|
198
175
|
return s.length <= max ? s : `${s.slice(0, max)}\n…[description truncated]`;
|
|
199
176
|
}
|
|
200
177
|
|
|
201
|
-
export interface AnthropicVisionProvider {
|
|
202
|
-
providerName: string;
|
|
203
|
-
provider: OcxProviderConfig;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* First enabled Anthropic OAuth provider whose active stored account is not marked for reauth.
|
|
208
|
-
* Delegates to the shared sidecar auth module (#2188) — same predicate as web-search.
|
|
209
|
-
*/
|
|
210
|
-
export function findAnthropicVisionProvider(config: OcxConfig): AnthropicVisionProvider | undefined {
|
|
211
|
-
const auth = resolveSidecarAuth(config);
|
|
212
|
-
if (!auth.isAnthropicAuth || !auth.anthropicProviderName || !auth.anthropicProvider) return undefined;
|
|
213
|
-
return { providerName: auth.anthropicProviderName, provider: auth.anthropicProvider };
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
export function resolveVisionBackend(
|
|
217
|
-
explicit: "openai" | "anthropic" | "routed" | undefined,
|
|
218
|
-
anthropicSidecar: AnthropicVisionProvider | undefined,
|
|
219
|
-
): "openai" | "anthropic" {
|
|
220
|
-
if (explicit === "openai" || explicit === "anthropic") return explicit;
|
|
221
|
-
// "routed" collapses to the legacy default order until its describe executor
|
|
222
|
-
// lands (roadmap 170 → 180 revised): a persisted routed backend without a
|
|
223
|
-
// dispatchable arm degrades exactly like unset rather than crashing. wp3
|
|
224
|
-
// replaces this collapse with the real routed arm in planVisionSidecar.
|
|
225
|
-
return anthropicSidecar ? "anthropic" : "openai";
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
/** Native model used by the OpenAI vision helper, including its bounded default. */
|
|
229
|
-
export function resolveOpenAiVisionModel(config: Pick<OcxConfig, "visionSidecar">): string {
|
|
230
|
-
const configured = config.visionSidecar?.model;
|
|
231
|
-
// Namespaced routed ids never reach the forward executor (see
|
|
232
|
-
// resolveEffectiveVisionModel).
|
|
233
|
-
return configured && !configured.includes("/") ? configured : DEFAULT_VISION_MODEL;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
/** Effective describer model for the backend `planVisionSidecar` selected. */
|
|
237
|
-
export function resolveEffectiveVisionModel(
|
|
238
|
-
config: Pick<OcxConfig, "visionSidecar">,
|
|
239
|
-
backend: "openai" | "anthropic",
|
|
240
|
-
): string {
|
|
241
|
-
const configured = config.visionSidecar?.model;
|
|
242
|
-
// A namespaced "provider/model" id belongs to the routed backend only; the
|
|
243
|
-
// forward/OAuth executors POST the model string verbatim, so it falls back
|
|
244
|
-
// to the side's default here (PUT coherence rejects new writes of this
|
|
245
|
-
// shape, but a legacy or hand-edited config must not break the executor).
|
|
246
|
-
const usable = configured && !configured.includes("/") ? configured : undefined;
|
|
247
|
-
return backend === "anthropic"
|
|
248
|
-
? usable || DEFAULT_ANTHROPIC_VISION_MODEL
|
|
249
|
-
: usable || DEFAULT_VISION_MODEL;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
/** A user/developer/toolResult message can carry images (toolResult: e.g. Codex view_image output). */
|
|
253
|
-
function carriesImages(role: string): boolean {
|
|
254
|
-
return role === "user" || role === "developer" || role === "toolResult";
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
function messagesHaveImage(parsed: OcxParsedRequest): boolean {
|
|
258
|
-
return parsed.context.messages.some(m =>
|
|
259
|
-
carriesImages(m.role) && Array.isArray(m.content) && (m.content as OcxContentPart[]).some(p => p.type === "image"));
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
export function shouldResolveOpenAiVisionSidecar(
|
|
263
|
-
config: OcxConfig,
|
|
264
|
-
provider: OcxProviderConfig,
|
|
265
|
-
modelId: string,
|
|
266
|
-
parsed: OcxParsedRequest,
|
|
267
|
-
): boolean {
|
|
268
|
-
if (!isModelTextOnly(provider, modelId) || !messagesHaveImage(parsed)) return false;
|
|
269
|
-
const cfg = config.visionSidecar ?? {};
|
|
270
|
-
if (cfg.enabled === false) return false;
|
|
271
|
-
return resolveVisionBackend(cfg.backend, findAnthropicVisionProvider(config)) === "openai";
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
export interface VisionPlan {
|
|
275
|
-
backend: "openai" | "anthropic" | "routed";
|
|
276
|
-
forwardSidecar?: ResolvedOpenAiForwardSidecar;
|
|
277
|
-
anthropicSidecar?: AnthropicVisionProvider;
|
|
278
|
-
/** Namespaced "provider/model" describer for the routed backend (roadmap 180). */
|
|
279
|
-
routedModel?: string;
|
|
280
|
-
/** Loopback dispatch inputs for the routed backend. */
|
|
281
|
-
routedConfig?: Pick<OcxConfig, "port" | "apiKeys">;
|
|
282
|
-
settings: VisionSettings;
|
|
283
|
-
maxDescriptionsPerTurn: number;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
/**
|
|
287
|
-
* Decide whether the vision sidecar should pre-describe images for this request, returning the plan
|
|
288
|
-
* if so. Active when: the routed model is in `provider.noVisionModels`, the request actually carries
|
|
289
|
-
* an image, the sidecar isn't disabled, and the selected backend has usable auth. Returns undefined
|
|
290
|
-
* otherwise (the caller strips images before sending to a text-only model).
|
|
291
|
-
*/
|
|
292
|
-
export function planVisionSidecar(
|
|
293
|
-
config: OcxConfig,
|
|
294
|
-
provider: OcxProviderConfig,
|
|
295
|
-
modelId: string,
|
|
296
|
-
parsed: OcxParsedRequest,
|
|
297
|
-
openAiSidecar?: ResolvedOpenAiForwardSidecar,
|
|
298
|
-
): VisionPlan | undefined {
|
|
299
|
-
if (!isModelTextOnly(provider, modelId)) return undefined;
|
|
300
|
-
if (!messagesHaveImage(parsed)) return undefined;
|
|
301
|
-
const cfg = config.visionSidecar ?? {};
|
|
302
|
-
if (cfg.enabled === false) return undefined;
|
|
303
|
-
|
|
304
|
-
// Routed arm (roadmap 180 revised): explicit backend + NAMESPACED explicit
|
|
305
|
-
// model only — never inferred from credential availability. Plan-time
|
|
306
|
-
// fence: the target must not be provably blind, and must not itself be a
|
|
307
|
-
// model this planner would re-enter for (belt; the terminal marker on the
|
|
308
|
-
// loopback request is the braces).
|
|
309
|
-
if (cfg.backend === "routed") {
|
|
310
|
-
const routedModel = cfg.model;
|
|
311
|
-
const sep = routedModel ? routedModel.indexOf("/") : -1;
|
|
312
|
-
if (routedModel && sep > 0) {
|
|
313
|
-
const targetProvider = routedModel.slice(0, sep);
|
|
314
|
-
const targetId = routedModel.slice(sep + 1);
|
|
315
|
-
const targetProviderConfig = config.providers?.[targetProvider];
|
|
316
|
-
const targetVisible = modelAcceptsImageInput(config, { provider: targetProvider, id: targetId }) !== false
|
|
317
|
-
&& !(targetProviderConfig && isModelTextOnly(targetProviderConfig, targetId));
|
|
318
|
-
if (targetVisible) {
|
|
319
|
-
return {
|
|
320
|
-
backend: "routed",
|
|
321
|
-
routedModel,
|
|
322
|
-
routedConfig: { port: config.port, ...(config.apiKeys ? { apiKeys: config.apiKeys } : {}) },
|
|
323
|
-
settings: {
|
|
324
|
-
model: routedModel,
|
|
325
|
-
reasoning: DEFAULT_REASONING,
|
|
326
|
-
timeoutMs: resolveVisionTimeoutMs(cfg.timeoutMs),
|
|
327
|
-
},
|
|
328
|
-
maxDescriptionsPerTurn: resolveMaxDescriptionsPerTurn(cfg.maxDescriptionsPerTurn),
|
|
329
|
-
};
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
// Misconfigured routed backend (bare id, unknown provider, or provably
|
|
333
|
-
// blind target): fall through to the legacy default order below rather
|
|
334
|
-
// than dispatching a describe that cannot work.
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
const anthropicSidecar = findAnthropicVisionProvider(config);
|
|
338
|
-
const backend = resolveVisionBackend(cfg.backend, anthropicSidecar);
|
|
339
|
-
// A namespaced routed model must never reach the forward/OAuth executors
|
|
340
|
-
// (they POST the string verbatim); the effective-model resolver falls back
|
|
341
|
-
// to each side's default in that case.
|
|
342
|
-
const model = resolveEffectiveVisionModel(config, backend);
|
|
343
|
-
const maxDescriptionsPerTurn = resolveMaxDescriptionsPerTurn(cfg.maxDescriptionsPerTurn);
|
|
344
178
|
|
|
345
|
-
if (backend === "anthropic") {
|
|
346
|
-
if (!anthropicSidecar) return undefined;
|
|
347
|
-
return {
|
|
348
|
-
backend,
|
|
349
|
-
anthropicSidecar,
|
|
350
|
-
settings: {
|
|
351
|
-
model,
|
|
352
|
-
reasoning: normalizeVisionReasoningForModel(model, cfg.reasoning) ?? DEFAULT_REASONING,
|
|
353
|
-
timeoutMs: resolveVisionTimeoutMs(cfg.timeoutMs),
|
|
354
|
-
},
|
|
355
|
-
maxDescriptionsPerTurn,
|
|
356
|
-
};
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
if (!openAiSidecar) return undefined;
|
|
360
|
-
return {
|
|
361
|
-
backend,
|
|
362
|
-
forwardSidecar: openAiSidecar,
|
|
363
|
-
settings: {
|
|
364
|
-
model,
|
|
365
|
-
reasoning: normalizeVisionReasoningForModel(model, cfg.reasoning) ?? DEFAULT_REASONING,
|
|
366
|
-
timeoutMs: resolveVisionTimeoutMs(cfg.timeoutMs),
|
|
367
|
-
},
|
|
368
|
-
maxDescriptionsPerTurn,
|
|
369
|
-
};
|
|
370
|
-
}
|
|
371
179
|
|
|
372
180
|
interface ImageJob {
|
|
373
181
|
imageUrl: string;
|
|
@@ -385,71 +193,6 @@ function renderDescription(out: { text: string; error?: string }): OcxTextConten
|
|
|
385
193
|
};
|
|
386
194
|
}
|
|
387
195
|
|
|
388
|
-
const IMAGE_OMITTED_TEXT = "[image omitted: this model is text-only and the vision sidecar is unavailable (no ChatGPT login)]";
|
|
389
|
-
|
|
390
|
-
function isPlainRecord(value: unknown): value is Record<string, unknown> {
|
|
391
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
/**
|
|
395
|
-
* Keep the native Responses passthrough body aligned with image replacements made in the parsed
|
|
396
|
-
* message graph. The passthrough adapter serializes `_rawBody`, while translated adapters serialize
|
|
397
|
-
* `context.messages`; updating only the latter would send the original pixels to a text-only
|
|
398
|
-
* Responses upstream even after the vision sidecar produced a caption.
|
|
399
|
-
*
|
|
400
|
-
* Rewrites only image-bearing user/developer messages and tool outputs. All other native Responses
|
|
401
|
-
* items (reasoning, calls, ids, compaction, and provider-specific metadata) remain byte-structurally
|
|
402
|
-
* untouched.
|
|
403
|
-
*/
|
|
404
|
-
function syncRawBodyImageDescriptions(parsed: OcxParsedRequest, descriptions: readonly string[]): void {
|
|
405
|
-
const rawBody = parsed._rawBody;
|
|
406
|
-
if (!isPlainRecord(rawBody) || !Array.isArray(rawBody.input)) return;
|
|
407
|
-
|
|
408
|
-
let nextDescription = 0;
|
|
409
|
-
const rewriteImages = (value: unknown, nonEmptyImageUrlsOnly: boolean): unknown => {
|
|
410
|
-
if (Array.isArray(value)) {
|
|
411
|
-
let changed = false;
|
|
412
|
-
const rewritten = value.map(entry => {
|
|
413
|
-
const next = rewriteImages(entry, nonEmptyImageUrlsOnly);
|
|
414
|
-
if (next !== entry) changed = true;
|
|
415
|
-
return next;
|
|
416
|
-
});
|
|
417
|
-
return changed ? rewritten : value;
|
|
418
|
-
}
|
|
419
|
-
if (!isPlainRecord(value)) return value;
|
|
420
|
-
if (value.type === "input_image" && typeof value.image_url === "string") {
|
|
421
|
-
if (nonEmptyImageUrlsOnly && value.image_url.length === 0) {
|
|
422
|
-
return { type: "input_text", text: IMAGE_OMITTED_TEXT };
|
|
423
|
-
}
|
|
424
|
-
const description = descriptions[nextDescription++];
|
|
425
|
-
return { type: "input_text", text: description ?? IMAGE_OMITTED_TEXT };
|
|
426
|
-
}
|
|
427
|
-
return value;
|
|
428
|
-
};
|
|
429
|
-
|
|
430
|
-
let changed = false;
|
|
431
|
-
const input = rawBody.input.map(item => {
|
|
432
|
-
if (!isPlainRecord(item)) return item;
|
|
433
|
-
const type = typeof item.type === "string" ? item.type : (typeof item.role === "string" ? "message" : "");
|
|
434
|
-
const role = typeof item.role === "string" ? item.role : "";
|
|
435
|
-
const isMessageContent = (
|
|
436
|
-
(type === "message" && (role === "user" || role === "developer"))
|
|
437
|
-
|| type === "agent_message"
|
|
438
|
-
);
|
|
439
|
-
const field = isMessageContent
|
|
440
|
-
? "content"
|
|
441
|
-
: (type === "function_call_output" || type === "custom_tool_call_output")
|
|
442
|
-
? "output"
|
|
443
|
-
: undefined;
|
|
444
|
-
if (!field) return item;
|
|
445
|
-
const rewritten = rewriteImages(item[field], isMessageContent);
|
|
446
|
-
if (rewritten === item[field]) return item;
|
|
447
|
-
changed = true;
|
|
448
|
-
return { ...item, [field]: rewritten };
|
|
449
|
-
});
|
|
450
|
-
|
|
451
|
-
if (changed) rawBody.input = input;
|
|
452
|
-
}
|
|
453
196
|
|
|
454
197
|
function sha256(value: string | Uint8Array): string {
|
|
455
198
|
return createHash("sha256").update(value).digest("hex");
|
|
@@ -635,33 +378,3 @@ export async function describeImagesInPlace(
|
|
|
635
378
|
}
|
|
636
379
|
syncRawBodyImageDescriptions(parsed, descriptions);
|
|
637
380
|
}
|
|
638
|
-
|
|
639
|
-
/**
|
|
640
|
-
* Fail-closed image strip for sidecar-covered models when NO sidecar plan exists (no forward
|
|
641
|
-
* provider / missing forwarded auth / sidecar disabled): the upstream is text-only, so forwarding
|
|
642
|
-
* raw images would 400 or silently confuse it. Replace each image with an explicit marker so the
|
|
643
|
-
* model (and the user, via its reply) knows the image was dropped rather than ignored.
|
|
644
|
-
*/
|
|
645
|
-
export function stripImagesInPlace(parsed: OcxParsedRequest, translatorBudget?: TranslatorBudget): boolean {
|
|
646
|
-
let stripped = false;
|
|
647
|
-
const descriptions: string[] = [];
|
|
648
|
-
for (const msg of parsed.context.messages) {
|
|
649
|
-
if (!carriesImages(msg.role) || !Array.isArray(msg.content)) continue;
|
|
650
|
-
const parts = msg.content as OcxContentPart[];
|
|
651
|
-
if (!parts.some(p => p.type === "image")) continue;
|
|
652
|
-
msg.content = parts.map(p => {
|
|
653
|
-
if (p.type !== "image") return p;
|
|
654
|
-
const replacement = { type: "text", text: IMAGE_OMITTED_TEXT } as OcxContentPart;
|
|
655
|
-
descriptions.push((replacement as OcxTextContent).text);
|
|
656
|
-
const reservation = translatorBudget?.reserveTransient(
|
|
657
|
-
descriptionEncoder.encode((replacement as OcxTextContent).text).byteLength,
|
|
658
|
-
{ kind: "request_copies" },
|
|
659
|
-
);
|
|
660
|
-
reservation?.commitRetained();
|
|
661
|
-
return replacement;
|
|
662
|
-
});
|
|
663
|
-
stripped = true;
|
|
664
|
-
}
|
|
665
|
-
syncRawBodyImageDescriptions(parsed, descriptions);
|
|
666
|
-
return stripped;
|
|
667
|
-
}
|