@eddyskywalker/dsh-chatgpt-subscription 0.48.4 → 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 +10 -0
- package/lib/client.js +2381 -179
- package/lib/client.js.map +1 -1
- package/lib/index.js +829 -29
- 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/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) {
|
|
@@ -10812,7 +10812,9 @@ const DEFAULT_PREFERENCES = {
|
|
|
10812
10812
|
contextWindowOverrides: {},
|
|
10813
10813
|
proxyMode: "auto",
|
|
10814
10814
|
customProxyUrl: null,
|
|
10815
|
-
compactionRoute: null
|
|
10815
|
+
compactionRoute: null,
|
|
10816
|
+
subagentDefaultModels: [],
|
|
10817
|
+
subagentModelsByMainModel: {}
|
|
10816
10818
|
};
|
|
10817
10819
|
const SEARCH_PROVIDER_CODEX = "codex";
|
|
10818
10820
|
function isSearchProviderPreference(value) {
|
|
@@ -10827,6 +10829,105 @@ function isCodexReasoningSummary(value) {
|
|
|
10827
10829
|
function isProxyMode(value) {
|
|
10828
10830
|
return value === "auto" || value === "custom" || value === "direct";
|
|
10829
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
|
+
}
|
|
10830
10931
|
//#endregion
|
|
10831
10932
|
//#region src/host/common/legacy-preferences.ts
|
|
10832
10933
|
/**
|
|
@@ -10993,6 +11094,10 @@ function sectionOf(document, id) {
|
|
|
10993
11094
|
*/
|
|
10994
11095
|
function registerPreferenceStore(settings) {
|
|
10995
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
|
+
}));
|
|
10996
11101
|
const schema = z.object({
|
|
10997
11102
|
enabled: z.boolean().default(DEFAULT_PREFERENCES.enabled ?? true),
|
|
10998
11103
|
quickQuotaVisible: z.boolean().default(DEFAULT_PREFERENCES.quickQuotaVisible),
|
|
@@ -11022,7 +11127,9 @@ function registerPreferenceStore(settings) {
|
|
|
11022
11127
|
compactionRoute: z.union([z.object({
|
|
11023
11128
|
provider: z.string(),
|
|
11024
11129
|
model: z.string()
|
|
11025
|
-
}), 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({})
|
|
11026
11133
|
});
|
|
11027
11134
|
if (!hasRegister(settings)) {
|
|
11028
11135
|
const fileStore = new FilePreferencesStore(schema, preferencesPath(), readLegacyPreferences);
|
|
@@ -11080,6 +11187,16 @@ var SettingsPreferenceStore = class {
|
|
|
11080
11187
|
model: patch.compactionRoute.model.trim()
|
|
11081
11188
|
};
|
|
11082
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
|
+
}
|
|
11083
11200
|
if (patch.customProxyUrl !== void 0) if (patch.customProxyUrl !== null) {
|
|
11084
11201
|
const trimmed = patch.customProxyUrl.trim();
|
|
11085
11202
|
if (trimmed.length > 0 && !/^https?:\/\//i.test(trimmed) && !/^socks5?:\/\//i.test(trimmed)) normalized.customProxyUrl = `http://${trimmed}`;
|
|
@@ -14006,6 +14123,15 @@ function readPreferencesUpdate(value, current) {
|
|
|
14006
14123
|
model: value.compactionRoute.model.trim()
|
|
14007
14124
|
};
|
|
14008
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
|
+
}
|
|
14009
14135
|
if ("contextWindowOverrides" in value) {
|
|
14010
14136
|
if (!isRecord$55(value.contextWindowOverrides)) throw new PreferenceError("contextWindowOverrides must be an object.");
|
|
14011
14137
|
const overrides = {};
|
|
@@ -26611,6 +26737,17 @@ async function projectToolResult(input) {
|
|
|
26611
26737
|
}
|
|
26612
26738
|
//#endregion
|
|
26613
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
|
+
*/
|
|
26614
26751
|
function isRecord$50(value) {
|
|
26615
26752
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
26616
26753
|
}
|
|
@@ -26708,7 +26845,7 @@ function base64Length$1(bytes) {
|
|
|
26708
26845
|
* the only estimate available, so it stays the fallback and every call site that
|
|
26709
26846
|
* has no resolved map behaves exactly as before.
|
|
26710
26847
|
*/
|
|
26711
|
-
function
|
|
26848
|
+
function inlinePayloadOf(block) {
|
|
26712
26849
|
let data = asString$21(block.data) || asString$21(block.base64);
|
|
26713
26850
|
const source = isRecord$50(block.source) ? block.source : void 0;
|
|
26714
26851
|
if (!data && source) data = asString$21(source.data) || asString$21(source.base64);
|
|
@@ -26716,7 +26853,11 @@ function requestImagePayloadLength(block, images) {
|
|
|
26716
26853
|
const matched = data.match(/^data:([^;,]+);base64,(.*)$/s);
|
|
26717
26854
|
if (matched) data = matched[2] ?? "";
|
|
26718
26855
|
}
|
|
26719
|
-
|
|
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;
|
|
26720
26861
|
const attachment = attachmentOf$3(block);
|
|
26721
26862
|
if (!attachment) return void 0;
|
|
26722
26863
|
if (images === void 0) return base64Length$1(attachment.bytes);
|
|
@@ -26901,6 +27042,186 @@ function offloadOldestRequestImages$1(options, maxBytes = MAX_REQUEST_IMAGE_BYTE
|
|
|
26901
27042
|
return replaceOldestRequestImages(options, imagesToOffloadCount(requestImagePayloadLengths(options, images), maxBytes), images);
|
|
26902
27043
|
}
|
|
26903
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
|
+
/**
|
|
26904
27225
|
* Whether a request still carries at least one image the mapper can send.
|
|
26905
27226
|
*
|
|
26906
27227
|
* A request whose images have all been replaced by placeholders can no longer
|
|
@@ -27462,11 +27783,26 @@ function effectiveContextWindow(tokenWindow, env = process.env) {
|
|
|
27462
27783
|
/**
|
|
27463
27784
|
* Base64 image payload one request on this route may carry.
|
|
27464
27785
|
*
|
|
27465
|
-
*
|
|
27466
|
-
*
|
|
27467
|
-
*
|
|
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.
|
|
27795
|
+
*/
|
|
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.
|
|
27468
27804
|
*/
|
|
27469
|
-
const
|
|
27805
|
+
const DEFAULT_LOW_REQUEST_IMAGE_BYTES = 10 * 1024 * 1024;
|
|
27470
27806
|
/** Image budget this process enforces, honouring the deployment override. */
|
|
27471
27807
|
function maxRequestImageBytes$1(env = process.env) {
|
|
27472
27808
|
const raw = (env.DSH_KIMI_CODE_MAX_IMAGE_BYTES ?? "").trim();
|
|
@@ -27476,6 +27812,22 @@ function maxRequestImageBytes$1(env = process.env) {
|
|
|
27476
27812
|
return Math.floor(parsed);
|
|
27477
27813
|
}
|
|
27478
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
|
+
/**
|
|
27479
27831
|
* Which wire dialect a model id is served over.
|
|
27480
27832
|
*
|
|
27481
27833
|
* Always the OpenAI-compatible chat completions dialect: this line uses that one
|
|
@@ -31344,7 +31696,8 @@ function buildAnthropicRequest(options, images = NO_RESOLVED_IMAGES$3, media = {
|
|
|
31344
31696
|
}
|
|
31345
31697
|
/** Build the body for whichever endpoint serves `wire`. */
|
|
31346
31698
|
function buildRequest(options, wire, images = NO_RESOLVED_IMAGES$3, preserveThinking = preserveThinkingEnabled(), media = {}) {
|
|
31347
|
-
|
|
31699
|
+
const cause = trackPrefixStability(options);
|
|
31700
|
+
recordDriftCause(cause === "stable" && (pendingImageOffloads.get(cacheScopeKey(options.sessionId)) ?? 0) > 0 ? "image-offload" : cause, options.sessionId);
|
|
31348
31701
|
return wire === "anthropic" ? buildAnthropicRequest(options, images, media) : buildOpenAIRequest(options, images, preserveThinking, media);
|
|
31349
31702
|
}
|
|
31350
31703
|
/**
|
|
@@ -31948,6 +32301,94 @@ function recordDriftCause(cause, sessionId) {
|
|
|
31948
32301
|
function getLastDriftCause(sessionId) {
|
|
31949
32302
|
return driftCauses.get(cacheScopeKey(sessionId));
|
|
31950
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
|
+
}
|
|
31951
32392
|
/** Last lift per session. */
|
|
31952
32393
|
const thinkingCoercions = /* @__PURE__ */ new Map();
|
|
31953
32394
|
/**
|
|
@@ -32326,6 +32767,9 @@ function normalizeRecord(value) {
|
|
|
32326
32767
|
if (lastActiveAt === void 0 || updatedAt === void 0) return void 0;
|
|
32327
32768
|
const drift = typeof value.lastDrift === "string" && value.lastDrift !== "" ? value.lastDrift : void 0;
|
|
32328
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);
|
|
32329
32773
|
return {
|
|
32330
32774
|
lastActiveAt,
|
|
32331
32775
|
updatedAt,
|
|
@@ -32338,10 +32782,46 @@ function normalizeRecord(value) {
|
|
|
32338
32782
|
cacheWrite5mTokens: count$1(value.cacheWrite5mTokens),
|
|
32339
32783
|
cacheWrite1hTokens: count$1(value.cacheWrite1hTokens),
|
|
32340
32784
|
...drift === void 0 ? {} : { lastDrift: drift },
|
|
32341
|
-
...warmupStreak > 0 ? { warmupConsecutiveHits: warmupStreak } : {}
|
|
32785
|
+
...warmupStreak > 0 ? { warmupConsecutiveHits: warmupStreak } : {},
|
|
32786
|
+
...budgetDropped.length > 0 ? { budgetDropped } : {},
|
|
32787
|
+
...lastRequestImageBytes === void 0 ? {} : { lastRequestImageBytes },
|
|
32788
|
+
...lastDroppedImages === void 0 ? {} : { lastDroppedImages }
|
|
32342
32789
|
};
|
|
32343
32790
|
}
|
|
32344
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
|
+
/**
|
|
32345
32825
|
* Combine process memory with the durable record into the hint's two inputs.
|
|
32346
32826
|
*
|
|
32347
32827
|
* The process supplies the idle baseline whenever it has served the conversation,
|
|
@@ -33476,6 +33956,13 @@ function noteSessionCacheState(sessionId, state) {
|
|
|
33476
33956
|
const lastDrift = getLastDriftCause(sessionId);
|
|
33477
33957
|
reportDriftCause(sessionId, lastDrift);
|
|
33478
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;
|
|
33479
33966
|
sessionCacheState.write(sessionId, {
|
|
33480
33967
|
lastActiveAt: now,
|
|
33481
33968
|
totalTokens: state.cacheReadTokens + state.inputTokens,
|
|
@@ -33487,6 +33974,9 @@ function noteSessionCacheState(sessionId, state) {
|
|
|
33487
33974
|
cacheWrite5mTokens: stats.cacheWrite5mTokens,
|
|
33488
33975
|
cacheWrite1hTokens: stats.cacheWrite1hTokens,
|
|
33489
33976
|
...lastDrift === void 0 ? {} : { lastDrift },
|
|
33977
|
+
...budgetDropped === void 0 ? {} : { budgetDropped },
|
|
33978
|
+
...lastRequestImageBytes === void 0 ? {} : { lastRequestImageBytes },
|
|
33979
|
+
...lastDroppedImages === void 0 ? {} : { lastDroppedImages },
|
|
33490
33980
|
updatedAt: now
|
|
33491
33981
|
});
|
|
33492
33982
|
const warmupState = recordCacheWarmupUsage({
|
|
@@ -33715,7 +34205,10 @@ var KimiCodeAdapter = class extends LlmAdapter {
|
|
|
33715
34205
|
const entry = catalog.find((model) => model.id === options.model);
|
|
33716
34206
|
const normalized = normalizeGenerateOptions(options);
|
|
33717
34207
|
const images = await resolveRequestImages$1(normalized, this.options.attachments, signal, KIMI_CODE_REQUEST_IMAGE_MAX_EDGE);
|
|
33718
|
-
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);
|
|
33719
34212
|
const gatedEffort = coerceThinkingEffort(options.model, mapReasoningEffort(options.reasoningEffort === void 0 || options.reasoningEffort === null ? void 0 : String(options.reasoningEffort)));
|
|
33720
34213
|
if (gatedEffort.coercedFrom !== null) recordThinkingCoercion({
|
|
33721
34214
|
model: options.model,
|
|
@@ -33935,6 +34428,19 @@ var KimiCodeAdapter = class extends LlmAdapter {
|
|
|
33935
34428
|
...after === void 0 ? {} : { providerRetryAfterMs: after }
|
|
33936
34429
|
});
|
|
33937
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
|
+
}
|
|
33938
34444
|
if (response.body === null) throw new LlmError("Kimi Code returned an empty response body", "PROVIDER_ERROR");
|
|
33939
34445
|
const reader = response.body.getReader();
|
|
33940
34446
|
const decoder = new TextDecoder();
|
|
@@ -34505,6 +35011,7 @@ function cacheStatsOrNull(sessionId) {
|
|
|
34505
35011
|
...stats,
|
|
34506
35012
|
...lastDrift === void 0 ? {} : { lastDrift },
|
|
34507
35013
|
...warmup === void 0 ? {} : { warmup },
|
|
35014
|
+
imageBudget: imageBudgetOf(sessionId),
|
|
34508
35015
|
cacheWriteTokens: stats.cacheWriteTokens,
|
|
34509
35016
|
cacheWrite5mTokens: stats.cacheWrite5mTokens,
|
|
34510
35017
|
cacheWrite1hTokens: stats.cacheWrite1hTokens
|
|
@@ -34557,7 +35064,28 @@ function durableCacheStats(sessionId) {
|
|
|
34557
35064
|
cacheWrite5mTokens: durable.cacheWrite5mTokens,
|
|
34558
35065
|
cacheWrite1hTokens: durable.cacheWrite1hTokens,
|
|
34559
35066
|
...lastDrift === void 0 ? {} : { lastDrift },
|
|
34560
|
-
...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
|
|
34561
35089
|
};
|
|
34562
35090
|
}
|
|
34563
35091
|
/**
|
|
@@ -34574,7 +35102,8 @@ function driftCauseOf(value) {
|
|
|
34574
35102
|
case "system-prompt":
|
|
34575
35103
|
case "tools":
|
|
34576
35104
|
case "cache-key":
|
|
34577
|
-
case "cold-key":
|
|
35105
|
+
case "cold-key":
|
|
35106
|
+
case "image-offload": return value;
|
|
34578
35107
|
default: return;
|
|
34579
35108
|
}
|
|
34580
35109
|
}
|
|
@@ -80176,6 +80705,124 @@ function subagentModelSelectionPreference(settings) {
|
|
|
80176
80705
|
}
|
|
80177
80706
|
}
|
|
80178
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
|
+
/**
|
|
80179
80826
|
* Read the route an inherit-mode child would run on: the calling agent's own
|
|
80180
80827
|
* effective request route. This mirrors the delegation seam's
|
|
80181
80828
|
* `parentAgentOptionsForDelegation`, where the latest request header owns
|
|
@@ -80389,9 +81036,36 @@ function delegationModeOf(policies, toolName) {
|
|
|
80389
81036
|
* @param scope - Whether an unrecorded Session falls back to the preference.
|
|
80390
81037
|
* @returns the authorized routes in force for this agent, or an empty list when unrestricted.
|
|
80391
81038
|
*/
|
|
80392
|
-
function effectiveAllowedRoutes(agent, preference, sessions, scope = "session") {
|
|
81039
|
+
function effectiveAllowedRoutes(agent, preference, sessions, scope = "session", lockSnapshot = false) {
|
|
80393
81040
|
if (preference !== void 0 && !preference.enabled) return [];
|
|
80394
|
-
|
|
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;
|
|
80395
81069
|
}
|
|
80396
81070
|
/**
|
|
80397
81071
|
* State a Session's routing posture in the `list_subagent_models` result.
|
|
@@ -80421,8 +81095,96 @@ function enrichListSubagentModelsOutput(output, allowed) {
|
|
|
80421
81095
|
}
|
|
80422
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):";
|
|
80423
81097
|
if (output.includes(header)) return output;
|
|
81098
|
+
const filtered = filterListSubagentModelsOutput(output, allowed);
|
|
80424
81099
|
const suffix = `${header}\n${allowed.map((route) => `- ${route.provider}/${route.model} -> provider: "${route.provider}", model: "${route.model}"`).join("\n")}`;
|
|
80425
|
-
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
|
+
};
|
|
80426
81188
|
}
|
|
80427
81189
|
/**
|
|
80428
81190
|
* The statement appended to `list_subagent_models` when no allowlist governs the
|
|
@@ -80533,7 +81295,7 @@ function rewriteAuthorizedPromptAssembly(assembly, allowed, toolNames = DEFAULT_
|
|
|
80533
81295
|
* @param inheritToolNames - Delegation tools that start a child on the caller's own route.
|
|
80534
81296
|
* @returns a denial reason, or undefined to leave the call untouched.
|
|
80535
81297
|
*/
|
|
80536
|
-
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) {
|
|
80537
81299
|
const mode = delegationModeOf([...toolNames.map((name) => ({
|
|
80538
81300
|
name,
|
|
80539
81301
|
mode: "explicit"
|
|
@@ -80542,7 +81304,7 @@ function delegationDenialReason(agent, toolName, args, preference, sessions, too
|
|
|
80542
81304
|
mode: "inherit"
|
|
80543
81305
|
}))], toolName);
|
|
80544
81306
|
if (mode === void 0) return void 0;
|
|
80545
|
-
const allowed = effectiveAllowedRoutes(agent, preference, sessions, scope);
|
|
81307
|
+
const allowed = effectiveAllowedRoutes(agent, preference, sessions, scope, lockSnapshot);
|
|
80546
81308
|
if (allowed.length === 0) return void 0;
|
|
80547
81309
|
const request = asRecord$2(args) ?? {};
|
|
80548
81310
|
const requestedProvider = asString$3(request["provider"]);
|
|
@@ -80570,7 +81332,26 @@ function delegationDenialReason(agent, toolName, args, preference, sessions, too
|
|
|
80570
81332
|
* @returns a guard that denies delegations outside the Session allowlist.
|
|
80571
81333
|
*/
|
|
80572
81334
|
function createSubagentAuthorization(options) {
|
|
80573
|
-
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);
|
|
80574
81355
|
}
|
|
80575
81356
|
/** Delegation names this guard recognizes when configuration omits them. */
|
|
80576
81357
|
function normalizeDelegationToolNames(value) {
|
|
@@ -80662,14 +81443,19 @@ function installSubagentModelAuthorization(ctx, sessions, config = {}) {
|
|
|
80662
81443
|
toolNames,
|
|
80663
81444
|
inheritToolNames,
|
|
80664
81445
|
scope,
|
|
80665
|
-
declaresRouteParameters: routeParameterProbe(ctx)
|
|
81446
|
+
declaresRouteParameters: routeParameterProbe(ctx),
|
|
81447
|
+
routingOverrides: config.routingOverrides
|
|
81448
|
+
});
|
|
81449
|
+
const currentPreference = () => mergedPreference({
|
|
81450
|
+
settings,
|
|
81451
|
+
routingOverrides: config.routingOverrides
|
|
80666
81452
|
});
|
|
80667
81453
|
const disposeGuard = ctx.tools.guard((exec) => authorize(exec.agent, exec.name, exec.arguments));
|
|
80668
81454
|
const eventHost = ctx;
|
|
80669
81455
|
const disposePostExecute = eventHost.on?.("tools/post-execute", async (exec, result, next) => {
|
|
80670
81456
|
const decision = await next();
|
|
80671
81457
|
if (exec.name !== "list_subagent_models" || result.isError === true || decision?.kind !== "accept") return decision;
|
|
80672
|
-
const allowed = effectiveAllowedRoutes(exec.agent,
|
|
81458
|
+
const allowed = effectiveAllowedRoutes(exec.agent, currentPreference(), sessions, scope, true);
|
|
80673
81459
|
const currentValue = decision.value !== void 0 ? decision.value : result.value;
|
|
80674
81460
|
if (typeof currentValue === "string") {
|
|
80675
81461
|
const enriched = enrichListSubagentModelsOutput(currentValue, allowed);
|
|
@@ -80697,7 +81483,7 @@ function installSubagentModelAuthorization(ctx, sessions, config = {}) {
|
|
|
80697
81483
|
return decision;
|
|
80698
81484
|
});
|
|
80699
81485
|
const disposeAssemble = eventHost.on?.("system-prompt/assemble", async (_assembly, context, next) => {
|
|
80700
|
-
return rewriteAuthorizedPromptAssembly(await next(), effectiveAllowedRoutes(context?.agent ?? context?.scope,
|
|
81486
|
+
return rewriteAuthorizedPromptAssembly(await next(), effectiveAllowedRoutes(context?.agent ?? context?.scope, currentPreference(), sessions, scope, true), toolNames);
|
|
80701
81487
|
});
|
|
80702
81488
|
return () => {
|
|
80703
81489
|
disposeGuard();
|
|
@@ -94013,7 +94799,16 @@ function apply(ctx, pluginConfig = {}) {
|
|
|
94013
94799
|
return installSubagentModelAuthorization(scoped, sessions, {
|
|
94014
94800
|
toolNames: delegationToolNames,
|
|
94015
94801
|
inheritToolNames: delegationInheritNames,
|
|
94016
|
-
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
|
+
}
|
|
94017
94812
|
});
|
|
94018
94813
|
}, "dsh-chatgpt-subscription: subagent model authorization");
|
|
94019
94814
|
});
|
|
@@ -94361,7 +95156,12 @@ function apply(ctx, pluginConfig = {}) {
|
|
|
94361
95156
|
const sessions = ctx.get("sessions");
|
|
94362
95157
|
const target = sessions.get(sessionId);
|
|
94363
95158
|
if (target === void 0) throw new Error(`Unknown session "${sessionId}".`);
|
|
94364
|
-
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;
|
|
94365
95165
|
return Promise.resolve({
|
|
94366
95166
|
sessionId,
|
|
94367
95167
|
allowedModels: (allowed ?? []).map((route) => ({
|