@bitkyc08/opencodex 2.67.0 → 2.68.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/bin/ocx.mjs +3 -0
- package/gui/dist/assets/App-DCBismRi.css +1 -0
- package/gui/dist/assets/App-mpsibsFj.js +51 -0
- package/gui/dist/assets/Tray-Bn8WhKBH.js +1 -0
- package/gui/dist/assets/{index-DtNmX7hW.css → index-BDUBS8PW.css} +1 -1
- package/gui/dist/assets/index-kUE8tuAK.js +86 -0
- package/gui/dist/assets/quota-summary-DL-bNuNL.js +1 -0
- package/gui/dist/index.html +21 -2
- package/package.json +1 -1
- package/src/adapters/anthropic-image-guard.ts +13 -1
- package/src/adapters/anthropic-image-normalize.ts +3 -2
- package/src/adapters/anthropic-output-schema.ts +36 -0
- package/src/adapters/anthropic.ts +10 -2
- package/src/adapters/base.ts +7 -0
- package/src/adapters/codebuddy/live-models.ts +180 -0
- package/src/adapters/coding-agent/protocol.ts +91 -12
- package/src/adapters/coding-agent/turn.ts +32 -27
- package/src/adapters/devin/cloud-direct/index.ts +1 -0
- package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +18 -4
- package/src/adapters/devin.ts +9 -6
- package/src/adapters/google-http.ts +4 -4
- package/src/adapters/google.ts +93 -3
- package/src/adapters/kiro/adapter.ts +6 -2
- package/src/adapters/kiro/stream.ts +12 -2
- package/src/adapters/kiro/usage.ts +3 -1
- package/src/adapters/kiro-errors.ts +11 -1
- package/src/adapters/kiro-events.ts +27 -1
- package/src/adapters/kiro-refusal.ts +30 -0
- package/src/adapters/kiro-retry.ts +71 -31
- package/src/adapters/openai-chat/deepseek-artifact-schema.ts +45 -0
- package/src/adapters/openai-chat/messages.ts +31 -4
- package/src/adapters/openai-chat/serialized-tool-call-content.ts +52 -7
- package/src/adapters/openai-chat/tool-call-id-remint.ts +65 -0
- package/src/adapters/openai-chat/tool-schema.ts +6 -2
- package/src/adapters/openai-chat.ts +2 -2
- package/src/adapters/openai-responses/muse-tool-choice.ts +31 -0
- package/src/adapters/openai-responses/passthrough.ts +9 -3
- package/src/adapters/opencode-go-additional-tools.ts +19 -10
- package/src/adapters/physical-send.ts +10 -3
- package/src/adapters/registry.ts +2 -1
- package/src/adapters/responses-tool-schema.ts +31 -3
- package/src/adapters/run-turn-queue.ts +63 -23
- package/src/adapters/unique-tool-call-ids.ts +63 -0
- package/src/adapters/xai-web-search.ts +32 -2
- package/src/bridge/sse.ts +14 -2
- package/src/chat/inbound.ts +20 -6
- package/src/claude/agents-inject.ts +4 -0
- package/src/claude/desktop-3p.ts +53 -13
- package/src/claude/desktop-profile.ts +41 -4
- package/src/claude/inbound-model-options.ts +14 -2
- package/src/claude/inbound.ts +1 -1
- package/src/claude/intercept/connect-proxy.ts +17 -1
- package/src/claude/intercept/local-ca.ts +7 -1
- package/src/claude/message-threads.ts +28 -0
- package/src/cli/account-api.ts +13 -0
- package/src/cli/account-auth.ts +49 -2
- package/src/cli/account-extended.ts +4 -2
- package/src/cli/account.ts +4 -2
- package/src/cli/capabilities.ts +17 -0
- package/src/cli/claude.ts +3 -1
- package/src/cli/dispatch.ts +62 -5
- package/src/cli/index.ts +84 -79
- package/src/cli/minimax.ts +4 -2
- package/src/cli/opencode.ts +6 -1
- package/src/cli/restart-handoff.ts +110 -0
- package/src/cli/status.ts +51 -0
- package/src/client/connect.ts +34 -15
- package/src/client/link-ingress.ts +102 -0
- package/src/client/link-join.ts +37 -16
- package/src/client/link-relay.ts +228 -45
- package/src/client/link-state.ts +54 -0
- package/src/client/link-status.ts +39 -0
- package/src/client/link-teardown.ts +2 -2
- package/src/client/link-tunnel.ts +615 -86
- package/src/client/machine-api.ts +10 -4
- package/src/client/machine-listener.ts +57 -10
- package/src/client/runtime.ts +210 -35
- package/src/clients/config-export/constants.ts +1 -12
- package/src/clients/config-export/contracts.ts +2 -0
- package/src/clients/config-export/model-metadata.ts +9 -3
- package/src/clients/config-export/omp.ts +1 -1
- package/src/clients/config-export/zcode-store.ts +2 -2
- package/src/clients/config-export.ts +36 -3
- package/src/codex/app-server-processes.ts +32 -0
- package/src/codex/app-server-restart-service.ts +20 -3
- package/src/codex/catalog/access-programs.ts +50 -0
- package/src/codex/catalog/build-entries.ts +13 -0
- package/src/codex/catalog/derive-entry.ts +1 -0
- package/src/codex/catalog/model-hints.ts +11 -0
- package/src/codex/catalog/parsing.ts +6 -2
- package/src/codex/catalog/provider-models.ts +81 -0
- package/src/codex/catalog/retained-sync.ts +3 -0
- package/src/codex/codex-write-lock.ts +2 -2
- package/src/codex/convergence.ts +2 -0
- package/src/codex/desired-state.ts +14 -4
- package/src/codex/home.ts +21 -3
- package/src/codex/inject/restore.ts +17 -0
- package/src/codex/inject/routing-classify.ts +3 -2
- package/src/codex/inject-coordination.ts +4 -1
- package/src/codex/inject.ts +8 -1
- package/src/codex/journal.ts +6 -2
- package/src/codex/management-convergence.ts +9 -0
- package/src/codex/model-entitlements.ts +28 -5
- package/src/codex/routing/idle-window.ts +58 -0
- package/src/codex/routing-drift.ts +134 -0
- package/src/codex/routing-healer.ts +419 -0
- package/src/codex/routing.ts +10 -0
- package/src/codex/runtime.ts +188 -57
- package/src/codex/sibling-handoff.ts +71 -0
- package/src/codex/sibling-start.ts +139 -0
- package/src/codex/sync.ts +5 -3
- package/src/combos/failover.ts +32 -5
- package/src/combos/request.ts +11 -3
- package/src/combos/reset-window.ts +10 -5
- package/src/combos/resolve.ts +36 -6
- package/src/config/diagnostics.ts +2 -1
- package/src/config/load-degrade.ts +2 -0
- package/src/config/paths.ts +7 -1
- package/src/config/process-state.ts +10 -1
- package/src/config/schema/compaction-recovery.ts +15 -0
- package/src/config/schema/config-schema.ts +2 -0
- package/src/config/schema/leaf-validators.ts +9 -3
- package/src/generated/compatibility-version.json +360 -204
- package/src/images/loop.ts +47 -32
- package/src/integrations/catalog-refresh.ts +4 -0
- package/src/integrations/omp-yaml-source.ts +1 -0
- package/src/lib/claude-request-projection.ts +101 -0
- package/src/lib/local-account-switch-capability.ts +48 -0
- package/src/lib/local-upstream.ts +146 -0
- package/src/lib/package-tree-integrity.ts +25 -3
- package/src/lib/package-tree-retarget.ts +130 -0
- package/src/lib/process-control.ts +4 -1
- package/src/lib/system-restart-contract.ts +106 -0
- package/src/lib/upstream-retry.ts +57 -1
- package/src/link/ports.ts +9 -0
- package/src/link/ssh-argv.ts +16 -0
- package/src/link/ssh-runner.ts +70 -2
- package/src/link/tunnel-state.ts +58 -9
- package/src/oauth/account-quota-rank.ts +29 -18
- package/src/oauth/generic-account-failover.ts +179 -22
- package/src/oauth/index.ts +17 -4
- package/src/oauth/kiro-account-load.ts +107 -0
- package/src/oauth/kiro-device-login.ts +312 -0
- package/src/oauth/kiro-terminal-failover.ts +28 -0
- package/src/oauth/login-flow-state.ts +7 -1
- package/src/oauth/pool-settings-capability.ts +23 -14
- package/src/oauth/store.ts +56 -3
- package/src/oauth/types.ts +4 -0
- package/src/plugins/loader.ts +350 -0
- package/src/plugins/upstream-hooks.ts +119 -0
- package/src/protocols/encoders/adapter-events.ts +9 -2
- package/src/providers/account-quota-disk.ts +42 -4
- package/src/providers/codebuddy-models.ts +6 -2
- package/src/providers/command-code-efforts.ts +33 -10
- package/src/providers/kiro-account-state-disk.ts +44 -0
- package/src/providers/kiro-model-catalog.ts +162 -0
- package/src/providers/kiro-models.ts +5 -4
- package/src/providers/kiro-quota-metrics.ts +35 -0
- package/src/providers/kiro-usage.ts +118 -14
- package/src/providers/quota/account-cache.ts +43 -7
- package/src/providers/quota/antigravity.ts +8 -3
- package/src/providers/quota/kiro-account-probe.ts +12 -0
- package/src/providers/quota/vendor-probes-key.ts +31 -26
- package/src/providers/quota/vendor-probes-oauth.ts +16 -7
- package/src/providers/quota-types.ts +14 -0
- package/src/providers/quota.ts +21 -21
- package/src/providers/registry/entries-extended.ts +5 -3
- package/src/providers/request-pacing.ts +410 -12
- package/src/remote-control/workspace-codex-runtime.ts +4 -3
- package/src/responses/citation-markers.ts +132 -65
- package/src/responses/hosted-tool-policy.ts +14 -3
- package/src/responses/parser-content.ts +3 -2
- package/src/responses/parser.ts +4 -1
- package/src/responses/schema.ts +4 -0
- package/src/responses/visualization-directives.ts +182 -0
- package/src/server/chat-native-sse.ts +24 -12
- package/src/server/chat-native.ts +137 -68
- package/src/server/claude-messages.ts +147 -12
- package/src/server/index/link-listener.ts +7 -0
- package/src/server/index/optional-listeners.ts +4 -1
- package/src/server/index/package-tree-guard.ts +35 -16
- package/src/server/index/serve-options.ts +4 -2
- package/src/server/index/startup-warnings.ts +6 -0
- package/src/server/index.ts +7 -7
- package/src/server/inference/client-encoder-delivery.ts +3 -0
- package/src/server/inference/context.ts +30 -2
- package/src/server/local-account-switch-auth.ts +79 -0
- package/src/server/management/agent-settings-routes.ts +11 -3
- package/src/server/management/config-routes.ts +28 -11
- package/src/server/management/context.ts +2 -0
- package/src/server/management/link-routes.ts +123 -39
- package/src/server/management/logs-usage-routes.ts +9 -42
- package/src/server/management/model-rows.ts +1 -0
- package/src/server/management/oauth-account-routes.ts +67 -12
- package/src/server/management/route-registry.ts +5 -5
- package/src/server/management/sibling-guard.ts +60 -0
- package/src/server/management/storage-log-guard-routes.ts +72 -6
- package/src/server/management/system-restart.ts +92 -135
- package/src/server/management/system-routes.ts +1 -1
- package/src/server/management-api.ts +31 -3
- package/src/server/management-auth.ts +3 -0
- package/src/server/port-reclaim.ts +111 -1
- package/src/server/proxy-liveness.ts +16 -0
- package/src/server/relay.ts +16 -15
- package/src/server/request-log-filter.ts +28 -27
- package/src/server/request-log.ts +7 -3
- package/src/server/request-metrics.ts +28 -0
- package/src/server/responses/adapter-continuation.ts +151 -21
- package/src/server/responses/adapter-delivery.ts +21 -7
- package/src/server/responses/adapter-dispatch.ts +239 -51
- package/src/server/responses/codex-ws-pool.ts +5 -4
- package/src/server/responses/codex-ws-request.ts +4 -1
- package/src/server/responses/compact.ts +8 -4
- package/src/server/responses/compaction-recovery-policy.ts +116 -0
- package/src/server/responses/compaction-recovery.ts +330 -0
- package/src/server/responses/core-combo-failure.ts +1 -0
- package/src/server/responses/core-combo.ts +5 -1
- package/src/server/responses/core-lifetime.ts +22 -0
- package/src/server/responses/core-options.ts +16 -1
- package/src/server/responses/core.ts +14 -4
- package/src/server/responses/empty-completion-guard.ts +2 -0
- package/src/server/responses/fetch-helpers.ts +82 -31
- package/src/server/responses/passthrough-delivery.ts +3 -2
- package/src/server/responses/passthrough-dispatch.ts +12 -2
- package/src/server/responses/passthrough-execution.ts +2 -2
- package/src/server/responses/request-send-budget.ts +21 -5
- package/src/server/responses/request-transport.ts +116 -18
- package/src/server/responses/reset-replay.ts +32 -0
- package/src/server/responses/run-turn-execution.ts +136 -19
- package/src/server/responses/sidecar-execution.ts +41 -8
- package/src/server/responses/terminal-guard.ts +2 -0
- package/src/server/responses/ws-upstream.ts +44 -4
- package/src/server/restart-replacement.ts +382 -0
- package/src/server/startup-health-cache.ts +23 -4
- package/src/server/stop-teardown.ts +35 -1
- package/src/server/system-env.ts +3 -0
- package/src/service/windows-taskxml.ts +11 -7
- package/src/service/windows-wrapper-exit.ts +8 -0
- package/src/stall-timeout.ts +35 -10
- package/src/storage/policy-job.ts +4 -0
- package/src/storage/scanner.ts +177 -29
- package/src/storage/storage-mutation-coordinator.ts +17 -0
- package/src/tray/windows-tray.ps1 +50 -23
- package/src/types/config.ts +4 -0
- package/src/types/provider.ts +5 -1
- package/src/types/request.ts +13 -2
- package/src/update/index.ts +3 -1
- package/src/update/job.ts +6 -2
- package/src/update/mise-launcher-target.ts +100 -0
- package/src/update/worker-launch.ts +46 -0
- package/src/usage/log.ts +2 -0
- package/src/web-search/loop.ts +5 -2
- package/gui/dist/assets/App-BJsT8Icc.css +0 -1
- package/gui/dist/assets/App-D3pNGiN4.js +0 -51
- package/gui/dist/assets/Tray-yb05wCjb.js +0 -1
- package/gui/dist/assets/index-CouvvtMV.js +0 -86
- package/gui/dist/assets/tray-data-CYGdjCJ7.js +0 -1
|
@@ -16,6 +16,12 @@
|
|
|
16
16
|
* Strip, do not translate. The `turnNviewN` ids are turn-scoped and opaque, and the
|
|
17
17
|
* response carries no mapping from them to a URL, so there is nothing to convert them
|
|
18
18
|
* into. Structured `url_citation` annotations are a separate path and are untouched.
|
|
19
|
+
*
|
|
20
|
+
* Only citation spans are stripped. The same delimiters carry other inline directives
|
|
21
|
+
* that the Codex App renders, such as `\uE200visualize\uE202{"path":…}\uE201` from the
|
|
22
|
+
* bundled visualize plugin (#6039). A span is a citation only when the keyword between
|
|
23
|
+
* START and its first SEPARATOR or END is one of `CITATION_KEYWORDS`; any other span
|
|
24
|
+
* passes through byte for byte.
|
|
19
25
|
*/
|
|
20
26
|
|
|
21
27
|
/** Opens a citation span. */
|
|
@@ -25,6 +31,12 @@ export const CITATION_MARKER_SEPARATOR = "\uE202";
|
|
|
25
31
|
/** Closes a citation span. */
|
|
26
32
|
export const CITATION_MARKER_END = "\uE201";
|
|
27
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Keywords that mark a span as a ChatGPT citation. `filecite` is the uploaded-file variant
|
|
36
|
+
* of `cite` and uses the same `turnN…` reference grammar.
|
|
37
|
+
*/
|
|
38
|
+
const CITATION_KEYWORDS: readonly string[] = ["cite", "filecite"];
|
|
39
|
+
|
|
28
40
|
/** True when the text contains any of the three delimiters. Cheap pre-check. */
|
|
29
41
|
export function hasCitationMarker(text: string): boolean {
|
|
30
42
|
return text.includes(CITATION_MARKER_START)
|
|
@@ -33,34 +45,15 @@ export function hasCitationMarker(text: string): boolean {
|
|
|
33
45
|
}
|
|
34
46
|
|
|
35
47
|
/**
|
|
36
|
-
*
|
|
48
|
+
* Upper bound on the length of a span (START through END inclusive).
|
|
37
49
|
*
|
|
38
|
-
* A
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
50
|
+
* A real citation is `cite` plus a few turn-scoped ids, so it is far under this. Without a
|
|
51
|
+
* bound, a backend that emits a START and never terminates it makes the streaming filter
|
|
52
|
+
* withhold the rest of the response. A non-citation directive keeps later STARTs literal
|
|
53
|
+
* only within the same bound, so one unterminated directive cannot hide a real citation for
|
|
54
|
+
* the rest of the message.
|
|
42
55
|
*/
|
|
43
|
-
|
|
44
|
-
if (!text.includes(CITATION_MARKER_START)) return text;
|
|
45
|
-
// Walk START-delimited segments exactly like the streaming filter below: a START whose
|
|
46
|
-
// own segment (up to the next START) contains an END within the span bound is a span and
|
|
47
|
-
// is removed; a START that is superseded by another START before any END, or whose span
|
|
48
|
-
// exceeds MAX_CITATION_SPAN_LENGTH, is malformed text and stays verbatim. Pairing an
|
|
49
|
-
// earlier malformed START with a later span's END would delete real answer text and,
|
|
50
|
-
// worse, disagree with what the streaming deltas already emitted (#3843). The bound is
|
|
51
|
-
// shared with the streaming filter for the same reason: a span it has already released
|
|
52
|
-
// as over-bound must not be swallowed here when the END finally arrives.
|
|
53
|
-
let start = text.indexOf(CITATION_MARKER_START);
|
|
54
|
-
let out = text.slice(0, start);
|
|
55
|
-
while (start !== -1) {
|
|
56
|
-
const nextStart = text.indexOf(CITATION_MARKER_START, start + 1);
|
|
57
|
-
const segment = text.slice(start, nextStart === -1 ? text.length : nextStart);
|
|
58
|
-
const end = segment.indexOf(CITATION_MARKER_END, 1);
|
|
59
|
-
out += end === -1 || end + 1 > MAX_CITATION_SPAN_LENGTH ? segment : segment.slice(end + 1);
|
|
60
|
-
start = nextStart;
|
|
61
|
-
}
|
|
62
|
-
return out;
|
|
63
|
-
}
|
|
56
|
+
const MAX_CITATION_SPAN_LENGTH = 4_096;
|
|
64
57
|
|
|
65
58
|
export interface CitationMarkerFilter {
|
|
66
59
|
/** Feed one streaming delta; returns the portion safe to emit now. */
|
|
@@ -70,63 +63,137 @@ export interface CitationMarkerFilter {
|
|
|
70
63
|
}
|
|
71
64
|
|
|
72
65
|
/**
|
|
73
|
-
*
|
|
74
|
-
* on the text the streaming filter withholds for one unterminated START.
|
|
66
|
+
* Remove every complete citation span from a whole string.
|
|
75
67
|
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
* the text was chunked.
|
|
68
|
+
* This runs the streaming filter over the whole text, so the bridge's re-stripped
|
|
69
|
+
* `output_text.done` always equals the concatenated deltas it already emitted (#3843).
|
|
70
|
+
* Anything that is not a complete citation span stays verbatim: an unterminated START, a
|
|
71
|
+
* stray SEPARATOR or END, and every non-citation directive.
|
|
81
72
|
*/
|
|
82
|
-
|
|
73
|
+
export function stripCitationMarkers(text: string): string {
|
|
74
|
+
if (!text.includes(CITATION_MARKER_START)) return text;
|
|
75
|
+
const filter = createCitationMarkerFilter();
|
|
76
|
+
return filter.push(text) + filter.flush();
|
|
77
|
+
}
|
|
83
78
|
|
|
84
79
|
/**
|
|
85
|
-
* Streaming filter
|
|
80
|
+
* Streaming filter, as a per-character state machine so the result never depends on how the
|
|
81
|
+
* text was split into deltas.
|
|
86
82
|
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
83
|
+
* - `text`: ordinary text, emitted at once. A START opens `keyword`.
|
|
84
|
+
* - `keyword`: the START and keyword so far are withheld while the keyword can still become
|
|
85
|
+
* a citation keyword. A SEPARATOR after a citation keyword enters `citation`; an END right
|
|
86
|
+
* after one removes the span. Any other keyword releases the text and enters `other`.
|
|
87
|
+
* - `citation`: withheld until its END, then removed. A new START means this one was
|
|
88
|
+
* malformed, so it is released verbatim; so is a span that reaches the length bound.
|
|
89
|
+
* - `other`: the keyword of a non-citation span, emitted at once. A START here means the
|
|
90
|
+
* earlier one was malformed and opens a new span; a SEPARATOR enters `opaque`.
|
|
91
|
+
* - `opaque`: the body of a non-citation directive such as `visualize` (#6039), emitted at
|
|
92
|
+
* once. STARTs inside it are payload, not new spans, until its END or the length bound.
|
|
91
93
|
*
|
|
92
|
-
* A
|
|
93
|
-
*
|
|
94
|
+
* A stream that ends while text is withheld releases it verbatim, so nothing the model
|
|
95
|
+
* actually said is lost.
|
|
94
96
|
*/
|
|
95
97
|
export function createCitationMarkerFilter(): CitationMarkerFilter {
|
|
96
|
-
|
|
98
|
+
let mode: "text" | "keyword" | "citation" | "other" | "opaque" = "text";
|
|
99
|
+
// The withheld START, keyword, and body in `keyword` and `citation` modes.
|
|
97
100
|
let held = "";
|
|
101
|
+
// Characters from START so far in `other` and `opaque` modes.
|
|
102
|
+
let spanLength = 0;
|
|
103
|
+
const isKeywordPrefix = (keyword: string): boolean => CITATION_KEYWORDS.some(k => k.startsWith(keyword));
|
|
104
|
+
|
|
98
105
|
return {
|
|
99
106
|
push(delta: string): string {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
107
|
+
let out = "";
|
|
108
|
+
let i = 0;
|
|
109
|
+
while (i < delta.length) {
|
|
110
|
+
if (mode === "text") {
|
|
111
|
+
const next = delta.indexOf(CITATION_MARKER_START, i);
|
|
112
|
+
if (next === -1) {
|
|
113
|
+
out += delta.slice(i);
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
out += delta.slice(i, next);
|
|
117
|
+
held = CITATION_MARKER_START;
|
|
118
|
+
mode = "keyword";
|
|
119
|
+
i = next + 1;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
const ch = delta[i]!;
|
|
123
|
+
if (mode === "other" || mode === "opaque") {
|
|
124
|
+
if (spanLength + 1 > MAX_CITATION_SPAN_LENGTH) {
|
|
125
|
+
// Past the bound this is ordinary text again; handle the character in `text`.
|
|
126
|
+
mode = "text";
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (ch === CITATION_MARKER_START && mode === "other") {
|
|
130
|
+
held = CITATION_MARKER_START;
|
|
131
|
+
mode = "keyword";
|
|
132
|
+
} else {
|
|
133
|
+
out += ch;
|
|
134
|
+
spanLength += 1;
|
|
135
|
+
if (ch === CITATION_MARKER_END) mode = "text";
|
|
136
|
+
else if (ch === CITATION_MARKER_SEPARATOR) mode = "opaque";
|
|
137
|
+
}
|
|
138
|
+
i += 1;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
i += 1;
|
|
142
|
+
if (ch === CITATION_MARKER_START) {
|
|
143
|
+
// The withheld START was malformed: release it and open a new span here.
|
|
144
|
+
out += held;
|
|
145
|
+
held = CITATION_MARKER_START;
|
|
146
|
+
mode = "keyword";
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
if (mode === "citation") {
|
|
150
|
+
if (ch === CITATION_MARKER_END) {
|
|
151
|
+
held = "";
|
|
152
|
+
mode = "text";
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
held += ch;
|
|
156
|
+
// One more character plus an END would exceed the bound, so this is not a span.
|
|
157
|
+
if (held.length >= MAX_CITATION_SPAN_LENGTH) {
|
|
158
|
+
out += held;
|
|
159
|
+
held = "";
|
|
160
|
+
mode = "text";
|
|
161
|
+
}
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
// mode === "keyword"
|
|
165
|
+
if (ch === CITATION_MARKER_SEPARATOR || ch === CITATION_MARKER_END) {
|
|
166
|
+
if (CITATION_KEYWORDS.includes(held.slice(1))) {
|
|
167
|
+
if (ch === CITATION_MARKER_END) {
|
|
168
|
+
held = "";
|
|
169
|
+
mode = "text";
|
|
170
|
+
} else {
|
|
171
|
+
held += ch;
|
|
172
|
+
mode = "citation";
|
|
173
|
+
}
|
|
174
|
+
} else {
|
|
175
|
+
out += held + ch;
|
|
176
|
+
spanLength = held.length + 1;
|
|
177
|
+
held = "";
|
|
178
|
+
mode = ch === CITATION_MARKER_END ? "text" : "opaque";
|
|
179
|
+
}
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
held += ch;
|
|
183
|
+
if (!isKeywordPrefix(held.slice(1))) {
|
|
184
|
+
out += held;
|
|
185
|
+
spanLength = held.length;
|
|
186
|
+
held = "";
|
|
187
|
+
mode = "other";
|
|
122
188
|
}
|
|
123
|
-
start = nextStart;
|
|
124
189
|
}
|
|
125
190
|
return out;
|
|
126
191
|
},
|
|
127
192
|
flush(): string {
|
|
128
193
|
const rest = held;
|
|
129
194
|
held = "";
|
|
195
|
+
spanLength = 0;
|
|
196
|
+
mode = "text";
|
|
130
197
|
return rest;
|
|
131
198
|
},
|
|
132
199
|
};
|
|
@@ -1,11 +1,22 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* Hosted tools rejected by specific native model slugs or exact provider destinations.
|
|
3
|
+
*
|
|
4
|
+
* Add a row only for a destination that refuses the tool itself.
|
|
5
|
+
*/
|
|
2
6
|
const UNSUPPORTED_HOSTED_TOOLS: ReadonlyArray<{
|
|
3
7
|
match: (model: string, baseUrl?: string) => boolean;
|
|
4
8
|
tools: ReadonlySet<string>;
|
|
5
9
|
}> = [
|
|
6
10
|
{
|
|
7
|
-
|
|
8
|
-
|
|
11
|
+
// MiMo rejects hosted search with "tool type 'web_search' is not supported by this gateway phase" (#5501).
|
|
12
|
+
match: (_model, baseUrl) => {
|
|
13
|
+
if (!baseUrl) return false;
|
|
14
|
+
try {
|
|
15
|
+
return /(?:^|\.)xiaomimimo\.com$/i.test(new URL(baseUrl).hostname);
|
|
16
|
+
} catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
},
|
|
9
20
|
tools: new Set(["web_search", "web_search_preview"]),
|
|
10
21
|
},
|
|
11
22
|
];
|
|
@@ -9,7 +9,7 @@ type InputBlock =
|
|
|
9
9
|
| { type: "input_text"; text: string }
|
|
10
10
|
| { type: "text"; text: string }
|
|
11
11
|
| { type: "input_image"; image_url?: string; file_id?: string; detail?: string }
|
|
12
|
-
| { type: "input_video"; video_url?: string }
|
|
12
|
+
| { type: "input_video"; video_url?: string; processing?: string }
|
|
13
13
|
// codex-rs protocol/src/models.rs sends audio as input_audio with an audio_url.
|
|
14
14
|
| { type: "input_audio"; audio_url?: string; format?: string }
|
|
15
15
|
| { type: "input_file"; file_id?: string; filename?: string; file_data?: string };
|
|
@@ -62,7 +62,8 @@ export function inputContentParts(blocks: unknown): string | OcxContentPart[] {
|
|
|
62
62
|
// the request never carried, which is worse than dropping malformed input.
|
|
63
63
|
} else if (block.type === "input_video") {
|
|
64
64
|
const videoUrl = nonEmptyString(block.video_url);
|
|
65
|
-
|
|
65
|
+
const processing = nonEmptyString((block as { processing?: string }).processing);
|
|
66
|
+
if (videoUrl) parts.push({ type: "video", videoUrl, ...(processing ? { processing } : {}) });
|
|
66
67
|
} else if (block.type === "input_audio") {
|
|
67
68
|
// Upstream Codex sends input_audio with an audio_url (codex-rs
|
|
68
69
|
// protocol/src/models.rs). The IR has no audio carrier and no adapter consumes
|
package/src/responses/parser.ts
CHANGED
|
@@ -27,6 +27,7 @@ import { isObj, inputContentParts, outputTextOf, outputToToolResultContent, tool
|
|
|
27
27
|
import { mapToolChoice, buildTools, customToolNamespaces } from "./parser-tools";
|
|
28
28
|
import { parseTextFormat } from "./parser-text-format";
|
|
29
29
|
import { externalTaskInputContent } from "./task-input";
|
|
30
|
+
import { normalizeVisualizationContext } from "./visualization-directives";
|
|
30
31
|
|
|
31
32
|
/**
|
|
32
33
|
* Wrap a remembered proxy-side signature as provider metadata for a replayed tool call.
|
|
@@ -644,7 +645,9 @@ export function parseRequest(
|
|
|
644
645
|
return {
|
|
645
646
|
modelId: data.model,
|
|
646
647
|
...(data.previous_response_id ? { previousResponseId: data.previous_response_id } : {}),
|
|
647
|
-
|
|
648
|
+
// Codex App visualization references in the private-use form are invisible to some models;
|
|
649
|
+
// hand every model the app's ASCII directive instead (visualization-directives.ts).
|
|
650
|
+
context: normalizeVisualizationContext(context),
|
|
648
651
|
stream: data.stream === true,
|
|
649
652
|
options,
|
|
650
653
|
_rawBody: body,
|
package/src/responses/schema.ts
CHANGED
|
@@ -14,6 +14,10 @@ const inputImageBlockSchema = z.object({
|
|
|
14
14
|
const inputVideoBlockSchema = z.object({
|
|
15
15
|
type: z.literal("input_video"),
|
|
16
16
|
video_url: z.string().min(1),
|
|
17
|
+
// Gemini agentic video understanding (#3271). z.object() strips unknown keys,
|
|
18
|
+
// so without declaring it here the mode is dropped before any adapter sees it
|
|
19
|
+
// and the request silently degrades to frame-by-frame decoding.
|
|
20
|
+
processing: z.string().min(1).optional(),
|
|
17
21
|
});
|
|
18
22
|
const inputFileBlockSchema = z.object({
|
|
19
23
|
type: z.literal("input_file"),
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex App visualization references for models that cannot see private-use characters.
|
|
3
|
+
*
|
|
4
|
+
* The bundled Visualize skill teaches the model to answer with
|
|
5
|
+
*
|
|
6
|
+
* \uE200visualize\uE202{"path":"/abs/chart.html"}\uE201
|
|
7
|
+
*
|
|
8
|
+
* and the Codex App renders that span as an inline visualization. Some providers drop Basic
|
|
9
|
+
* Multilingual Plane private-use characters before the model sees them (every Claude route checked
|
|
10
|
+
* on 2026-09-27, direct and through Cursor), so the model reads and writes a bare
|
|
11
|
+
* `visualize{"path":...}` that the app shows verbatim.
|
|
12
|
+
*
|
|
13
|
+
* The app does not need the private-use form. Its own renderer rewrites each span into the plain
|
|
14
|
+
* directive `::codex-inline-vis{path="/abs/chart.html"}` before parsing, and it renders that
|
|
15
|
+
* directive when it appears directly. Rewriting the span the same way in model-visible text gives
|
|
16
|
+
* every model an ASCII instruction it can read and repeat.
|
|
17
|
+
*
|
|
18
|
+
* The rules mirror the app (26.924.22138, `f2` in app-shared) with two deliberate differences: code
|
|
19
|
+
* blocks are rewritten too, because the skill's example sits in a fenced block, and the skill's
|
|
20
|
+
* `<absolute-path>/<title>.html` placeholder is accepted so the model still sees the template.
|
|
21
|
+
* Matching follows the app's regex `/\uE200visualize\uE202([^\uE201]+)\uE201/g` exactly, scanned in
|
|
22
|
+
* linear time. Anything the app would keep as-is is kept as-is here.
|
|
23
|
+
*
|
|
24
|
+
* Only the parsed context is rewritten. The raw request body, which native passthrough serializes
|
|
25
|
+
* and which is stored for `previous_response_id` replay, is never touched.
|
|
26
|
+
*/
|
|
27
|
+
import { posix } from "node:path";
|
|
28
|
+
import type { OcxContentPart, OcxContext, OcxMessage, OcxTextContent } from "../types";
|
|
29
|
+
|
|
30
|
+
const START = "\uE200";
|
|
31
|
+
const END = "\uE201";
|
|
32
|
+
const PREFIX = `${START}visualize\uE202`;
|
|
33
|
+
const INLINE_DIRECTIVE = "codex-inline-vis";
|
|
34
|
+
const LIVE_DIRECTIVE = "codex-live-vis";
|
|
35
|
+
/** The placeholder path in the Visualize skill's own template. */
|
|
36
|
+
const TEMPLATE_PATH = "<absolute-path>/<title>.html";
|
|
37
|
+
const HTML_BASENAME = /^[a-z0-9]+(?:-[a-z0-9]+)*\.html$/;
|
|
38
|
+
const PARENT_SEGMENT = /(?:^|[\\/])\.\.(?:[\\/]|$)/;
|
|
39
|
+
const UNSAFE_ATTRIBUTE = /["\n\r]/;
|
|
40
|
+
|
|
41
|
+
/** The app's absolute-path test: POSIX (not `//`), drive letter, UNC with either slash. */
|
|
42
|
+
function isAbsoluteAppPath(path: string): boolean {
|
|
43
|
+
return (path.startsWith("/") && !path.startsWith("//"))
|
|
44
|
+
|| /^[A-Za-z]:[\\/]/.test(path)
|
|
45
|
+
|| /^\\\\[^\\]+\\[^\\]+/.test(path)
|
|
46
|
+
|| /^\/\/[^/]+\/[^/]+/.test(path);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface VisualizationReference {
|
|
50
|
+
path: string | null;
|
|
51
|
+
title?: string;
|
|
52
|
+
type?: "inline" | "live";
|
|
53
|
+
mode?: "wide";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** The app's payload schema: `path` is required but nullable; the rest are optional and typed. */
|
|
57
|
+
function parseReference(value: unknown): VisualizationReference | undefined {
|
|
58
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
59
|
+
const record = value as Record<string, unknown>;
|
|
60
|
+
if (!("path" in record) || (record.path !== null && typeof record.path !== "string")) return undefined;
|
|
61
|
+
if (record.title !== undefined && typeof record.title !== "string") return undefined;
|
|
62
|
+
if (record.type !== undefined && record.type !== "inline" && record.type !== "live") return undefined;
|
|
63
|
+
if (record.mode !== undefined && record.mode !== "wide") return undefined;
|
|
64
|
+
if (record.wide !== undefined && typeof record.wide !== "boolean") return undefined;
|
|
65
|
+
return {
|
|
66
|
+
path: record.path as string | null,
|
|
67
|
+
...(record.title !== undefined ? { title: record.title as string } : {}),
|
|
68
|
+
...(record.type !== undefined ? { type: record.type as "inline" | "live" } : {}),
|
|
69
|
+
...(record.mode !== undefined ? { mode: "wide" as const } : {}),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** The ASCII directive for one span payload, or undefined when the app would keep the span. */
|
|
74
|
+
function toAsciiDirective(payload: string): string | undefined {
|
|
75
|
+
const isJson = payload.startsWith("{");
|
|
76
|
+
let raw: unknown = { path: payload };
|
|
77
|
+
if (isJson) {
|
|
78
|
+
try {
|
|
79
|
+
raw = JSON.parse(payload);
|
|
80
|
+
} catch {
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const reference = parseReference(raw);
|
|
85
|
+
if (!reference) return undefined;
|
|
86
|
+
const live = reference.type === "live";
|
|
87
|
+
const { path } = reference;
|
|
88
|
+
if (path === null) return live ? `::${LIVE_DIRECTIVE}{}` : undefined;
|
|
89
|
+
|
|
90
|
+
const template = isJson && path === TEMPLATE_PATH;
|
|
91
|
+
if (!template) {
|
|
92
|
+
const basename = posix.basename(path.replaceAll("\\", "/"));
|
|
93
|
+
if (PARENT_SEGMENT.test(path) || UNSAFE_ATTRIBUTE.test(path) || !HTML_BASENAME.test(basename)) return undefined;
|
|
94
|
+
if (!isAbsoluteAppPath(path) && (isJson || path !== basename)) return undefined;
|
|
95
|
+
}
|
|
96
|
+
const attribute = template || isAbsoluteAppPath(path) ? "path" : "file";
|
|
97
|
+
const title = reference.title !== undefined && !UNSAFE_ATTRIBUTE.test(reference.title)
|
|
98
|
+
? ` title="${reference.title}"`
|
|
99
|
+
: "";
|
|
100
|
+
const mode = !live && reference.mode === "wide" ? ` mode="wide"` : "";
|
|
101
|
+
return `::${live ? LIVE_DIRECTIVE : INLINE_DIRECTIVE}{${attribute}="${path}"${title}${mode}}`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Rewrite every visualization span in `text` to the app's ASCII directive.
|
|
106
|
+
*
|
|
107
|
+
* Returns the same string when nothing changes. The scan finds the same matches as the app's regex:
|
|
108
|
+
* a prefix, one or more characters that are not END, then END. When no END follows a prefix, no
|
|
109
|
+
* later prefix can match either, so the scan stops instead of rescanning the tail for every prefix.
|
|
110
|
+
*/
|
|
111
|
+
export function normalizeVisualizationText(text: string): string {
|
|
112
|
+
let prefixAt = text.indexOf(PREFIX);
|
|
113
|
+
if (prefixAt === -1) return text;
|
|
114
|
+
let out = "";
|
|
115
|
+
let copiedTo = 0;
|
|
116
|
+
while (prefixAt !== -1) {
|
|
117
|
+
const payloadStart = prefixAt + PREFIX.length;
|
|
118
|
+
const endAt = text.indexOf(END, payloadStart);
|
|
119
|
+
if (endAt === -1) break;
|
|
120
|
+
if (endAt === payloadStart) {
|
|
121
|
+
// Empty payload: the regex moves on one character and tries again.
|
|
122
|
+
prefixAt = text.indexOf(PREFIX, prefixAt + 1);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
const directive = toAsciiDirective(text.slice(payloadStart, endAt));
|
|
126
|
+
if (directive !== undefined) {
|
|
127
|
+
out += text.slice(copiedTo, prefixAt) + directive;
|
|
128
|
+
copiedTo = endAt + 1;
|
|
129
|
+
}
|
|
130
|
+
prefixAt = text.indexOf(PREFIX, endAt + 1);
|
|
131
|
+
}
|
|
132
|
+
return copiedTo === 0 ? text : out + text.slice(copiedTo);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function normalizeParts<P extends { type: string }>(parts: P[]): P[] {
|
|
136
|
+
let changed = false;
|
|
137
|
+
const next = parts.map((part) => {
|
|
138
|
+
if (part.type !== "text") return part;
|
|
139
|
+
const text = (part as unknown as OcxTextContent).text;
|
|
140
|
+
if (typeof text !== "string") return part;
|
|
141
|
+
const normalized = normalizeVisualizationText(text);
|
|
142
|
+
if (normalized === text) return part;
|
|
143
|
+
changed = true;
|
|
144
|
+
return { ...part, text: normalized };
|
|
145
|
+
});
|
|
146
|
+
return changed ? next : parts;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function normalizeContent<P extends { type: string }>(content: string | P[]): string | P[] {
|
|
150
|
+
return typeof content === "string" ? normalizeVisualizationText(content) : normalizeParts(content);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function normalizeMessage(message: OcxMessage): OcxMessage {
|
|
154
|
+
if (message.role === "assistant") {
|
|
155
|
+
const content = normalizeParts(message.content);
|
|
156
|
+
return content === message.content ? message : { ...message, content };
|
|
157
|
+
}
|
|
158
|
+
const content = normalizeContent<OcxContentPart>(message.content);
|
|
159
|
+
return content === message.content ? message : { ...message, content } as OcxMessage;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Rewrite visualization spans in the conversation text a routed model reads: the system prompt,
|
|
164
|
+
* string message content, and text parts of every role. Other fields, including images, tool calls,
|
|
165
|
+
* reasoning parts, and tool definitions, are returned by reference. The context itself is returned
|
|
166
|
+
* unchanged when no text changed.
|
|
167
|
+
*/
|
|
168
|
+
export function normalizeVisualizationContext(context: OcxContext): OcxContext {
|
|
169
|
+
let changed = false;
|
|
170
|
+
const systemPrompt = context.systemPrompt?.map((text) => {
|
|
171
|
+
const normalized = normalizeVisualizationText(text);
|
|
172
|
+
if (normalized !== text) changed = true;
|
|
173
|
+
return normalized;
|
|
174
|
+
});
|
|
175
|
+
const messages = context.messages.map((message) => {
|
|
176
|
+
const normalized = normalizeMessage(message);
|
|
177
|
+
if (normalized !== message) changed = true;
|
|
178
|
+
return normalized;
|
|
179
|
+
});
|
|
180
|
+
if (!changed) return context;
|
|
181
|
+
return { ...context, ...(systemPrompt ? { systemPrompt } : {}), messages };
|
|
182
|
+
}
|
|
@@ -129,6 +129,11 @@ interface NativeChatSseOptions {
|
|
|
129
129
|
translatorBudget: TranslatorBudget;
|
|
130
130
|
signal: AbortSignal;
|
|
131
131
|
stallTimeoutSec?: number;
|
|
132
|
+
/**
|
|
133
|
+
* The upstream is local infrastructure (loopback / private / `.local` / `.lan`); an unset
|
|
134
|
+
* `stallTimeoutSec` then resolves to disabled so a slow local model is not cut mid-turn.
|
|
135
|
+
*/
|
|
136
|
+
localUpstream?: boolean;
|
|
132
137
|
onFirstOutput?: () => void;
|
|
133
138
|
onUsage: (usage: OcxUsage) => void;
|
|
134
139
|
onTerminal?: (status: number, message?: string) => void;
|
|
@@ -157,7 +162,12 @@ export function nativeChatSse(
|
|
|
157
162
|
let settled = false;
|
|
158
163
|
let cancelled = false;
|
|
159
164
|
let cancelledBySignal = false;
|
|
160
|
-
const
|
|
165
|
+
const stallSec = resolveStallTimeoutSec(options.stallTimeoutSec, { localUpstream: options.localUpstream });
|
|
166
|
+
// 0 is an explicit disabled budget (local upstream, or the operator's stallTimeoutSec: 0). Map
|
|
167
|
+
// it to an Infinity deadline so every `now >= pullDeadline` / `remaining <= 0` check stays
|
|
168
|
+
// false and the read race is skipped, instead of arming an immediate kill on the first pull.
|
|
169
|
+
const stallEnabled = stallSec > 0;
|
|
170
|
+
const stallMs = stallEnabled ? stallSec * 1_000 : Number.POSITIVE_INFINITY;
|
|
161
171
|
// Count only active upstream consumption; downstream backpressure must not
|
|
162
172
|
// consume the provider's inactivity allowance. Comments and empty deltas do.
|
|
163
173
|
let remainingStallMs = stallMs;
|
|
@@ -332,17 +342,19 @@ export function nativeChatSse(
|
|
|
332
342
|
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
333
343
|
let read: Awaited<ReturnType<typeof reader.read>>;
|
|
334
344
|
try {
|
|
335
|
-
read =
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
345
|
+
read = stallEnabled
|
|
346
|
+
? await Promise.race([
|
|
347
|
+
reader.read(),
|
|
348
|
+
new Promise<never>((_, reject) => {
|
|
349
|
+
const check = () => {
|
|
350
|
+
const remaining = pullDeadline - performance.now();
|
|
351
|
+
if (remaining <= 0) reject(stalled);
|
|
352
|
+
else timeout = setTimeout(check, Math.min(remaining, 2_147_483_647));
|
|
353
|
+
};
|
|
354
|
+
check();
|
|
355
|
+
}),
|
|
356
|
+
])
|
|
357
|
+
: await reader.read();
|
|
346
358
|
} finally {
|
|
347
359
|
if (timeout !== undefined) clearTimeout(timeout);
|
|
348
360
|
}
|