@eddyskywalker/dsh-chatgpt-subscription 0.48.3 → 0.49.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/CHANGELOG.md +21 -0
- package/lib/client.js +2381 -179
- package/lib/client.js.map +1 -1
- package/lib/index.js +875 -33
- package/lib/types/client/index.d.ts +2 -0
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/kimi-code/KimiCodeSection.d.ts.map +1 -1
- package/lib/types/client/kimi-code/locales.d.ts +14 -2
- package/lib/types/client/kimi-code/locales.d.ts.map +1 -1
- package/lib/types/client/locales-ru/kimi-code.d.ts.map +1 -1
- package/lib/types/client/subagent/SubagentCard.d.ts +35 -0
- package/lib/types/client/subagent/SubagentCard.d.ts.map +1 -0
- package/lib/types/client/subagent/index.d.ts +28 -0
- package/lib/types/client/subagent/index.d.ts.map +1 -0
- package/lib/types/client/subagent/locales.d.ts +172 -0
- package/lib/types/client/subagent/locales.d.ts.map +1 -0
- package/lib/types/client/subagent/styles.d.ts +20 -0
- package/lib/types/client/subagent/styles.d.ts.map +1 -0
- package/lib/types/client/subagent/subagent-card-controller.d.ts +329 -0
- package/lib/types/client/subagent/subagent-card-controller.d.ts.map +1 -0
- package/lib/types/compat.d.ts +1 -1
- package/lib/types/host/common/llm-compat.d.ts +3 -1
- package/lib/types/host/common/llm-compat.d.ts.map +1 -1
- package/lib/types/host/common/request-images.d.ts +66 -0
- package/lib/types/host/common/request-images.d.ts.map +1 -1
- package/lib/types/host/kimi-code/adapter.d.ts.map +1 -1
- package/lib/types/host/kimi-code/mapper.d.ts +53 -3
- package/lib/types/host/kimi-code/mapper.d.ts.map +1 -1
- package/lib/types/host/kimi-code/routes.d.ts.map +1 -1
- package/lib/types/host/kimi-code/session-cache-state.d.ts +14 -0
- package/lib/types/host/kimi-code/session-cache-state.d.ts.map +1 -1
- package/lib/types/host/kimi-code/types.d.ts +29 -4
- package/lib/types/host/kimi-code/types.d.ts.map +1 -1
- package/lib/types/host/preferences.d.ts.map +1 -1
- package/lib/types/host/routes.d.ts.map +1 -1
- package/lib/types/host/subagent-model-authorization.d.ts +130 -2
- package/lib/types/host/subagent-model-authorization.d.ts.map +1 -1
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/shared/contracts.d.ts +28 -0
- package/lib/types/shared/contracts.d.ts.map +1 -1
- package/lib/types/shared/kimi-code-contracts.d.ts +10 -1
- package/lib/types/shared/kimi-code-contracts.d.ts.map +1 -1
- package/lib/types/shared/preferences.d.ts +52 -1
- package/lib/types/shared/preferences.d.ts.map +1 -1
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -145,7 +145,7 @@ const ROUTE_PREFIX$5 = "/api/dsh-chatgpt-subscription";
|
|
|
145
145
|
* `test/responses-client.test.ts` asserts the two agree, so a release that
|
|
146
146
|
* forgets this constant fails the suite instead of shipping quietly.
|
|
147
147
|
*/
|
|
148
|
-
const PLUGIN_VERSION = "0.
|
|
148
|
+
const PLUGIN_VERSION = "0.49.0";
|
|
149
149
|
const CODEX_CHATGPT_PROVIDER_ID = "codex-chatgpt";
|
|
150
150
|
const CODEX_API_BASE = "https://chatgpt.com/backend-api/codex";
|
|
151
151
|
const CODEX_RESPONSES_URL = `${CODEX_API_BASE}/responses`;
|
|
@@ -859,7 +859,7 @@ async function rehydrateSnapshot(key) {
|
|
|
859
859
|
* so a backend that flattens the envelope does not read as an empty catalog.
|
|
860
860
|
*/
|
|
861
861
|
function parseListing(payload) {
|
|
862
|
-
const record = asRecord$
|
|
862
|
+
const record = asRecord$20(payload);
|
|
863
863
|
const models = Array.isArray(payload) ? payload : record?.models;
|
|
864
864
|
if (!Array.isArray(models)) return [];
|
|
865
865
|
return models.flatMap((item) => parseListingEntry$1(item));
|
|
@@ -873,7 +873,7 @@ function parseListing(payload) {
|
|
|
873
873
|
* back to a conservative answer rather than to a guess.
|
|
874
874
|
*/
|
|
875
875
|
function parseListingEntry$1(value) {
|
|
876
|
-
const record = asRecord$
|
|
876
|
+
const record = asRecord$20(value);
|
|
877
877
|
if (record === void 0) return [];
|
|
878
878
|
const id = asString$27(record.slug) ?? asString$27(record.id);
|
|
879
879
|
if (id === void 0) return [];
|
|
@@ -909,7 +909,7 @@ function parseEfforts(value) {
|
|
|
909
909
|
if (!Array.isArray(value)) return null;
|
|
910
910
|
const out = [];
|
|
911
911
|
for (const item of value) {
|
|
912
|
-
const effort = asString$27(item) ?? asString$27(asRecord$
|
|
912
|
+
const effort = asString$27(item) ?? asString$27(asRecord$20(item)?.effort);
|
|
913
913
|
if (effort !== void 0 && !out.includes(effort)) out.push(effort);
|
|
914
914
|
}
|
|
915
915
|
return out.length > 0 ? out : null;
|
|
@@ -924,7 +924,7 @@ function parseEfforts(value) {
|
|
|
924
924
|
function accountKeyFor$1(credentials) {
|
|
925
925
|
return credentials.accountId || "default";
|
|
926
926
|
}
|
|
927
|
-
function asRecord$
|
|
927
|
+
function asRecord$20(value) {
|
|
928
928
|
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
929
929
|
}
|
|
930
930
|
function asString$27(value) {
|
|
@@ -5025,12 +5025,52 @@ function readBlocks(raw) {
|
|
|
5025
5025
|
return raw.filter((block) => isRecord$58(block) && typeof block.type === "string");
|
|
5026
5026
|
}
|
|
5027
5027
|
/**
|
|
5028
|
+
* The first value that is a non-empty string.
|
|
5029
|
+
*
|
|
5030
|
+
* Used for fields the two generations spell in different places: a value that
|
|
5031
|
+
* is present but empty must not shadow a real one further down the chain, which
|
|
5032
|
+
* is what `??` alone does.
|
|
5033
|
+
*/
|
|
5034
|
+
function firstNonEmptyString(...values) {
|
|
5035
|
+
for (const value of values) if (typeof value === "string" && value !== "") return value;
|
|
5036
|
+
return "";
|
|
5037
|
+
}
|
|
5038
|
+
/**
|
|
5039
|
+
* The result block a `role: 'tool'` message already carries inside its body.
|
|
5040
|
+
*
|
|
5041
|
+
* Some harness generations deliver the call id inside the result block rather
|
|
5042
|
+
* than on the message, and one shape carries both: `role: 'tool'` with a
|
|
5043
|
+
* `tool-result` block holding the id. Reading only the message-level fields
|
|
5044
|
+
* leaves {@link toolCallIdOf} nothing to find.
|
|
5045
|
+
*/
|
|
5046
|
+
function embeddedToolResult(content) {
|
|
5047
|
+
if (content.length !== 1) return void 0;
|
|
5048
|
+
const [block] = content;
|
|
5049
|
+
return block?.type === "tool-result" ? block : void 0;
|
|
5050
|
+
}
|
|
5051
|
+
/**
|
|
5052
|
+
* The call id one `role: 'tool'` message associates with its `tool-call`.
|
|
5053
|
+
*
|
|
5054
|
+
* The id is the only thing tying a result to the call it answers: a mapper that
|
|
5055
|
+
* sends it empty makes every result an orphan and tells the repair pass that no
|
|
5056
|
+
* call was answered, which some gateways reject outright (WorkBuddy answers
|
|
5057
|
+
* `11148`, "tool calls and tool results do not match"). It therefore has to be
|
|
5058
|
+
* read from every place the harness generations put it — the message, the
|
|
5059
|
+
* message's source, and the result block inside the body — and an empty string
|
|
5060
|
+
* must not shadow a later, real one.
|
|
5061
|
+
*/
|
|
5062
|
+
function toolCallIdOf(raw, source, embedded) {
|
|
5063
|
+
return firstNonEmptyString(raw.toolCallId, raw.callId, source?.callId, embedded?.toolCallId);
|
|
5064
|
+
}
|
|
5065
|
+
/**
|
|
5028
5066
|
* Normalize one harness message list into {@link Message}.
|
|
5029
5067
|
*
|
|
5030
5068
|
* A 0.1.7 `role: 'tool'` message is re-wrapped as the user-role message holding
|
|
5031
5069
|
* one {@link ToolResultBlock} that the mappers expect, keeping the tool
|
|
5032
5070
|
* provenance (`kind: 'tool'`) they use to tell a real tool result from a user
|
|
5033
|
-
* turn.
|
|
5071
|
+
* turn. A message that already carries that block keeps its own body instead of
|
|
5072
|
+
* nesting a second `tool-result` around it. Everything a newer harness adds
|
|
5073
|
+
* beside that keeps its own shape.
|
|
5034
5074
|
* @param messages - Messages straight off the request options, in either generation's shape.
|
|
5035
5075
|
* @returns The same conversation in the pre-0.1.7 vocabulary.
|
|
5036
5076
|
*/
|
|
@@ -5041,15 +5081,17 @@ function normalizeMessages(messages) {
|
|
|
5041
5081
|
const content = readBlocks(raw.content);
|
|
5042
5082
|
if (raw.role === "tool") {
|
|
5043
5083
|
const source = isRecord$58(raw.source) ? raw.source : void 0;
|
|
5044
|
-
const
|
|
5084
|
+
const embedded = embeddedToolResult(content);
|
|
5085
|
+
const callId = toolCallIdOf(raw, source, embedded);
|
|
5086
|
+
const isError = raw.isError === true || embedded?.isError === true;
|
|
5045
5087
|
normalized.push({
|
|
5046
5088
|
id: raw.id,
|
|
5047
5089
|
role: "user",
|
|
5048
5090
|
content: [{
|
|
5049
5091
|
type: "tool-result",
|
|
5050
5092
|
toolCallId: callId,
|
|
5051
|
-
content,
|
|
5052
|
-
...
|
|
5093
|
+
content: embedded === void 0 ? content : embedded.content,
|
|
5094
|
+
...isError ? { isError: true } : {}
|
|
5053
5095
|
}],
|
|
5054
5096
|
source: {
|
|
5055
5097
|
kind: "tool",
|
|
@@ -10770,7 +10812,9 @@ const DEFAULT_PREFERENCES = {
|
|
|
10770
10812
|
contextWindowOverrides: {},
|
|
10771
10813
|
proxyMode: "auto",
|
|
10772
10814
|
customProxyUrl: null,
|
|
10773
|
-
compactionRoute: null
|
|
10815
|
+
compactionRoute: null,
|
|
10816
|
+
subagentDefaultModels: [],
|
|
10817
|
+
subagentModelsByMainModel: {}
|
|
10774
10818
|
};
|
|
10775
10819
|
const SEARCH_PROVIDER_CODEX = "codex";
|
|
10776
10820
|
function isSearchProviderPreference(value) {
|
|
@@ -10785,6 +10829,105 @@ function isCodexReasoningSummary(value) {
|
|
|
10785
10829
|
function isProxyMode(value) {
|
|
10786
10830
|
return value === "auto" || value === "custom" || value === "direct";
|
|
10787
10831
|
}
|
|
10832
|
+
function asRecord$19(value) {
|
|
10833
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
10834
|
+
}
|
|
10835
|
+
/**
|
|
10836
|
+
* Read one non-empty trimmed string field.
|
|
10837
|
+
* @param value - Candidate field value from stored JSON.
|
|
10838
|
+
* @returns the trimmed string, or undefined when it is not a non-empty string.
|
|
10839
|
+
*/
|
|
10840
|
+
function trimmedString(value) {
|
|
10841
|
+
if (typeof value !== "string") return void 0;
|
|
10842
|
+
const trimmed = value.trim();
|
|
10843
|
+
return trimmed.length === 0 ? void 0 : trimmed;
|
|
10844
|
+
}
|
|
10845
|
+
/**
|
|
10846
|
+
* Whether one value is a complete `{ provider, model }` route.
|
|
10847
|
+
*
|
|
10848
|
+
* Both halves are trimmed and must be non-empty: a route with a blank provider
|
|
10849
|
+
* names no adapter, and a blank model names no model, so neither can ever be
|
|
10850
|
+
* authorized. Stored documents are untrusted JSON, which is why this narrows
|
|
10851
|
+
* rather than asserts.
|
|
10852
|
+
* @param value - Candidate route.
|
|
10853
|
+
* @returns whether the value is a usable route.
|
|
10854
|
+
*/
|
|
10855
|
+
function isSubagentModelRoute(value) {
|
|
10856
|
+
const record = asRecord$19(value);
|
|
10857
|
+
if (record === void 0) return false;
|
|
10858
|
+
return trimmedString(record["provider"]) !== void 0 && trimmedString(record["model"]) !== void 0;
|
|
10859
|
+
}
|
|
10860
|
+
/**
|
|
10861
|
+
* Normalize one route list, deduplicating it without reordering.
|
|
10862
|
+
*
|
|
10863
|
+
* Order is preserved because it is the order the settings card shows and the
|
|
10864
|
+
* order `list_subagent_models` prints: a set would silently reshuffle the list
|
|
10865
|
+
* a user arranged.
|
|
10866
|
+
* @param value - Candidate route list from stored JSON.
|
|
10867
|
+
* @returns the trimmed, deduplicated routes, or null when the value is not an
|
|
10868
|
+
* array of well-formed routes.
|
|
10869
|
+
*/
|
|
10870
|
+
function normalizeSubagentModelRoutes(value) {
|
|
10871
|
+
if (!Array.isArray(value)) return null;
|
|
10872
|
+
const routes = [];
|
|
10873
|
+
const seen = /* @__PURE__ */ new Set();
|
|
10874
|
+
for (const entry of value) {
|
|
10875
|
+
if (!isSubagentModelRoute(entry)) return null;
|
|
10876
|
+
const provider = entry.provider.trim();
|
|
10877
|
+
const model = entry.model.trim();
|
|
10878
|
+
const key = `${provider}/${model}`;
|
|
10879
|
+
if (seen.has(key)) continue;
|
|
10880
|
+
seen.add(key);
|
|
10881
|
+
routes.push({
|
|
10882
|
+
provider,
|
|
10883
|
+
model
|
|
10884
|
+
});
|
|
10885
|
+
}
|
|
10886
|
+
return routes;
|
|
10887
|
+
}
|
|
10888
|
+
/**
|
|
10889
|
+
* Whether one key names a main agent route as `provider/model`.
|
|
10890
|
+
*
|
|
10891
|
+
* The split is at the FIRST slash, so a model id that legally contains a slash
|
|
10892
|
+
* (`command-code` + `deepseek/deepseek-v4.1-flash`) still round-trips: the
|
|
10893
|
+
* provider half ends at the first separator and the model half keeps the rest.
|
|
10894
|
+
* @param key - Candidate map key.
|
|
10895
|
+
* @returns whether the key carries a non-empty provider and a non-empty model.
|
|
10896
|
+
*/
|
|
10897
|
+
function isValidMainModelKey(key) {
|
|
10898
|
+
if (typeof key !== "string") return false;
|
|
10899
|
+
const trimmed = key.trim();
|
|
10900
|
+
const separator = trimmed.indexOf("/");
|
|
10901
|
+
if (separator <= 0) return false;
|
|
10902
|
+
return trimmed.slice(separator + 1).length > 0;
|
|
10903
|
+
}
|
|
10904
|
+
/**
|
|
10905
|
+
* Merge one `subagentModelsByMainModel` patch into the stored map.
|
|
10906
|
+
*
|
|
10907
|
+
* A merge rather than a replace, so the card can clear or set one main model
|
|
10908
|
+
* without resending every other entry — and `null` (or `[]`) is how one entry
|
|
10909
|
+
* is cleared, since a stored map has no other way to say "no override".
|
|
10910
|
+
* @param current - Map already stored in the preferences document.
|
|
10911
|
+
* @param patch - Incoming patch from a settings card or a route body.
|
|
10912
|
+
* @returns the merged map, or null when any key or value is malformed.
|
|
10913
|
+
*/
|
|
10914
|
+
function mergeSubagentModelsByMainModel(current, patch) {
|
|
10915
|
+
const patchRecord = asRecord$19(patch);
|
|
10916
|
+
if (patchRecord === void 0) return null;
|
|
10917
|
+
const merged = { ...current };
|
|
10918
|
+
for (const [rawKey, rawRoutes] of Object.entries(patchRecord)) {
|
|
10919
|
+
const key = rawKey.trim();
|
|
10920
|
+
if (!isValidMainModelKey(key)) return null;
|
|
10921
|
+
if (rawRoutes === null || Array.isArray(rawRoutes) && rawRoutes.length === 0) {
|
|
10922
|
+
delete merged[key];
|
|
10923
|
+
continue;
|
|
10924
|
+
}
|
|
10925
|
+
const routes = normalizeSubagentModelRoutes(rawRoutes);
|
|
10926
|
+
if (routes === null) return null;
|
|
10927
|
+
merged[key] = routes;
|
|
10928
|
+
}
|
|
10929
|
+
return merged;
|
|
10930
|
+
}
|
|
10788
10931
|
//#endregion
|
|
10789
10932
|
//#region src/host/common/legacy-preferences.ts
|
|
10790
10933
|
/**
|
|
@@ -10951,6 +11094,10 @@ function sectionOf(document, id) {
|
|
|
10951
11094
|
*/
|
|
10952
11095
|
function registerPreferenceStore(settings) {
|
|
10953
11096
|
const contextWindowOverrides = z.object(Object.fromEntries(CODEX_MODEL_CATALOG.map((entry) => [entry.id, z.number().step(1).min(1).max(contextWindowLimitForModel(entry.id))]))).default({});
|
|
11097
|
+
const subagentModelRoutes = z.array(z.object({
|
|
11098
|
+
provider: z.string(),
|
|
11099
|
+
model: z.string()
|
|
11100
|
+
}));
|
|
10954
11101
|
const schema = z.object({
|
|
10955
11102
|
enabled: z.boolean().default(DEFAULT_PREFERENCES.enabled ?? true),
|
|
10956
11103
|
quickQuotaVisible: z.boolean().default(DEFAULT_PREFERENCES.quickQuotaVisible),
|
|
@@ -10980,7 +11127,9 @@ function registerPreferenceStore(settings) {
|
|
|
10980
11127
|
compactionRoute: z.union([z.object({
|
|
10981
11128
|
provider: z.string(),
|
|
10982
11129
|
model: z.string()
|
|
10983
|
-
}), z.const(null)]).default(DEFAULT_PREFERENCES.compactionRoute ?? null)
|
|
11130
|
+
}), z.const(null)]).default(DEFAULT_PREFERENCES.compactionRoute ?? null),
|
|
11131
|
+
subagentDefaultModels: subagentModelRoutes.default([]),
|
|
11132
|
+
subagentModelsByMainModel: z.dict(subagentModelRoutes).default({})
|
|
10984
11133
|
});
|
|
10985
11134
|
if (!hasRegister(settings)) {
|
|
10986
11135
|
const fileStore = new FilePreferencesStore(schema, preferencesPath(), readLegacyPreferences);
|
|
@@ -11038,6 +11187,16 @@ var SettingsPreferenceStore = class {
|
|
|
11038
11187
|
model: patch.compactionRoute.model.trim()
|
|
11039
11188
|
};
|
|
11040
11189
|
}
|
|
11190
|
+
if (patch.subagentDefaultModels !== void 0) {
|
|
11191
|
+
const routes = normalizeSubagentModelRoutes(patch.subagentDefaultModels);
|
|
11192
|
+
if (routes === null) throw new PreferenceError("Unsupported subagentDefaultModels preference.");
|
|
11193
|
+
normalized.subagentDefaultModels = routes;
|
|
11194
|
+
}
|
|
11195
|
+
if (patch.subagentModelsByMainModel !== void 0) {
|
|
11196
|
+
const merged = mergeSubagentModelsByMainModel(this.scope.get().subagentModelsByMainModel, patch.subagentModelsByMainModel);
|
|
11197
|
+
if (merged === null) throw new PreferenceError("Unsupported subagentModelsByMainModel preference.");
|
|
11198
|
+
normalized.subagentModelsByMainModel = merged;
|
|
11199
|
+
}
|
|
11041
11200
|
if (patch.customProxyUrl !== void 0) if (patch.customProxyUrl !== null) {
|
|
11042
11201
|
const trimmed = patch.customProxyUrl.trim();
|
|
11043
11202
|
if (trimmed.length > 0 && !/^https?:\/\//i.test(trimmed) && !/^socks5?:\/\//i.test(trimmed)) normalized.customProxyUrl = `http://${trimmed}`;
|
|
@@ -13964,6 +14123,15 @@ function readPreferencesUpdate(value, current) {
|
|
|
13964
14123
|
model: value.compactionRoute.model.trim()
|
|
13965
14124
|
};
|
|
13966
14125
|
}
|
|
14126
|
+
if ("subagentDefaultModels" in value) {
|
|
14127
|
+
const routes = normalizeSubagentModelRoutes(value.subagentDefaultModels);
|
|
14128
|
+
if (routes === null) throw new PreferenceError("subagentDefaultModels must be an array of { provider, model } routes.");
|
|
14129
|
+
patch.subagentDefaultModels = routes;
|
|
14130
|
+
}
|
|
14131
|
+
if ("subagentModelsByMainModel" in value) {
|
|
14132
|
+
if (mergeSubagentModelsByMainModel(current.subagentModelsByMainModel, value.subagentModelsByMainModel) === null) throw new PreferenceError("subagentModelsByMainModel must map \"provider/model\" keys to arrays of { provider, model } routes, or null to clear one.");
|
|
14133
|
+
patch.subagentModelsByMainModel = value.subagentModelsByMainModel;
|
|
14134
|
+
}
|
|
13967
14135
|
if ("contextWindowOverrides" in value) {
|
|
13968
14136
|
if (!isRecord$55(value.contextWindowOverrides)) throw new PreferenceError("contextWindowOverrides must be an object.");
|
|
13969
14137
|
const overrides = {};
|
|
@@ -26569,6 +26737,17 @@ async function projectToolResult(input) {
|
|
|
26569
26737
|
}
|
|
26570
26738
|
//#endregion
|
|
26571
26739
|
//#region src/host/common/request-images.ts
|
|
26740
|
+
/**
|
|
26741
|
+
* Image resolution, byte budgeting and data-URL handling for one request.
|
|
26742
|
+
*
|
|
26743
|
+
* Shared by the provider lines that send inline images, because the budget is a
|
|
26744
|
+
* property of the deployment rather than of any one endpoint, and because two
|
|
26745
|
+
* lines that budget differently send different request bytes for the same picture.
|
|
26746
|
+
*
|
|
26747
|
+
* The notable decision is SCALE, not drop: see {@link resolveRequestImages}.
|
|
26748
|
+
*
|
|
26749
|
+
* @module dsh-chatgpt-subscription/request-images
|
|
26750
|
+
*/
|
|
26572
26751
|
function isRecord$50(value) {
|
|
26573
26752
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
26574
26753
|
}
|
|
@@ -26666,7 +26845,7 @@ function base64Length$1(bytes) {
|
|
|
26666
26845
|
* the only estimate available, so it stays the fallback and every call site that
|
|
26667
26846
|
* has no resolved map behaves exactly as before.
|
|
26668
26847
|
*/
|
|
26669
|
-
function
|
|
26848
|
+
function inlinePayloadOf(block) {
|
|
26670
26849
|
let data = asString$21(block.data) || asString$21(block.base64);
|
|
26671
26850
|
const source = isRecord$50(block.source) ? block.source : void 0;
|
|
26672
26851
|
if (!data && source) data = asString$21(source.data) || asString$21(source.base64);
|
|
@@ -26674,7 +26853,11 @@ function requestImagePayloadLength(block, images) {
|
|
|
26674
26853
|
const matched = data.match(/^data:([^;,]+);base64,(.*)$/s);
|
|
26675
26854
|
if (matched) data = matched[2] ?? "";
|
|
26676
26855
|
}
|
|
26677
|
-
|
|
26856
|
+
return data === void 0 || data === "" ? void 0 : data;
|
|
26857
|
+
}
|
|
26858
|
+
function requestImagePayloadLength(block, images) {
|
|
26859
|
+
const data = inlinePayloadOf(block);
|
|
26860
|
+
if (data !== void 0) return data.length;
|
|
26678
26861
|
const attachment = attachmentOf$3(block);
|
|
26679
26862
|
if (!attachment) return void 0;
|
|
26680
26863
|
if (images === void 0) return base64Length$1(attachment.bytes);
|
|
@@ -26859,6 +27042,186 @@ function offloadOldestRequestImages$1(options, maxBytes = MAX_REQUEST_IMAGE_BYTE
|
|
|
26859
27042
|
return replaceOldestRequestImages(options, imagesToOffloadCount(requestImagePayloadLengths(options, images), maxBytes), images);
|
|
26860
27043
|
}
|
|
26861
27044
|
/**
|
|
27045
|
+
* A stable identity for one image occurrence.
|
|
27046
|
+
*
|
|
27047
|
+
* Two occurrences of the SAME picture have to share one identity, and that
|
|
27048
|
+
* identity has to survive the turn: a durable attachment keeps its id, and an
|
|
27049
|
+
* inline payload keeps its bytes, so hashing the payload names the same
|
|
27050
|
+
* picture the same way on every later request.
|
|
27051
|
+
*
|
|
27052
|
+
* `undefined` for a block that carries no sendable payload: an image that
|
|
27053
|
+
* resolved to `unavailable` is serialized as a short text placeholder, so it
|
|
27054
|
+
* is not an image on the wire and has nothing to remember.
|
|
27055
|
+
*/
|
|
27056
|
+
function requestImageKey(block, images) {
|
|
27057
|
+
if (requestImagePayloadLength(block, images) === void 0) return void 0;
|
|
27058
|
+
const attachment = attachmentOf$3(block);
|
|
27059
|
+
if (attachment !== void 0) return `attachment:${attachment.attachmentId}`;
|
|
27060
|
+
const data = inlinePayloadOf(block);
|
|
27061
|
+
if (data === void 0) return void 0;
|
|
27062
|
+
return `inline:${createHash("sha256").update(data).digest("hex")}`;
|
|
27063
|
+
}
|
|
27064
|
+
/**
|
|
27065
|
+
* Every sendable image occurrence one request carries, in request order.
|
|
27066
|
+
*
|
|
27067
|
+
* The traversal is the one `collectRequestImagePayloads` uses, tool results
|
|
27068
|
+
* included: a screenshot inside a tool result reaches the wire, so an
|
|
27069
|
+
* occurrence the budget cannot see is an occurrence it cannot budget for.
|
|
27070
|
+
*/
|
|
27071
|
+
function collectImageOccurrences(options, images) {
|
|
27072
|
+
const occurrences = [];
|
|
27073
|
+
const walk = (content) => {
|
|
27074
|
+
if (!Array.isArray(content)) return;
|
|
27075
|
+
for (const block of content) {
|
|
27076
|
+
if (!isRecord$50(block)) continue;
|
|
27077
|
+
if (block.type === "image") {
|
|
27078
|
+
const bytes = requestImagePayloadLength(block, images);
|
|
27079
|
+
const key = requestImageKey(block, images);
|
|
27080
|
+
if (bytes !== void 0 && key !== void 0) occurrences.push({
|
|
27081
|
+
key,
|
|
27082
|
+
bytes
|
|
27083
|
+
});
|
|
27084
|
+
continue;
|
|
27085
|
+
}
|
|
27086
|
+
if (block.type === "tool-result") walk(block.content);
|
|
27087
|
+
}
|
|
27088
|
+
};
|
|
27089
|
+
for (const message of options.messages) walk(message.content);
|
|
27090
|
+
return occurrences;
|
|
27091
|
+
}
|
|
27092
|
+
/**
|
|
27093
|
+
* The identities of the images one request still carries, each once.
|
|
27094
|
+
*
|
|
27095
|
+
* The caller compares two of these to learn what a later build gave back: the
|
|
27096
|
+
* difference names the images a body-fit pass dropped after the budget pass
|
|
27097
|
+
* had already run.
|
|
27098
|
+
*/
|
|
27099
|
+
function collectRemainingImageKeys(options, images) {
|
|
27100
|
+
const keys = [];
|
|
27101
|
+
const seen = /* @__PURE__ */ new Set();
|
|
27102
|
+
for (const occurrence of collectImageOccurrences(options, images)) {
|
|
27103
|
+
if (seen.has(occurrence.key)) continue;
|
|
27104
|
+
seen.add(occurrence.key);
|
|
27105
|
+
keys.push(occurrence.key);
|
|
27106
|
+
}
|
|
27107
|
+
return keys;
|
|
27108
|
+
}
|
|
27109
|
+
/**
|
|
27110
|
+
* Replace every occurrence of a dropped image with the placeholder.
|
|
27111
|
+
*
|
|
27112
|
+
* Unlike {@link replaceOldestRequestImages} this replaces by IDENTITY rather
|
|
27113
|
+
* than by position: a sticky budget drops a picture, and the picture has to
|
|
27114
|
+
* stay dropped for the rest of the session wherever it appears.
|
|
27115
|
+
*/
|
|
27116
|
+
function replaceDroppedRequestImages(options, droppedKeys, images) {
|
|
27117
|
+
if (droppedKeys.size === 0) return {
|
|
27118
|
+
options,
|
|
27119
|
+
count: 0
|
|
27120
|
+
};
|
|
27121
|
+
let count = 0;
|
|
27122
|
+
const replaceIn = (content) => {
|
|
27123
|
+
if (!Array.isArray(content)) return {
|
|
27124
|
+
content,
|
|
27125
|
+
replaced: false
|
|
27126
|
+
};
|
|
27127
|
+
let replaced = false;
|
|
27128
|
+
return {
|
|
27129
|
+
content: content.map((block) => {
|
|
27130
|
+
if (!isRecord$50(block)) return block;
|
|
27131
|
+
if (block.type === "image") {
|
|
27132
|
+
const key = requestImageKey(block, images);
|
|
27133
|
+
if (key === void 0 || !droppedKeys.has(key)) return block;
|
|
27134
|
+
replaced = true;
|
|
27135
|
+
count += 1;
|
|
27136
|
+
return {
|
|
27137
|
+
type: "text",
|
|
27138
|
+
text: OMITTED_IMAGE_TEXT$1
|
|
27139
|
+
};
|
|
27140
|
+
}
|
|
27141
|
+
if (block.type === "tool-result" && Array.isArray(block.content)) {
|
|
27142
|
+
const inner = replaceIn(block.content);
|
|
27143
|
+
if (!inner.replaced) return block;
|
|
27144
|
+
replaced = true;
|
|
27145
|
+
return {
|
|
27146
|
+
...block,
|
|
27147
|
+
content: inner.content
|
|
27148
|
+
};
|
|
27149
|
+
}
|
|
27150
|
+
return block;
|
|
27151
|
+
}),
|
|
27152
|
+
replaced
|
|
27153
|
+
};
|
|
27154
|
+
};
|
|
27155
|
+
let changed = false;
|
|
27156
|
+
const messages = options.messages.map((message) => {
|
|
27157
|
+
const result = replaceIn(message.content);
|
|
27158
|
+
if (!result.replaced) return message;
|
|
27159
|
+
changed = true;
|
|
27160
|
+
return {
|
|
27161
|
+
...message,
|
|
27162
|
+
content: result.content
|
|
27163
|
+
};
|
|
27164
|
+
});
|
|
27165
|
+
return {
|
|
27166
|
+
options: changed ? {
|
|
27167
|
+
...options,
|
|
27168
|
+
messages
|
|
27169
|
+
} : options,
|
|
27170
|
+
count
|
|
27171
|
+
};
|
|
27172
|
+
}
|
|
27173
|
+
/**
|
|
27174
|
+
* Offload images against a budget that RECOVERS instead of a budget that resets.
|
|
27175
|
+
*
|
|
27176
|
+
* WHY THE TWO WATERMARKS. A single ceiling makes the retained prefix depend
|
|
27177
|
+
* on the request: two conversations carrying the same images drop a different
|
|
27178
|
+
* number of them as one more image arrives, so the prefix the service cached
|
|
27179
|
+
* is rewritten on the next turn and the cache is cold again. Native `kimi-cli`
|
|
27180
|
+
* therefore keeps a HIGH mark (20 MiB) and a LOW mark (10 MiB): the request
|
|
27181
|
+
* is allowed to grow to the high mark, and once it passes it, the oldest
|
|
27182
|
+
* images are given back until the request is back under the LOW mark. The gap
|
|
27183
|
+
* between the two is the slack that keeps the number of retained images — and
|
|
27184
|
+
* therefore the cached prefix — stable across turns.
|
|
27185
|
+
*
|
|
27186
|
+
* WHY THE DROPS ARE STICKY. The images a request gives back are replaced for
|
|
27187
|
+
* the REST of the session: `priorDroppedKeys` carries the identities a
|
|
27188
|
+
* previous turn already dropped, and they are replaced again here. A picture
|
|
27189
|
+
* that was omitted stays omitted, so the request the service caches stops
|
|
27190
|
+
* changing once the session has settled under its low mark.
|
|
27191
|
+
*
|
|
27192
|
+
* @param options - the request to budget.
|
|
27193
|
+
* @param maxBytes - high-water mark; images are untouched below it.
|
|
27194
|
+
* @param lowBytes - low-water mark; the trimming stops once the request is back under it.
|
|
27195
|
+
* @param images - the resolution the request will be sent with.
|
|
27196
|
+
* @param priorDroppedKeys - identities dropped by earlier turns.
|
|
27197
|
+
*/
|
|
27198
|
+
function offloadRequestImagesWithStickyBudget(options, maxBytes = MAX_REQUEST_IMAGE_BYTES$1, lowBytes = Math.max(1, Math.floor(maxBytes / 2)), images, priorDroppedKeys) {
|
|
27199
|
+
const allDroppedKeys = new Set(priorDroppedKeys ?? []);
|
|
27200
|
+
const pending = /* @__PURE__ */ new Map();
|
|
27201
|
+
for (const occurrence of collectImageOccurrences(options, images)) {
|
|
27202
|
+
if (allDroppedKeys.has(occurrence.key)) continue;
|
|
27203
|
+
pending.set(occurrence.key, (pending.get(occurrence.key) ?? 0) + occurrence.bytes);
|
|
27204
|
+
}
|
|
27205
|
+
let total = 0;
|
|
27206
|
+
for (const bytes of pending.values()) total += bytes;
|
|
27207
|
+
const newlyDroppedKeys = [];
|
|
27208
|
+
if (total > maxBytes) for (const [key, bytes] of pending) {
|
|
27209
|
+
if (total <= lowBytes) break;
|
|
27210
|
+
if (bytes === 0) continue;
|
|
27211
|
+
allDroppedKeys.add(key);
|
|
27212
|
+
newlyDroppedKeys.push(key);
|
|
27213
|
+
total -= bytes;
|
|
27214
|
+
}
|
|
27215
|
+
const replaced = replaceDroppedRequestImages(options, allDroppedKeys, images);
|
|
27216
|
+
return {
|
|
27217
|
+
options: replaced.options,
|
|
27218
|
+
remainingBytes: total,
|
|
27219
|
+
droppedCount: replaced.count,
|
|
27220
|
+
newlyDroppedKeys,
|
|
27221
|
+
allDroppedKeys
|
|
27222
|
+
};
|
|
27223
|
+
}
|
|
27224
|
+
/**
|
|
26862
27225
|
* Whether a request still carries at least one image the mapper can send.
|
|
26863
27226
|
*
|
|
26864
27227
|
* A request whose images have all been replaced by placeholders can no longer
|
|
@@ -27420,11 +27783,26 @@ function effectiveContextWindow(tokenWindow, env = process.env) {
|
|
|
27420
27783
|
/**
|
|
27421
27784
|
* Base64 image payload one request on this route may carry.
|
|
27422
27785
|
*
|
|
27423
|
-
*
|
|
27424
|
-
*
|
|
27425
|
-
*
|
|
27786
|
+
* Kimi's own CLI budgets request media at 20 MiB (REQUEST_MEDIA_BUDGET_BYTES),
|
|
27787
|
+
* and this route now speaks the same number. The previous 1.5 MB figure was
|
|
27788
|
+
* Kimi's TEXT-and-image body ceiling applied to the images alone, which threw
|
|
27789
|
+
* away pictures the service was perfectly willing to accept: a conversation
|
|
27790
|
+
* that carried three screenshots lost two of them to a limit that never
|
|
27791
|
+
* existed upstream.
|
|
27792
|
+
*
|
|
27793
|
+
* DSH_KIMI_CODE_MAX_IMAGE_BYTES overrides it under the same rules: ignored,
|
|
27794
|
+
* never applied as a disabled guard.
|
|
27426
27795
|
*/
|
|
27427
|
-
const DEFAULT_MAX_REQUEST_IMAGE_BYTES$1 =
|
|
27796
|
+
const DEFAULT_MAX_REQUEST_IMAGE_BYTES$1 = 20 * 1024 * 1024;
|
|
27797
|
+
/**
|
|
27798
|
+
* The mark the request is brought back under once it has passed the high one.
|
|
27799
|
+
*
|
|
27800
|
+
* Half the high mark, exactly as the official client does
|
|
27801
|
+
* (REQUEST_MEDIA_BUDGET_LOW_BYTES). The gap between the two is the slack that
|
|
27802
|
+
* keeps the number of retained images stable from turn to turn, and a stable
|
|
27803
|
+
* image count is what keeps the cached prefix valid.
|
|
27804
|
+
*/
|
|
27805
|
+
const DEFAULT_LOW_REQUEST_IMAGE_BYTES = 10 * 1024 * 1024;
|
|
27428
27806
|
/** Image budget this process enforces, honouring the deployment override. */
|
|
27429
27807
|
function maxRequestImageBytes$1(env = process.env) {
|
|
27430
27808
|
const raw = (env.DSH_KIMI_CODE_MAX_IMAGE_BYTES ?? "").trim();
|
|
@@ -27434,6 +27812,22 @@ function maxRequestImageBytes$1(env = process.env) {
|
|
|
27434
27812
|
return Math.floor(parsed);
|
|
27435
27813
|
}
|
|
27436
27814
|
/**
|
|
27815
|
+
* The low-water mark this process enforces, honouring the deployment override.
|
|
27816
|
+
*
|
|
27817
|
+
* Half of whatever the deployment pinned, so an override moves both marks
|
|
27818
|
+
* together: a deployment that lowers the ceiling to 4 MB gets a 2 MB low mark
|
|
27819
|
+
* rather than the built-in 10 MB, which would let every request sit above the
|
|
27820
|
+
* mark it is supposed to recover to. A deployment that pins nothing gets
|
|
27821
|
+
* Kimi's own 10 MiB.
|
|
27822
|
+
*/
|
|
27823
|
+
function lowRequestImageBytes(env = process.env) {
|
|
27824
|
+
const raw = (env.DSH_KIMI_CODE_MAX_IMAGE_BYTES ?? "").trim();
|
|
27825
|
+
if (raw === "") return DEFAULT_LOW_REQUEST_IMAGE_BYTES;
|
|
27826
|
+
const parsed = Number(raw);
|
|
27827
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return DEFAULT_LOW_REQUEST_IMAGE_BYTES;
|
|
27828
|
+
return Math.max(1, Math.floor(Math.floor(parsed) / 2));
|
|
27829
|
+
}
|
|
27830
|
+
/**
|
|
27437
27831
|
* Which wire dialect a model id is served over.
|
|
27438
27832
|
*
|
|
27439
27833
|
* Always the OpenAI-compatible chat completions dialect: this line uses that one
|
|
@@ -31302,7 +31696,8 @@ function buildAnthropicRequest(options, images = NO_RESOLVED_IMAGES$3, media = {
|
|
|
31302
31696
|
}
|
|
31303
31697
|
/** Build the body for whichever endpoint serves `wire`. */
|
|
31304
31698
|
function buildRequest(options, wire, images = NO_RESOLVED_IMAGES$3, preserveThinking = preserveThinkingEnabled(), media = {}) {
|
|
31305
|
-
|
|
31699
|
+
const cause = trackPrefixStability(options);
|
|
31700
|
+
recordDriftCause(cause === "stable" && (pendingImageOffloads.get(cacheScopeKey(options.sessionId)) ?? 0) > 0 ? "image-offload" : cause, options.sessionId);
|
|
31306
31701
|
return wire === "anthropic" ? buildAnthropicRequest(options, images, media) : buildOpenAIRequest(options, images, preserveThinking, media);
|
|
31307
31702
|
}
|
|
31308
31703
|
/**
|
|
@@ -31906,6 +32301,94 @@ function recordDriftCause(cause, sessionId) {
|
|
|
31906
32301
|
function getLastDriftCause(sessionId) {
|
|
31907
32302
|
return driftCauses.get(cacheScopeKey(sessionId));
|
|
31908
32303
|
}
|
|
32304
|
+
/**
|
|
32305
|
+
* Image identities this session has given back to the image budget.
|
|
32306
|
+
*
|
|
32307
|
+
* Scoped per session for the same reason the prefix snapshots are: DSH runs
|
|
32308
|
+
* several conversations — and subagents — at once, so one process-wide set
|
|
32309
|
+
* would let one conversation's omission replace another conversation's image.
|
|
32310
|
+
*/
|
|
32311
|
+
const sessionDroppedImageKeys = /* @__PURE__ */ new Map();
|
|
32312
|
+
/**
|
|
32313
|
+
* What the most recent request of one session carried, in image bytes.
|
|
32314
|
+
*/
|
|
32315
|
+
const sessionImageBudgetStats = /* @__PURE__ */ new Map();
|
|
32316
|
+
/**
|
|
32317
|
+
* Images the request CURRENTLY BEING BUILT has already dropped.
|
|
32318
|
+
*
|
|
32319
|
+
* Separate from {@link sessionDroppedImageKeys} because the two answer
|
|
32320
|
+
* different questions: the set is what the session remembers, and this counter
|
|
32321
|
+
* is what the request in flight did. The counter is what lets the drift
|
|
32322
|
+
* attribution name the offload, whichever order the build and the recording
|
|
32323
|
+
* happen in.
|
|
32324
|
+
*/
|
|
32325
|
+
const pendingImageOffloads = /* @__PURE__ */ new Map();
|
|
32326
|
+
/**
|
|
32327
|
+
* The image identities this session has already dropped.
|
|
32328
|
+
*
|
|
32329
|
+
* `durableDropped` seeds the memory the first time a session is asked about,
|
|
32330
|
+
* so a session that ran before the restart keeps the omissions it made: the
|
|
32331
|
+
* budget is sticky, and a picture that was given back must stay given back.
|
|
32332
|
+
*/
|
|
32333
|
+
function getSessionDroppedImageKeys(sessionId, durableDropped) {
|
|
32334
|
+
if (sessionId === void 0 || sessionId.trim() === "") return /* @__PURE__ */ new Set();
|
|
32335
|
+
const scope = cacheScopeKey(sessionId);
|
|
32336
|
+
const existing = sessionDroppedImageKeys.get(scope);
|
|
32337
|
+
if (existing !== void 0) return existing;
|
|
32338
|
+
const seeded = /* @__PURE__ */ new Set();
|
|
32339
|
+
for (const key of durableDropped ?? []) if (typeof key === "string" && key !== "") seeded.add(key);
|
|
32340
|
+
rememberScoped(sessionDroppedImageKeys, scope, seeded);
|
|
32341
|
+
return seeded;
|
|
32342
|
+
}
|
|
32343
|
+
/**
|
|
32344
|
+
* Note that the request being built has dropped images, before its body is built.
|
|
32345
|
+
*
|
|
32346
|
+
* The drift attribution reads this back when the prefix is fingerprinted, so
|
|
32347
|
+
* the order of the two does not matter: a request that drops an image is
|
|
32348
|
+
* attributed to the offload whether the body was built before or after the
|
|
32349
|
+
* drop was recorded.
|
|
32350
|
+
*/
|
|
32351
|
+
function markPendingImageOffload(sessionId, newlyDroppedCount) {
|
|
32352
|
+
const scope = cacheScopeKey(sessionId);
|
|
32353
|
+
if (newlyDroppedCount <= 0) {
|
|
32354
|
+
pendingImageOffloads.delete(scope);
|
|
32355
|
+
return;
|
|
32356
|
+
}
|
|
32357
|
+
rememberScoped(pendingImageOffloads, scope, newlyDroppedCount);
|
|
32358
|
+
}
|
|
32359
|
+
/**
|
|
32360
|
+
* Record what one request's image budget did.
|
|
32361
|
+
*
|
|
32362
|
+
* The dropped identities are kept for the whole session: the budget is sticky,
|
|
32363
|
+
* so an image given back stays given back, and the next turn has to know
|
|
32364
|
+
* which ones those are.
|
|
32365
|
+
*/
|
|
32366
|
+
function recordSessionImageBudget(sessionId, input) {
|
|
32367
|
+
const scope = cacheScopeKey(sessionId);
|
|
32368
|
+
const named = sessionId !== void 0 && sessionId.trim() !== "";
|
|
32369
|
+
const keys = named ? sessionDroppedImageKeys.get(scope) ?? /* @__PURE__ */ new Set() : /* @__PURE__ */ new Set();
|
|
32370
|
+
if (named) {
|
|
32371
|
+
for (const key of input.allDroppedKeys) if (typeof key === "string" && key !== "") keys.add(key);
|
|
32372
|
+
rememberScoped(sessionDroppedImageKeys, scope, keys);
|
|
32373
|
+
}
|
|
32374
|
+
rememberScoped(sessionImageBudgetStats, scope, {
|
|
32375
|
+
requestImageBytes: input.requestImageBytes,
|
|
32376
|
+
droppedImages: input.droppedImages,
|
|
32377
|
+
droppedKeys: named ? [...keys] : []
|
|
32378
|
+
});
|
|
32379
|
+
markPendingImageOffload(sessionId, input.newlyDroppedCount);
|
|
32380
|
+
if (input.newlyDroppedCount > 0 && getLastDriftCause(sessionId) === "stable") recordDriftCause("image-offload", sessionId);
|
|
32381
|
+
}
|
|
32382
|
+
/**
|
|
32383
|
+
* What one session's last request carried of images, when it is known.
|
|
32384
|
+
*
|
|
32385
|
+
* `undefined` for a session that has not sent a request, rather than a zeroed
|
|
32386
|
+
* answer: "no request yet" and "a request that carried no images" are
|
|
32387
|
+
* different facts.
|
|
32388
|
+
*/
|
|
32389
|
+
function getSessionImageBudgetStats(sessionId) {
|
|
32390
|
+
return sessionImageBudgetStats.get(cacheScopeKey(sessionId));
|
|
32391
|
+
}
|
|
31909
32392
|
/** Last lift per session. */
|
|
31910
32393
|
const thinkingCoercions = /* @__PURE__ */ new Map();
|
|
31911
32394
|
/**
|
|
@@ -32284,6 +32767,9 @@ function normalizeRecord(value) {
|
|
|
32284
32767
|
if (lastActiveAt === void 0 || updatedAt === void 0) return void 0;
|
|
32285
32768
|
const drift = typeof value.lastDrift === "string" && value.lastDrift !== "" ? value.lastDrift : void 0;
|
|
32286
32769
|
const warmupStreak = count$1(value.warmupConsecutiveHits);
|
|
32770
|
+
const budgetDropped = droppedImageKeys(value.budgetDropped);
|
|
32771
|
+
const lastRequestImageBytes = nonNegative(value.lastRequestImageBytes);
|
|
32772
|
+
const lastDroppedImages = nonNegative(value.lastDroppedImages);
|
|
32287
32773
|
return {
|
|
32288
32774
|
lastActiveAt,
|
|
32289
32775
|
updatedAt,
|
|
@@ -32296,10 +32782,46 @@ function normalizeRecord(value) {
|
|
|
32296
32782
|
cacheWrite5mTokens: count$1(value.cacheWrite5mTokens),
|
|
32297
32783
|
cacheWrite1hTokens: count$1(value.cacheWrite1hTokens),
|
|
32298
32784
|
...drift === void 0 ? {} : { lastDrift: drift },
|
|
32299
|
-
...warmupStreak > 0 ? { warmupConsecutiveHits: warmupStreak } : {}
|
|
32785
|
+
...warmupStreak > 0 ? { warmupConsecutiveHits: warmupStreak } : {},
|
|
32786
|
+
...budgetDropped.length > 0 ? { budgetDropped } : {},
|
|
32787
|
+
...lastRequestImageBytes === void 0 ? {} : { lastRequestImageBytes },
|
|
32788
|
+
...lastDroppedImages === void 0 ? {} : { lastDroppedImages }
|
|
32300
32789
|
};
|
|
32301
32790
|
}
|
|
32302
32791
|
/**
|
|
32792
|
+
* How many dropped image identities one record keeps.
|
|
32793
|
+
*
|
|
32794
|
+
* A session that pastes hundreds of pictures would otherwise grow this
|
|
32795
|
+
* document without bound, and it is a diagnostic file: the identities that
|
|
32796
|
+
* matter are the ones a later turn still has to replace.
|
|
32797
|
+
*/
|
|
32798
|
+
const MAX_BUDGET_DROPPED_KEYS = 512;
|
|
32799
|
+
/**
|
|
32800
|
+
* The dropped image identities a stored record carries.
|
|
32801
|
+
*
|
|
32802
|
+
* Only non-empty strings, deduplicated and capped: the file is written from
|
|
32803
|
+
* whatever the process happened to hold, and a malformed entry must not take
|
|
32804
|
+
* the record down with it.
|
|
32805
|
+
*/
|
|
32806
|
+
function droppedImageKeys(value) {
|
|
32807
|
+
if (!Array.isArray(value)) return [];
|
|
32808
|
+
const keys = [];
|
|
32809
|
+
const seen = /* @__PURE__ */ new Set();
|
|
32810
|
+
for (const entry of value) {
|
|
32811
|
+
if (typeof entry !== "string" || entry === "") continue;
|
|
32812
|
+
if (seen.has(entry)) continue;
|
|
32813
|
+
seen.add(entry);
|
|
32814
|
+
keys.push(entry);
|
|
32815
|
+
if (keys.length >= MAX_BUDGET_DROPPED_KEYS) break;
|
|
32816
|
+
}
|
|
32817
|
+
return keys;
|
|
32818
|
+
}
|
|
32819
|
+
/** A stored byte or image count, or undefined when the field is absent or absurd. */
|
|
32820
|
+
function nonNegative(value) {
|
|
32821
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) return void 0;
|
|
32822
|
+
return Math.floor(value);
|
|
32823
|
+
}
|
|
32824
|
+
/**
|
|
32303
32825
|
* Combine process memory with the durable record into the hint's two inputs.
|
|
32304
32826
|
*
|
|
32305
32827
|
* The process supplies the idle baseline whenever it has served the conversation,
|
|
@@ -33434,6 +33956,13 @@ function noteSessionCacheState(sessionId, state) {
|
|
|
33434
33956
|
const lastDrift = getLastDriftCause(sessionId);
|
|
33435
33957
|
reportDriftCause(sessionId, lastDrift);
|
|
33436
33958
|
const now = Date.now();
|
|
33959
|
+
const imageBudget = getSessionImageBudgetStats(sessionId);
|
|
33960
|
+
const priorRecord = sessionCacheState.read(sessionId);
|
|
33961
|
+
const budgetDropped = imageBudget !== void 0 && imageBudget.droppedKeys.length > 0 ? imageBudget.droppedKeys : priorRecord?.budgetDropped;
|
|
33962
|
+
const liveBytes = imageBudget?.requestImageBytes ?? 0;
|
|
33963
|
+
const liveDropped = imageBudget?.droppedImages ?? 0;
|
|
33964
|
+
const lastRequestImageBytes = liveBytes > 0 ? liveBytes : priorRecord?.lastRequestImageBytes;
|
|
33965
|
+
const lastDroppedImages = liveDropped > 0 ? liveDropped : priorRecord?.lastDroppedImages;
|
|
33437
33966
|
sessionCacheState.write(sessionId, {
|
|
33438
33967
|
lastActiveAt: now,
|
|
33439
33968
|
totalTokens: state.cacheReadTokens + state.inputTokens,
|
|
@@ -33445,6 +33974,9 @@ function noteSessionCacheState(sessionId, state) {
|
|
|
33445
33974
|
cacheWrite5mTokens: stats.cacheWrite5mTokens,
|
|
33446
33975
|
cacheWrite1hTokens: stats.cacheWrite1hTokens,
|
|
33447
33976
|
...lastDrift === void 0 ? {} : { lastDrift },
|
|
33977
|
+
...budgetDropped === void 0 ? {} : { budgetDropped },
|
|
33978
|
+
...lastRequestImageBytes === void 0 ? {} : { lastRequestImageBytes },
|
|
33979
|
+
...lastDroppedImages === void 0 ? {} : { lastDroppedImages },
|
|
33448
33980
|
updatedAt: now
|
|
33449
33981
|
});
|
|
33450
33982
|
const warmupState = recordCacheWarmupUsage({
|
|
@@ -33673,7 +34205,10 @@ var KimiCodeAdapter = class extends LlmAdapter {
|
|
|
33673
34205
|
const entry = catalog.find((model) => model.id === options.model);
|
|
33674
34206
|
const normalized = normalizeGenerateOptions(options);
|
|
33675
34207
|
const images = await resolveRequestImages$1(normalized, this.options.attachments, signal, KIMI_CODE_REQUEST_IMAGE_MAX_EDGE);
|
|
33676
|
-
const
|
|
34208
|
+
const priorDroppedKeys = getSessionDroppedImageKeys(options.sessionId, options.sessionId === void 0 ? void 0 : sessionCacheState.read(options.sessionId)?.budgetDropped);
|
|
34209
|
+
const stickyOffload = offloadRequestImagesWithStickyBudget(normalized, maxRequestImageBytes$1(), lowRequestImageBytes(), images, priorDroppedKeys);
|
|
34210
|
+
const imageTrimmed = stickyOffload.options;
|
|
34211
|
+
markPendingImageOffload(options.sessionId, stickyOffload.newlyDroppedKeys.length);
|
|
33677
34212
|
const gatedEffort = coerceThinkingEffort(options.model, mapReasoningEffort(options.reasoningEffort === void 0 || options.reasoningEffort === null ? void 0 : String(options.reasoningEffort)));
|
|
33678
34213
|
if (gatedEffort.coercedFrom !== null) recordThinkingCoercion({
|
|
33679
34214
|
model: options.model,
|
|
@@ -33893,6 +34428,19 @@ var KimiCodeAdapter = class extends LlmAdapter {
|
|
|
33893
34428
|
...after === void 0 ? {} : { providerRetryAfterMs: after }
|
|
33894
34429
|
});
|
|
33895
34430
|
}
|
|
34431
|
+
if (measured !== void 0) {
|
|
34432
|
+
const requestImageBytes = requestImagePayloadLengths(measured.options, images).reduce((sum, bytes) => sum + bytes, 0);
|
|
34433
|
+
const trimmedKeys = new Set(collectRemainingImageKeys(imageTrimmed, images));
|
|
34434
|
+
const measuredKeys = new Set(collectRemainingImageKeys(measured.options, images));
|
|
34435
|
+
const bodyFitDroppedKeys = [...trimmedKeys].filter((key) => !measuredKeys.has(key));
|
|
34436
|
+
const allDroppedKeys = /* @__PURE__ */ new Set([...stickyOffload.allDroppedKeys, ...bodyFitDroppedKeys]);
|
|
34437
|
+
recordSessionImageBudget(options.sessionId, {
|
|
34438
|
+
allDroppedKeys,
|
|
34439
|
+
newlyDroppedCount: stickyOffload.newlyDroppedKeys.length + bodyFitDroppedKeys.length,
|
|
34440
|
+
requestImageBytes,
|
|
34441
|
+
droppedImages: stickyOffload.droppedCount + bodyFitDroppedKeys.length
|
|
34442
|
+
});
|
|
34443
|
+
}
|
|
33896
34444
|
if (response.body === null) throw new LlmError("Kimi Code returned an empty response body", "PROVIDER_ERROR");
|
|
33897
34445
|
const reader = response.body.getReader();
|
|
33898
34446
|
const decoder = new TextDecoder();
|
|
@@ -34463,6 +35011,7 @@ function cacheStatsOrNull(sessionId) {
|
|
|
34463
35011
|
...stats,
|
|
34464
35012
|
...lastDrift === void 0 ? {} : { lastDrift },
|
|
34465
35013
|
...warmup === void 0 ? {} : { warmup },
|
|
35014
|
+
imageBudget: imageBudgetOf(sessionId),
|
|
34466
35015
|
cacheWriteTokens: stats.cacheWriteTokens,
|
|
34467
35016
|
cacheWrite5mTokens: stats.cacheWrite5mTokens,
|
|
34468
35017
|
cacheWrite1hTokens: stats.cacheWrite1hTokens
|
|
@@ -34515,7 +35064,28 @@ function durableCacheStats(sessionId) {
|
|
|
34515
35064
|
cacheWrite5mTokens: durable.cacheWrite5mTokens,
|
|
34516
35065
|
cacheWrite1hTokens: durable.cacheWrite1hTokens,
|
|
34517
35066
|
...lastDrift === void 0 ? {} : { lastDrift },
|
|
34518
|
-
...warmup === void 0 ? {} : { warmup }
|
|
35067
|
+
...warmup === void 0 ? {} : { warmup },
|
|
35068
|
+
imageBudget: imageBudgetOf(sessionId)
|
|
35069
|
+
};
|
|
35070
|
+
}
|
|
35071
|
+
/**
|
|
35072
|
+
* The image budget this route enforces and what the last turn of one session
|
|
35073
|
+
* spent of it.
|
|
35074
|
+
*
|
|
35075
|
+
* The live numbers win over the durable ones, because the durable record
|
|
35076
|
+
* describes the last run of this machine and the live numbers describe the
|
|
35077
|
+
* request that actually went out. The ceilings are read from the environment
|
|
35078
|
+
* every time, so a deployment that pinned a different budget sees its own
|
|
35079
|
+
* number rather than the built-in one.
|
|
35080
|
+
*/
|
|
35081
|
+
function imageBudgetOf(sessionId) {
|
|
35082
|
+
const live = getSessionImageBudgetStats(sessionId);
|
|
35083
|
+
const durable = sessionCacheState.read(sessionId);
|
|
35084
|
+
return {
|
|
35085
|
+
budgetBytes: maxRequestImageBytes$1(),
|
|
35086
|
+
lowBudgetBytes: lowRequestImageBytes(),
|
|
35087
|
+
requestImageBytes: live?.requestImageBytes ?? durable?.lastRequestImageBytes ?? 0,
|
|
35088
|
+
droppedImages: live?.droppedImages ?? durable?.lastDroppedImages ?? 0
|
|
34519
35089
|
};
|
|
34520
35090
|
}
|
|
34521
35091
|
/**
|
|
@@ -34532,7 +35102,8 @@ function driftCauseOf(value) {
|
|
|
34532
35102
|
case "system-prompt":
|
|
34533
35103
|
case "tools":
|
|
34534
35104
|
case "cache-key":
|
|
34535
|
-
case "cold-key":
|
|
35105
|
+
case "cold-key":
|
|
35106
|
+
case "image-offload": return value;
|
|
34536
35107
|
default: return;
|
|
34537
35108
|
}
|
|
34538
35109
|
}
|
|
@@ -80134,6 +80705,124 @@ function subagentModelSelectionPreference(settings) {
|
|
|
80134
80705
|
}
|
|
80135
80706
|
}
|
|
80136
80707
|
/**
|
|
80708
|
+
* Main-model route per root Session, locked the first time it is observed.
|
|
80709
|
+
*
|
|
80710
|
+
* Without this, a mid-session model switch would silently re-point a Session's
|
|
80711
|
+
* child allowlist: the guard would start answering with a different main model's
|
|
80712
|
+
* routes, so the routes advertised in the prompt change, the prefix cache is
|
|
80713
|
+
* invalidated, and a model can be handed a list its own earlier turn contradicted.
|
|
80714
|
+
* Locking the FIRST resolved route keeps one Session's delegation policy stable
|
|
80715
|
+
* for its whole life, which is the same promise the durable policy event makes.
|
|
80716
|
+
*
|
|
80717
|
+
* Keyed weakly by the root Session object, so a closed Session's entry goes with
|
|
80718
|
+
* it. A Session object that is not an object (a minimal fixture) is simply not
|
|
80719
|
+
* locked.
|
|
80720
|
+
*/
|
|
80721
|
+
const lockedRootMainRoutes = /* @__PURE__ */ new WeakMap();
|
|
80722
|
+
/**
|
|
80723
|
+
* Authorized child routes per root Session, locked the first time they are used.
|
|
80724
|
+
*
|
|
80725
|
+
* Locking the main ROUTE is not enough on its own: the route is only the key
|
|
80726
|
+
* into `byMainModel`, and the list it resolves to is read from the live
|
|
80727
|
+
* preference on every step. A Settings edit made at step 5 of a running session
|
|
80728
|
+
* would therefore swap the list under it — the prompt-assembly hook rewrites the
|
|
80729
|
+
* `subagent` tool description and the `tools:sdk` section from that list, so the
|
|
80730
|
+
* next request would carry a different prefix, drop the provider's prefix cache,
|
|
80731
|
+
* and contradict the Settings card's promise that a routing change applies to
|
|
80732
|
+
* new sessions only. The first resolved list is frozen here for the same reason
|
|
80733
|
+
* the durable policy event is a snapshot: one Session is governed by one policy.
|
|
80734
|
+
*/
|
|
80735
|
+
const lockedRootAllowedRoutes = /* @__PURE__ */ new WeakMap();
|
|
80736
|
+
/**
|
|
80737
|
+
* Narrow one candidate route pair into an exact route.
|
|
80738
|
+
* @param provider - Candidate provider id.
|
|
80739
|
+
* @param model - Candidate model id.
|
|
80740
|
+
* @returns the route, or undefined unless both halves are non-empty strings.
|
|
80741
|
+
*/
|
|
80742
|
+
function routeOf(provider, model) {
|
|
80743
|
+
const providerId = asString$3(provider);
|
|
80744
|
+
const modelId = asString$3(model);
|
|
80745
|
+
if (providerId === void 0 || modelId === void 0) return void 0;
|
|
80746
|
+
if (providerId.length === 0 || modelId.length === 0) return void 0;
|
|
80747
|
+
return {
|
|
80748
|
+
provider: providerId,
|
|
80749
|
+
model: modelId
|
|
80750
|
+
};
|
|
80751
|
+
}
|
|
80752
|
+
/**
|
|
80753
|
+
* Read the route a Session's next request uses, when its header names one.
|
|
80754
|
+
* @param session - Session whose request header is read.
|
|
80755
|
+
* @returns the exact route, or undefined when the header is absent or silent.
|
|
80756
|
+
*/
|
|
80757
|
+
function headerRouteOf(session) {
|
|
80758
|
+
let header;
|
|
80759
|
+
try {
|
|
80760
|
+
header = session?.requestHeader?.();
|
|
80761
|
+
} catch {
|
|
80762
|
+
return;
|
|
80763
|
+
}
|
|
80764
|
+
return routeOf(header?.config?.provider, header?.config?.model);
|
|
80765
|
+
}
|
|
80766
|
+
/**
|
|
80767
|
+
* Resolve the top-level Session of one agent's lineage, together with the route
|
|
80768
|
+
* its main model runs on.
|
|
80769
|
+
*
|
|
80770
|
+
* The main model is a property of the ROOT Session: a child subagent's own route
|
|
80771
|
+
* is the child's, and reading it would authorize children by what a child chose
|
|
80772
|
+
* rather than by what the user's main agent is. Walking up through
|
|
80773
|
+
* `header.parentSession` finds that root, and the root's route is read from its
|
|
80774
|
+
* own header (or, for an agent that IS the root, from the live model accessor and
|
|
80775
|
+
* then the delegation hint, which is what an unstarted Session has).
|
|
80776
|
+
*
|
|
80777
|
+
* Once resolved for a Session that has already sent a request, the route is
|
|
80778
|
+
* locked in {@link lockedRootMainRoutes}, so the answer never drifts mid-session.
|
|
80779
|
+
* @param agent - Calling agent.
|
|
80780
|
+
* @param sessions - Session registry used for ancestor lookup.
|
|
80781
|
+
* @param modelSelectionHint - Route fields the caller already resolved.
|
|
80782
|
+
* @param lockSnapshot - Lock even a route that has no request header yet; the
|
|
80783
|
+
* prompt-assembly, post-execute and denial paths pass true, so a Session's
|
|
80784
|
+
* first turn fixes its policy for the rest of its life.
|
|
80785
|
+
* @returns the root Session (when one exists) and the main route (when known).
|
|
80786
|
+
*/
|
|
80787
|
+
function resolveRootSessionAndMainRoute(agent, sessions, modelSelectionHint, lockSnapshot = false) {
|
|
80788
|
+
let rootSession = agent?.session;
|
|
80789
|
+
for (let depth = 0; depth < 8 && rootSession !== void 0; depth += 1) {
|
|
80790
|
+
const header = rootSession.header;
|
|
80791
|
+
if (header?.origin !== "subagent") break;
|
|
80792
|
+
const parentId = asString$3(header.parentSession);
|
|
80793
|
+
if (parentId === void 0) break;
|
|
80794
|
+
const parent = sessions.get(parentId);
|
|
80795
|
+
if (parent === void 0) break;
|
|
80796
|
+
rootSession = parent;
|
|
80797
|
+
}
|
|
80798
|
+
if (rootSession === void 0) return { mainRoute: routeOf(modelSelectionHint?.provider, modelSelectionHint?.model) };
|
|
80799
|
+
const lockable = typeof rootSession === "object" && rootSession !== null;
|
|
80800
|
+
const locked = lockable ? lockedRootMainRoutes.get(rootSession) : void 0;
|
|
80801
|
+
if (locked !== void 0) return {
|
|
80802
|
+
rootSession,
|
|
80803
|
+
mainRoute: locked
|
|
80804
|
+
};
|
|
80805
|
+
let mainRoute;
|
|
80806
|
+
if (agent?.session === rootSession || agent?.session === void 0) {
|
|
80807
|
+
let live;
|
|
80808
|
+
try {
|
|
80809
|
+
live = agent?.model?.current?.();
|
|
80810
|
+
} catch {
|
|
80811
|
+
live = void 0;
|
|
80812
|
+
}
|
|
80813
|
+
mainRoute = headerRouteOf(rootSession) ?? routeOf(live?.provider, live?.model) ?? routeOf(modelSelectionHint?.provider, modelSelectionHint?.model);
|
|
80814
|
+
} else {
|
|
80815
|
+
const rootOptions = rootSession.options;
|
|
80816
|
+
mainRoute = headerRouteOf(rootSession) ?? routeOf(rootOptions?.provider, rootOptions?.model);
|
|
80817
|
+
}
|
|
80818
|
+
if (mainRoute === void 0) return { rootSession };
|
|
80819
|
+
if (lockable && (lockSnapshot || headerRouteOf(rootSession) !== void 0)) lockedRootMainRoutes.set(rootSession, mainRoute);
|
|
80820
|
+
return {
|
|
80821
|
+
rootSession,
|
|
80822
|
+
mainRoute
|
|
80823
|
+
};
|
|
80824
|
+
}
|
|
80825
|
+
/**
|
|
80137
80826
|
* Read the route an inherit-mode child would run on: the calling agent's own
|
|
80138
80827
|
* effective request route. This mirrors the delegation seam's
|
|
80139
80828
|
* `parentAgentOptionsForDelegation`, where the latest request header owns
|
|
@@ -80347,9 +81036,36 @@ function delegationModeOf(policies, toolName) {
|
|
|
80347
81036
|
* @param scope - Whether an unrecorded Session falls back to the preference.
|
|
80348
81037
|
* @returns the authorized routes in force for this agent, or an empty list when unrestricted.
|
|
80349
81038
|
*/
|
|
80350
|
-
function effectiveAllowedRoutes(agent, preference, sessions, scope = "session") {
|
|
81039
|
+
function effectiveAllowedRoutes(agent, preference, sessions, scope = "session", lockSnapshot = false) {
|
|
80351
81040
|
if (preference !== void 0 && !preference.enabled) return [];
|
|
80352
|
-
|
|
81041
|
+
const baseRoutes = authorizedRoutesFor(agent, sessions) ?? (scope === "preference" ? preference?.allowedModels ?? [] : []);
|
|
81042
|
+
if (baseRoutes.length === 0) return [];
|
|
81043
|
+
return mainModelRoutesFor(agent, sessions, preference, baseRoutes, lockSnapshot);
|
|
81044
|
+
}
|
|
81045
|
+
/**
|
|
81046
|
+
* Apply the per-main-model override to a Session's authorized routes.
|
|
81047
|
+
*
|
|
81048
|
+
* Precedence is exact-match, then fallback, then the Session's own list: an
|
|
81049
|
+
* entry for the main model the Session actually runs is the most specific
|
|
81050
|
+
* statement available, the fallback list covers every other main model, and the
|
|
81051
|
+
* Session policy is what governs when the deployment configured neither.
|
|
81052
|
+
* @param agent - Calling agent.
|
|
81053
|
+
* @param sessions - Session registry used for ancestor lookup.
|
|
81054
|
+
* @param preference - Current Host preference, when the settings service exists.
|
|
81055
|
+
* @param baseRoutes - Routes the Session itself authorized; never empty here.
|
|
81056
|
+
* @param lockSnapshot - Freeze the resolved main route AND the resolved route
|
|
81057
|
+
* list for this Session now.
|
|
81058
|
+
* @returns the routes that actually govern this delegation.
|
|
81059
|
+
*/
|
|
81060
|
+
function mainModelRoutesFor(agent, sessions, preference, baseRoutes, lockSnapshot = false) {
|
|
81061
|
+
const { rootSession, mainRoute } = resolveRootSessionAndMainRoute(agent, sessions, void 0, lockSnapshot);
|
|
81062
|
+
const lockable = typeof rootSession === "object" && rootSession !== null;
|
|
81063
|
+
const locked = lockable ? lockedRootAllowedRoutes.get(rootSession) : void 0;
|
|
81064
|
+
if (locked !== void 0) return locked;
|
|
81065
|
+
const overrideRoutes = mainRoute === void 0 ? void 0 : preference?.byMainModel?.[`${mainRoute.provider}/${mainRoute.model}`];
|
|
81066
|
+
const resolved = overrideRoutes !== void 0 && overrideRoutes.length > 0 ? overrideRoutes : preference?.defaultAllowedModels !== void 0 && preference.defaultAllowedModels.length > 0 ? preference.defaultAllowedModels : baseRoutes;
|
|
81067
|
+
if (lockable && (lockSnapshot || headerRouteOf(rootSession) !== void 0)) lockedRootAllowedRoutes.set(rootSession, [...resolved]);
|
|
81068
|
+
return resolved;
|
|
80353
81069
|
}
|
|
80354
81070
|
/**
|
|
80355
81071
|
* State a Session's routing posture in the `list_subagent_models` result.
|
|
@@ -80379,8 +81095,96 @@ function enrichListSubagentModelsOutput(output, allowed) {
|
|
|
80379
81095
|
}
|
|
80380
81096
|
const header = "Authorized child routes for this Session (already filtered by the Session allowlist; only these routes are accepted, and `provider` and `model` must be passed as separate fields):";
|
|
80381
81097
|
if (output.includes(header)) return output;
|
|
81098
|
+
const filtered = filterListSubagentModelsOutput(output, allowed);
|
|
80382
81099
|
const suffix = `${header}\n${allowed.map((route) => `- ${route.provider}/${route.model} -> provider: "${route.provider}", model: "${route.model}"`).join("\n")}`;
|
|
80383
|
-
return
|
|
81100
|
+
return filtered.trim().length === 0 ? suffix : `${filtered}\n\n${suffix}`;
|
|
81101
|
+
}
|
|
81102
|
+
/** One rendered `provider — Name` line of the upstream provider list. */
|
|
81103
|
+
const PROVIDER_LINE = /^([^/\s]+)\s+—\s+.+$/;
|
|
81104
|
+
/** One rendered `provider/model — Name` line of the upstream model list. */
|
|
81105
|
+
const MODEL_LINE = /^([^/\s]+)\/([^\s]+)\s+—\s+.+$/;
|
|
81106
|
+
/**
|
|
81107
|
+
* Drop the upstream lines that name a route this Session does not authorize.
|
|
81108
|
+
*
|
|
81109
|
+
* The upstream discovery tool reads the union policy recorded on the Session, so
|
|
81110
|
+
* its raw answer can list a provider or a `provider/model` line that belongs to
|
|
81111
|
+
* another main model's entry and NOT to the routes this main model is governed
|
|
81112
|
+
* by. Leaving those lines in place is worse than a cosmetic mismatch: the
|
|
81113
|
+
* documented way to read this output is to copy a line into `subagent`, so every
|
|
81114
|
+
* stale line is an invitation to a delegation the guard will refuse.
|
|
81115
|
+
*
|
|
81116
|
+
* Only the two known renderings are filtered, and only lines that MATCH one of
|
|
81117
|
+
* them are candidates: any other line (a heading, a hint, an empty answer) is
|
|
81118
|
+
* passed through untouched, so a generation that changes its wording degrades to
|
|
81119
|
+
* the old behavior rather than to an empty answer.
|
|
81120
|
+
* @param output - Original text returned by `list_subagent_models`.
|
|
81121
|
+
* @param allowed - Routes the calling Session authorizes; never empty here.
|
|
81122
|
+
* @returns the output with unauthorized lines removed.
|
|
81123
|
+
*/
|
|
81124
|
+
function filterListSubagentModelsOutput(output, allowed) {
|
|
81125
|
+
if (allowed.length === 0 || output.length === 0) return output;
|
|
81126
|
+
if (output.includes("\nReasoning efforts:\n")) return output;
|
|
81127
|
+
const lines = output.split("\n");
|
|
81128
|
+
const modelList = lines.some((line) => MODEL_LINE.test(line));
|
|
81129
|
+
let matched = false;
|
|
81130
|
+
const kept = [];
|
|
81131
|
+
for (const line of lines) {
|
|
81132
|
+
if (modelList) {
|
|
81133
|
+
const match = MODEL_LINE.exec(line);
|
|
81134
|
+
if (match === null) {
|
|
81135
|
+
kept.push(line);
|
|
81136
|
+
continue;
|
|
81137
|
+
}
|
|
81138
|
+
matched = true;
|
|
81139
|
+
if (routesInclude(allowed, match[1], match[2])) kept.push(line);
|
|
81140
|
+
continue;
|
|
81141
|
+
}
|
|
81142
|
+
const match = PROVIDER_LINE.exec(line);
|
|
81143
|
+
if (match === null) {
|
|
81144
|
+
kept.push(line);
|
|
81145
|
+
continue;
|
|
81146
|
+
}
|
|
81147
|
+
matched = true;
|
|
81148
|
+
if (allowed.some((route) => route.provider === match[1])) kept.push(line);
|
|
81149
|
+
}
|
|
81150
|
+
if (!matched) return output;
|
|
81151
|
+
const keptText = kept.join("\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
81152
|
+
if (keptText.length > 0) return keptText;
|
|
81153
|
+
if (modelList) return `(no advertised models for ${MODEL_LINE.exec(output)?.[1] ?? ""})`;
|
|
81154
|
+
return "(no LLM providers)";
|
|
81155
|
+
}
|
|
81156
|
+
/**
|
|
81157
|
+
* Merge live per-main-model overrides into a resolved preference.
|
|
81158
|
+
*
|
|
81159
|
+
* The overrides are a separate seam from the settings namespace because they
|
|
81160
|
+
* live in THIS plugin's own preference document, not in the upstream
|
|
81161
|
+
* `subagent-model-selection` namespace. They also have to work on a harness
|
|
81162
|
+
* where that namespace cannot be registered at all (0.1.7 dropped
|
|
81163
|
+
* `settings.register`): there, an override is the only per-main-model statement
|
|
81164
|
+
* available, so it stands up an enabled preference by itself rather than being
|
|
81165
|
+
* dropped for lack of a base to attach to.
|
|
81166
|
+
* @param base - Preference resolved from the settings namespace, when any.
|
|
81167
|
+
* @param overrides - Live overrides read from this plugin's preferences.
|
|
81168
|
+
* @returns the merged preference, or undefined when neither source says anything.
|
|
81169
|
+
*/
|
|
81170
|
+
function mergePreferenceOverrides(base, overrides) {
|
|
81171
|
+
const hasDefault = overrides?.defaultAllowedModels !== void 0;
|
|
81172
|
+
const hasByMain = overrides?.byMainModel !== void 0;
|
|
81173
|
+
if (base === void 0) {
|
|
81174
|
+
if (!hasDefault && !hasByMain) return void 0;
|
|
81175
|
+
return {
|
|
81176
|
+
enabled: true,
|
|
81177
|
+
allowedModels: overrides?.defaultAllowedModels ?? [],
|
|
81178
|
+
...hasDefault ? { defaultAllowedModels: overrides?.defaultAllowedModels } : {},
|
|
81179
|
+
...hasByMain ? { byMainModel: overrides?.byMainModel } : {}
|
|
81180
|
+
};
|
|
81181
|
+
}
|
|
81182
|
+
if (!hasDefault && !hasByMain) return base;
|
|
81183
|
+
return {
|
|
81184
|
+
...base,
|
|
81185
|
+
...hasDefault ? { defaultAllowedModels: overrides?.defaultAllowedModels } : {},
|
|
81186
|
+
...hasByMain ? { byMainModel: overrides?.byMainModel } : {}
|
|
81187
|
+
};
|
|
80384
81188
|
}
|
|
80385
81189
|
/**
|
|
80386
81190
|
* The statement appended to `list_subagent_models` when no allowlist governs the
|
|
@@ -80491,7 +81295,7 @@ function rewriteAuthorizedPromptAssembly(assembly, allowed, toolNames = DEFAULT_
|
|
|
80491
81295
|
* @param inheritToolNames - Delegation tools that start a child on the caller's own route.
|
|
80492
81296
|
* @returns a denial reason, or undefined to leave the call untouched.
|
|
80493
81297
|
*/
|
|
80494
|
-
function delegationDenialReason(agent, toolName, args, preference, sessions, toolNames = DEFAULT_DELEGATION_TOOLS, scope = "session", inheritToolNames = [], declaresRouteParameters) {
|
|
81298
|
+
function delegationDenialReason(agent, toolName, args, preference, sessions, toolNames = DEFAULT_DELEGATION_TOOLS, scope = "session", inheritToolNames = [], declaresRouteParameters, lockSnapshot = true) {
|
|
80495
81299
|
const mode = delegationModeOf([...toolNames.map((name) => ({
|
|
80496
81300
|
name,
|
|
80497
81301
|
mode: "explicit"
|
|
@@ -80500,7 +81304,7 @@ function delegationDenialReason(agent, toolName, args, preference, sessions, too
|
|
|
80500
81304
|
mode: "inherit"
|
|
80501
81305
|
}))], toolName);
|
|
80502
81306
|
if (mode === void 0) return void 0;
|
|
80503
|
-
const allowed = effectiveAllowedRoutes(agent, preference, sessions, scope);
|
|
81307
|
+
const allowed = effectiveAllowedRoutes(agent, preference, sessions, scope, lockSnapshot);
|
|
80504
81308
|
if (allowed.length === 0) return void 0;
|
|
80505
81309
|
const request = asRecord$2(args) ?? {};
|
|
80506
81310
|
const requestedProvider = asString$3(request["provider"]);
|
|
@@ -80528,7 +81332,26 @@ function delegationDenialReason(agent, toolName, args, preference, sessions, too
|
|
|
80528
81332
|
* @returns a guard that denies delegations outside the Session allowlist.
|
|
80529
81333
|
*/
|
|
80530
81334
|
function createSubagentAuthorization(options) {
|
|
80531
|
-
return (agent, toolName, args) => delegationDenialReason(agent, toolName, args,
|
|
81335
|
+
return (agent, toolName, args) => delegationDenialReason(agent, toolName, args, mergedPreference(options), options.sessions, options.toolNames, options.scope ?? "session", options.inheritToolNames ?? [], options.declaresRouteParameters);
|
|
81336
|
+
}
|
|
81337
|
+
/**
|
|
81338
|
+
* Resolve the preference one guard evaluation uses.
|
|
81339
|
+
*
|
|
81340
|
+
* The namespace preference and this plugin's per-main-model overrides are
|
|
81341
|
+
* separate seams — the override document outlives a harness that cannot register
|
|
81342
|
+
* the namespace at all — so they are merged here, once, rather than at each of
|
|
81343
|
+
* the four call sites that need the result.
|
|
81344
|
+
* @param options - Runtime inputs the guard closed over.
|
|
81345
|
+
* @returns the merged preference, or undefined when neither seam says anything.
|
|
81346
|
+
*/
|
|
81347
|
+
function mergedPreference(options) {
|
|
81348
|
+
let overrides;
|
|
81349
|
+
try {
|
|
81350
|
+
overrides = options.routingOverrides?.();
|
|
81351
|
+
} catch {
|
|
81352
|
+
overrides = void 0;
|
|
81353
|
+
}
|
|
81354
|
+
return mergePreferenceOverrides(subagentModelSelectionPreference(options.settings), overrides);
|
|
80532
81355
|
}
|
|
80533
81356
|
/** Delegation names this guard recognizes when configuration omits them. */
|
|
80534
81357
|
function normalizeDelegationToolNames(value) {
|
|
@@ -80620,14 +81443,19 @@ function installSubagentModelAuthorization(ctx, sessions, config = {}) {
|
|
|
80620
81443
|
toolNames,
|
|
80621
81444
|
inheritToolNames,
|
|
80622
81445
|
scope,
|
|
80623
|
-
declaresRouteParameters: routeParameterProbe(ctx)
|
|
81446
|
+
declaresRouteParameters: routeParameterProbe(ctx),
|
|
81447
|
+
routingOverrides: config.routingOverrides
|
|
81448
|
+
});
|
|
81449
|
+
const currentPreference = () => mergedPreference({
|
|
81450
|
+
settings,
|
|
81451
|
+
routingOverrides: config.routingOverrides
|
|
80624
81452
|
});
|
|
80625
81453
|
const disposeGuard = ctx.tools.guard((exec) => authorize(exec.agent, exec.name, exec.arguments));
|
|
80626
81454
|
const eventHost = ctx;
|
|
80627
81455
|
const disposePostExecute = eventHost.on?.("tools/post-execute", async (exec, result, next) => {
|
|
80628
81456
|
const decision = await next();
|
|
80629
81457
|
if (exec.name !== "list_subagent_models" || result.isError === true || decision?.kind !== "accept") return decision;
|
|
80630
|
-
const allowed = effectiveAllowedRoutes(exec.agent,
|
|
81458
|
+
const allowed = effectiveAllowedRoutes(exec.agent, currentPreference(), sessions, scope, true);
|
|
80631
81459
|
const currentValue = decision.value !== void 0 ? decision.value : result.value;
|
|
80632
81460
|
if (typeof currentValue === "string") {
|
|
80633
81461
|
const enriched = enrichListSubagentModelsOutput(currentValue, allowed);
|
|
@@ -80655,7 +81483,7 @@ function installSubagentModelAuthorization(ctx, sessions, config = {}) {
|
|
|
80655
81483
|
return decision;
|
|
80656
81484
|
});
|
|
80657
81485
|
const disposeAssemble = eventHost.on?.("system-prompt/assemble", async (_assembly, context, next) => {
|
|
80658
|
-
return rewriteAuthorizedPromptAssembly(await next(), effectiveAllowedRoutes(context?.agent ?? context?.scope,
|
|
81486
|
+
return rewriteAuthorizedPromptAssembly(await next(), effectiveAllowedRoutes(context?.agent ?? context?.scope, currentPreference(), sessions, scope, true), toolNames);
|
|
80659
81487
|
});
|
|
80660
81488
|
return () => {
|
|
80661
81489
|
disposeGuard();
|
|
@@ -93971,7 +94799,16 @@ function apply(ctx, pluginConfig = {}) {
|
|
|
93971
94799
|
return installSubagentModelAuthorization(scoped, sessions, {
|
|
93972
94800
|
toolNames: delegationToolNames,
|
|
93973
94801
|
inheritToolNames: delegationInheritNames,
|
|
93974
|
-
scope: pluginConfig.subagentModelScope ?? "session"
|
|
94802
|
+
scope: pluginConfig.subagentModelScope ?? "session",
|
|
94803
|
+
routingOverrides: () => {
|
|
94804
|
+
const current = preferences.status();
|
|
94805
|
+
const defaultAllowedModels = current.subagentDefaultModels ?? [];
|
|
94806
|
+
const byMainModel = current.subagentModelsByMainModel ?? {};
|
|
94807
|
+
return {
|
|
94808
|
+
...defaultAllowedModels.length > 0 ? { defaultAllowedModels } : {},
|
|
94809
|
+
...Object.keys(byMainModel).length > 0 ? { byMainModel } : {}
|
|
94810
|
+
};
|
|
94811
|
+
}
|
|
93975
94812
|
});
|
|
93976
94813
|
}, "dsh-chatgpt-subscription: subagent model authorization");
|
|
93977
94814
|
});
|
|
@@ -94319,7 +95156,12 @@ function apply(ctx, pluginConfig = {}) {
|
|
|
94319
95156
|
const sessions = ctx.get("sessions");
|
|
94320
95157
|
const target = sessions.get(sessionId);
|
|
94321
95158
|
if (target === void 0) throw new Error(`Unknown session "${sessionId}".`);
|
|
94322
|
-
const
|
|
95159
|
+
const audited = auditedRoutesOf(target);
|
|
95160
|
+
const current = preferences.status();
|
|
95161
|
+
const mainRoute = resolveRootSessionAndMainRoute({ session: target }, sessions).mainRoute;
|
|
95162
|
+
const override = mainRoute === void 0 ? void 0 : (current.subagentModelsByMainModel ?? {})[`${mainRoute.provider}/${mainRoute.model}`];
|
|
95163
|
+
const fallback = current.subagentDefaultModels ?? [];
|
|
95164
|
+
const allowed = audited === void 0 ? audited : override !== void 0 && override.length > 0 ? override : fallback.length > 0 ? fallback : audited;
|
|
94323
95165
|
return Promise.resolve({
|
|
94324
95166
|
sessionId,
|
|
94325
95167
|
allowedModels: (allowed ?? []).map((route) => ({
|