dsh-lcx-codex 0.4.3 → 0.4.4-pre.1
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/lib/abort-error.js +47 -0
- package/lib/client.js +97 -98
- package/lib/dsh-responses.js +70 -33
- package/lib/index.js +27 -11
- package/lib/invocation-policy-scope.js +4 -8
- package/lib/native-checkpoint.js +42 -3
- package/lib/pi-responses-runtime.js +251 -97
- package/lib/responses-request.js +2 -2
- package/lib/responses-stream.js +35 -8
- package/lib/search-usage.js +5 -13
- package/lib/service-mutex.js +4 -8
- package/lib/transport.js +6 -6
- package/package.json +28 -27
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
const ABORT_MESSAGE = "The operation was aborted";
|
|
3
|
+
function abortInstance() {
|
|
4
|
+
if (typeof DOMException === "function")
|
|
5
|
+
return new DOMException(ABORT_MESSAGE, "AbortError");
|
|
6
|
+
const error = new Error(ABORT_MESSAGE);
|
|
7
|
+
error.name = "AbortError";
|
|
8
|
+
return error;
|
|
9
|
+
}
|
|
10
|
+
// `DOMException.prototype.code` is a getter-only accessor, so own properties are
|
|
11
|
+
// defined rather than assigned. Both stay non-enumerable, like a native `cause`.
|
|
12
|
+
function define(target, key, value) {
|
|
13
|
+
try {
|
|
14
|
+
Object.defineProperty(target, key, {
|
|
15
|
+
value,
|
|
16
|
+
configurable: true,
|
|
17
|
+
writable: true,
|
|
18
|
+
enumerable: false,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
catch { }
|
|
22
|
+
}
|
|
23
|
+
function synthesize(reason) {
|
|
24
|
+
const error = abortInstance();
|
|
25
|
+
define(error, "code", "LCX_ABORTED");
|
|
26
|
+
if (reason !== undefined && reason !== null)
|
|
27
|
+
define(error, "cause", reason);
|
|
28
|
+
return error;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Normalize an abort reason into something safe to throw. `fallback` is used only when
|
|
32
|
+
* the reason is absent, preserving the older `signal.reason ?? error` behaviour.
|
|
33
|
+
*/
|
|
34
|
+
export function normalizeAbortReason(reason, fallback) {
|
|
35
|
+
if (reason instanceof Error)
|
|
36
|
+
return reason;
|
|
37
|
+
if (reason === undefined || reason === null) {
|
|
38
|
+
if (fallback instanceof Error)
|
|
39
|
+
return fallback;
|
|
40
|
+
return synthesize(undefined);
|
|
41
|
+
}
|
|
42
|
+
return synthesize(reason);
|
|
43
|
+
}
|
|
44
|
+
/** The error to throw for `signal`'s cancellation. */
|
|
45
|
+
export function abortError(signal, fallback) {
|
|
46
|
+
return normalizeAbortReason(signal?.reason, fallback);
|
|
47
|
+
}
|
package/lib/client.js
CHANGED
|
@@ -727,7 +727,6 @@
|
|
|
727
727
|
// src/search-accounting.ts
|
|
728
728
|
var object = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
729
729
|
var count = (v) => typeof v === "number" && Number.isSafeInteger(v) && v >= 0;
|
|
730
|
-
var zeroBuckets = () => ({ uncachedInputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 });
|
|
731
730
|
var isSearchTool = (name) => name === "web_search" || name === "websearch_gpt_advanced";
|
|
732
731
|
function auxiliaryUsageOf(event, toolName) {
|
|
733
732
|
if (!object(event) || event.type !== "tool/result" || !object(event.data?.meta)) return [];
|
|
@@ -744,38 +743,6 @@
|
|
|
744
743
|
}
|
|
745
744
|
return result;
|
|
746
745
|
}
|
|
747
|
-
function addUsage(base, records) {
|
|
748
|
-
if (!records.length) return base;
|
|
749
|
-
const next = { ...base };
|
|
750
|
-
for (const { usage: u } of records) {
|
|
751
|
-
next.uncachedInputTokens += u.inputTokens;
|
|
752
|
-
next.outputTokens += u.outputTokens;
|
|
753
|
-
next.cacheReadTokens += u.cacheReadTokens;
|
|
754
|
-
next.cacheWriteTokens += u.cacheWriteTokens;
|
|
755
|
-
}
|
|
756
|
-
if (!Object.values(next).every(count)) throw new Error("LCX search usage exceeds safe counters");
|
|
757
|
-
return next;
|
|
758
|
-
}
|
|
759
|
-
function mergeBuckets(base, extra) {
|
|
760
|
-
if (!object(base) || Object.values(extra).every((n) => n === 0)) return base;
|
|
761
|
-
const next = { ...base };
|
|
762
|
-
for (const key of Object.keys(extra)) {
|
|
763
|
-
if (count(base[key])) next[key] = base[key] + extra[key];
|
|
764
|
-
}
|
|
765
|
-
return next;
|
|
766
|
-
}
|
|
767
|
-
function addTurnUsage(base, records) {
|
|
768
|
-
if (!object(base) || !records.length || !count(base.totalTokens)) return base;
|
|
769
|
-
const next = mergeBuckets(base, addUsage(zeroBuckets(), records));
|
|
770
|
-
next.totalTokens = base.totalTokens + records.reduce((n, r) => n + r.usage.totalTokens, 0);
|
|
771
|
-
delete next.reasoningTokens;
|
|
772
|
-
if (Array.isArray(base.routes)) {
|
|
773
|
-
const routes = /* @__PURE__ */ new Map();
|
|
774
|
-
for (const r of [...base.routes, ...records]) routes.set(`${r.provider}\0${r.model}`, { provider: r.provider, model: r.model });
|
|
775
|
-
next.routes = [...routes.values()];
|
|
776
|
-
}
|
|
777
|
-
return next;
|
|
778
|
-
}
|
|
779
746
|
|
|
780
747
|
// src/client/search-usage-ui.ts
|
|
781
748
|
var searchUsageDefinition = {
|
|
@@ -803,64 +770,70 @@
|
|
|
803
770
|
return scope === "turn" && s?.complete ? { kind: "turn", turn: s.turn, key: "lcx-search-usage", value: s.records } : null;
|
|
804
771
|
}
|
|
805
772
|
};
|
|
773
|
+
function tokenCount(value) {
|
|
774
|
+
return String(value);
|
|
775
|
+
}
|
|
776
|
+
function recordsTotal(records) {
|
|
777
|
+
let total = 0;
|
|
778
|
+
for (const record2 of records) {
|
|
779
|
+
const value = record2.usage.totalTokens;
|
|
780
|
+
if (!Number.isSafeInteger(value) || value < 0) continue;
|
|
781
|
+
total += value;
|
|
782
|
+
if (!Number.isSafeInteger(total)) return 0;
|
|
783
|
+
}
|
|
784
|
+
return total;
|
|
785
|
+
}
|
|
786
|
+
function bucketsTotal(value) {
|
|
787
|
+
if (!object(value)) return 0;
|
|
788
|
+
let total = 0;
|
|
789
|
+
for (const key of ["uncachedInputTokens", "outputTokens", "cacheReadTokens", "cacheWriteTokens"]) {
|
|
790
|
+
const count2 = value[key];
|
|
791
|
+
if (!Number.isSafeInteger(count2) || count2 < 0) return 0;
|
|
792
|
+
total += count2;
|
|
793
|
+
if (!Number.isSafeInteger(total)) return 0;
|
|
794
|
+
}
|
|
795
|
+
return total;
|
|
796
|
+
}
|
|
797
|
+
function usageView(createElement, scope, total, t) {
|
|
798
|
+
if (total <= 0) return null;
|
|
799
|
+
const label = t?.(scope === "turn" ? "usageTurn" : "usageSession") ?? "Search usage";
|
|
800
|
+
return createElement("span", {
|
|
801
|
+
className: `lcx-search-usage lcx-search-usage-${scope}`,
|
|
802
|
+
"data-lcx-search-usage": scope,
|
|
803
|
+
title: `${label}: ${tokenCount(total)}`
|
|
804
|
+
}, `${label}: ${tokenCount(total)}`);
|
|
805
|
+
}
|
|
806
|
+
function TurnTailUsage({ turn, t }, createElement) {
|
|
807
|
+
if (!createElement) return null;
|
|
808
|
+
return usageView(createElement, "turn", recordsTotal(turn.data.get("lcx-search-usage") ?? []), t);
|
|
809
|
+
}
|
|
810
|
+
function SessionUsage({ useProjection, t }, createElement) {
|
|
811
|
+
if (!createElement) return null;
|
|
812
|
+
const extra = useProjection("lcxSearchUsage");
|
|
813
|
+
const auxiliary = object(extra) ? extra.auxiliary : void 0;
|
|
814
|
+
return usageView(createElement, "session", bucketsTotal(auxiliary), t);
|
|
815
|
+
}
|
|
806
816
|
function installUsageSlots(slots, createElement) {
|
|
817
|
+
const registrations = [
|
|
818
|
+
[
|
|
819
|
+
{ name: "conversation.chat.turnTail", id: "lcx-search-usage-turn", order: 0, locale: "lcx-codex" },
|
|
820
|
+
(props) => TurnTailUsage(props, createElement)
|
|
821
|
+
],
|
|
822
|
+
[
|
|
823
|
+
{ name: "conversation.composer.dock", id: "lcx-search-usage-session", order: 10, locale: "lcx-codex" },
|
|
824
|
+
(props) => SessionUsage(props, createElement)
|
|
825
|
+
]
|
|
826
|
+
];
|
|
807
827
|
const cleanups = [];
|
|
808
|
-
for (const [name,
|
|
809
|
-
["conversation.composer.dock", "stats"],
|
|
810
|
-
["conversation.composer.bar", ""],
|
|
811
|
-
["conversation.chat.node", "turn-tail"]
|
|
812
|
-
]) {
|
|
813
|
-
let effect;
|
|
828
|
+
for (const [name, component] of registrations) {
|
|
814
829
|
try {
|
|
815
|
-
effect = slots.inject(name, () => {
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
entries = slots.entriesOfSlot(name);
|
|
819
|
-
} catch {
|
|
820
|
-
return;
|
|
821
|
-
}
|
|
822
|
-
if (!Array.isArray(entries) || entries.length === 0) return;
|
|
823
|
-
const candidates = key === "" ? [entries[0]] : entries.filter((e) => e?.options?.id === key || e?.options?.key === key);
|
|
824
|
-
if (candidates.length !== 1) return;
|
|
825
|
-
const base = candidates[0], original = base?.component;
|
|
826
|
-
if (!base || typeof original !== "function" || !base.options || typeof base.options !== "object") return;
|
|
827
|
-
const descriptor = Object.getOwnPropertyDescriptor(base, "component");
|
|
828
|
-
if (descriptor && descriptor.set === void 0 && descriptor.writable === false) return;
|
|
829
|
-
function WithSearchUsage(props) {
|
|
830
|
-
if (key === "turn-tail") {
|
|
831
|
-
const location = props?.node?.location;
|
|
832
|
-
const records = (location?.kind === "turn" || location?.kind === "step") && location.turn?.data?.get ? location.turn.data.get("lcx-search-usage") : void 0;
|
|
833
|
-
if (!records?.length) return createElement(original, props);
|
|
834
|
-
return createElement(original, { ...props, node: { ...props.node, data: { ...props.node.data, tokenUsage: addTurnUsage(props.node.data.tokenUsage, records) } } });
|
|
835
|
-
}
|
|
836
|
-
if (typeof props?.useProjection !== "function") return createElement(original, props);
|
|
837
|
-
const extra = props.useProjection("lcxSearchUsage");
|
|
838
|
-
const breakdown = props.useProjection("contextBreakdown");
|
|
839
|
-
const useProjection = (projection) => {
|
|
840
|
-
const value = props.useProjection(projection);
|
|
841
|
-
if (projection === "tokenUsage" && extra?.auxiliary) return mergeBuckets(value, extra.auxiliary);
|
|
842
|
-
if (projection === "contextPressure" && extra?.aggregateContext && object(breakdown)) {
|
|
843
|
-
const tokens = breakdown.systemTokens + breakdown.toolsTokens + breakdown.messageTokens;
|
|
844
|
-
if (Number.isSafeInteger(tokens) && tokens >= 0 && object(value)) return { ...value, pressureTokens: tokens, projectedTokens: tokens };
|
|
845
|
-
}
|
|
846
|
-
return value;
|
|
847
|
-
};
|
|
848
|
-
return createElement(original, { ...props, useProjection });
|
|
849
|
-
}
|
|
850
|
-
try {
|
|
851
|
-
base.component = WithSearchUsage;
|
|
852
|
-
} catch {
|
|
853
|
-
return;
|
|
854
|
-
}
|
|
855
|
-
if (base.component !== WithSearchUsage) return;
|
|
856
|
-
return () => {
|
|
857
|
-
if (base.component === WithSearchUsage) base.component = original;
|
|
858
|
-
};
|
|
830
|
+
const effect = slots.inject(name.name, () => {
|
|
831
|
+
const dispose = slots.register(name, component);
|
|
832
|
+
return typeof dispose === "function" ? dispose : void 0;
|
|
859
833
|
});
|
|
834
|
+
if (typeof effect === "function") cleanups.push(effect);
|
|
860
835
|
} catch {
|
|
861
|
-
continue;
|
|
862
836
|
}
|
|
863
|
-
if (typeof effect === "function") cleanups.push(effect);
|
|
864
837
|
}
|
|
865
838
|
return () => {
|
|
866
839
|
for (const dispose of cleanups.reverse()) dispose();
|
|
@@ -894,7 +867,7 @@
|
|
|
894
867
|
const DEFAULTS = Object.fromEntries(
|
|
895
868
|
FIELDS.map((field) => [field, false])
|
|
896
869
|
);
|
|
897
|
-
const css = `.lcx-card{border:1px solid var(--dsw-alias-border-l2);border-radius:8px;list-style:none}.lcx-head{width:100%;display:flex;justify-content:space-between;padding:14px 16px;border:0;background:transparent;color:inherit}.lcx-body{border-top:1px solid var(--dsw-alias-border-l2);padding:12px 16px}.lcx-row{display:flex;gap:9px;padding:8px 0}.lcx-row small,.lcx-help{display:block;font-size:12px;line-height:17px;color:var(--dsw-alias-label-tertiary)}.lcx-group{border-top:1px solid var(--dsw-alias-border-l2);margin-top:10px;padding-top:14px}.lcx-group strong{font-size:14px}.lcx-foot{display:flex;justify-content:flex-end;gap:8px;margin-top:10px}.lcx-foot button{padding:6px 12px;border-radius:8px;border:1px solid var(--dsw-alias-border-l2);background:transparent;color:inherit}` + inlineMediaCss;
|
|
870
|
+
const css = `.lcx-card{border:1px solid var(--dsw-alias-border-l2);border-radius:8px;list-style:none}.lcx-head{width:100%;display:flex;justify-content:space-between;padding:14px 16px;border:0;background:transparent;color:inherit}.lcx-body{border-top:1px solid var(--dsw-alias-border-l2);padding:12px 16px}.lcx-row{display:flex;gap:9px;padding:8px 0}.lcx-row small,.lcx-help{display:block;font-size:12px;line-height:17px;color:var(--dsw-alias-label-tertiary)}.lcx-group{border-top:1px solid var(--dsw-alias-border-l2);margin-top:10px;padding-top:14px}.lcx-group strong{font-size:14px}.lcx-foot{display:flex;justify-content:flex-end;gap:8px;margin-top:10px}.lcx-foot button{padding:6px 12px;border-radius:8px;border:1px solid var(--dsw-alias-border-l2);background:transparent;color:inherit}.lcx-search-usage{display:inline-flex;align-items:center;gap:4px;margin:2px 0;color:var(--dsw-alias-label-tertiary);font:12px/18px system-ui}.lcx-search-usage-session{padding:0 4px}.lcx-search-usage-turn{padding:0 6px}` + inlineMediaCss;
|
|
898
871
|
function mountCss() {
|
|
899
872
|
if (typeof document === "undefined") return () => {
|
|
900
873
|
};
|
|
@@ -910,6 +883,7 @@
|
|
|
910
883
|
const copy = {
|
|
911
884
|
zh: {
|
|
912
885
|
title: "Responses / Codex \u80FD\u529B",
|
|
886
|
+
summary: "GPT Responses route \u4E0E\u641C\u7D22\u80FD\u529B",
|
|
913
887
|
desc: "LCX \u8DDF\u968F DSH \u5F53\u524D\u4F1A\u8BDD\u9009\u62E9\u7684 GPT Responses route\uFF1B\u6A21\u578B\u3001endpoint \u548C credential \u7EE7\u7EED\u53EA\u7531 DSH \u7BA1\u7406\u3002",
|
|
914
888
|
enabled: "\u542F\u7528 LCX\uFF08\u63A5\u7BA1\u5F53\u524D GPT Responses \u4F1A\u8BDD\uFF09",
|
|
915
889
|
enabledHelp: "\u6B64\u5F00\u5173\u4EC5\u63A5\u7BA1 GPT Responses \u4F1A\u8BDD\uFF1BGrok \u539F\u751F\u641C\u7D22\u7531\u4E0B\u65B9\u72EC\u7ACB\u5F00\u5173\u63A7\u5236\uFF0C\u5176\u4ED6\u975E GPT \u6A21\u578B\u7EE7\u7EED\u4F7F\u7528 DSH \u539F\u751F\u8DEF\u5F84\u3002",
|
|
@@ -939,6 +913,8 @@
|
|
|
939
913
|
mediaVideo: "\u89C6\u9891",
|
|
940
914
|
mediaFilter: "\u5A92\u4F53\u7C7B\u578B",
|
|
941
915
|
mediaOpen: "\u6253\u5F00\u539F\u59CB\u5A92\u4F53",
|
|
916
|
+
usageTurn: "\u672C\u8F6E\u641C\u7D22\u7528\u91CF",
|
|
917
|
+
usageSession: "\u641C\u7D22\u7528\u91CF",
|
|
942
918
|
grokTitle: "Grok \u539F\u751F\u641C\u7D22",
|
|
943
919
|
grokDesc: "\u4F7F\u7528 DSH \u5F53\u524D\u9009\u62E9\u7684 Grok \u6A21\u578B\u53CA\u5176\u670D\u52A1\u914D\u7F6E\uFF1B\u4E0E GPT \u529F\u80FD\u5F00\u5173\u72EC\u7ACB\u3002\u5F00\u542F\u4EFB\u4E00\u641C\u7D22\u540E\uFF0CGrok \u4EC5\u4F7F\u7528\u539F\u751F\u641C\u7D22\uFF0C\u7F51\u9875\u8BFB\u53D6\u548C\u5176\u4ED6\u5DE5\u5177\u4ECD\u53EF\u7528\u3002",
|
|
944
920
|
grokWeb: "\u542F\u7528\u539F\u751F Web Search",
|
|
@@ -952,6 +928,7 @@
|
|
|
952
928
|
},
|
|
953
929
|
en: {
|
|
954
930
|
title: "Responses / Codex capabilities",
|
|
931
|
+
summary: "GPT Responses route and search capabilities",
|
|
955
932
|
desc: "LCX follows the GPT Responses route selected by the current DSH session. Model, endpoint and credentials remain DSH-owned.",
|
|
956
933
|
enabled: "Enable LCX (own current GPT Responses conversation)",
|
|
957
934
|
enabledHelp: "Applies only to GPT Responses conversations. Grok native search is controlled independently below; other non-GPT models continue through DSH normally.",
|
|
@@ -981,6 +958,8 @@
|
|
|
981
958
|
mediaVideo: "Video",
|
|
982
959
|
mediaFilter: "Media type",
|
|
983
960
|
mediaOpen: "Open original media",
|
|
961
|
+
usageTurn: "Turn search usage",
|
|
962
|
+
usageSession: "Search usage",
|
|
984
963
|
grokTitle: "Grok native search",
|
|
985
964
|
grokDesc: "Uses the Grok model and provider profile currently selected in DSH, independently of the GPT switch. When either search is enabled, Grok uses native search while page reading and other tools remain available.",
|
|
986
965
|
grokWeb: "Enable native Web Search",
|
|
@@ -1102,12 +1081,14 @@
|
|
|
1102
1081
|
saveError;
|
|
1103
1082
|
store;
|
|
1104
1083
|
stop;
|
|
1084
|
+
saveEpoch;
|
|
1105
1085
|
constructor(scope) {
|
|
1106
1086
|
this.scope = scope;
|
|
1107
1087
|
this.draft = null;
|
|
1108
1088
|
this.dirty = false;
|
|
1109
1089
|
this.saving = false;
|
|
1110
1090
|
this.saveError = false;
|
|
1091
|
+
this.saveEpoch = 0;
|
|
1111
1092
|
this.store = createSnapshotStore(this.projection());
|
|
1112
1093
|
mediaStore.set({ enabled: this.value().searchMediaPreview });
|
|
1113
1094
|
this.stop = scope.subscribe(() => {
|
|
@@ -1159,10 +1140,19 @@
|
|
|
1159
1140
|
this.saveError = false;
|
|
1160
1141
|
this.publish();
|
|
1161
1142
|
}
|
|
1143
|
+
leavePage() {
|
|
1144
|
+
this.saveEpoch += 1;
|
|
1145
|
+
this.draft = null;
|
|
1146
|
+
this.dirty = false;
|
|
1147
|
+
this.saving = false;
|
|
1148
|
+
this.saveError = false;
|
|
1149
|
+
this.publish();
|
|
1150
|
+
}
|
|
1162
1151
|
async save() {
|
|
1163
1152
|
if (!this.dirty || this.saving || !this.snapshot().writable || this.snapshot().status !== "ready") return;
|
|
1164
1153
|
const next = this.draftValue(), prev = this.value();
|
|
1165
1154
|
const fields = FIELDS.filter((field) => next[field] !== prev[field]);
|
|
1155
|
+
const saveEpoch = ++this.saveEpoch;
|
|
1166
1156
|
this.saving = true;
|
|
1167
1157
|
this.saveError = false;
|
|
1168
1158
|
this.publish();
|
|
@@ -1173,16 +1163,20 @@
|
|
|
1173
1163
|
path: [field],
|
|
1174
1164
|
value: next[field]
|
|
1175
1165
|
})));
|
|
1166
|
+
if (saveEpoch !== this.saveEpoch) return;
|
|
1176
1167
|
if (this.snapshot().status !== "ready" || fields.some((field) => this.value()[field] !== next[field]))
|
|
1177
1168
|
throw new Error("Settings mutation was not confirmed");
|
|
1178
1169
|
}
|
|
1170
|
+
if (saveEpoch !== this.saveEpoch) return;
|
|
1179
1171
|
this.draft = null;
|
|
1180
1172
|
this.dirty = false;
|
|
1181
1173
|
} catch {
|
|
1182
|
-
this.saveError = true;
|
|
1174
|
+
if (saveEpoch === this.saveEpoch) this.saveError = true;
|
|
1183
1175
|
} finally {
|
|
1184
|
-
this.
|
|
1185
|
-
|
|
1176
|
+
if (saveEpoch === this.saveEpoch) {
|
|
1177
|
+
this.saving = false;
|
|
1178
|
+
this.publish();
|
|
1179
|
+
}
|
|
1186
1180
|
}
|
|
1187
1181
|
}
|
|
1188
1182
|
inject() {
|
|
@@ -1194,7 +1188,8 @@
|
|
|
1194
1188
|
setMediaPreview: (value) => this.edit("searchMediaPreview", value),
|
|
1195
1189
|
edit: (field, value) => this.edit(field, value),
|
|
1196
1190
|
save: () => void this.save(),
|
|
1197
|
-
discard: () => this.discard()
|
|
1191
|
+
discard: () => this.discard(),
|
|
1192
|
+
leavePage: () => this.leavePage()
|
|
1198
1193
|
};
|
|
1199
1194
|
}
|
|
1200
1195
|
}
|
|
@@ -1226,7 +1221,13 @@
|
|
|
1226
1221
|
}
|
|
1227
1222
|
function Card(props) {
|
|
1228
1223
|
const t = props.t, s = props.useLcxCard((state) => state), [open, setOpen] = React.useState(false);
|
|
1224
|
+
React.useEffect(
|
|
1225
|
+
() => props.view === "page" ? () => props.leavePage() : void 0,
|
|
1226
|
+
[props.view, props.leavePage]
|
|
1227
|
+
);
|
|
1229
1228
|
if (!s.available) return null;
|
|
1229
|
+
if (props.view === "summary")
|
|
1230
|
+
return React.createElement("span", { className: "lcx-summary" }, t("summary"));
|
|
1230
1231
|
const disabled = !s.writable || s.saving;
|
|
1231
1232
|
return React.createElement(
|
|
1232
1233
|
"li",
|
|
@@ -1345,10 +1346,8 @@
|
|
|
1345
1346
|
function apply(ctx) {
|
|
1346
1347
|
const slots = ctx.slots ?? ctx.get("slots"), svc = ctx.settingsScope ?? ctx.get("settingsScope"), locale = ctx.locale ?? ctx.get("locale"), uiConversation = ctx.uiConversation ?? ctx.get("uiConversation");
|
|
1347
1348
|
if (!slots || !svc || !locale || !uiConversation) return;
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
ctx.effect(() => installUsageSlots(slots, React.createElement), "lcx search billing slots");
|
|
1351
|
-
}
|
|
1349
|
+
ctx.effect(() => uiConversation.events.register(searchUsageDefinition), "lcx search billing data");
|
|
1350
|
+
ctx.effect(() => installUsageSlots(slots, React.createElement), "lcx search billing slots");
|
|
1352
1351
|
ctx.effect(mountCss, "lcx-codex styles");
|
|
1353
1352
|
for (const language of ["zh", "en"])
|
|
1354
1353
|
ctx.effect(() => locale.register(NAMESPACE, language, copy[language]), `lcx-codex ${language} dictionary`);
|
|
@@ -1364,12 +1363,12 @@
|
|
|
1364
1363
|
}, label);
|
|
1365
1364
|
const controller = new Controller(svc.bind({ namespace: NAMESPACE }));
|
|
1366
1365
|
installSlot(
|
|
1367
|
-
"
|
|
1368
|
-
"lcx-codex
|
|
1366
|
+
"plugins.bundle.config",
|
|
1367
|
+
"lcx-codex plugin configuration",
|
|
1369
1368
|
() => slots.register(
|
|
1370
1369
|
{
|
|
1371
|
-
name: "
|
|
1372
|
-
key:
|
|
1370
|
+
name: "plugins.bundle.config",
|
|
1371
|
+
key: "dsh-lcx-codex",
|
|
1373
1372
|
locale: NAMESPACE,
|
|
1374
1373
|
inject: () => controller.inject()
|
|
1375
1374
|
},
|
package/lib/dsh-responses.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { offloadedImageText,
|
|
2
|
-
import {
|
|
1
|
+
import { IMAGE_OFFLOAD_REQUIRED_CODE, LlmError, offloadedImageText, projectOffloadedImages, requiredImageOffload, requestImageHandleText, resolveImageAttachmentAccess, } from "@deepseek-ai/dsh-llm";
|
|
2
|
+
import { requestImageDimensions } from "@deepseek-ai/dsh-attachment";
|
|
3
|
+
import { convertResponsesMessages, convertResponsesTools, createGrammarToolInputProperties, getBuiltinModels, normalizeContext, } from "./pi-responses-runtime.js";
|
|
3
4
|
const DEFAULT_MAX_REQUEST_IMAGE_BYTES = 20 * 1024 * 1024;
|
|
4
5
|
const DEFAULT_REQUEST_IMAGE_PIXEL_BUDGET = 2048 * 2048;
|
|
5
6
|
const DEFAULT_REQUEST_IMAGE_MAX_BYTES = 1024 * 1024;
|
|
@@ -87,42 +88,45 @@ export async function resolveModelImageSupport(ctx, route, signal) {
|
|
|
87
88
|
return "unknown";
|
|
88
89
|
}
|
|
89
90
|
}
|
|
90
|
-
function attachmentResolver(ctx, options) {
|
|
91
|
+
function attachmentResolver(ctx, options, requestImages) {
|
|
91
92
|
const attachments = attachmentServices(ctx?.attachments);
|
|
92
93
|
return async (block, signal) => {
|
|
93
94
|
if (attachments === undefined)
|
|
94
95
|
throw error("LCX requires the DSH request-image attachment API", "LCX_COMPACT_IMAGE_API_UNAVAILABLE");
|
|
95
|
-
const request =
|
|
96
|
+
const request = requestImages === undefined
|
|
97
|
+
? await attachments.store.readImageRequest(block.attachment, { ...requestImageDimensions(block.attachment.width, block.attachment.height, options.requestImagePixelBudget), maxBytes: options.requestImageMaxBytes }, signal)
|
|
98
|
+
: requestImages.get(String(block.attachment.attachmentId));
|
|
99
|
+
if (request === undefined)
|
|
100
|
+
throw error("DSH attachment returned no request-image bytes", "LCX_COMPACT_IMAGE_UNAVAILABLE");
|
|
96
101
|
const data = request.data;
|
|
97
102
|
const mediaType = request.mediaType ?? block.attachment.mediaType;
|
|
98
103
|
if (!(data instanceof Uint8Array) && !Buffer.isBuffer(data))
|
|
99
104
|
throw error("DSH attachment returned no request-image bytes", "LCX_COMPACT_IMAGE_UNAVAILABLE");
|
|
100
105
|
if (typeof mediaType !== "string" || !mediaType.startsWith("image/"))
|
|
101
106
|
throw error("DSH attachment returned an invalid request-image media type", "LCX_COMPACT_IMAGE_UNAVAILABLE");
|
|
102
|
-
const ref = request.attachment ?? block.attachment;
|
|
103
107
|
return {
|
|
104
108
|
data: Buffer.from(data),
|
|
105
109
|
mediaType,
|
|
106
|
-
ref,
|
|
110
|
+
ref: block.attachment,
|
|
107
111
|
request,
|
|
108
|
-
access: imageAccess(ctx, attachments,
|
|
112
|
+
access: imageAccess(ctx, attachments, block.attachment),
|
|
109
113
|
};
|
|
110
114
|
};
|
|
111
115
|
}
|
|
112
|
-
async function imagePart(block, ctx, options, imageMap) {
|
|
116
|
+
async function imagePart(block, ctx, options, imageMap, requestImages) {
|
|
113
117
|
if (options.imageSupport === "unsupported")
|
|
114
118
|
return { type: "input_text", text: "[image omitted because the target model does not support image input]" };
|
|
115
|
-
const image = await attachmentResolver(ctx, options)(block, options.signal);
|
|
119
|
+
const image = await attachmentResolver(ctx, options, requestImages)(block, options.signal);
|
|
116
120
|
if (Math.ceil(image.data.byteLength / 3) * 4 > options.maxRequestImageBytes)
|
|
117
121
|
throw error("one image exceeds the configured LCX request image bound", "LCX_COMPACT_IMAGE_TOO_LARGE");
|
|
118
122
|
const imageUrl = `data:${image.mediaType};base64,${image.data.toString("base64")}`;
|
|
119
123
|
imageMap.set(imageUrl, structuredClone(image.ref));
|
|
120
124
|
return { type: "input_image", detail: "auto", image_url: imageUrl };
|
|
121
125
|
}
|
|
122
|
-
async function piImageParts(block, ctx, options, imageMap) {
|
|
126
|
+
async function piImageParts(block, ctx, options, imageMap, requestImages) {
|
|
123
127
|
if (options.imageSupport === "unsupported")
|
|
124
128
|
return [{ type: "text", text: "[image omitted because the target model does not support image input]" }];
|
|
125
|
-
const image = await attachmentResolver(ctx, options)(block, options.signal);
|
|
129
|
+
const image = await attachmentResolver(ctx, options, requestImages)(block, options.signal);
|
|
126
130
|
if (Math.ceil(image.data.byteLength / 3) * 4 > options.maxRequestImageBytes)
|
|
127
131
|
throw error("one image exceeds the configured LCX request image bound", "LCX_COMPACT_IMAGE_TOO_LARGE");
|
|
128
132
|
const data = image.data.toString("base64");
|
|
@@ -292,17 +296,17 @@ function toPiAssistant(message, onReplayDegrade) {
|
|
|
292
296
|
return foreignAssistant(message);
|
|
293
297
|
}
|
|
294
298
|
}
|
|
295
|
-
async function piToolContent(blocks, ctx, options, imageMap) {
|
|
299
|
+
async function piToolContent(blocks, ctx, options, imageMap, requestImages) {
|
|
296
300
|
const content = [];
|
|
297
301
|
for (const block of blocks) {
|
|
298
302
|
if (block.type === "text")
|
|
299
303
|
content.push({ type: "text", text: block.text });
|
|
300
304
|
else if (block.type === "image")
|
|
301
|
-
content.push(...(await piImageParts(block, ctx, options, imageMap)));
|
|
305
|
+
content.push(...(await piImageParts(block, ctx, options, imageMap, requestImages)));
|
|
302
306
|
else if (block.type === "reasoning")
|
|
303
307
|
continue;
|
|
304
308
|
else if (block.type === "tool-result")
|
|
305
|
-
content.push(...(await piToolContent(block.content, ctx, options, imageMap)));
|
|
309
|
+
content.push(...(await piToolContent(block.content, ctx, options, imageMap, requestImages)));
|
|
306
310
|
else
|
|
307
311
|
throw unsupportedContent(block.type);
|
|
308
312
|
}
|
|
@@ -331,12 +335,39 @@ function projectFiles(messages, ctx) {
|
|
|
331
335
|
content: projectFileContent(message.content, fileRequestText),
|
|
332
336
|
}));
|
|
333
337
|
}
|
|
338
|
+
function collectRequestImageRefs(blocks, refs) {
|
|
339
|
+
for (const block of blocks) {
|
|
340
|
+
if (block.type === "image") {
|
|
341
|
+
if (block.offloaded !== true)
|
|
342
|
+
refs.set(String(block.attachment.attachmentId), block.attachment);
|
|
343
|
+
}
|
|
344
|
+
else if (block.type === "tool-result") {
|
|
345
|
+
collectRequestImageRefs(block.content, refs);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
async function prepareRequestImageVersions(messages, ctx, options) {
|
|
350
|
+
const attachments = attachmentServices(ctx?.attachments);
|
|
351
|
+
if (attachments === undefined)
|
|
352
|
+
return new Map();
|
|
353
|
+
const refs = new Map();
|
|
354
|
+
for (const message of messages)
|
|
355
|
+
collectRequestImageRefs(message.content, refs);
|
|
356
|
+
const versions = await Promise.all([...refs.values()].map(async (ref) => {
|
|
357
|
+
const target = {
|
|
358
|
+
...requestImageDimensions(ref.width, ref.height, options.requestImagePixelBudget),
|
|
359
|
+
maxBytes: options.requestImageMaxBytes,
|
|
360
|
+
};
|
|
361
|
+
return [String(ref.attachmentId), await attachments.store.readImageRequest(ref, target, options.signal)];
|
|
362
|
+
}));
|
|
363
|
+
return new Map(versions);
|
|
364
|
+
}
|
|
334
365
|
function flattenMessageText(message) {
|
|
335
366
|
return message.content
|
|
336
367
|
.map((block) => (block.type === "text" ? block.text : ""))
|
|
337
368
|
.join("");
|
|
338
369
|
}
|
|
339
|
-
async function dshToPiMessages(messages, ctx, options, imageMap) {
|
|
370
|
+
async function dshToPiMessages(messages, ctx, options, imageMap, requestImages) {
|
|
340
371
|
const result = [];
|
|
341
372
|
const toolNames = new Map();
|
|
342
373
|
for (const message of messages) {
|
|
@@ -370,7 +401,7 @@ async function dshToPiMessages(messages, ctx, options, imageMap) {
|
|
|
370
401
|
if (block.type === "text")
|
|
371
402
|
content.push({ type: "text", text: block.text });
|
|
372
403
|
else if (block.type === "image")
|
|
373
|
-
content.push(...(await piImageParts(block, ctx, options, imageMap)));
|
|
404
|
+
content.push(...(await piImageParts(block, ctx, options, imageMap, requestImages)));
|
|
374
405
|
}
|
|
375
406
|
if (content.length > 0)
|
|
376
407
|
result.push({ role: "user", content, timestamp: 0 });
|
|
@@ -379,7 +410,7 @@ async function dshToPiMessages(messages, ctx, options, imageMap) {
|
|
|
379
410
|
if (block.type !== "tool-result")
|
|
380
411
|
continue;
|
|
381
412
|
const toolCallId = String(block.toolCallId);
|
|
382
|
-
result.push({ role: "toolResult", toolCallId, toolName: toolNames.get(toolCallId) ?? "unknown", content: await piToolContent(block.content, ctx, options, imageMap
|
|
413
|
+
result.push({ role: "toolResult", toolCallId, toolName: toolNames.get(toolCallId) ?? "unknown", content: await piToolContent(block.content, ctx, options, imageMap, requestImages), isError: block.isError === true, timestamp: 0 });
|
|
383
414
|
}
|
|
384
415
|
}
|
|
385
416
|
return result;
|
|
@@ -419,34 +450,40 @@ export async function serializeDshMessages(messages, ctx, options = {}) {
|
|
|
419
450
|
const imageMap = new Map();
|
|
420
451
|
const fileProjected = projectFiles(messages, ctx);
|
|
421
452
|
const attachments = attachmentServices(ctx?.attachments);
|
|
422
|
-
const
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
453
|
+
const requestImages = normalized.imageSupport === "unsupported"
|
|
454
|
+
? new Map()
|
|
455
|
+
: await prepareRequestImageVersions(fileProjected, ctx, normalized);
|
|
456
|
+
const offloadImages = attachments === undefined || normalized.imageSupport === "unsupported"
|
|
457
|
+
? 0
|
|
458
|
+
: requiredImageOffload(fileProjected, { representation: "base64", maxBytes: normalized.maxRequestImageBytes }, (block) => {
|
|
459
|
+
// `requiredImageOffload` applies the base64 expansion itself for a
|
|
460
|
+
// base64 representation, so this reports raw request-version bytes.
|
|
461
|
+
const request = requestImages.get(String(block.attachment.attachmentId));
|
|
462
|
+
return request?.bytes ?? block.attachment.bytes;
|
|
463
|
+
});
|
|
464
|
+
if (offloadImages > 0) {
|
|
465
|
+
throw new LlmError(`LCX request images exceed the configured base64 bound; ${offloadImages} more oldest occurrence(s) must be offloaded.`, IMAGE_OFFLOAD_REQUIRED_CODE, { offloadImages });
|
|
466
|
+
}
|
|
467
|
+
const projected = projectOffloadedImages(fileProjected, (ref) => offloadedImageText(ref, attachments === undefined ? undefined : imageAccess(ctx, attachments, ref)));
|
|
427
468
|
const model = resolvePiResponsesModel(normalized);
|
|
428
|
-
const
|
|
469
|
+
const dshContext = { systemPrompt: normalized.systemPrompt, messages: await dshToPiMessages(projected, ctx, normalized, imageMap, requestImages), tools: [...normalized.tools] };
|
|
429
470
|
const compat = isObject(model.compat) ? model.compat : {};
|
|
430
471
|
const supportsStrictMode = compat.supportsStrictMode === true;
|
|
431
472
|
const supportsOpenAIGrammarTools = compat.supportsOpenAIGrammarTools === true;
|
|
432
473
|
const supportsAdditionalTools = compat.supportsAdditionalTools === true;
|
|
433
474
|
const supportsToolSearch = compat.supportsToolSearch === true;
|
|
434
475
|
const deferredToolsMode = supportsAdditionalTools ? "additional-tools" : supportsToolSearch ? "tool-search" : undefined;
|
|
435
|
-
const grammarToolInputProperties = createGrammarToolInputProperties(
|
|
476
|
+
const grammarToolInputProperties = createGrammarToolInputProperties(dshContext.tools, supportsOpenAIGrammarTools);
|
|
436
477
|
// DSH has no authoritative added-tool provenance; keep its full catalog immediate.
|
|
437
478
|
const immediateTools = new Map();
|
|
438
|
-
for (const tool of
|
|
479
|
+
for (const tool of dshContext.tools)
|
|
439
480
|
if (tool.name)
|
|
440
481
|
immediateTools.set(tool.name, tool);
|
|
441
|
-
const toolOptions = { supportsStrictMode, supportsOpenAIGrammarTools };
|
|
442
|
-
const piContext =
|
|
443
|
-
systemPrompt: context.systemPrompt,
|
|
444
|
-
messages: context.messages,
|
|
445
|
-
tools: context.tools,
|
|
446
|
-
};
|
|
482
|
+
const toolOptions = { supportsStrictMode, supportsOpenAIGrammarTools, toolSearchResult: false };
|
|
483
|
+
const piContext = normalizeContext(dshContext);
|
|
447
484
|
const piModel = model;
|
|
448
|
-
const convertedInput = convertResponsesMessages(piModel, piContext, new Set(["openai", "openai-codex", "opencode"]), { includeSystemPrompt: normalized.includeSystemPrompt, grammarToolInputProperties,
|
|
449
|
-
const input = restorePortableAssistantPhases(convertedInput,
|
|
485
|
+
const convertedInput = convertResponsesMessages(piModel, piContext, new Set(["openai", "openai-codex", "opencode"]), { includeSystemPrompt: normalized.includeSystemPrompt, grammarToolInputProperties, supportsAdditionalTools, supportsToolSearch, toolOptions });
|
|
486
|
+
const input = restorePortableAssistantPhases(convertedInput, dshContext.messages, { provider: model.provider, id: model.id, api: model.api });
|
|
450
487
|
const tools = options.tools === undefined ? undefined : convertResponsesTools([...immediateTools.values()], toolOptions);
|
|
451
488
|
return { input, imageMap, tools, model: piModel, grammarToolInputProperties, deferredToolsMode };
|
|
452
489
|
}
|
package/lib/index.js
CHANGED
|
@@ -1045,25 +1045,41 @@ function installInjected(ctx, configInput = {}) {
|
|
|
1045
1045
|
ctx.inject(["compaction"], (compactionCtx) => {
|
|
1046
1046
|
patchCompactionPressureService(resolveContextService(compactionCtx, "compaction"), state, () => runtimeConfig, ctx, compactionPatchRecords);
|
|
1047
1047
|
});
|
|
1048
|
-
|
|
1048
|
+
/**
|
|
1049
|
+
* LCX instrumentation is optional capability, never a creation precondition.
|
|
1050
|
+
* DSH 0.1.6 awaits `agent/created` serially, so an escaping failure would make
|
|
1051
|
+
* an otherwise valid Agent uncreatable; `agent/status` stays a contained emit.
|
|
1052
|
+
* Both edges therefore fail closed locally: the Agent runs uninstrumented and
|
|
1053
|
+
* the reason is logged, rather than propagating into the host lifecycle.
|
|
1054
|
+
*/
|
|
1055
|
+
const instrumentAgent = (agent, edge) => {
|
|
1056
|
+
try {
|
|
1057
|
+
managedAgents.add(agent);
|
|
1058
|
+
syncAgentTools(agent);
|
|
1059
|
+
const installed = patchCompactionPressureForAgent(agent, state, () => runtimeConfig, ctx, compactionPatchRecords);
|
|
1060
|
+
if (installed)
|
|
1061
|
+
ctx.logger?.info?.("[lcx-codex] pressure coordination installed through AgentPresets service resolver");
|
|
1062
|
+
}
|
|
1063
|
+
catch (error) {
|
|
1064
|
+
ctx.logger?.warn?.(`[lcx-codex] agent instrumentation skipped on ${edge}: ${String(error)}`);
|
|
1065
|
+
}
|
|
1066
|
+
};
|
|
1067
|
+
/** DSH 0.1.6 lifecycle source, recorded for diagnostics only. */
|
|
1068
|
+
const startSource = (value) => typeof value === "string" && /^[a-z]+$/u.test(value) ? value : "unknown";
|
|
1069
|
+
ctx.on("agent/created", (payload) => {
|
|
1070
|
+
const agent = payload?.agent;
|
|
1049
1071
|
if (agent === null || typeof agent !== "object")
|
|
1050
1072
|
return;
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
if (installed)
|
|
1055
|
-
ctx.logger?.info?.("[lcx-codex] pressure coordination installed through AgentPresets service resolver");
|
|
1073
|
+
// `source` is startup | resume | clear | compact. Recorded, not branched on:
|
|
1074
|
+
// no runtime evidence yet that a source needs different instrumentation.
|
|
1075
|
+
instrumentAgent(agent, `agent/created(${startSource(payload?.source)})`);
|
|
1056
1076
|
}, { global: true });
|
|
1057
1077
|
ctx.on("agent/status", ({ agent, status }) => {
|
|
1058
1078
|
if (status !== "running" ||
|
|
1059
1079
|
agent === null ||
|
|
1060
1080
|
typeof agent !== "object")
|
|
1061
1081
|
return;
|
|
1062
|
-
|
|
1063
|
-
syncAgentTools(agent);
|
|
1064
|
-
const installed = patchCompactionPressureForAgent(agent, state, () => runtimeConfig, ctx, compactionPatchRecords);
|
|
1065
|
-
if (installed)
|
|
1066
|
-
ctx.logger?.info?.("[lcx-codex] pressure coordination installed through AgentPresets service resolver");
|
|
1082
|
+
instrumentAgent(agent, "agent/status(running)");
|
|
1067
1083
|
}, { global: true });
|
|
1068
1084
|
ctx.on("llm/stream", (options, next) => {
|
|
1069
1085
|
if (options.purpose === "session-title")
|