aegis-desktop 0.8.17 → 0.8.19

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.
@@ -240,6 +240,57 @@ function providerKeyFromEnv(provider) {
240
240
  const DEEPSEEK_REASONING_MODEL_RE = /^deepseek-(v4(\.\d+)?-(flash|pro)|flash|pro|reasoner)$/;
241
241
  const EFFORT_TOKEN_BUDGET = { low: 8192, medium: 16384, high: 32768 };
242
242
 
243
+ /**
244
+ * DeepSeek native reasoning level per config rung.
245
+ *
246
+ * The field is `reasoning_effort`, NOT `effort`. Verified live 2026-10-02: a
247
+ * DeepSeek body carrying `effort` returns HTTP 200 and is ignored, while
248
+ * `reasoning_effort` binds and rejects an unknown value with a 422 naming its
249
+ * enum (none|minimal|low|medium|high|xhigh|ultra|max). Behaviourally,
250
+ * `reasoning_effort:"none"` produced 0 reasoning_content chars and `"max"`
251
+ * produced 138, whereas `effort:"none"` still produced 112 — thinking stayed
252
+ * on. So the old field was inert on every turn, and inertness is the worst
253
+ * failure here: it 200s, nothing surfaces, and the model merely thinks at the
254
+ * wrong depth.
255
+ *
256
+ * Every rung of the knob has a namesake in that enum, so the map is 1:1 rather
257
+ * than the earlier (low|max, no medium) fold, which rested on the mistaken
258
+ * belief that DeepSeek had no `medium`. `max_tokens` (EFFORT_TOKEN_BUDGET)
259
+ * still scales as the output ceiling, since hidden chain-of-thought shares that
260
+ * budget on DeepSeek.
261
+ *
262
+ * Mirrors the upstream engine's src/backend.js DEEPSEEK_EFFORT verbatim;
263
+ * desktop/renderer/budget.js carries the renderer's copy for the same
264
+ * no-drift guarantee as DEEPSEEK_REASONING_MODEL_RE above.
265
+ */
266
+ const DEEPSEEK_EFFORT = { low: 'low', medium: 'medium', high: 'high' };
267
+
268
+ /**
269
+ * Per-family effort translation, mirroring the upstream engine's
270
+ * EFFORT_DIALECTS. The knob is one rung here (low|medium|high) but it is NOT
271
+ * portable across vendors, and passing it through untranslated is a correctness
272
+ * bug rather than a no-op: Groq 400s any level outside a model's supported set,
273
+ * and Gemini 3 fails on `medium` outright while low/high succeed. So each
274
+ * family states its own wire field and level map, and a family with no such
275
+ * field gets NOTHING — an unknown body field either 400s or is silently
276
+ * dropped, and both are worse than stating no opinion.
277
+ *
278
+ * xAI's `*-non-reasoning` row is left alone entirely: the model exists to not
279
+ * reason, and sending it an effort field is precisely the request the user
280
+ * picked that row to avoid. grok-4.20 and gemini-2.0 stay bare too — no
281
+ * documented level field exists for them, and guessing on a strict validator
282
+ * is a 400 rather than an upgrade.
283
+ */
284
+ const EFFORT_IDENTITY = { low: 'low', medium: 'medium', high: 'high' };
285
+ const EFFORT_INERT_RE = /non-reasoning/i;
286
+ const EFFORT_DIALECTS = [
287
+ { provider: 'deepseek', re: DEEPSEEK_REASONING_MODEL_RE, field: 'reasoning_effort', map: DEEPSEEK_EFFORT },
288
+ { provider: 'openai', re: /^(gpt-5|o[0-9])/i, field: 'reasoning_effort', map: EFFORT_IDENTITY },
289
+ { provider: 'groq', re: /^(openai\/gpt-oss|qwen\/qwen3)/i, field: 'reasoning_effort', map: EFFORT_IDENTITY },
290
+ { provider: 'xai', re: /^grok-(4\.[5-9]|[5-9])/, field: 'reasoning_effort', map: EFFORT_IDENTITY },
291
+ { provider: 'google', re: /^gemini-(2\.5|3|[4-9])/, field: 'reasoning_effort', map: { ...EFFORT_IDENTITY, medium: 'high' } },
292
+ ];
293
+
243
294
  /**
244
295
  * Idle-stream budget for a pooled brain call ("work autonomously"). The
245
296
  * generic watchdog in vendor/aegis.js kills a stream that goes 60s without a
@@ -296,6 +347,24 @@ function reasoningBudget(model, maxTokens, effort) {
296
347
  return undefined;
297
348
  }
298
349
 
350
+ /**
351
+ * The native effort verdict for this provider+model: `{ field, level }`, or
352
+ * `undefined` when the family has no effort knob. `undefined` means "say
353
+ * nothing", never "send a default" — inventing a level is how the knob silently
354
+ * overrides the vendor's own default. Supersedes the DeepSeek-only
355
+ * deepseekEffort() it replaces: same DeepSeek mapping, now stated for every
356
+ * family that has one instead of only the one that used to be wired.
357
+ */
358
+ function nativeEffort(provider, model, effort) {
359
+ const m = String(model || '');
360
+ if (EFFORT_INERT_RE.test(m)) return undefined;
361
+ const eff = effort === 'low' || effort === 'medium' ? effort : 'high';
362
+ for (const d of EFFORT_DIALECTS) {
363
+ if (d.provider === provider && d.re.test(m)) return { field: d.field, level: d.map[eff] };
364
+ }
365
+ return undefined;
366
+ }
367
+
299
368
  /** Relay model entries arrive as ids or objects; keep only real model ids. */
300
369
  function normalizeCatalog(models) {
301
370
  if (!Array.isArray(models)) return [];
@@ -307,35 +376,32 @@ function normalizeCatalog(models) {
307
376
  /**
308
377
  * The Aegis Cloud (pooled) dropdown.
309
378
  *
310
- * It offers NO aegiscloud model. The catalog (`/api/v1/models`) lists
311
- * per-provider ids (`deepseek`, `anthropic`, `groq`, `openai`, ...) alongside
312
- * five pooled-brain tier spellings (`{aegis,nexus}-brain[-smart|-neo]`) that all
313
- * route the same worker pool, and this host must offer neither: "omit the
314
- * aegiscloud models" is the instruction, and a host that omitted the brain row
315
- * while leaving `deepseek` on screen would have kept exactly the per-provider
316
- * pin the Aegis Cloud policy is about. The class stays selectable and its
317
- * dropdown keeps the "server default (auto)" row, so the pooled lane still runs
318
- * — it just advertises nothing to pin.
379
+ * It offers the server's whole catalog (`/api/v1/models`): the pooled brain
380
+ * first, under its one name (Nexus), then every per-provider id the account can
381
+ * pin (`deepseek`, `anthropic`, `groq`, `openai`, ...).
382
+ *
383
+ * Two narrower shapes were shipped here and both are reverted, because both
384
+ * produced the same report — a user asking where the models went. First the
385
+ * class collapsed to a single "Nexus" row (`filterAegisCatalog`), which ended
386
+ * the two-host disagreement about the brain's five tier spellings by removing
387
+ * the choice: a funded account had `deepseek` advertised to it and no way to
388
+ * pin it. Then it offered nothing at all — `omitAegisCloudModels` composed with
389
+ * that collapse returns `[]`, because the omission removes the only entry the
390
+ * collapse looks for. The rule both hosts call now is `offerableCatalog`:
391
+ * everything the server advertises, the tier spellings folded into the one row
392
+ * they point at.
319
393
  *
320
394
  * What is NOT changed: the account key, and everything the key is *for* — the
321
395
  * pooled turn itself, cloud memory sync, and the BYOK handling fee that is
322
- * billed to this account (see the byok branch below, which still offers every
323
- * provider id the server's catalog names). Omitting a model list is not
324
- * disconnecting the lane.
325
- *
326
- * The omission itself is one shared rule (`omitAegisCloudModels`, and the
327
- * collapse it is composed with) in `client/brain-catalog.js`, shared with the
328
- * CLI: two copies is how this host and the terminal previously came to offer
329
- * the same account two different model lists. Resolved the two ways this repo
330
- * resolves every shared module — repo-relative in a checkout, and this app's
331
- * staged vendor/ tree.
396
+ * billed to this account (see the byok branch below).
332
397
  *
333
- * `filterAegisCatalog` is kept in the composition rather than dropped for the
334
- * shorter `models: []`: a payload with no brain tier at all (a self-hosted or
335
- * trimmed deployment) must still yield nothing here rather than leaking its
336
- * per-provider ids, and that is precisely the case the collapse already answers.
398
+ * The list itself is one shared rule in `client/brain-catalog.js`, shared with
399
+ * the CLI: two copies is how this host and the terminal previously came to
400
+ * offer the same account two different model lists. Resolved the two ways this
401
+ * repo resolves every shared module — repo-relative in a checkout, and this
402
+ * app's staged vendor/ tree.
337
403
  */
338
- const { filterAegisCatalog, omitAegisCloudModels } = requireSharedBrain();
404
+ const { offerableCatalog } = requireSharedBrain();
339
405
 
340
406
  /**
341
407
  * The BYOK additions, applied to the server's provider catalog below. Same
@@ -744,7 +810,7 @@ function createLocalEngine({
744
810
  // direction too: a path wrongly claimed stops being reported as foreign.
745
811
  const result = await T.executeTool(name, args, toolCtx);
746
812
  if (guard && (name === 'writeFile' || name === 'editFile') && result && result.ok !== false) {
747
- recordWrite(guard, args && args.path);
813
+ recordWrite(guard, args && args.file_path);
748
814
  }
749
815
  return result;
750
816
  }
@@ -793,7 +859,7 @@ function createLocalEngine({
793
859
  // unconditionally, making every line below it unreachable — the
794
860
  // approved-write path recorded nothing at all.)
795
861
  if (res && res.ok !== false && toolCtx && toolCtx.guard) {
796
- recordWrite(toolCtx.guard, args && args.path);
862
+ recordWrite(toolCtx.guard, args && args.file_path);
797
863
  }
798
864
  return res;
799
865
  }
@@ -855,20 +921,16 @@ function createLocalEngine({
855
921
  // entry so the class is usable the moment a key lands.
856
922
  if (!aegis.apiKey) return { class: cls, models: [], needsKey: true };
857
923
  const data = await aegis.listModels();
858
- // No aegiscloud model is offered — see the rule above. The class, the
859
- // auto row and the pooled turn are untouched; only the pinnable list is
860
- // emptied, which is why `needsKey` keeps its meaning and the renderer's
861
- // hint for this class is unchanged.
924
+ // The whole catalog, aliases folded — see the rule above. What the list
925
+ // contains does not touch `needsKey` or the renderer's per-class hint.
862
926
  return {
863
927
  class: cls,
864
- // Omit FIRST, then collapse: the omission has to see the payload's own
865
- // `alias_of` bookkeeping, which the collapse deliberately strips (a
866
- // collapsed row IS the selection, so a renderer must not filter it back
867
- // out as a hidden alias). Filtering a collapsed row instead would leave
868
- // a renamed tier — `nexus-brain-v2`, an alias OF the brain under a
869
- // spelling this release has never seen — on screen, which is the one
870
- // case the rule exists to catch.
871
- models: filterAegisCatalog(omitAegisCloudModels(normalizeCatalog(data && data.models))),
928
+ // One call, because the folding has to see the payload's own `alias_of`
929
+ // bookkeeping: a pre-collapsed row IS the selection, so a renderer must
930
+ // not filter it back out as a hidden alias, and folding what has already
931
+ // been folded would leave a renamed tier (`nexus-brain-v2`, an alias OF
932
+ // the brain under a spelling this release has never seen) on screen.
933
+ models: offerableCatalog(normalizeCatalog(data && data.models)),
872
934
  };
873
935
  }
874
936
  if (cls === 'byok') {
@@ -1153,6 +1215,12 @@ function createLocalEngine({
1153
1215
  // automatically by byokChatCompletion() as X-AEGIS-Key so the account
1154
1216
  // gets billed the handling fee; see client/aegis.js.
1155
1217
  const { provider, model: bareModel } = splitByokModel(opts.model);
1218
+ // Translate the rung for THIS vendor once, and carry the verdict rather
1219
+ // than a bare level: the relay validates strictly upstream and the field
1220
+ // name is not portable (DeepSeek's OpenAI-compatible surface takes
1221
+ // `reasoning_effort`, same spelling as OpenAI's). Undefined for a family
1222
+ // with no dialect, which the transport then omits entirely.
1223
+ const effort = nativeEffort(provider, bareModel, opts.effort);
1156
1224
  try {
1157
1225
  return await aegis.byokChatCompletion({
1158
1226
  provider,
@@ -1162,6 +1230,13 @@ function createLocalEngine({
1162
1230
  system: opts.system,
1163
1231
  messages: opts.messages,
1164
1232
  maxTokens: opts.maxTokens,
1233
+ // Native effort knob, field and level both from the dialect table.
1234
+ // `bareModel` is already provider-less, so it matches the dialect
1235
+ // regexes without further stripping. Only a dialect whose field is
1236
+ // `reasoning_effort` is forwarded: DeepSeek used to ride a separate
1237
+ // `effort` body key, which the API 200s and ignores, so that whole
1238
+ // second channel is gone rather than left as a way to reintroduce it.
1239
+ reasoningEffort: effort && effort.field === 'reasoning_effort' ? effort.level : undefined,
1165
1240
  stream: true,
1166
1241
  onStream: opts.onDelta,
1167
1242
  signal: opts.signal,
@@ -1863,4 +1938,4 @@ function createLocalEngine({
1863
1938
  };
1864
1939
  }
1865
1940
 
1866
- module.exports = { CLASSES, createLocalEngine, extractToolCalls, parseArgs, reasoningBudget };
1941
+ module.exports = { CLASSES, createLocalEngine, extractToolCalls, parseArgs, reasoningBudget, nativeEffort };