@lexwdex-org/opencode-dcp 3.3.3 → 3.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -862,6 +862,37 @@ var ParseErrorCode;
862
862
  ParseErrorCode2[ParseErrorCode2["InvalidCharacter"] = 16] = "InvalidCharacter";
863
863
  })(ParseErrorCode || (ParseErrorCode = {}));
864
864
 
865
+ // lib/config-env-override.ts
866
+ function applyExternalModelEnvOverride(config) {
867
+ const envUrl = process.env.OPENCODE_DCP_EXTERNAL_COMPRESS_URL;
868
+ const envApiKey = process.env.OPENCODE_DCP_EXTERNAL_COMPRESS_KEY;
869
+ const envModel = process.env.OPENCODE_DCP_EXTERNAL_COMPRESS_MODEL;
870
+ const envTimeout = process.env.OPENCODE_DCP_EXTERNAL_COMPRESS_TIMEOUT;
871
+ const envRetries = process.env.OPENCODE_DCP_EXTERNAL_COMPRESS_RETRIES;
872
+ if (envUrl && envModel) {
873
+ const externalModel = {
874
+ url: envUrl,
875
+ model: envModel
876
+ };
877
+ if (envApiKey) {
878
+ externalModel.apiKey = envApiKey;
879
+ }
880
+ if (envTimeout) {
881
+ const parsed = parseInt(envTimeout, 10);
882
+ if (parsed > 0) {
883
+ externalModel.timeout = parsed;
884
+ }
885
+ }
886
+ if (envRetries) {
887
+ const parsed = parseInt(envRetries, 10);
888
+ if (parsed >= 0) {
889
+ externalModel.retries = parsed;
890
+ }
891
+ }
892
+ config.compress.externalModel = externalModel;
893
+ }
894
+ }
895
+
865
896
  // lib/config.ts
866
897
  var DEFAULT_PROTECTED_TOOLS = [
867
898
  "task",
@@ -912,6 +943,7 @@ var VALID_CONFIG_KEYS = /* @__PURE__ */ new Set([
912
943
  "compress.protectedTools",
913
944
  "compress.protectTags",
914
945
  "compress.protectUserMessages",
946
+ "compress.externalModel",
915
947
  "strategies",
916
948
  "strategies.deduplication",
917
949
  "strategies.deduplication.enabled",
@@ -1213,6 +1245,30 @@ function validateConfigTypes(config) {
1213
1245
  actual: JSON.stringify(compress.permission)
1214
1246
  });
1215
1247
  }
1248
+ if (compress.externalModel !== void 0) {
1249
+ if (typeof compress.externalModel !== "object" || compress.externalModel === null || Array.isArray(compress.externalModel)) {
1250
+ errors.push({
1251
+ key: "compress.externalModel",
1252
+ expected: "object with url and model",
1253
+ actual: typeof compress.externalModel
1254
+ });
1255
+ } else {
1256
+ if (typeof compress.externalModel.url !== "string") {
1257
+ errors.push({
1258
+ key: "compress.externalModel.url",
1259
+ expected: "string",
1260
+ actual: typeof compress.externalModel.url
1261
+ });
1262
+ }
1263
+ if (typeof compress.externalModel.model !== "string") {
1264
+ errors.push({
1265
+ key: "compress.externalModel.model",
1266
+ expected: "string",
1267
+ actual: typeof compress.externalModel.model
1268
+ });
1269
+ }
1270
+ }
1271
+ }
1216
1272
  if (compress.showCompression !== void 0 && typeof compress.showCompression !== "boolean") {
1217
1273
  errors.push({
1218
1274
  key: "compress.showCompression",
@@ -1342,7 +1398,8 @@ var defaultConfig = {
1342
1398
  nudgeForce: "strong",
1343
1399
  protectedTools: [...COMPRESS_DEFAULT_PROTECTED_TOOLS],
1344
1400
  protectTags: false,
1345
- protectUserMessages: false
1401
+ protectUserMessages: false,
1402
+ externalModel: void 0
1346
1403
  },
1347
1404
  strategies: {
1348
1405
  deduplication: {
@@ -1465,7 +1522,8 @@ function mergeCompress(base, override) {
1465
1522
  nudgeForce: override.nudgeForce ?? base.nudgeForce,
1466
1523
  protectedTools: [.../* @__PURE__ */ new Set([...base.protectedTools, ...override.protectedTools ?? []])],
1467
1524
  protectTags: override.protectTags ?? base.protectTags,
1468
- protectUserMessages: override.protectUserMessages ?? base.protectUserMessages
1525
+ protectUserMessages: override.protectUserMessages ?? base.protectUserMessages,
1526
+ externalModel: override.externalModel ?? base.externalModel
1469
1527
  };
1470
1528
  }
1471
1529
  function mergeCommands(base, override) {
@@ -1509,7 +1567,8 @@ function deepCloneConfig(config) {
1509
1567
  ...config.compress,
1510
1568
  modelMaxLimits: { ...config.compress.modelMaxLimits },
1511
1569
  modelMinLimits: { ...config.compress.modelMinLimits },
1512
- protectedTools: [...config.compress.protectedTools]
1570
+ protectedTools: [...config.compress.protectedTools],
1571
+ externalModel: config.compress.externalModel ? { ...config.compress.externalModel } : void 0
1513
1572
  },
1514
1573
  strategies: {
1515
1574
  deduplication: {
@@ -1591,6 +1650,7 @@ Using previous/default values`
1591
1650
  showConfigWarnings(ctx, layer.path, result.data, layer.isProject);
1592
1651
  config = mergeLayer(config, result.data);
1593
1652
  }
1653
+ applyExternalModelEnvOverride(config);
1594
1654
  return config;
1595
1655
  }
1596
1656
 
@@ -1793,7 +1853,12 @@ function countAllMessageTokens(msg) {
1793
1853
  }
1794
1854
 
1795
1855
  // lib/prompts/extensions/tool.ts
1796
- var RANGE_FORMAT_EXTENSION = `
1856
+ function buildRangeFormatExtension(externalModelEnabled) {
1857
+ const summaryField = externalModelEnabled ? `summary?: string // \u66FF\u6362\u8303\u56F4\u5185\u6240\u6709\u5185\u5BB9\u7684\u5B8C\u6574\u6280\u672F\u6458\u8981\uFF08\u53EF\u7701\u7565\uFF1B\u5916\u90E8\u6A21\u578B\u751F\u6210\uFF09` : `summary: string // \u66FF\u6362\u8303\u56F4\u5185\u6240\u6709\u5185\u5BB9\u7684\u5B8C\u6574\u6280\u672F\u6458\u8981`;
1858
+ const externalHint = externalModelEnabled ? `
1859
+
1860
+ \u5F53\u542F\u7528\u5916\u90E8\u6A21\u578B\u538B\u7F29\u65F6\uFF0Csummary \u5B57\u6BB5\u53EF\u7701\u7565\uFF1B\u63D2\u4EF6\u5C06\u81EA\u52A8\u8C03\u7528\u5916\u90E8\u6A21\u578B\u751F\u6210\u3002` : "";
1861
+ return `
1797
1862
  \u538B\u7F29\u683C\u5F0F
1798
1863
 
1799
1864
  \`\`\`
@@ -1803,12 +1868,18 @@ var RANGE_FORMAT_EXTENSION = `
1803
1868
  {
1804
1869
  startId: string, // \u8303\u56F4\u5F00\u59CB\u7684\u8FB9\u754C ID\uFF1AmNNNN \u6216 bN
1805
1870
  endId: string, // \u8303\u56F4\u7ED3\u675F\u7684\u8FB9\u754C ID\uFF1AmNNNN \u6216 bN
1806
- summary: string // \u66FF\u6362\u8303\u56F4\u5185\u6240\u6709\u5185\u5BB9\u7684\u5B8C\u6574\u6280\u672F\u6458\u8981
1871
+ ${summaryField}
1807
1872
  }
1808
1873
  ]
1809
1874
  }
1810
- \`\`\``;
1811
- var MESSAGE_FORMAT_EXTENSION = `
1875
+ \`\`\`${externalHint}`;
1876
+ }
1877
+ function buildMessageFormatExtension(externalModelEnabled) {
1878
+ const summaryField = externalModelEnabled ? `summary?: string // \u66FF\u6362\u8BE5\u6761\u6D88\u606F\u7684\u5B8C\u6574\u6280\u672F\u6458\u8981\uFF08\u53EF\u7701\u7565\uFF1B\u5916\u90E8\u6A21\u578B\u751F\u6210\uFF09` : `summary: string // \u66FF\u6362\u8BE5\u6761\u6D88\u606F\u7684\u5B8C\u6574\u6280\u672F\u6458\u8981`;
1879
+ const externalHint = externalModelEnabled ? `
1880
+
1881
+ \u5F53\u542F\u7528\u5916\u90E8\u6A21\u578B\u538B\u7F29\u65F6\uFF0Csummary \u5B57\u6BB5\u53EF\u7701\u7565\uFF1B\u63D2\u4EF6\u5C06\u81EA\u52A8\u8C03\u7528\u5916\u90E8\u6A21\u578B\u751F\u6210\u3002` : "";
1882
+ return `
1812
1883
  \u538B\u7F29\u683C\u5F0F
1813
1884
 
1814
1885
  \`\`\`
@@ -1818,11 +1889,14 @@ var MESSAGE_FORMAT_EXTENSION = `
1818
1889
  {
1819
1890
  messageId: string, // \u4EC5\u539F\u59CB\u6D88\u606F ID\uFF1AmNNNN\uFF08\u5FFD\u7565 priority \u7B49\u5143\u6570\u636E\u5C5E\u6027\uFF09
1820
1891
  topic: string, // \u6B64\u5355\u6761\u6D88\u606F\u6458\u8981\u7684\u77ED\u6807\u7B7E\uFF083-5 \u4E2A\u8BCD\uFF09
1821
- summary: string // \u66FF\u6362\u8BE5\u6761\u6D88\u606F\u7684\u5B8C\u6574\u6280\u672F\u6458\u8981
1892
+ ${summaryField}
1822
1893
  }
1823
1894
  ]
1824
1895
  }
1825
- \`\`\``;
1896
+ \`\`\`${externalHint}`;
1897
+ }
1898
+ var RANGE_FORMAT_EXTENSION = buildRangeFormatExtension(false);
1899
+ var MESSAGE_FORMAT_EXTENSION = buildMessageFormatExtension(false);
1826
1900
 
1827
1901
  // lib/message-ids.ts
1828
1902
  var MESSAGE_REF_REGEX = /^m(\d{4})$/;
@@ -1985,11 +2059,17 @@ function resolveBoundaryIds(context, state, startId, endId) {
1985
2059
  const issues = [];
1986
2060
  const parsedStartId = parseBoundaryId(startId);
1987
2061
  const parsedEndId = parseBoundaryId(endId);
2062
+ const hint = formatAvailableBoundaryHint(lookup);
2063
+ const guidance = formatBoundaryUsageGuidance();
1988
2064
  if (parsedStartId === null) {
1989
- issues.push("startId is invalid. Use an injected message ID (mNNNN) or block ID (bN).");
2065
+ issues.push(
2066
+ `startId is invalid. Use an injected message ID (mNNNN) or block ID (bN).${hint}${guidance}`
2067
+ );
1990
2068
  }
1991
2069
  if (parsedEndId === null) {
1992
- issues.push("endId is invalid. Use an injected message ID (mNNNN) or block ID (bN).");
2070
+ issues.push(
2071
+ `endId is invalid. Use an injected message ID (mNNNN) or block ID (bN).${hint}${guidance}`
2072
+ );
1993
2073
  }
1994
2074
  if (issues.length > 0) {
1995
2075
  throw new Error(
@@ -2002,16 +2082,18 @@ function resolveBoundaryIds(context, state, startId, endId) {
2002
2082
  const startReference = lookup.get(parsedStartId.ref);
2003
2083
  const endReference = lookup.get(parsedEndId.ref);
2004
2084
  if (!startReference || !endReference) {
2005
- const availableBlockRefs = Array.from(lookup.keys()).filter((key) => key.startsWith("b")).sort((a, b) => Number.parseInt(a.slice(1), 10) - Number.parseInt(b.slice(1), 10)).join(", ");
2006
- const blockHint = availableBlockRefs ? ` Available block IDs: ${availableBlockRefs}.` : " No block IDs available.";
2007
2085
  if (!startReference) {
2086
+ const wasKnown = state.messageIds.byRef.has(parsedStartId.ref);
2087
+ const reason = wasKnown ? " (message was likely compacted out of the conversation)" : " (invalid ID)";
2008
2088
  issues.push(
2009
- `startId ${parsedStartId.ref} is not available in the current conversation context. Choose an injected ID visible in context.${blockHint}`
2089
+ `startId ${parsedStartId.ref} is not available${reason}. Choose an injected ID visible in context.${hint}${guidance}`
2010
2090
  );
2011
2091
  }
2012
2092
  if (!endReference) {
2093
+ const wasKnown = state.messageIds.byRef.has(parsedEndId.ref);
2094
+ const reason = wasKnown ? " (message was likely compacted out of the conversation)" : " (invalid ID)";
2013
2095
  issues.push(
2014
- `endId ${parsedEndId.ref} is not available in the current conversation context. Choose an injected ID visible in context.${blockHint}`
2096
+ `endId ${parsedEndId.ref} is not available${reason}. Choose an injected ID visible in context.${hint}${guidance}`
2015
2097
  );
2016
2098
  }
2017
2099
  }
@@ -2117,6 +2199,17 @@ function resolveAnchorMessageId(startReference) {
2117
2199
  }
2118
2200
  return startReference.messageId;
2119
2201
  }
2202
+ function formatAvailableBoundaryHint(lookup) {
2203
+ const allKeys = Array.from(lookup.keys());
2204
+ const availableBlockRefs = allKeys.filter((key) => key.startsWith("b")).sort((a, b) => Number.parseInt(a.slice(1), 10) - Number.parseInt(b.slice(1), 10));
2205
+ const availableMsgRefs = allKeys.filter((key) => key.startsWith("m")).sort((a, b) => Number.parseInt(a.slice(1), 10) - Number.parseInt(b.slice(1), 10));
2206
+ const blockHint = availableBlockRefs.length ? ` Available block IDs: ${availableBlockRefs.join(", ")}.` : " No block IDs available.";
2207
+ const msgHint = availableMsgRefs.length === 0 ? " No message IDs available." : availableMsgRefs.length <= 10 ? ` Available message IDs: ${availableMsgRefs.join(", ")}.` : ` Available message IDs: ${availableMsgRefs[0]} to ${availableMsgRefs[availableMsgRefs.length - 1]} (${availableMsgRefs.length} total).`;
2208
+ return `${msgHint}${blockHint}`;
2209
+ }
2210
+ function formatBoundaryUsageGuidance() {
2211
+ return " Use exactly one of the listed injected IDs. For already compressed content, use its bN block ID.";
2212
+ }
2120
2213
  function buildBoundaryLookup(context, state) {
2121
2214
  const lookup = /* @__PURE__ */ new Map();
2122
2215
  for (const [messageRef, messageId] of state.messageIds.byRef) {
@@ -2434,8 +2527,10 @@ function validateArgs(args) {
2434
2527
  if (typeof entry?.topic !== "string" || entry.topic.trim().length === 0) {
2435
2528
  throw new Error(`${prefix}.topic is required and must be a non-empty string`);
2436
2529
  }
2437
- if (typeof entry?.summary !== "string" || entry.summary.trim().length === 0) {
2438
- throw new Error(`${prefix}.summary is required and must be a non-empty string`);
2530
+ if (entry?.summary !== void 0) {
2531
+ if (typeof entry.summary !== "string" || entry.summary.trim().length === 0) {
2532
+ throw new Error(`${prefix}.summary must be a non-empty string when provided`);
2533
+ }
2439
2534
  }
2440
2535
  }
2441
2536
  }
@@ -4153,8 +4248,118 @@ ${output}`);
4153
4248
  return summary + heading + protectedOutputs.join("");
4154
4249
  }
4155
4250
 
4251
+ // lib/compress/external-inference.ts
4252
+ var ExternalModelError = class extends Error {
4253
+ constructor(message, kind, cause) {
4254
+ super(message);
4255
+ this.kind = kind;
4256
+ this.cause = cause;
4257
+ this.name = "ExternalModelError";
4258
+ }
4259
+ };
4260
+ function resolveCompressBaseUrl(url) {
4261
+ const normalized = url.replace(/\/$/, "");
4262
+ if (normalized.endsWith("/chat/completions")) {
4263
+ return normalized;
4264
+ }
4265
+ return `${normalized}/chat/completions`;
4266
+ }
4267
+ async function callExternalModel(endpoint, cfg, req) {
4268
+ const controller = new AbortController();
4269
+ const timeoutMs = cfg.timeout ?? 12e4;
4270
+ const timeoutHandle = setTimeout(() => controller.abort(), timeoutMs);
4271
+ let response;
4272
+ try {
4273
+ response = await fetch(endpoint, {
4274
+ method: "POST",
4275
+ headers: {
4276
+ "Content-Type": "application/json",
4277
+ ...cfg.apiKey ? { Authorization: `Bearer ${cfg.apiKey}` } : {}
4278
+ },
4279
+ body: JSON.stringify({
4280
+ model: cfg.model,
4281
+ messages: [
4282
+ { role: "system", content: req.systemPrompt },
4283
+ { role: "user", content: req.userContent }
4284
+ ],
4285
+ temperature: 0
4286
+ }),
4287
+ signal: controller.signal
4288
+ });
4289
+ } catch (error) {
4290
+ if (error instanceof Error && error.name === "AbortError") {
4291
+ throw new ExternalModelError(
4292
+ `External model request timed out after ${timeoutMs}ms`,
4293
+ "timeout",
4294
+ error
4295
+ );
4296
+ }
4297
+ throw new ExternalModelError(
4298
+ `External model request failed: ${error instanceof Error ? error.message : String(error)}`,
4299
+ "network",
4300
+ error
4301
+ );
4302
+ } finally {
4303
+ clearTimeout(timeoutHandle);
4304
+ }
4305
+ if (!response.ok) {
4306
+ let errorBody = "";
4307
+ try {
4308
+ errorBody = await response.text();
4309
+ } catch {
4310
+ }
4311
+ throw new ExternalModelError(
4312
+ `External model returned HTTP ${response.status}: ${errorBody.slice(0, 200)}`,
4313
+ "http",
4314
+ { status: response.status, body: errorBody }
4315
+ );
4316
+ }
4317
+ let parsed;
4318
+ try {
4319
+ parsed = await response.json();
4320
+ } catch (error) {
4321
+ throw new ExternalModelError("External model returned invalid JSON", "parse", error);
4322
+ }
4323
+ const content = parsed?.choices?.[0]?.message?.content;
4324
+ if (typeof content !== "string" || content.trim().length === 0) {
4325
+ throw new ExternalModelError(
4326
+ "External model returned empty or missing summary content",
4327
+ "empty",
4328
+ parsed
4329
+ );
4330
+ }
4331
+ return content;
4332
+ }
4333
+ async function generateSummaryViaExternal(cfg, req) {
4334
+ const endpoint = resolveCompressBaseUrl(cfg.url);
4335
+ const maxRetries = cfg.retries ?? 1;
4336
+ let lastError;
4337
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
4338
+ try {
4339
+ return await callExternalModel(endpoint, cfg, req);
4340
+ } catch (error) {
4341
+ lastError = error;
4342
+ }
4343
+ }
4344
+ throw lastError;
4345
+ }
4346
+
4156
4347
  // lib/compress/message.ts
4157
- function buildSchema() {
4348
+ function extractMessageText(messageId, searchContext) {
4349
+ const msg = searchContext.rawMessagesById.get(messageId);
4350
+ if (!msg) return "";
4351
+ const parts = [];
4352
+ for (const part of msg.parts) {
4353
+ if (part.type === "text" && typeof part.text === "string") {
4354
+ parts.push(part.text);
4355
+ }
4356
+ }
4357
+ return parts.join("\n\n");
4358
+ }
4359
+ function buildSchema(externalModelEnabled) {
4360
+ const summaryField = externalModelEnabled ? tool.schema.string().optional().describe(
4361
+ "Complete technical summary replacing that one message (optional when external model is configured)"
4362
+ ) : tool.schema.string().describe("Complete technical summary replacing that one message");
4158
4363
  return {
4159
4364
  topic: tool.schema.string().describe(
4160
4365
  "Short label (3-5 words) for the overall batch - e.g., 'Closed Research Notes'"
@@ -4163,7 +4368,7 @@ function buildSchema() {
4163
4368
  tool.schema.object({
4164
4369
  messageId: tool.schema.string().describe("Raw message ID to compress (e.g. m0001)"),
4165
4370
  topic: tool.schema.string().describe("Short label (3-5 words) for this one message summary"),
4166
- summary: tool.schema.string().describe("Complete technical summary replacing that one message")
4371
+ summary: summaryField
4167
4372
  })
4168
4373
  ).describe("Batch of individual message summaries to create in one tool call")
4169
4374
  };
@@ -4171,9 +4376,10 @@ function buildSchema() {
4171
4376
  function createCompressMessageTool(ctx) {
4172
4377
  ctx.prompts.reload();
4173
4378
  const runtimePrompts = ctx.prompts.getRuntimePrompts();
4379
+ const externalModelEnabled = ctx.config.compress.externalModel !== void 0;
4174
4380
  return tool({
4175
- description: runtimePrompts.compressMessage + MESSAGE_FORMAT_EXTENSION,
4176
- args: buildSchema(),
4381
+ description: runtimePrompts.compressMessage + buildMessageFormatExtension(externalModelEnabled),
4382
+ args: buildSchema(externalModelEnabled),
4177
4383
  async execute(args, toolCtx) {
4178
4384
  const input = args;
4179
4385
  validateArgs(input);
@@ -4192,9 +4398,29 @@ function createCompressMessageTool(ctx) {
4192
4398
  if (plans.length === 0 && skippedCount > 0) {
4193
4399
  throw new Error(formatIssues(skippedIssues, skippedCount));
4194
4400
  }
4401
+ if (ctx.config.compress.externalModel) {
4402
+ for (const plan of plans) {
4403
+ if (plan.entry.summary === void 0) {
4404
+ const userContent = extractMessageText(plan.entry.messageId, searchContext);
4405
+ const generated = await generateSummaryViaExternal(
4406
+ ctx.config.compress.externalModel,
4407
+ {
4408
+ systemPrompt: runtimePrompts.compressMessage,
4409
+ userContent
4410
+ }
4411
+ );
4412
+ plan.entry.summary = generated;
4413
+ }
4414
+ }
4415
+ }
4195
4416
  const notifications = [];
4196
4417
  const preparedPlans = [];
4197
4418
  for (const plan of plans) {
4419
+ if (plan.entry.summary === void 0) {
4420
+ throw new Error(
4421
+ "\u7F3A\u5C11 summary\uFF1A\u672A\u914D\u7F6E\u5916\u90E8\u6A21\u578B\uFF0C\u8BF7\u63D0\u4F9B summary \u6216\u914D\u7F6E OPENCODE_DCP_EXTERNAL_COMPRESS_URL/MODEL"
4422
+ );
4423
+ }
4198
4424
  const summaryWithPromptInfo = appendProtectedPromptInfo(
4199
4425
  plan.entry.summary,
4200
4426
  plan.selection,
@@ -4275,8 +4501,10 @@ function validateArgs2(args) {
4275
4501
  if (typeof entry?.endId !== "string" || entry.endId.trim().length === 0) {
4276
4502
  throw new Error(`${prefix}.endId is required and must be a non-empty string`);
4277
4503
  }
4278
- if (typeof entry?.summary !== "string" || entry.summary.trim().length === 0) {
4279
- throw new Error(`${prefix}.summary is required and must be a non-empty string`);
4504
+ if (entry?.summary !== void 0) {
4505
+ if (typeof entry.summary !== "string" || entry.summary.trim().length === 0) {
4506
+ throw new Error(`${prefix}.summary must be a non-empty string when provided`);
4507
+ }
4280
4508
  }
4281
4509
  }
4282
4510
  }
@@ -4482,7 +4710,23 @@ ${right}`;
4482
4710
  }
4483
4711
 
4484
4712
  // lib/compress/range.ts
4485
- function buildSchema2() {
4713
+ function extractRangeText(selection, searchContext) {
4714
+ const parts = [];
4715
+ for (const messageId of selection.messageIds) {
4716
+ const msg = searchContext.rawMessagesById.get(messageId);
4717
+ if (!msg) continue;
4718
+ for (const part of msg.parts) {
4719
+ if (part.type === "text" && typeof part.text === "string") {
4720
+ parts.push(part.text);
4721
+ }
4722
+ }
4723
+ }
4724
+ return parts.join("\n\n");
4725
+ }
4726
+ function buildSchema2(externalModelEnabled) {
4727
+ const summaryField = externalModelEnabled ? tool2.schema.string().optional().describe(
4728
+ "Complete technical summary replacing all content in range (optional when external model is configured)"
4729
+ ) : tool2.schema.string().describe("Complete technical summary replacing all content in range");
4486
4730
  return {
4487
4731
  topic: tool2.schema.string().describe("Short label (3-5 words) for display - e.g., 'Auth System Exploration'"),
4488
4732
  content: tool2.schema.array(
@@ -4491,7 +4735,7 @@ function buildSchema2() {
4491
4735
  "Message or block ID marking the beginning of range (e.g. m0001, b2)"
4492
4736
  ),
4493
4737
  endId: tool2.schema.string().describe("Message or block ID marking the end of range (e.g. m0012, b5)"),
4494
- summary: tool2.schema.string().describe("Complete technical summary replacing all content in range")
4738
+ summary: summaryField
4495
4739
  })
4496
4740
  ).describe(
4497
4741
  "One or more ranges to compress, each with start/end boundaries and a summary"
@@ -4501,9 +4745,10 @@ function buildSchema2() {
4501
4745
  function createCompressRangeTool(ctx) {
4502
4746
  ctx.prompts.reload();
4503
4747
  const runtimePrompts = ctx.prompts.getRuntimePrompts();
4748
+ const externalModelEnabled = ctx.config.compress.externalModel !== void 0;
4504
4749
  return tool2({
4505
- description: runtimePrompts.compressRange + RANGE_FORMAT_EXTENSION,
4506
- args: buildSchema2(),
4750
+ description: runtimePrompts.compressRange + buildRangeFormatExtension(externalModelEnabled),
4751
+ args: buildSchema2(externalModelEnabled),
4507
4752
  async execute(args, toolCtx) {
4508
4753
  const input = args;
4509
4754
  validateArgs2(input);
@@ -4515,10 +4760,30 @@ function createCompressRangeTool(ctx) {
4515
4760
  );
4516
4761
  const resolvedPlans = resolveRanges(input, searchContext, ctx.state);
4517
4762
  validateNonOverlapping(resolvedPlans);
4763
+ if (ctx.config.compress.externalModel) {
4764
+ for (const plan of resolvedPlans) {
4765
+ if (plan.entry.summary === void 0) {
4766
+ const userContent = extractRangeText(plan.selection, searchContext);
4767
+ const generated = await generateSummaryViaExternal(
4768
+ ctx.config.compress.externalModel,
4769
+ {
4770
+ systemPrompt: runtimePrompts.compressRange,
4771
+ userContent
4772
+ }
4773
+ );
4774
+ plan.entry.summary = generated;
4775
+ }
4776
+ }
4777
+ }
4518
4778
  const notifications = [];
4519
4779
  const preparedPlans = [];
4520
4780
  let totalCompressedMessages = 0;
4521
4781
  for (const plan of resolvedPlans) {
4782
+ if (plan.entry.summary === void 0) {
4783
+ throw new Error(
4784
+ "\u7F3A\u5C11 summary\uFF1A\u672A\u914D\u7F6E\u5916\u90E8\u6A21\u578B\uFF0C\u8BF7\u63D0\u4F9B summary \u6216\u914D\u7F6E OPENCODE_DCP_EXTERNAL_COMPRESS_URL/MODEL"
4785
+ );
4786
+ }
4522
4787
  const parsedPlaceholders = parseBlockPlaceholders(plan.entry.summary);
4523
4788
  const missingBlockIds = validateSummaryPlaceholders(
4524
4789
  parsedPlaceholders,