@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
|
@@ -8,6 +8,15 @@
|
|
|
8
8
|
//
|
|
9
9
|
// This is the same intent boundary as `src/lib/tool-argument-integers.ts`:
|
|
10
10
|
// repair the one faithful reading, leave genuine patch content alone.
|
|
11
|
+
//
|
|
12
|
+
// One accepted exception, added 2026-09-05: a body that is ITSELF one complete
|
|
13
|
+
// operation-bearing envelope, submitted as the `exec` body. That shape is never valid
|
|
14
|
+
// JavaScript — `*** Begin Patch` fails to parse at the leading `**` — so it has zero
|
|
15
|
+
// executable readings and exactly one faithful one. `isCompletePatchEnvelope` recognizes
|
|
16
|
+
// it and the CALLERS retarget it to the apply_patch helper; the repair functions here are
|
|
17
|
+
// unchanged and still return every exec body byte-identical. JavaScript that merely
|
|
18
|
+
// mentions an envelope keeps a real executable reading and is left alone.
|
|
19
|
+
// See devlog/_plan/260905_apply_patch_envelope_gap.
|
|
11
20
|
|
|
12
21
|
const PATCH_BEGIN = "*** Begin Patch";
|
|
13
22
|
const PATCH_END = "*** End Patch";
|
|
@@ -43,6 +52,43 @@ export function normalizeApplyPatchDelimiters(text: string): string {
|
|
|
43
52
|
return `${PATCH_BEGIN}${beginBreak}${body}${endBreak}${PATCH_END}${trailingBreak}`;
|
|
44
53
|
}
|
|
45
54
|
|
|
55
|
+
/**
|
|
56
|
+
* True when a body is one COMPLETE top-level patch envelope carrying a real file
|
|
57
|
+
* operation.
|
|
58
|
+
*
|
|
59
|
+
* A code-mode `exec` body is JavaScript, and this shape is never valid JavaScript: a
|
|
60
|
+
* leading `*** Begin Patch` fails to parse at the `**`. So a body satisfying this
|
|
61
|
+
* predicate cannot be a program the caller meant to run, which is what makes reading it
|
|
62
|
+
* as an apply_patch call the one faithful reading rather than a guess.
|
|
63
|
+
*
|
|
64
|
+
* Deliberately strict, and reusing the same two checks `normalizeApplyPatchDelimiters`
|
|
65
|
+
* uses: a second, looser notion of "looks like a patch" is how a repair boundary drifts.
|
|
66
|
+
* JavaScript that merely CONTAINS an envelope in a string, template, or comment fails the
|
|
67
|
+
* anchored match and is left alone.
|
|
68
|
+
*/
|
|
69
|
+
export function isCompletePatchEnvelope(text: string): boolean {
|
|
70
|
+
const match = TOP_LEVEL_PATCH_ENVELOPE.exec(text);
|
|
71
|
+
if (!match) return false;
|
|
72
|
+
return PATCH_OPERATION_LINE.test(match[3] ?? "");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* True when a PARTIAL buffer could still grow into a complete patch envelope.
|
|
77
|
+
*
|
|
78
|
+
* Streaming decides per delta, before the body is complete, while
|
|
79
|
+
* `isCompletePatchEnvelope` can only answer once it is. Without this the bridge would
|
|
80
|
+
* stream raw envelope bytes and then replace them with compiled helper JavaScript at
|
|
81
|
+
* completion — the rewind that path explicitly refuses to perform.
|
|
82
|
+
*
|
|
83
|
+
* Deliberately one-sided: it holds a buffer that MIGHT become an envelope and never
|
|
84
|
+
* claims one will. A held buffer that turns out to be something else still reaches the
|
|
85
|
+
* client in the authoritative completed item; only the live preview is skipped.
|
|
86
|
+
*/
|
|
87
|
+
export function mayBecomePatchEnvelope(text: string): boolean {
|
|
88
|
+
if (text === "") return false;
|
|
89
|
+
return text.startsWith(PATCH_BEGIN) || PATCH_BEGIN.startsWith(text);
|
|
90
|
+
}
|
|
91
|
+
|
|
46
92
|
/**
|
|
47
93
|
* Repair freeform input before Codex sees it.
|
|
48
94
|
*
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
isCompletePatchEnvelope,
|
|
3
|
+
normalizeApplyPatchDelimiters,
|
|
4
|
+
unwrapFreeformToolInput,
|
|
5
|
+
} from "./apply-patch-envelope";
|
|
6
|
+
import { declaresCodeModeExec } from "../types/tools";
|
|
2
7
|
|
|
3
8
|
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
4
9
|
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
@@ -51,3 +56,36 @@ export function compileCodeModeHelperInput(argumentsText: unknown, toolName: str
|
|
|
51
56
|
}
|
|
52
57
|
return `const result = await tools.exec_command(${JSON.stringify(args)});\ntext(result);`;
|
|
53
58
|
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Resolve the effective code-mode helper for one freeform call.
|
|
62
|
+
*
|
|
63
|
+
* `codeModeHelperName` already covers the NAME-based case: a provider emitted
|
|
64
|
+
* `apply_patch` under a declared `exec` catalog, so `normalizeDeclaredToolName` rewrote
|
|
65
|
+
* the name and recorded the original. That decision happens at tool-call start, before
|
|
66
|
+
* any arguments exist, so it cannot see a provider that got the NAME right and the BODY
|
|
67
|
+
* wrong.
|
|
68
|
+
*
|
|
69
|
+
* This adds that second case: the name is already `exec` so nothing was rewritten, but
|
|
70
|
+
* the body is a complete patch envelope and therefore cannot be the JavaScript that
|
|
71
|
+
* `exec` runs. Same inference the name-based path makes, drawn from the payload.
|
|
72
|
+
*
|
|
73
|
+
* Returns undefined for everything else, including JavaScript that merely mentions a
|
|
74
|
+
* patch envelope — that body is a real program and is forwarded byte-identical.
|
|
75
|
+
*/
|
|
76
|
+
export function resolveCodeModeHelperName(
|
|
77
|
+
codeModeHelperName: string | undefined,
|
|
78
|
+
toolName: string,
|
|
79
|
+
argumentsText: unknown,
|
|
80
|
+
namespace?: string,
|
|
81
|
+
declaredNames?: ReadonlySet<string>,
|
|
82
|
+
): string | undefined {
|
|
83
|
+
if (codeModeHelperName) return codeModeHelperName;
|
|
84
|
+
if (toolName !== "exec" || namespace !== undefined) return undefined;
|
|
85
|
+
// `exec` is a name, not a guarantee. Without a catalog that is genuinely code mode, a
|
|
86
|
+
// caller-defined `exec` could legitimately take patch text, and handing it generated
|
|
87
|
+
// `tools.apply_patch(...)` JavaScript would be the mis-route this repair exists to avoid.
|
|
88
|
+
if (!declaresCodeModeExec(declaredNames)) return undefined;
|
|
89
|
+
if (typeof argumentsText !== "string" || argumentsText === "") return undefined;
|
|
90
|
+
return isCompletePatchEnvelope(unwrapFreeformToolInput(argumentsText)) ? "apply_patch" : undefined;
|
|
91
|
+
}
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
repairFreeformToolInput,
|
|
5
5
|
unwrapFreeformToolInput,
|
|
6
6
|
} from "./apply-patch-envelope";
|
|
7
|
-
import { compileCodeModeHelperInput } from "./code-mode-helper-compat";
|
|
7
|
+
import { compileCodeModeHelperInput, resolveCodeModeHelperName } from "./code-mode-helper-compat";
|
|
8
8
|
import { collectResponsesToolGroups } from "./tool-groups";
|
|
9
9
|
|
|
10
10
|
const ROUTED_CUSTOM_TOOL_PASSTHROUGH = new Set(["apply_patch"]);
|
|
@@ -281,17 +281,23 @@ export function restoreRoutedCustomCalls(
|
|
|
281
281
|
) {
|
|
282
282
|
const sourceInput = item.type === "function_call" ? item.arguments : item.input;
|
|
283
283
|
const aliased = targetName !== wireName;
|
|
284
|
+
const itemNamespace = typeof item.namespace === "string" ? item.namespace : undefined;
|
|
285
|
+
// Name-based alias first; otherwise let a raw patch envelope submitted as the `exec`
|
|
286
|
+
// body resolve to the same apply_patch helper (devlog/_plan/260905_apply_patch_envelope_gap).
|
|
287
|
+
const helper = aliased && sourceInput !== ""
|
|
288
|
+
? item.name
|
|
289
|
+
: resolveCodeModeHelperName(undefined, targetName, sourceInput, itemNamespace, declaredNames);
|
|
284
290
|
const restored: Record<string, unknown> = {
|
|
285
291
|
...item,
|
|
286
292
|
type: "custom_tool_call",
|
|
287
293
|
id: customToolItemId(item.id),
|
|
288
294
|
name: aliased ? targetName : item.name,
|
|
289
|
-
input:
|
|
290
|
-
? compileCodeModeHelperInput(sourceInput,
|
|
295
|
+
input: helper
|
|
296
|
+
? compileCodeModeHelperInput(sourceInput, helper)
|
|
291
297
|
: repairFreeformToolInput(
|
|
292
298
|
sourceInput,
|
|
293
299
|
targetName,
|
|
294
|
-
|
|
300
|
+
itemNamespace,
|
|
295
301
|
),
|
|
296
302
|
};
|
|
297
303
|
delete restored.arguments;
|
|
@@ -1,9 +1,17 @@
|
|
|
1
|
-
/** Hosted tools rejected by specific native model slugs. */
|
|
2
|
-
const UNSUPPORTED_HOSTED_TOOLS: ReadonlyArray<{
|
|
1
|
+
/** Hosted tools rejected by specific native model slugs or exact provider destinations. */
|
|
2
|
+
const UNSUPPORTED_HOSTED_TOOLS: ReadonlyArray<{
|
|
3
|
+
match: (model: string, baseUrl?: string) => boolean;
|
|
4
|
+
tools: ReadonlySet<string>;
|
|
5
|
+
}> = [
|
|
3
6
|
{ match: model => model.includes("codex-spark"), tools: new Set(["image_generation", "tool_search"]) },
|
|
7
|
+
{
|
|
8
|
+
match: (model, baseUrl) => model === "grok-4.6"
|
|
9
|
+
&& baseUrl?.replace(/\/+$/, "") === "https://opencode.ai/zen/go/v1",
|
|
10
|
+
tools: new Set(["web_search", "web_search_preview"]),
|
|
11
|
+
},
|
|
4
12
|
];
|
|
5
13
|
|
|
6
14
|
/** True when forwarding this hosted tool to the model would be rejected upstream. */
|
|
7
|
-
export function isHostedToolUnsupportedForModel(modelId: string, tool: string): boolean {
|
|
8
|
-
return UNSUPPORTED_HOSTED_TOOLS.some(entry => entry.match(modelId) && entry.tools.has(tool));
|
|
15
|
+
export function isHostedToolUnsupportedForModel(modelId: string, tool: string, baseUrl?: string): boolean {
|
|
16
|
+
return UNSUPPORTED_HOSTED_TOOLS.some(entry => entry.match(modelId, baseUrl) && entry.tools.has(tool));
|
|
9
17
|
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import type { OcxContentPart, OcxTextContent } from "../types";
|
|
2
|
+
|
|
3
|
+
export function isObj(v: unknown): v is Record<string, unknown> {
|
|
4
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
type InputBlock =
|
|
8
|
+
| { type: "input_text"; text: string }
|
|
9
|
+
| { type: "text"; text: string }
|
|
10
|
+
| { type: "input_image"; image_url?: string; file_id?: string; detail?: string }
|
|
11
|
+
| { type: "input_video"; video_url?: string }
|
|
12
|
+
| { type: "input_file"; file_id?: string; filename?: string; file_data?: string };
|
|
13
|
+
|
|
14
|
+
/** A usable reference string, or undefined. Empty strings and non-strings are not references. */
|
|
15
|
+
function nonEmptyString(value: unknown): string | undefined {
|
|
16
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function inputContentParts(blocks: unknown): string | OcxContentPart[] {
|
|
20
|
+
if (typeof blocks === "string") return blocks;
|
|
21
|
+
// The catch-all can also hand back a non-array `content` (an object, a number), which would
|
|
22
|
+
// throw at the loop below before any per-block guard runs.
|
|
23
|
+
if (!Array.isArray(blocks)) return [];
|
|
24
|
+
const parts: OcxContentPart[] = [];
|
|
25
|
+
for (const raw of blocks) {
|
|
26
|
+
// A malformed message item fails its strict schema and falls through to inputItemSchema's
|
|
27
|
+
// permissive catch-all, so blocks reaching here are NOT guaranteed to match the declared
|
|
28
|
+
// shape. Validate each field before use, as outputToToolResultContent already does.
|
|
29
|
+
if (!isObj(raw)) continue;
|
|
30
|
+
const block = raw as InputBlock;
|
|
31
|
+
if (block.type === "input_text" || block.type === "text") {
|
|
32
|
+
if (typeof raw.text === "string") parts.push({ type: "text", text: raw.text });
|
|
33
|
+
} else if (block.type === "input_image") {
|
|
34
|
+
const b = block as { image_url?: string; file_id?: string; detail?: string };
|
|
35
|
+
const imageUrl = nonEmptyString(b.image_url);
|
|
36
|
+
const fileId = nonEmptyString(b.file_id);
|
|
37
|
+
const detail = nonEmptyString(b.detail);
|
|
38
|
+
if (imageUrl) {
|
|
39
|
+
// Preserve the image as a structured part — adapters send it as a native image block.
|
|
40
|
+
// NEVER inline the (often base64 data-URL) image_url as text: that explodes the token count.
|
|
41
|
+
parts.push({ type: "image", imageUrl, ...(detail ? { detail: normalizeImageDetail(detail) } : {}) });
|
|
42
|
+
} else if (fileId) {
|
|
43
|
+
parts.push({ type: "text", text: `[image: ${fileId}]` }); // file_id ref → no inline data
|
|
44
|
+
}
|
|
45
|
+
// No usable reference: omit the block. A "[image: ?]" marker would claim an attachment
|
|
46
|
+
// the request never carried, which is worse than dropping malformed input.
|
|
47
|
+
} else if (block.type === "input_video") {
|
|
48
|
+
const videoUrl = nonEmptyString(block.video_url);
|
|
49
|
+
if (videoUrl) parts.push({ type: "video", videoUrl });
|
|
50
|
+
} else if (block.type === "input_file") {
|
|
51
|
+
const b = block as { file_id?: string; filename?: string; file_data?: string };
|
|
52
|
+
const fileId = nonEmptyString(b.file_id);
|
|
53
|
+
const fileData = nonEmptyString(b.file_data);
|
|
54
|
+
const filename = nonEmptyString(b.filename);
|
|
55
|
+
if (fileId) {
|
|
56
|
+
parts.push({ type: "text", text: `[file: ${fileId}]` });
|
|
57
|
+
} else if (fileData) {
|
|
58
|
+
// Inline file_data is often large base64. Preserve only its presence and name, never bytes.
|
|
59
|
+
parts.push({ type: "text", text: filename ? `[file: ${filename}]` : "[file: inline data]" });
|
|
60
|
+
}
|
|
61
|
+
// A bare filename is not a file resource in the Responses schema, so omit it rather than
|
|
62
|
+
// fabricating a "[file: ...]" marker for an attachment that was never sent.
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// Collapse to a plain string only for a single TEXT part; images must stay structured.
|
|
66
|
+
if (parts.length === 1 && parts[0].type === "text") return parts[0].text;
|
|
67
|
+
return parts;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
type OutputBlock = { type: "output_text"; text: string } | { type: "text"; text: string } | { type: "refusal"; refusal: string };
|
|
71
|
+
|
|
72
|
+
export function outputTextOf(blocks: unknown): OcxTextContent[] {
|
|
73
|
+
if (typeof blocks === "string") return blocks.length > 0 ? [{ type: "text", text: blocks }] : [];
|
|
74
|
+
if (!Array.isArray(blocks)) return [];
|
|
75
|
+
const out: OcxTextContent[] = [];
|
|
76
|
+
for (const raw of blocks) {
|
|
77
|
+
// Same catch-all caveat as inputContentParts: validate before use.
|
|
78
|
+
if (!isObj(raw)) continue;
|
|
79
|
+
const b = raw as OutputBlock;
|
|
80
|
+
if (b.type === "output_text" || b.type === "text") {
|
|
81
|
+
if (typeof raw.text === "string") out.push({ type: "text", text: raw.text });
|
|
82
|
+
} else if (b.type === "refusal") {
|
|
83
|
+
if (typeof raw.refusal === "string") out.push({ type: "text", text: `[refusal: ${raw.refusal}]` });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return out;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Tool-call output content. Preserves images (e.g. Codex `view_image` returns
|
|
91
|
+
* `input_image` items): returns content parts when any image is present, else a plain joined string.
|
|
92
|
+
* Never inlines an image_url as text (that would explode the token count).
|
|
93
|
+
*/
|
|
94
|
+
export function outputToToolResultContent(output: string | unknown[] | undefined): string | OcxContentPart[] {
|
|
95
|
+
if (typeof output === "string") return output;
|
|
96
|
+
if (!Array.isArray(output)) return "";
|
|
97
|
+
const parts: OcxContentPart[] = [];
|
|
98
|
+
let hasImage = false;
|
|
99
|
+
for (const raw of output) {
|
|
100
|
+
if (!isObj(raw)) continue;
|
|
101
|
+
if (raw.type === "output_text" || raw.type === "text" || raw.type === "input_text") {
|
|
102
|
+
if (typeof raw.text === "string") parts.push({ type: "text", text: raw.text });
|
|
103
|
+
} else if (raw.type === "refusal" && typeof raw.refusal === "string") {
|
|
104
|
+
parts.push({ type: "text", text: `[refusal: ${raw.refusal}]` });
|
|
105
|
+
} else if (raw.type === "input_image") {
|
|
106
|
+
const imageUrl = nonEmptyString(raw.image_url);
|
|
107
|
+
const fileId = nonEmptyString(raw.file_id);
|
|
108
|
+
if (imageUrl) {
|
|
109
|
+
parts.push({ type: "image", imageUrl, ...(typeof raw.detail === "string" ? { detail: normalizeImageDetail(raw.detail) } : {}) });
|
|
110
|
+
hasImage = true;
|
|
111
|
+
} else if (fileId) {
|
|
112
|
+
parts.push({ type: "text", text: `[image: ${fileId}]` });
|
|
113
|
+
}
|
|
114
|
+
} else if (raw.type === "encrypted_content") {
|
|
115
|
+
// codex-rs FunctionCallOutputContentItem::EncryptedContent — opaque to routed models.
|
|
116
|
+
parts.push({ type: "text", text: "[encrypted content omitted]" });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (!hasImage) return parts.map(p => (p.type === "text" ? p.text : "")).join("");
|
|
120
|
+
return parts;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function toolOutputContainsEncryptedContent(output: string | unknown[] | undefined): boolean {
|
|
124
|
+
return Array.isArray(output) && output.some(raw => isObj(raw) && raw.type === "encrypted_content");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* codex-rs ImageDetail allows "original", but chat-completions providers only accept
|
|
129
|
+
* auto|low|high on image_url.detail — degrade "original" to "high" (the codex default).
|
|
130
|
+
*/
|
|
131
|
+
function normalizeImageDetail(detail: string): string {
|
|
132
|
+
return detail === "original" ? "high" : detail;
|
|
133
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { OcxRequestOptions } from "../types";
|
|
2
|
+
import { isObj } from "./parser-content";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The Responses `text.format` object when it requests structured output (json_schema or
|
|
6
|
+
* json_object), undefined otherwise. Acceptance is identical to the boolean detector this
|
|
7
|
+
* replaces; unknown or malformed formats are ignored, never rejected, so the native
|
|
8
|
+
* passthrough keeps forwarding whatever the caller sent via `_rawBody`.
|
|
9
|
+
*/
|
|
10
|
+
export function parseTextFormat(text: unknown): OcxRequestOptions["textFormat"] {
|
|
11
|
+
if (!isObj(text)) return undefined;
|
|
12
|
+
const format = (text as { format?: unknown }).format;
|
|
13
|
+
if (!isObj(format)) return undefined;
|
|
14
|
+
const f = format as { type?: unknown; name?: unknown; description?: unknown; schema?: unknown; strict?: unknown };
|
|
15
|
+
if (f.type === "json_object") return { type: "json_object" };
|
|
16
|
+
if (f.type !== "json_schema") return undefined;
|
|
17
|
+
return {
|
|
18
|
+
type: "json_schema",
|
|
19
|
+
...(typeof f.name === "string" ? { name: f.name } : {}),
|
|
20
|
+
...(typeof f.description === "string" ? { description: f.description } : {}),
|
|
21
|
+
...(isObj(f.schema) ? { schema: f.schema as Record<string, unknown> } : {}),
|
|
22
|
+
...(typeof f.strict === "boolean" ? { strict: f.strict } : {}),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import type { OcxRequestOptions, OcxTool } from "../types";
|
|
2
|
+
import { isObj } from "./parser-content";
|
|
3
|
+
import { WEB_SEARCH_TOOL_NAME } from "../web-search/synthetic-tool";
|
|
4
|
+
import { buildImageTool, IMAGE_GEN_TOOL_NAME } from "../images/synthetic-tool";
|
|
5
|
+
import { toolSearchDescription, toolSearchParameters } from "./tool-search-compat";
|
|
6
|
+
|
|
7
|
+
export function mapToolChoice(value: unknown): OcxRequestOptions["toolChoice"] {
|
|
8
|
+
if (value === undefined || value === null) return undefined;
|
|
9
|
+
if (value === "auto" || value === "none" || value === "required") return value;
|
|
10
|
+
if (isObj(value) && "type" in value) {
|
|
11
|
+
const t = (value as { type: string }).type;
|
|
12
|
+
if ((t === "function" || t === "custom") && "name" in value) {
|
|
13
|
+
return { name: (value as { name: string }).name };
|
|
14
|
+
}
|
|
15
|
+
// Hosted image tool types (with or without a name) map to the synthetic image_gen wire name.
|
|
16
|
+
if (t === "image_generation" || t === "image_gen") {
|
|
17
|
+
return { name: IMAGE_GEN_TOOL_NAME };
|
|
18
|
+
}
|
|
19
|
+
if (t === "allowed_tools" && Array.isArray(value.tools)) {
|
|
20
|
+
const names = value.tools
|
|
21
|
+
.map(allowedToolName)
|
|
22
|
+
.filter((name): name is string => Boolean(name));
|
|
23
|
+
return names.length > 0
|
|
24
|
+
? { allowedTools: [...new Set(names)], mode: value.mode === "required" ? "required" : "auto" }
|
|
25
|
+
: "none";
|
|
26
|
+
}
|
|
27
|
+
return "auto";
|
|
28
|
+
}
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function allowedToolName(tool: unknown): string | undefined {
|
|
33
|
+
if (!isObj(tool)) return undefined;
|
|
34
|
+
if (typeof tool.name === "string" && tool.name.length > 0) return tool.name;
|
|
35
|
+
if (tool.type === "web_search" || tool.type === "web_search_preview") return WEB_SEARCH_TOOL_NAME;
|
|
36
|
+
if (tool.type === "image_generation" || tool.type === "image_gen") return IMAGE_GEN_TOOL_NAME;
|
|
37
|
+
if (tool.type === "tool_search") return "tool_search";
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function buildTools(tools: unknown[] | undefined): OcxTool[] | undefined {
|
|
42
|
+
if (!tools) return undefined;
|
|
43
|
+
const out: OcxTool[] = [];
|
|
44
|
+
const normalizeParameters = (raw: unknown): Record<string, unknown> => {
|
|
45
|
+
if (isObj(raw) && raw.type === "object") return raw;
|
|
46
|
+
return { ...(isObj(raw) ? raw : {}), type: "object" };
|
|
47
|
+
};
|
|
48
|
+
const pushFn = (t: Record<string, unknown>, namespace?: string) => {
|
|
49
|
+
// Hosted image_generation already installed the synthetic root tool. A later
|
|
50
|
+
// ordinary root `image_gen` must not create a second un-namespaced identity.
|
|
51
|
+
if (
|
|
52
|
+
!namespace
|
|
53
|
+
&& t.name === IMAGE_GEN_TOOL_NAME
|
|
54
|
+
&& out.some(tool => tool.name === IMAGE_GEN_TOOL_NAME && !tool.namespace && tool.imageGeneration)
|
|
55
|
+
) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const tool: OcxTool = {
|
|
59
|
+
name: t.name as string,
|
|
60
|
+
description: (t.description as string) ?? "",
|
|
61
|
+
parameters: normalizeParameters(t.parameters),
|
|
62
|
+
};
|
|
63
|
+
if (t.strict !== undefined) tool.strict = t.strict as boolean;
|
|
64
|
+
if (namespace) tool.namespace = namespace;
|
|
65
|
+
out.push(tool);
|
|
66
|
+
};
|
|
67
|
+
const pushCustom = (t: Record<string, unknown>, namespace?: string) => {
|
|
68
|
+
// Hosted image_generation already installed the synthetic root tool. A later
|
|
69
|
+
// root custom `image_gen` would collide on the same wire name with a different
|
|
70
|
+
// `freeform` flag and throw `ambiguous tool catalog`.
|
|
71
|
+
if (
|
|
72
|
+
!namespace
|
|
73
|
+
&& t.name === IMAGE_GEN_TOOL_NAME
|
|
74
|
+
&& out.some(tool => tool.name === IMAGE_GEN_TOOL_NAME && !tool.namespace && tool.imageGeneration)
|
|
75
|
+
) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
// Freeform custom tools are lowered to a single string `input` because chat models cannot
|
|
79
|
+
// emit Responses grammar payloads directly. Keep tool-specific input guidance scoped to the
|
|
80
|
+
// tool that owns it: leaking apply_patch syntax into `exec` or another freeform tool teaches
|
|
81
|
+
// routed models that the nested helper name is itself a callable top-level tool.
|
|
82
|
+
const inputDescription = t.name === "apply_patch"
|
|
83
|
+
? "Raw tool input. For apply_patch, begin exactly with `*** Begin Patch` (no trailing `***`), then use its standard patch envelope."
|
|
84
|
+
: "Raw freeform input for this tool.";
|
|
85
|
+
const tool: OcxTool = {
|
|
86
|
+
name: t.name as string,
|
|
87
|
+
description: (t.description as string) ?? "",
|
|
88
|
+
parameters: { type: "object", properties: { input: { type: "string", description: inputDescription } }, required: ["input"] },
|
|
89
|
+
freeform: true,
|
|
90
|
+
};
|
|
91
|
+
if (namespace) tool.namespace = namespace;
|
|
92
|
+
out.push(tool);
|
|
93
|
+
};
|
|
94
|
+
for (const t of tools) {
|
|
95
|
+
if (!isObj(t)) continue;
|
|
96
|
+
if (t.type === "function" && isObj(t.function) && typeof t.function.name === "string" && t.function.name.length > 0) {
|
|
97
|
+
pushFn(t.function as Record<string, unknown>);
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (t.type === "function" && typeof t.name === "string") {
|
|
101
|
+
pushFn(t);
|
|
102
|
+
} else if (t.type === "namespace" && Array.isArray(t.tools)) {
|
|
103
|
+
// Codex 0.147 groups its ordinary client tools under the reserved `functions` namespace,
|
|
104
|
+
// including freeform custom tools such as code-mode `exec`. Those children are still
|
|
105
|
+
// top-level Responses tools, so flatten them without a namespace. Other namespace groups
|
|
106
|
+
// are MCP-style and keep their namespace for round-trip routing.
|
|
107
|
+
const builtinFunctions = t.name === "functions";
|
|
108
|
+
const ns = typeof t.name === "string" && !builtinFunctions ? t.name : undefined;
|
|
109
|
+
for (const inner of t.tools as unknown[]) {
|
|
110
|
+
if (isObj(inner) && inner.type === "function" && typeof inner.name === "string") pushFn(inner, ns);
|
|
111
|
+
else if (isObj(inner) && inner.type === "custom" && typeof inner.name === "string") pushCustom(inner, ns);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
else if (t.type === "custom" && typeof t.name === "string") {
|
|
115
|
+
pushCustom(t);
|
|
116
|
+
}
|
|
117
|
+
else if (t.type === "tool_search") {
|
|
118
|
+
// Client-executed tool discovery — the gateway to deferred tools (subagents, extra MCP tools).
|
|
119
|
+
// Expose as a function so chat models can call it; the bridge relays it as a tool_search_call.
|
|
120
|
+
out.push({
|
|
121
|
+
name: "tool_search",
|
|
122
|
+
description: toolSearchDescription(t),
|
|
123
|
+
parameters: normalizeParameters(toolSearchParameters(t)),
|
|
124
|
+
toolSearch: true,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
else if (t.type === "image_generation" || t.type === "image_gen") {
|
|
128
|
+
// Keep Codex's image_gen visible to routed chat models. The hosted OpenAI tool
|
|
129
|
+
// cannot execute on Grok; the model still has to see a callable image_gen so
|
|
130
|
+
// Codex's client-side /v1/images request can fire and be relayed to xAI.
|
|
131
|
+
// Identity is the un-namespaced synthetic root (`imageGeneration: true`), not
|
|
132
|
+
// the bare name: a namespaced ordinary `image_gen` must not suppress it.
|
|
133
|
+
const synthetic = buildImageTool();
|
|
134
|
+
// Every un-namespaced `image_gen` collides on one wire name, so removing only
|
|
135
|
+
// the first leaves a second root behind and the catalog stays ambiguous.
|
|
136
|
+
// Drop all root collisions, keep namespaced entries, then insert exactly one
|
|
137
|
+
// synthetic root — at the earliest colliding position so declaration order is
|
|
138
|
+
// preserved for models that read the catalog positionally.
|
|
139
|
+
let insertAt = -1;
|
|
140
|
+
for (let i = out.length - 1; i >= 0; i -= 1) {
|
|
141
|
+
const tool = out[i]!;
|
|
142
|
+
if (tool.name !== IMAGE_GEN_TOOL_NAME || tool.namespace) continue;
|
|
143
|
+
out.splice(i, 1);
|
|
144
|
+
insertAt = i;
|
|
145
|
+
}
|
|
146
|
+
if (insertAt >= 0) out.splice(insertAt, 0, synthetic);
|
|
147
|
+
else out.push(synthetic);
|
|
148
|
+
}
|
|
149
|
+
else if (typeof t.name === "string" && t.type !== "web_search" && t.type !== "image_generation") {
|
|
150
|
+
// Any OTHER named tool (e.g. a native/computer-use tool type opencodex doesn't explicitly
|
|
151
|
+
// model) is client-executed — pass it through as a function so the routed model can read and
|
|
152
|
+
// call it naturally; the bridge relays its call as a function_call. Previously such tools were
|
|
153
|
+
// silently dropped, so the model never saw them.
|
|
154
|
+
pushFn(t);
|
|
155
|
+
}
|
|
156
|
+
// Hosted web_search is still dropped here — the web-search sidecar re-injects it.
|
|
157
|
+
}
|
|
158
|
+
return out.length > 0 ? out : undefined;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Namespace a custom tool was declared under, by its bare name.
|
|
163
|
+
*
|
|
164
|
+
* A `custom_tool_call` echoed back by the client carries only the bare name — the bridge
|
|
165
|
+
* emits `{"type":"custom_tool_call","name":"exec"}` even when the tool was declared as
|
|
166
|
+
* `mcp__functions__exec`. Without this lookup the namespace is lost on the return trip,
|
|
167
|
+
* and the adapters replay history through `namespacedToolName(namespace, name)`, which
|
|
168
|
+
* then produces a bare `exec` the provider may not have. Ordinary `function_call` items
|
|
169
|
+
* do not need this: they carry `namespace` on the wire.
|
|
170
|
+
*/
|
|
171
|
+
export function customToolNamespaces(tools: unknown): Map<string, string> {
|
|
172
|
+
const out = new Map<string, string>();
|
|
173
|
+
if (!Array.isArray(tools)) return out;
|
|
174
|
+
for (const spec of tools) {
|
|
175
|
+
if (!isObj(spec) || spec.type !== "namespace" || !Array.isArray(spec.tools)) continue;
|
|
176
|
+
const namespace = typeof spec.name === "string" ? spec.name : undefined;
|
|
177
|
+
// Codex 0.147 groups ordinary client tools under the reserved `functions` namespace and
|
|
178
|
+
// buildTools deliberately flattens those without a namespace. Mirror that here, or the
|
|
179
|
+
// reconstruction would invent a namespace the request never advertised.
|
|
180
|
+
if (!namespace || namespace === "functions") continue;
|
|
181
|
+
for (const inner of spec.tools) {
|
|
182
|
+
if (!isObj(inner) || inner.type !== "custom" || typeof inner.name !== "string") continue;
|
|
183
|
+
// Ambiguous bare names are already rejected upstream, so first declaration wins.
|
|
184
|
+
if (!out.has(inner.name)) out.set(inner.name, namespace);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return out;
|
|
188
|
+
}
|