aegis-desktop 0.7.6 → 0.7.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/local/engine.js +131 -5
- package/package.json +1 -1
- package/renderer/app.js +103 -55
package/lib/local/engine.js
CHANGED
|
@@ -87,8 +87,28 @@ const CLASSES = [
|
|
|
87
87
|
{ class: 'ollama', label: 'Ollama (local)', kind: 'local' },
|
|
88
88
|
{ class: 'openai-compat', label: 'Custom OpenAI-compatible', kind: 'custom' },
|
|
89
89
|
{ class: 'anthropic', label: 'Anthropic-compatible', kind: 'custom' },
|
|
90
|
+
{ class: 'byok', label: 'Bring your own key', kind: 'cloud' },
|
|
90
91
|
];
|
|
91
92
|
|
|
93
|
+
/** Local settings namespace for one BYOK provider's key. A `byok:` prefix
|
|
94
|
+
* keeps this out of the 'anthropic'/'openai-compat' CUSTOM_CLASSES' own
|
|
95
|
+
* namespaces, which are a different feature (a self-hosted/compatible
|
|
96
|
+
* endpoint's base URL + key) — same store, deliberately separate rows. */
|
|
97
|
+
function byokNamespace(providerId) {
|
|
98
|
+
return `byok:${providerId}`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Split a byok model id ("anthropic:claude-sonnet-5") into its provider and
|
|
102
|
+
* bare model parts. `modelId` may itself contain ':' (none of today's
|
|
103
|
+
* catalog ids do, but nothing guarantees that), so only the FIRST segment is
|
|
104
|
+
* the provider — the rest re-joins as the model. */
|
|
105
|
+
function splitByokModel(compound) {
|
|
106
|
+
const s = typeof compound === 'string' ? compound : '';
|
|
107
|
+
const i = s.indexOf(':');
|
|
108
|
+
if (i < 0) return { provider: '', model: s };
|
|
109
|
+
return { provider: s.slice(0, i), model: s.slice(i + 1) };
|
|
110
|
+
}
|
|
111
|
+
|
|
92
112
|
/**
|
|
93
113
|
* Mirrors aegiscodex-dev's src/backend.js DEEPSEEK_REASONING_MODEL_RE +
|
|
94
114
|
* EFFORT_TOKEN_BUDGET verbatim. DeepSeek's reasoning models (deepseek-flash,
|
|
@@ -657,6 +677,16 @@ function createLocalEngine({ aegis, settings, ollama, providers, tools, promptBu
|
|
|
657
677
|
if (c.class === 'aegis') {
|
|
658
678
|
return { ...c, configured: Boolean(aegis.apiKey) };
|
|
659
679
|
}
|
|
680
|
+
if (c.class === 'byok') {
|
|
681
|
+
// Configured means "at least one provider has a locally-stored key",
|
|
682
|
+
// not a single baseURL+key pair like the CUSTOM_CLASSES below — byok
|
|
683
|
+
// holds one row per provider (byokNamespace), so customStatus's shape
|
|
684
|
+
// does not apply here.
|
|
685
|
+
const anyConfigured = (settings.list() || []).some(
|
|
686
|
+
(s) => s && typeof s.provider === 'string' && s.provider.startsWith('byok:') && s.configured
|
|
687
|
+
);
|
|
688
|
+
return { ...c, configured: anyConfigured };
|
|
689
|
+
}
|
|
660
690
|
return { ...c, ...customStatus(c.class) };
|
|
661
691
|
});
|
|
662
692
|
}
|
|
@@ -681,6 +711,41 @@ function createLocalEngine({ aegis, settings, ollama, providers, tools, promptBu
|
|
|
681
711
|
const tags = await ollama.listTags();
|
|
682
712
|
return { class: cls, models: tags.map((t) => ({ id: t.id })) };
|
|
683
713
|
}
|
|
714
|
+
if (cls === 'byok') {
|
|
715
|
+
// The server's catalog names every provider it accepts a key for, the
|
|
716
|
+
// models each unlocks, and whether an AEGIS account key is even needed
|
|
717
|
+
// to ask (it is not — see byokProviders' own docstring). Deliberately
|
|
718
|
+
// NOT gated on aegis.apiKey the way the pooled class above is: BYOK's
|
|
719
|
+
// whole point is a caller who brings their own credential, and the
|
|
720
|
+
// catalog itself answers to an anonymous request.
|
|
721
|
+
let providers = [];
|
|
722
|
+
try {
|
|
723
|
+
const data = await aegis.byokProviders();
|
|
724
|
+
providers = (data && data.providers) || [];
|
|
725
|
+
} catch {
|
|
726
|
+
providers = [];
|
|
727
|
+
}
|
|
728
|
+
const models = [];
|
|
729
|
+
for (const p of providers) {
|
|
730
|
+
if (!p || !p.id) continue;
|
|
731
|
+
const local = settings.get(byokNamespace(p.id)) || {};
|
|
732
|
+
const rawModels = Array.isArray(p.models) ? p.models : [];
|
|
733
|
+
for (const m of rawModels) {
|
|
734
|
+
const modelId = typeof m === 'string' ? m : m && m.id;
|
|
735
|
+
if (!modelId) continue;
|
|
736
|
+
models.push({
|
|
737
|
+
id: `${p.id}:${modelId}`,
|
|
738
|
+
label: `${p.label || p.id} — ${modelId}`,
|
|
739
|
+
provider: p.id,
|
|
740
|
+
configured: Boolean(local.configured),
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
return {
|
|
745
|
+
class: cls, models, providers,
|
|
746
|
+
needsProviderKey: models.length > 0 && !models.some((m) => m.configured),
|
|
747
|
+
};
|
|
748
|
+
}
|
|
684
749
|
// Custom endpoints: the model id is the *user's* choice — a provider model
|
|
685
750
|
// name, never a URL. Offering the configured base URL as an `id` meant that
|
|
686
751
|
// leaving the default selection POSTed `model: "https://api.openai.com/v1"`,
|
|
@@ -867,6 +932,29 @@ function createLocalEngine({ aegis, settings, ollama, providers, tools, promptBu
|
|
|
867
932
|
});
|
|
868
933
|
}
|
|
869
934
|
|
|
935
|
+
if (cls === 'byok') {
|
|
936
|
+
// opts.model is still the compound "provider:model" id here — the
|
|
937
|
+
// relay wants them split (a `provider` field plus a bare `model`).
|
|
938
|
+
// opts.apiKey is the PROVIDER key resolved by chat() above (from the
|
|
939
|
+
// byokNamespace(provider) settings row), never the account's own AEGIS
|
|
940
|
+
// key — that one is baked into the `aegis` client already and attached
|
|
941
|
+
// automatically by byokChatCompletion() as X-AEGIS-Key so the account
|
|
942
|
+
// gets billed the handling fee; see client/aegis.js.
|
|
943
|
+
const { provider, model: bareModel } = splitByokModel(opts.model);
|
|
944
|
+
return aegis.byokChatCompletion({
|
|
945
|
+
provider,
|
|
946
|
+
model: bareModel,
|
|
947
|
+
providerKey: opts.apiKey,
|
|
948
|
+
prompt: opts.prompt,
|
|
949
|
+
system: opts.system,
|
|
950
|
+
messages: opts.messages,
|
|
951
|
+
maxTokens: opts.maxTokens,
|
|
952
|
+
stream: true,
|
|
953
|
+
onStream: opts.onDelta,
|
|
954
|
+
signal: opts.signal,
|
|
955
|
+
});
|
|
956
|
+
}
|
|
957
|
+
|
|
870
958
|
const common = {
|
|
871
959
|
baseURL: opts.cfg.baseURL,
|
|
872
960
|
apiKey: opts.apiKey,
|
|
@@ -887,7 +975,14 @@ function createLocalEngine({ aegis, settings, ollama, providers, tools, promptBu
|
|
|
887
975
|
async function chat(payload, onDelta) {
|
|
888
976
|
const cls = payload && payload.class;
|
|
889
977
|
const model = payload && payload.model;
|
|
890
|
-
|
|
978
|
+
// byok's model id is "provider:model" (see splitByokModel) — strip the
|
|
979
|
+
// provider prefix before pattern-matching, or a DeepSeek reasoning model
|
|
980
|
+
// selected under byok ("byok" model "deepseek:deepseek-v4-flash") never
|
|
981
|
+
// matches DEEPSEEK_REASONING_MODEL_RE's anchored pattern and silently
|
|
982
|
+
// gets no stated budget, which is exactly the "hidden CoT ate the whole
|
|
983
|
+
// default and returned nothing" failure this budget exists to prevent.
|
|
984
|
+
const reasoningModelId = cls === 'byok' ? splitByokModel(model).model : model;
|
|
985
|
+
const maxTokens = reasoningBudget(cls, reasoningModelId, payload && payload.maxTokens, payload && payload.effort);
|
|
891
986
|
// The caller's OWN number, kept apart from `maxTokens` above. That one
|
|
892
987
|
// collapses two different facts into a single value — "the caller stated
|
|
893
988
|
// 4096" and "effort implies 32768" — and the pooled path must treat them
|
|
@@ -937,8 +1032,12 @@ function createLocalEngine({ aegis, settings, ollama, providers, tools, promptBu
|
|
|
937
1032
|
const signal = controller.signal;
|
|
938
1033
|
|
|
939
1034
|
// A caller can opt out of the agent loop entirely (`tools: false`) and get
|
|
940
|
-
// the old single-shot turn back.
|
|
941
|
-
|
|
1035
|
+
// the old single-shot turn back. byok is ALWAYS single-shot: the
|
|
1036
|
+
// stateless relay (aegis.byokChatCompletion, /api/v1/byok/chat/completions)
|
|
1037
|
+
// has no tools/tool_choice parameter at all, so sending schemas here would
|
|
1038
|
+
// build a prompt promising tool access the transport silently drops —
|
|
1039
|
+
// the model would reason about exec/readFile and never see a result.
|
|
1040
|
+
const toolsEnabled = cls !== 'byok' && !(payload && payload.tools === false);
|
|
942
1041
|
const wire = cls === 'anthropic' ? 'anthropic' : 'openai';
|
|
943
1042
|
const toolSchemas = toolsEnabled ? T.toolsFor(wire, { includeSubagent: depth < MAX_SUBAGENT_DEPTH }) : [];
|
|
944
1043
|
const toolChoice = (payload && payload.toolChoice) || null;
|
|
@@ -965,8 +1064,35 @@ function createLocalEngine({ aegis, settings, ollama, providers, tools, promptBu
|
|
|
965
1064
|
const turnCwd = envFor(payload).cwd;
|
|
966
1065
|
|
|
967
1066
|
try {
|
|
968
|
-
|
|
969
|
-
|
|
1067
|
+
// byok's settings row lives under the provider named IN THE MODEL id
|
|
1068
|
+
// ("anthropic:claude-sonnet-5" -> byokNamespace('anthropic')), never
|
|
1069
|
+
// under the literal class name — one flat 'byok' row could not hold
|
|
1070
|
+
// more than one provider's key at a time.
|
|
1071
|
+
const byokParts = cls === 'byok' ? splitByokModel(model) : null;
|
|
1072
|
+
const cfg =
|
|
1073
|
+
cls === 'aegis' || cls === 'ollama' ? {}
|
|
1074
|
+
: cls === 'byok' ? settings.get(byokNamespace(byokParts.provider)) || {}
|
|
1075
|
+
: settings.get(cls) || {};
|
|
1076
|
+
const apiKey =
|
|
1077
|
+
cls === 'aegis' || cls === 'ollama' ? null
|
|
1078
|
+
: cls === 'byok' ? settings.rawKey(byokNamespace(byokParts.provider))
|
|
1079
|
+
: settings.rawKey(cls);
|
|
1080
|
+
|
|
1081
|
+
if (cls === 'byok' && (!byokParts.provider || !byokParts.model)) {
|
|
1082
|
+
const err = new Error(
|
|
1083
|
+
"byok: model must be \"<provider>:<model>\" (pick one from /models) — got " +
|
|
1084
|
+
JSON.stringify(model || '')
|
|
1085
|
+
);
|
|
1086
|
+
err.status = 400;
|
|
1087
|
+
throw err;
|
|
1088
|
+
}
|
|
1089
|
+
if (cls === 'byok' && !apiKey) {
|
|
1090
|
+
const err = new Error(
|
|
1091
|
+
`byok: no key saved for "${byokParts.provider}" — add one before chatting with this model.`
|
|
1092
|
+
);
|
|
1093
|
+
err.status = 400;
|
|
1094
|
+
throw err;
|
|
1095
|
+
}
|
|
970
1096
|
|
|
971
1097
|
// Custom classes carry no enumerable model list (see listModels), so a
|
|
972
1098
|
// blank id here means the user never typed one. Fail loudly in-process
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aegis-desktop",
|
|
3
3
|
"productName": "AEGIS Desktop",
|
|
4
|
-
"version": "0.7.
|
|
4
|
+
"version": "0.7.7",
|
|
5
5
|
"description": "Thin Electron host for AEGIS — a local chat UI over the shared client/aegis.js transport. Ships transport + UI only; engine logic stays server-side.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "AEGIS Code",
|
package/renderer/app.js
CHANGED
|
@@ -2658,6 +2658,12 @@ async function loadModels(cls) {
|
|
|
2658
2658
|
hint = null; // carries a link, built below
|
|
2659
2659
|
} else if (!list.length) {
|
|
2660
2660
|
hint = cls === 'ollama' ? 'Ollama not running or no models pulled.' : 'No models listed.';
|
|
2661
|
+
} else if (cls === 'byok' && data && data.needsProviderKey) {
|
|
2662
|
+
// Unlike the pooled 'aegis' class, byok still shows every model here —
|
|
2663
|
+
// the catalog answers with no key at all — but none of them are
|
|
2664
|
+
// usable until a provider key is saved in Provider settings below.
|
|
2665
|
+
hint = `${list.length} model${list.length === 1 ? '' : 's'} available — ` +
|
|
2666
|
+
'add a provider key in Provider settings below to use one.';
|
|
2661
2667
|
} else {
|
|
2662
2668
|
hint = `${list.length} model${list.length === 1 ? '' : 's'} available.`;
|
|
2663
2669
|
}
|
|
@@ -2718,6 +2724,71 @@ function applyCustomPreset(cls, modelId) {
|
|
|
2718
2724
|
|
|
2719
2725
|
// -------------------------------------------------------------- settings pane
|
|
2720
2726
|
|
|
2727
|
+
/**
|
|
2728
|
+
* One provider-settings row: name, an optional base-URL field, a key input,
|
|
2729
|
+
* a status label and Save/Remove buttons wired to the generic
|
|
2730
|
+
* `models.settings.*` surface. Shared by the two custom endpoints (which
|
|
2731
|
+
* need a base URL) and the byok providers (which do not — the server
|
|
2732
|
+
* dictates the endpoint; only the key is theirs to set).
|
|
2733
|
+
*/
|
|
2734
|
+
function buildSettingRow({ provider, name, cfg, showBaseURL, onSave, onRemove }) {
|
|
2735
|
+
const row = document.createElement('div');
|
|
2736
|
+
row.className = 'setting-row';
|
|
2737
|
+
// Targeted by applyCustomPreset() so picking a Model-card preset can
|
|
2738
|
+
// quick-fill the matching base URL here without a full loadSettings()
|
|
2739
|
+
// round trip.
|
|
2740
|
+
row.dataset.provider = provider;
|
|
2741
|
+
|
|
2742
|
+
const label = document.createElement('div');
|
|
2743
|
+
label.className = 'setting-name';
|
|
2744
|
+
label.textContent = name;
|
|
2745
|
+
row.appendChild(label);
|
|
2746
|
+
|
|
2747
|
+
let baseInput = null;
|
|
2748
|
+
if (showBaseURL) {
|
|
2749
|
+
baseInput = document.createElement('input');
|
|
2750
|
+
baseInput.type = 'text';
|
|
2751
|
+
baseInput.className = 'setting-input setting-base';
|
|
2752
|
+
baseInput.placeholder = 'base URL';
|
|
2753
|
+
baseInput.value = cfg.baseURL || '';
|
|
2754
|
+
row.appendChild(baseInput);
|
|
2755
|
+
}
|
|
2756
|
+
|
|
2757
|
+
const keyInput = document.createElement('input');
|
|
2758
|
+
keyInput.type = 'password';
|
|
2759
|
+
keyInput.className = 'setting-input';
|
|
2760
|
+
keyInput.placeholder = cfg.configured
|
|
2761
|
+
? `key ${cfg.keyMask} (blank = keep)`
|
|
2762
|
+
: 'API key';
|
|
2763
|
+
row.appendChild(keyInput);
|
|
2764
|
+
|
|
2765
|
+
const status = document.createElement('div');
|
|
2766
|
+
status.className = 'setting-status';
|
|
2767
|
+
status.textContent = cfg.configured ? `configured (${cfg.keyMask})` : 'no key';
|
|
2768
|
+
row.appendChild(status);
|
|
2769
|
+
|
|
2770
|
+
const actions = document.createElement('div');
|
|
2771
|
+
actions.className = 'setting-actions';
|
|
2772
|
+
|
|
2773
|
+
const saveBtn = document.createElement('button');
|
|
2774
|
+
saveBtn.type = 'button';
|
|
2775
|
+
saveBtn.className = 'ghost-btn';
|
|
2776
|
+
saveBtn.textContent = 'Save';
|
|
2777
|
+
saveBtn.addEventListener('click', () => onSave(baseInput ? baseInput.value.trim() : '', keyInput.value));
|
|
2778
|
+
actions.appendChild(saveBtn);
|
|
2779
|
+
|
|
2780
|
+
const removeBtn = document.createElement('button');
|
|
2781
|
+
removeBtn.type = 'button';
|
|
2782
|
+
removeBtn.className = 'ghost-btn danger';
|
|
2783
|
+
removeBtn.textContent = 'Remove';
|
|
2784
|
+
removeBtn.disabled = !cfg.configured;
|
|
2785
|
+
removeBtn.addEventListener('click', onRemove);
|
|
2786
|
+
actions.appendChild(removeBtn);
|
|
2787
|
+
|
|
2788
|
+
row.appendChild(actions);
|
|
2789
|
+
return row;
|
|
2790
|
+
}
|
|
2791
|
+
|
|
2721
2792
|
async function loadSettings() {
|
|
2722
2793
|
els.settingsList.innerHTML = '';
|
|
2723
2794
|
let settings = [];
|
|
@@ -2742,61 +2813,38 @@ async function loadSettings() {
|
|
|
2742
2813
|
configured: false,
|
|
2743
2814
|
keyMask: null,
|
|
2744
2815
|
};
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
const actions = document.createElement('div');
|
|
2779
|
-
actions.className = 'setting-actions';
|
|
2780
|
-
|
|
2781
|
-
const saveBtn = document.createElement('button');
|
|
2782
|
-
saveBtn.type = 'button';
|
|
2783
|
-
saveBtn.className = 'ghost-btn';
|
|
2784
|
-
saveBtn.textContent = 'Save';
|
|
2785
|
-
saveBtn.addEventListener('click', () =>
|
|
2786
|
-
saveSetting(provider, baseInput.value.trim(), keyInput.value)
|
|
2787
|
-
);
|
|
2788
|
-
actions.appendChild(saveBtn);
|
|
2789
|
-
|
|
2790
|
-
const removeBtn = document.createElement('button');
|
|
2791
|
-
removeBtn.type = 'button';
|
|
2792
|
-
removeBtn.className = 'ghost-btn danger';
|
|
2793
|
-
removeBtn.textContent = 'Remove';
|
|
2794
|
-
removeBtn.disabled = !cfg.configured;
|
|
2795
|
-
removeBtn.addEventListener('click', () => removeSetting(provider));
|
|
2796
|
-
actions.appendChild(removeBtn);
|
|
2797
|
-
|
|
2798
|
-
row.appendChild(actions);
|
|
2799
|
-
els.settingsList.appendChild(row);
|
|
2816
|
+
els.settingsList.appendChild(buildSettingRow({
|
|
2817
|
+
provider, name, cfg, showBaseURL: true,
|
|
2818
|
+
onSave: (baseURL, key) => saveSetting(provider, baseURL, key),
|
|
2819
|
+
onRemove: () => removeSetting(provider),
|
|
2820
|
+
}));
|
|
2821
|
+
}
|
|
2822
|
+
|
|
2823
|
+
// byok: one row per provider the server's catalog names (GET
|
|
2824
|
+
// /api/v1/byok/providers via the engine's listModels('byok')), not a fixed
|
|
2825
|
+
// pair like the two custom endpoints above — the catalog is the source of
|
|
2826
|
+
// truth so a provider added server-side shows up here with no client
|
|
2827
|
+
// release. No base-URL field: byok always talks to AEGIS's own relay
|
|
2828
|
+
// (/api/v1/byok/chat/completions), which is what attaches the AEGIS key
|
|
2829
|
+
// and makes the call billable — the provider key typed here authenticates
|
|
2830
|
+
// to the UPSTREAM provider only.
|
|
2831
|
+
try {
|
|
2832
|
+
const byokData = await models.listModels('byok');
|
|
2833
|
+
const byokProviders = Array.isArray(byokData && byokData.providers) ? byokData.providers : [];
|
|
2834
|
+
for (const p of byokProviders) {
|
|
2835
|
+
if (!p || !p.id) continue;
|
|
2836
|
+
const provider = `byok:${p.id}`;
|
|
2837
|
+
const local = settings.find((s) => s.provider === provider) || {
|
|
2838
|
+
provider, baseURL: '', configured: false, keyMask: null,
|
|
2839
|
+
};
|
|
2840
|
+
els.settingsList.appendChild(buildSettingRow({
|
|
2841
|
+
provider, name: `BYOK: ${p.label || p.id}`, cfg: local, showBaseURL: false,
|
|
2842
|
+
onSave: (_baseURL, key) => saveSetting(provider, '', key),
|
|
2843
|
+
onRemove: () => removeSetting(provider),
|
|
2844
|
+
}));
|
|
2845
|
+
}
|
|
2846
|
+
} catch {
|
|
2847
|
+
/* catalog unreachable (offline, server down) — the two custom rows above still work */
|
|
2800
2848
|
}
|
|
2801
2849
|
}
|
|
2802
2850
|
|