pikiloom 0.4.26 → 0.4.28
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/dashboard/dist/assets/{AgentTab-BNJ73P0m.js → AgentTab-DtjuI8NU.js} +1 -1
- package/dashboard/dist/assets/ConnectionModal-BIx6a5o1.js +1 -0
- package/dashboard/dist/assets/{DirBrowser-D-GT-G-w.js → DirBrowser-BINh_7NB.js} +1 -1
- package/dashboard/dist/assets/{ExtensionsTab-BrJMulJR.js → ExtensionsTab-CHpd147b.js} +1 -1
- package/dashboard/dist/assets/{IMAccessTab-auPnx468.js → IMAccessTab-C4DbGxbG.js} +1 -1
- package/dashboard/dist/assets/{Modal-lYfO9m-I.js → Modal-D_PUi-5O.js} +1 -1
- package/dashboard/dist/assets/{Modals-PRdWOIY9.js → Modals-BahDEMqf.js} +1 -1
- package/dashboard/dist/assets/{Select-BhbU-rll.js → Select-Cp5VXb_2.js} +1 -1
- package/dashboard/dist/assets/SessionPanel-DZVNXptx.js +1 -0
- package/dashboard/dist/assets/SystemTab-CLznS31f.js +1 -0
- package/dashboard/dist/assets/index-CLGovkkw.js +3 -0
- package/dashboard/dist/assets/index-CXIN3nTr.css +1 -0
- package/dashboard/dist/assets/index-DcYOI6rL.js +23 -0
- package/dashboard/dist/assets/{shared-C5uXvhZJ.js → shared-C8DCTJ44.js} +1 -1
- package/dashboard/dist/index.html +2 -2
- package/dist/agent/acp-client.js +0 -59
- package/dist/agent/artifacts.js +1 -38
- package/dist/agent/auto-update.js +2 -78
- package/dist/agent/await-resume.js +0 -20
- package/dist/agent/cli/auth.js +3 -51
- package/dist/agent/cli/catalog.js +0 -3
- package/dist/agent/cli/detector.js +1 -19
- package/dist/agent/cli/index.js +0 -3
- package/dist/agent/cli/registry.js +0 -25
- package/dist/agent/driver.js +0 -14
- package/dist/agent/drivers/claude-tui.js +2 -769
- package/dist/agent/drivers/claude.js +12 -631
- package/dist/agent/drivers/codex.js +11 -162
- package/dist/agent/drivers/gemini.js +6 -114
- package/dist/agent/drivers/hermes.js +0 -128
- package/dist/agent/goal.js +0 -45
- package/dist/agent/handover.js +1 -46
- package/dist/agent/images.js +7 -133
- package/dist/agent/index.js +1 -29
- package/dist/agent/mcp/bridge.js +1 -197
- package/dist/agent/mcp/extensions.js +5 -84
- package/dist/agent/mcp/oauth.js +2 -69
- package/dist/agent/mcp/registry.js +2 -33
- package/dist/agent/mcp/session-server.js +2 -45
- package/dist/agent/mcp/tools/ask-user.js +0 -6
- package/dist/agent/mcp/tools/await-resume.js +0 -15
- package/dist/agent/mcp/tools/goal.js +0 -25
- package/dist/agent/mcp/tools/types.js +0 -5
- package/dist/agent/mcp/tools/workspace.js +0 -23
- package/dist/agent/npm.js +0 -14
- package/dist/agent/session.js +86 -196
- package/dist/agent/skill-installer.js +3 -70
- package/dist/agent/skills.js +0 -43
- package/dist/agent/stream.js +5 -148
- package/dist/agent/types.js +0 -12
- package/dist/agent/utils.js +2 -120
- package/dist/bot/bot.js +3 -468
- package/dist/bot/command-ui.js +0 -45
- package/dist/bot/commands.js +0 -51
- package/dist/bot/headless-bot.js +0 -21
- package/dist/bot/host.js +1 -5
- package/dist/bot/human-loop.js +0 -8
- package/dist/bot/menu.js +0 -3
- package/dist/bot/orchestration.js +0 -3
- package/dist/bot/render-shared.js +0 -63
- package/dist/bot/session-hub.js +14 -92
- package/dist/bot/session-status.js +1 -5
- package/dist/bot/streaming.js +0 -53
- package/dist/browser-profile.js +0 -51
- package/dist/browser-supervisor.js +0 -75
- package/dist/catalog/cli-tools.js +0 -53
- package/dist/catalog/index.js +0 -18
- package/dist/catalog/local-models.js +0 -18
- package/dist/catalog/mcp-servers.js +0 -35
- package/dist/catalog/skill-repos.js +0 -32
- package/dist/channels/base.js +0 -19
- package/dist/channels/dingtalk/bot.js +0 -3
- package/dist/channels/dingtalk/channel.js +0 -33
- package/dist/channels/discord/bot.js +0 -7
- package/dist/channels/discord/channel.js +0 -22
- package/dist/channels/feishu/bot.js +0 -71
- package/dist/channels/feishu/channel.js +0 -78
- package/dist/channels/feishu/markdown.js +0 -5
- package/dist/channels/feishu/render.js +0 -45
- package/dist/channels/health.js +0 -46
- package/dist/channels/slack/bot.js +0 -7
- package/dist/channels/slack/channel.js +1 -27
- package/dist/channels/states.js +0 -3
- package/dist/channels/telegram/bot.js +0 -60
- package/dist/channels/telegram/channel.js +2 -99
- package/dist/channels/telegram/directory.js +0 -3
- package/dist/channels/telegram/live-preview.js +0 -8
- package/dist/channels/telegram/render.js +0 -30
- package/dist/channels/wecom/bot.js +0 -3
- package/dist/channels/wecom/channel.js +0 -58
- package/dist/channels/weixin/api.js +0 -9
- package/dist/channels/weixin/bot.js +0 -88
- package/dist/channels/weixin/channel.js +0 -6
- package/dist/cli/autostart.js +1 -44
- package/dist/cli/channel-supervisor.js +1 -59
- package/dist/cli/channels.js +0 -6
- package/dist/cli/main.js +1 -145
- package/dist/cli/onboarding.js +0 -6
- package/dist/cli/run.js +0 -11
- package/dist/cli/setup-wizard.js +0 -3
- package/dist/core/config/runtime-config.js +0 -38
- package/dist/core/config/user-config.js +0 -63
- package/dist/core/config/validation.js +0 -9
- package/dist/core/constants.js +0 -268
- package/dist/core/git.js +0 -31
- package/dist/core/legacy-compat.js +1 -28
- package/dist/core/logging.js +0 -3
- package/dist/core/platform.js +0 -28
- package/dist/core/process-control.js +0 -8
- package/dist/core/secrets/index.js +0 -17
- package/dist/core/secrets/inline-seal.js +0 -7
- package/dist/core/secrets/ref.js +0 -19
- package/dist/core/secrets/resolver.js +0 -8
- package/dist/core/secrets/store.js +1 -12
- package/dist/core/utils.js +1 -37
- package/dist/core/version.js +0 -3
- package/dist/dashboard/platform.js +1 -35
- package/dist/dashboard/routes/agents.js +2 -71
- package/dist/dashboard/routes/cli.js +0 -27
- package/dist/dashboard/routes/config.js +0 -54
- package/dist/dashboard/routes/extensions.js +0 -132
- package/dist/dashboard/routes/local-models.js +0 -68
- package/dist/dashboard/routes/models.js +0 -46
- package/dist/dashboard/routes/sessions.js +0 -100
- package/dist/dashboard/runtime.js +1 -58
- package/dist/dashboard/server.js +0 -49
- package/dist/dashboard/session-control.js +2 -69
- package/dist/model/catalog.js +1 -17
- package/dist/model/index.js +0 -15
- package/dist/model/injector.js +1 -181
- package/dist/model/provider-models.js +1 -48
- package/dist/model/responses-bridge.js +3 -45
- package/dist/model/store.js +0 -17
- package/dist/model/types.js +0 -12
- package/dist/model/validation.js +1 -18
- package/dist/pikichannel/adapter-pikiloom.js +0 -31
- package/dist/pikichannel/code.js +0 -12
- package/dist/pikichannel/codec.js +0 -14
- package/dist/pikichannel/host.js +4 -33
- package/dist/pikichannel/protocol.js +0 -52
- package/dist/pikichannel/rendezvous-broker.js +5 -26
- package/dist/pikichannel/rendezvous-host.js +3 -16
- package/dist/pikichannel/server.js +6 -55
- package/dist/pikichannel/transport.js +0 -25
- package/dist/pikichannel/transports/webrtc-host.js +0 -18
- package/dist/pikichannel/transports/webrtc-shared.js +4 -34
- package/dist/pikichannel/transports/websocket-host.js +1 -14
- package/dist/pikichannel/turn.js +2 -69
- package/dist/pikichannel/web/sdk.js +14 -71
- package/package.json +1 -1
- package/dashboard/dist/assets/ConnectionModal-Dt05H2f9.js +0 -1
- package/dashboard/dist/assets/SessionPanel-_0dLWvEb.js +0 -1
- package/dashboard/dist/assets/SystemTab-CN_QrRKt.js +0 -1
- package/dashboard/dist/assets/index-CP3LYJR2.js +0 -23
- package/dashboard/dist/assets/index-C_neLnaO.js +0 -3
- package/dashboard/dist/assets/index-CyCspgtc.css +0 -1
|
@@ -1,23 +1,9 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Public session task control surface for dashboard and API routes.
|
|
3
|
-
*/
|
|
4
1
|
import path from 'node:path';
|
|
5
2
|
import { getProjectSkillPaths, listSkills, stageSessionFiles, ensureManagedSession, findPikiloomSession, getDriverCapabilities, isPendingSessionId } from '../agent/index.js';
|
|
6
3
|
import { loadUserConfig } from '../core/config/user-config.js';
|
|
7
4
|
import { decomposeEffortSelection } from '../core/config/runtime-config.js';
|
|
8
5
|
import { runtime } from './runtime.js';
|
|
9
6
|
const KNOWN_AGENTS = new Set(['claude', 'codex', 'gemini', 'hermes']);
|
|
10
|
-
/**
|
|
11
|
-
* Parse a `/goal[ args]` prompt typed in the dashboard chat box. Returns null
|
|
12
|
-
* when the prompt is not a goal slash command. Sub-commands mirror the IM
|
|
13
|
-
* `handleGoalCommand` semantics (set / clear / pause / resume / status).
|
|
14
|
-
*
|
|
15
|
-
* Routing /goal through the native bridge is the dashboard's analog of what
|
|
16
|
-
* channels/{telegram,feishu,weixin}/bot.ts do via `handleGoalCommand` — before
|
|
17
|
-
* this hook, dashboard /goal was matched by the legacy `goal` skill resolver
|
|
18
|
-
* and silently rewritten to "Read SKILL.md and execute", which bypassed both
|
|
19
|
-
* the claude native /goal slash command and codex's thread/goal RPC.
|
|
20
|
-
*/
|
|
21
7
|
function parseGoalSlash(prompt) {
|
|
22
8
|
const trimmed = prompt.trim();
|
|
23
9
|
const m = trimmed.match(/^\/goal(?:\s+([\s\S]*))?$/);
|
|
@@ -35,22 +21,16 @@ function parseGoalSlash(prompt) {
|
|
|
35
21
|
return { action: 'resume', objective: '' };
|
|
36
22
|
return { action: 'set', objective: args };
|
|
37
23
|
}
|
|
38
|
-
/**
|
|
39
|
-
* Resolve a `/skill-name [args]` prompt into the full skill execution prompt.
|
|
40
|
-
* Returns null if the prompt is not a skill invocation or the skill is not found.
|
|
41
|
-
*/
|
|
42
24
|
function resolveSkillFromPrompt(workdir, prompt) {
|
|
43
25
|
const trimmed = prompt.trim();
|
|
44
26
|
if (!trimmed.startsWith('/'))
|
|
45
27
|
return null;
|
|
46
|
-
// Extract command name and args: "/skill-name some args" → name="skill-name", args="some args"
|
|
47
28
|
const match = trimmed.match(/^\/([^\s]+)(?:\s+(.*))?$/s);
|
|
48
29
|
if (!match)
|
|
49
30
|
return null;
|
|
50
31
|
const name = match[1];
|
|
51
32
|
const args = (match[2] || '').trim();
|
|
52
33
|
const { skills } = listSkills(workdir);
|
|
53
|
-
// Match by exact skill name (case-insensitive)
|
|
54
34
|
const skill = skills.find(s => s.name.toLowerCase() === name.toLowerCase());
|
|
55
35
|
if (!skill)
|
|
56
36
|
return null;
|
|
@@ -62,12 +42,6 @@ function resolveSkillFromPrompt(workdir, prompt) {
|
|
|
62
42
|
const resolvedPrompt = `${workdirHint}Read the skill definition at \`${targetPath}\` and execute the instructions defined there.${extra}`;
|
|
63
43
|
return { resolvedPrompt, skillName: skill.name };
|
|
64
44
|
}
|
|
65
|
-
/**
|
|
66
|
-
* Resolve a `handoverFrom` ref from the request's `previousAgent` /
|
|
67
|
-
* `previousSessionId` fields, validating that it points to a real, non-self,
|
|
68
|
-
* different-agent session managed by pikiloom. Returns null when the inputs
|
|
69
|
-
* are absent or invalid — handover is best-effort and silent-skip on bad data.
|
|
70
|
-
*/
|
|
71
45
|
function resolveHandoverFrom(request, targetAgent) {
|
|
72
46
|
const prevAgent = typeof request.previousAgent === 'string' ? request.previousAgent.trim() : '';
|
|
73
47
|
const prevSessionId = typeof request.previousSessionId === 'string' ? request.previousSessionId.trim() : '';
|
|
@@ -76,9 +50,9 @@ function resolveHandoverFrom(request, targetAgent) {
|
|
|
76
50
|
if (!KNOWN_AGENTS.has(prevAgent))
|
|
77
51
|
return null;
|
|
78
52
|
if (prevAgent === targetAgent)
|
|
79
|
-
return null;
|
|
53
|
+
return null;
|
|
80
54
|
if (isPendingSessionId(prevSessionId))
|
|
81
|
-
return null;
|
|
55
|
+
return null;
|
|
82
56
|
const record = findPikiloomSession(request.workdir, prevAgent, prevSessionId);
|
|
83
57
|
if (!record)
|
|
84
58
|
return null;
|
|
@@ -96,21 +70,13 @@ export async function queueDashboardSessionTask(request) {
|
|
|
96
70
|
? request.agent
|
|
97
71
|
: runtime.getRuntimeDefaultAgent(config);
|
|
98
72
|
const modelId = typeof request.model === 'string' ? request.model.trim() : '';
|
|
99
|
-
// "ultra" is a synthetic effort rung = max depth + Workflow orchestration;
|
|
100
|
-
// decompose it so the spawn carries a real --effort value plus the workflow
|
|
101
|
-
// flag (the per-send pick is the single knob — no separate workflow control).
|
|
102
73
|
const { effort: splitEffort, workflow: ultraWorkflow } = decomposeEffortSelection(typeof request.effort === 'string' ? request.effort : '');
|
|
103
74
|
const thinkingEffort = resolvedAgent === 'gemini' ? '' : splitEffort;
|
|
104
75
|
const workflowEnabled = ultraWorkflow || request.workflow === true;
|
|
105
|
-
// /goal — route directly to the goal bridge (claude native slash, codex RPC,
|
|
106
|
-
// or portable goal.json for gemini/hermes). Must run BEFORE skill resolution
|
|
107
|
-
// so the legacy `goal` skill doesn't grab the prompt and rewrite it into a
|
|
108
|
-
// "Read SKILL.md" instruction.
|
|
109
76
|
const goalCmd = parseGoalSlash(request.prompt || '');
|
|
110
77
|
if (goalCmd && request.sessionId && !isPendingSessionId(request.sessionId)) {
|
|
111
78
|
return runDashboardGoalSlash(bot, resolvedAgent, request, goalCmd, modelId, thinkingEffort);
|
|
112
79
|
}
|
|
113
|
-
// Resolve /skill-name prompts into full skill execution prompts
|
|
114
80
|
let prompt = request.prompt;
|
|
115
81
|
const skillResult = prompt ? resolveSkillFromPrompt(request.workdir, prompt) : null;
|
|
116
82
|
if (skillResult) {
|
|
@@ -119,13 +85,8 @@ export async function queueDashboardSessionTask(request) {
|
|
|
119
85
|
}
|
|
120
86
|
let sessionId = request.sessionId;
|
|
121
87
|
let attachments = request.attachments || [];
|
|
122
|
-
// Resolve handover source. Only meaningful when we're about to stage a fresh
|
|
123
|
-
// session (sessionId blank or pending). For an existing session we never
|
|
124
|
-
// replay handover — that session's own --resume history is canonical.
|
|
125
88
|
const isFreshSession = !sessionId || isPendingSessionId(sessionId);
|
|
126
89
|
const handoverFrom = isFreshSession ? resolveHandoverFrom(request, resolvedAgent) : null;
|
|
127
|
-
// Stage files into the session workspace so temp uploads survive cleanup.
|
|
128
|
-
// Also creates a new pending session when no sessionId is provided.
|
|
129
90
|
if (!sessionId || attachments.length) {
|
|
130
91
|
const staged = stageSessionFiles({
|
|
131
92
|
agent: resolvedAgent,
|
|
@@ -150,9 +111,6 @@ export async function queueDashboardSessionTask(request) {
|
|
|
150
111
|
attachments,
|
|
151
112
|
...(modelId ? { modelId } : {}),
|
|
152
113
|
...(thinkingEffort ? { thinkingEffort } : {}),
|
|
153
|
-
// Always thread the per-send workflow choice (even when false) so the run
|
|
154
|
-
// explicitly reflects the picked rung (Ultra ⇒ on) rather than any ambient
|
|
155
|
-
// default.
|
|
156
114
|
workflowEnabled,
|
|
157
115
|
...(handoverFrom ? { handoverFrom } : {}),
|
|
158
116
|
});
|
|
@@ -160,11 +118,6 @@ export async function queueDashboardSessionTask(request) {
|
|
|
160
118
|
async function runDashboardGoalSlash(bot, agent, request, cmd, modelId, thinkingEffort) {
|
|
161
119
|
const opts = { chatId: 'dashboard', modelId: modelId || undefined, thinkingEffort: thinkingEffort || undefined };
|
|
162
120
|
const sessionKey = `${agent}:${request.sessionId}`;
|
|
163
|
-
// Synthetic task id — for set / clear / resume on agents that internally
|
|
164
|
-
// submit a follow-up task (claude native slash, portable continuation),
|
|
165
|
-
// the real task id is owned by submitSessionTask. The dashboard's SSE
|
|
166
|
-
// stream listener picks that up via session events; this id is just to
|
|
167
|
-
// give the HTTP caller a non-empty taskId field.
|
|
168
121
|
const taskId = `goal-${cmd.action}-${Date.now().toString(36)}`;
|
|
169
122
|
try {
|
|
170
123
|
if (cmd.action === 'status') {
|
|
@@ -183,7 +136,6 @@ async function runDashboardGoalSlash(bot, agent, request, cmd, modelId, thinking
|
|
|
183
136
|
const goal = await bot.resumeSessionGoal(request.workdir, agent, request.sessionId, opts);
|
|
184
137
|
return { ok: true, taskId, sessionKey, queued: false, goal };
|
|
185
138
|
}
|
|
186
|
-
// set
|
|
187
139
|
const goal = await bot.setSessionGoal(request.workdir, agent, request.sessionId, {
|
|
188
140
|
objective: cmd.objective,
|
|
189
141
|
...opts,
|
|
@@ -209,26 +161,17 @@ export function forkDashboardSessionTask(request) {
|
|
|
209
161
|
return { ok: false, error: `Agent ${agent} does not support fork` };
|
|
210
162
|
}
|
|
211
163
|
const modelId = typeof request.model === 'string' ? request.model.trim() : '';
|
|
212
|
-
// Same "ultra" decomposition as the send path — a forked turn launched at
|
|
213
|
-
// Ultra inherits max depth + Workflow orchestration.
|
|
214
164
|
const { effort: splitEffort, workflow: ultraWorkflow } = decomposeEffortSelection(typeof request.effort === 'string' ? request.effort : '');
|
|
215
165
|
const thinkingEffort = agent === 'gemini' ? '' : splitEffort;
|
|
216
|
-
// Resolve /skill-name shorthand the same way send/queue does, so a forked
|
|
217
|
-
// turn that starts with `/skill-name` runs the skill against the child.
|
|
218
166
|
let prompt = request.prompt;
|
|
219
167
|
const skillResult = prompt ? resolveSkillFromPrompt(request.workdir, prompt) : null;
|
|
220
168
|
if (skillResult)
|
|
221
169
|
prompt = skillResult.resolvedPrompt;
|
|
222
|
-
// Make sure the parent has a managed record so `recordFork` (called after the
|
|
223
|
-
// child stream completes) can write the lineage on both sides. Native-only
|
|
224
|
-
// sessions (started outside pikiloom) won't have a record yet.
|
|
225
170
|
ensureManagedSession({
|
|
226
171
|
agent,
|
|
227
172
|
workdir: request.workdir,
|
|
228
173
|
sessionId: request.parentSessionId,
|
|
229
174
|
});
|
|
230
|
-
// Always create a fresh pending session for the child. stageSessionFiles
|
|
231
|
-
// also handles attachment imports into the new workspace.
|
|
232
175
|
const staged = stageSessionFiles({
|
|
233
176
|
agent,
|
|
234
177
|
workdir: request.workdir,
|
|
@@ -265,13 +208,6 @@ export function cancelSessionTask(taskId) {
|
|
|
265
208
|
const result = bot.cancelTask(taskId);
|
|
266
209
|
return { ok: true, recalled: result.cancelled || result.interrupted };
|
|
267
210
|
}
|
|
268
|
-
/**
|
|
269
|
-
* Stop only the currently running task for a session — queued follow-ups are
|
|
270
|
-
* preserved and run normally once the chain advances. Works on (agent,
|
|
271
|
-
* sessionId) rather than a single taskId so it still functions during the
|
|
272
|
-
* brief window after send/before the queued WS snapshot reaches the client.
|
|
273
|
-
* Per-row × buttons (→ cancelSessionTask) cancel one queued entry at a time.
|
|
274
|
-
*/
|
|
275
211
|
export function stopSessionTasks(agent, sessionId) {
|
|
276
212
|
const bot = runtime.getBotRef();
|
|
277
213
|
if (!bot)
|
|
@@ -286,9 +222,6 @@ export async function steerSessionTask(taskId) {
|
|
|
286
222
|
const result = await bot.steerTask(taskId);
|
|
287
223
|
return { ok: true, steered: result.steered };
|
|
288
224
|
}
|
|
289
|
-
// ---------------------------------------------------------------------------
|
|
290
|
-
// Interaction prompt control (human-in-the-loop)
|
|
291
|
-
// ---------------------------------------------------------------------------
|
|
292
225
|
export function interactionSelectOption(promptId, optionValue, opts) {
|
|
293
226
|
const bot = runtime.getBotRef();
|
|
294
227
|
if (!bot)
|
package/dist/model/catalog.js
CHANGED
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* models.dev catalog — read-only metadata about LLM providers and their
|
|
3
|
-
* models (pricing, context window, capabilities). We hit the public JSON
|
|
4
|
-
* endpoint and cache the result locally for 24h, with a fallback to the
|
|
5
|
-
* cached copy when offline.
|
|
6
|
-
*/
|
|
7
1
|
import fs from 'node:fs';
|
|
8
2
|
import os from 'node:os';
|
|
9
3
|
import path from 'node:path';
|
|
@@ -45,10 +39,6 @@ async function fetchFromNetwork() {
|
|
|
45
39
|
const text = await body.text();
|
|
46
40
|
return JSON.parse(text);
|
|
47
41
|
}
|
|
48
|
-
/**
|
|
49
|
-
* Get the catalog. Returns a cached copy if fresh; otherwise fetches in the
|
|
50
|
-
* background and falls back to the stale cache on failure.
|
|
51
|
-
*/
|
|
52
42
|
export async function getModelsDevCatalog(opts = {}) {
|
|
53
43
|
const now = Date.now();
|
|
54
44
|
if (!opts.forceRefresh && memCache && now - memCache.fetchedAt < CACHE_TTL_MS) {
|
|
@@ -70,7 +60,7 @@ export async function getModelsDevCatalog(opts = {}) {
|
|
|
70
60
|
}
|
|
71
61
|
catch (e) {
|
|
72
62
|
if (memCache)
|
|
73
|
-
return memCache.data;
|
|
63
|
+
return memCache.data;
|
|
74
64
|
throw e;
|
|
75
65
|
}
|
|
76
66
|
finally {
|
|
@@ -80,20 +70,14 @@ export async function getModelsDevCatalog(opts = {}) {
|
|
|
80
70
|
}
|
|
81
71
|
return inflight;
|
|
82
72
|
}
|
|
83
|
-
/** Lookup a single provider by its models.dev id (e.g. "openrouter"). */
|
|
84
73
|
export async function getCatalogProvider(providerId) {
|
|
85
74
|
const cat = await getModelsDevCatalog().catch(() => null);
|
|
86
75
|
return cat?.[providerId] || null;
|
|
87
76
|
}
|
|
88
|
-
/** Lookup a model entry within a provider. */
|
|
89
77
|
export async function getCatalogModel(providerId, modelId) {
|
|
90
78
|
const provider = await getCatalogProvider(providerId);
|
|
91
79
|
return provider?.models?.[modelId] || null;
|
|
92
80
|
}
|
|
93
|
-
/**
|
|
94
|
-
* Lightweight search: returns providers whose id/name match the query.
|
|
95
|
-
* If query is empty, returns all providers sorted by id.
|
|
96
|
-
*/
|
|
97
81
|
export async function searchCatalogProviders(query) {
|
|
98
82
|
const cat = await getModelsDevCatalog().catch(() => ({}));
|
|
99
83
|
const all = Object.values(cat);
|
package/dist/model/index.js
CHANGED
|
@@ -1,18 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pikiloom "Model" layer — barrel export.
|
|
3
|
-
*
|
|
4
|
-
* The Model layer is one of the four physical layers in pikiloom's
|
|
5
|
-
* architecture (Terminal / Agent / **Model** / Tool). It centralises:
|
|
6
|
-
* - Provider/Profile data model (types.ts)
|
|
7
|
-
* - Read-only catalog of providers/models from models.dev (catalog.ts)
|
|
8
|
-
* - Persistence (store.ts) over ~/.pikiloom/setting.json
|
|
9
|
-
* - Feishu-style credential validation (validation.ts)
|
|
10
|
-
* - Per-agent credential injection at spawn time (injector.ts)
|
|
11
|
-
*
|
|
12
|
-
* Adding a new agent driver only needs to:
|
|
13
|
-
* 1. Define a new AgentInjector entry in injector.ts
|
|
14
|
-
* 2. Read `resolveAgentInjection(agentId)` before spawning
|
|
15
|
-
*/
|
|
16
1
|
export { getModelsDevCatalog, getCatalogProvider, getCatalogModel, searchCatalogProviders, } from './catalog.js';
|
|
17
2
|
export { listProviders, getProvider, addProvider, updateProvider, removeProvider, setProviderValidation, listProfiles, getProfile, addProfile, updateProfile, removeProfile, getActiveProfileId, getActiveProfile, setActiveProfile, } from './store.js';
|
|
18
3
|
export { validateProvider } from './validation.js';
|
package/dist/model/injector.js
CHANGED
|
@@ -1,20 +1,9 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Credential injector — turn an active Profile into the env vars and
|
|
3
|
-
* additional argv that should be applied when spawning a specific agent.
|
|
4
|
-
*
|
|
5
|
-
* This is the single point where pikiloom's Profile abstraction is
|
|
6
|
-
* translated into per-agent quirks. Adding a new agent (e.g. OpenCode)
|
|
7
|
-
* = adding one entry to AGENT_INJECT_TABLE.
|
|
8
|
-
*/
|
|
9
1
|
import { resolveCredential } from '../core/secrets/index.js';
|
|
10
2
|
import { writeScopedLog } from '../core/logging.js';
|
|
11
3
|
import { getActiveProfile, getProvider } from './store.js';
|
|
12
4
|
import { peekProviderModelInfo, prefetchProviderModels } from './provider-models.js';
|
|
13
5
|
import { ensureResponsesBridge, upstreamToken } from './responses-bridge.js';
|
|
14
6
|
const EMPTY = { env: {}, argvAppend: [], detail: '' };
|
|
15
|
-
// ---------------------------------------------------------------------------
|
|
16
|
-
// Shared host-based provider identification
|
|
17
|
-
// ---------------------------------------------------------------------------
|
|
18
7
|
function providerHost(provider) {
|
|
19
8
|
try {
|
|
20
9
|
return new URL(provider.baseURL).host.toLowerCase();
|
|
@@ -23,12 +12,6 @@ function providerHost(provider) {
|
|
|
23
12
|
return '';
|
|
24
13
|
}
|
|
25
14
|
}
|
|
26
|
-
/**
|
|
27
|
-
* Stable slug used to identify the provider in TOML-style configs (Codex
|
|
28
|
-
* `model_providers.<slug>`, Hermes ACP `<slug>:<model>`). Host-aware so a
|
|
29
|
-
* "DeepSeek personal" provider always resolves to `deepseek`, regardless of
|
|
30
|
-
* its display name.
|
|
31
|
-
*/
|
|
32
15
|
function providerSlug(provider) {
|
|
33
16
|
if (provider.kind === 'anthropic')
|
|
34
17
|
return 'anthropic';
|
|
@@ -55,19 +38,9 @@ function providerSlug(provider) {
|
|
|
55
38
|
return 'doubao';
|
|
56
39
|
if (host.includes('openrouter'))
|
|
57
40
|
return 'openrouter';
|
|
58
|
-
// Unknown host: derive a stable slug from the hostname's leading label. (The
|
|
59
|
-
// old `return 'openrouter'` fallback mis-slugged every unrecognised provider —
|
|
60
|
-
// including localhost Ollama — as openrouter.) This never collides with
|
|
61
|
-
// codex's reserved built-in `openai`/`oss`/`ollama` ids, which are routed
|
|
62
|
-
// before we ever reach providerSlug.
|
|
63
41
|
const label = host.replace(/:\d+$/, '').replace(/^(www|api)\./, '').split('.')[0].replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
|
64
42
|
return label || 'byok';
|
|
65
43
|
}
|
|
66
|
-
/**
|
|
67
|
-
* Canonical env-var name(s) carrying the credential for a provider. Returned
|
|
68
|
-
* as a map so e.g. Google fans out to both `GOOGLE_API_KEY` and
|
|
69
|
-
* `GEMINI_API_KEY` for SDKs that expect either.
|
|
70
|
-
*/
|
|
71
44
|
function providerCredentialEnv(provider, apiKey) {
|
|
72
45
|
if (provider.kind === 'anthropic')
|
|
73
46
|
return { ANTHROPIC_API_KEY: apiKey };
|
|
@@ -96,53 +69,18 @@ function providerCredentialEnv(provider, apiKey) {
|
|
|
96
69
|
return { ARK_API_KEY: apiKey, DOUBAO_API_KEY: apiKey };
|
|
97
70
|
return { OPENROUTER_API_KEY: apiKey };
|
|
98
71
|
}
|
|
99
|
-
/** Pick the single env var Codex's `model_providers.<slug>.env_key` should reference. */
|
|
100
72
|
function codexEnvKey(provider) {
|
|
101
73
|
const keys = Object.keys(providerCredentialEnv(provider, ''));
|
|
102
74
|
return keys[0] || 'OPENROUTER_API_KEY';
|
|
103
75
|
}
|
|
104
|
-
/** Escape a string for embedding inside a TOML double-quoted string literal. */
|
|
105
76
|
function tomlEscape(value) {
|
|
106
77
|
return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
107
78
|
}
|
|
108
|
-
/**
|
|
109
|
-
* OpenAI-compatible providers that expose their Anthropic-protocol API under a
|
|
110
|
-
* dedicated namespace on the SAME origin (not the OpenAI base we store). Keyed
|
|
111
|
-
* by `providerSlug`; the value maps the origin → the Anthropic base URL.
|
|
112
|
-
*
|
|
113
|
-
* slug stored OpenAI baseURL Anthropic base
|
|
114
|
-
* ───────── ─────────────────────────────────── ───────────────────────────────
|
|
115
|
-
* deepseek https://api.deepseek.com https://api.deepseek.com/anthropic
|
|
116
|
-
* kimi https://api.moonshot.cn/v1 https://api.moonshot.cn/anthropic
|
|
117
|
-
* zai https://open.bigmodel.cn/api/paas/v4 https://open.bigmodel.cn/api/anthropic
|
|
118
|
-
*
|
|
119
|
-
* The SDK appends `/v1/messages`, so e.g. DeepSeek lands on
|
|
120
|
-
* `…/anthropic/v1/messages` — the endpoint DeepSeek actually serves the
|
|
121
|
-
* Anthropic protocol on (cf. src/agent/drivers/claude.ts).
|
|
122
|
-
*/
|
|
123
79
|
const ANTHROPIC_ENDPOINT_BY_SLUG = {
|
|
124
80
|
deepseek: origin => `${origin}/anthropic`,
|
|
125
81
|
kimi: origin => `${origin}/anthropic`,
|
|
126
82
|
zai: origin => `${origin}/api/anthropic`,
|
|
127
83
|
};
|
|
128
|
-
/**
|
|
129
|
-
* Anthropic-protocol baseURL for Claude BYOK. Claude Code speaks the Anthropic
|
|
130
|
-
* Messages API and appends `/v1/messages` to `ANTHROPIC_BASE_URL` itself, so
|
|
131
|
-
* the base must point at the provider's *Anthropic-compatible* root.
|
|
132
|
-
*
|
|
133
|
-
* The base must NOT carry a trailing `/v1` (otherwise requests land on
|
|
134
|
-
* `/v1/v1/messages` and 404). The canonical stored form is the OpenAI base
|
|
135
|
-
* (so `validateProvider`'s GET /models and the Codex/Hermes injectors keep
|
|
136
|
-
* working); we translate it here:
|
|
137
|
-
* - Anthropic-native (and Anthropic-shaped third parties): strip a trailing
|
|
138
|
-
* `/v1` and pass through verbatim.
|
|
139
|
-
* - Known OpenAI-compatible providers whose Anthropic API lives under a
|
|
140
|
-
* separate path (DeepSeek `/anthropic`, Kimi `/anthropic`, Zhipu
|
|
141
|
-
* `/api/anthropic`): rebuild from the origin via `ANTHROPIC_ENDPOINT_BY_SLUG`.
|
|
142
|
-
* Rebuilding from the origin (not the stored path) is idempotent — a user
|
|
143
|
-
* who already pasted the `/anthropic` URL doesn't get `/anthropic/anthropic`.
|
|
144
|
-
* - Anything else: best-effort, strip a trailing `/v1` (historical default).
|
|
145
|
-
*/
|
|
146
84
|
function claudeAnthropicBaseURL(provider) {
|
|
147
85
|
const raw = provider.baseURL.replace(/\/+$/, '');
|
|
148
86
|
if (provider.kind === 'anthropic')
|
|
@@ -160,14 +98,6 @@ function claudeAnthropicBaseURL(provider) {
|
|
|
160
98
|
}
|
|
161
99
|
return raw.replace(/\/v1$/, '');
|
|
162
100
|
}
|
|
163
|
-
/**
|
|
164
|
-
* First-party Anthropic = the official API host (`api.anthropic.com` / any
|
|
165
|
-
* `*.anthropic.com`). A Claude route counts as "direct" when it lands here —
|
|
166
|
-
* both the subscription path and an own-key BYOK profile pointed at
|
|
167
|
-
* api.anthropic.com. Everything else (OpenRouter, DeepSeek, domestic series, a
|
|
168
|
-
* self-hosted relay, localhost) is a third-party proxy. Unparseable → treat as
|
|
169
|
-
* proxy (safe default: suppressing attribution is harmless, churning isn't).
|
|
170
|
-
*/
|
|
171
101
|
function isFirstPartyAnthropic(baseURL) {
|
|
172
102
|
let host;
|
|
173
103
|
try {
|
|
@@ -178,17 +108,6 @@ function isFirstPartyAnthropic(baseURL) {
|
|
|
178
108
|
}
|
|
179
109
|
return host === 'anthropic.com' || host.endsWith('.anthropic.com');
|
|
180
110
|
}
|
|
181
|
-
/**
|
|
182
|
-
* Claude Code respects `ANTHROPIC_BASE_URL` + `ANTHROPIC_API_KEY` (or
|
|
183
|
-
* `ANTHROPIC_AUTH_TOKEN`) as a BYOK route. The CLI itself is unchanged.
|
|
184
|
-
* The model is overridden via opts.claudeModel (handled in stream.ts).
|
|
185
|
-
*
|
|
186
|
-
* For OpenAI-compatible providers (OpenRouter, DeepSeek native, …), the
|
|
187
|
-
* baseURL must point to an Anthropic-protocol-compatible endpoint
|
|
188
|
-
* (`/v1/messages`-shaped). `claudeAnthropicBaseURL` translates the stored
|
|
189
|
-
* OpenAI base into the right Anthropic root per provider (e.g. DeepSeek's
|
|
190
|
-
* `https://api.deepseek.com` → `https://api.deepseek.com/anthropic`).
|
|
191
|
-
*/
|
|
192
111
|
const claudeInjector = (provider, profile, apiKey) => {
|
|
193
112
|
if (provider.kind !== 'anthropic' && provider.kind !== 'openai-compatible') {
|
|
194
113
|
return {
|
|
@@ -202,16 +121,6 @@ const claudeInjector = (provider, profile, apiKey) => {
|
|
|
202
121
|
ANTHROPIC_API_KEY: apiKey,
|
|
203
122
|
ANTHROPIC_AUTH_TOKEN: apiKey,
|
|
204
123
|
};
|
|
205
|
-
// Claude Code >= 2.1.36 stamps a per-request `x-anthropic-billing-header`
|
|
206
|
-
// (cc_version / cc_entrypoint / cch=… — the cch token churns every turn).
|
|
207
|
-
// Third-party proxies (OpenRouter, DeepSeek /anthropic, domestic series, any
|
|
208
|
-
// OpenAI-compat or self-hosted Anthropic-shaped front) often key their
|
|
209
|
-
// prefix/KV cache on request headers, so the churn forces a full prompt
|
|
210
|
-
// reprocess every turn — slow and expensive. `0` makes claude omit the header
|
|
211
|
-
// (env-bool: 0/false/no/off). Only on proxy routes: first-party Anthropic
|
|
212
|
-
// (api.anthropic.com — subscription OR own-key direct) is left exactly as
|
|
213
|
-
// shipped; its cache is content/breakpoint based, so attribution is irrelevant
|
|
214
|
-
// there and we don't touch it.
|
|
215
124
|
if (!isFirstPartyAnthropic(baseURL)) {
|
|
216
125
|
env.CLAUDE_CODE_ATTRIBUTION_HEADER = '0';
|
|
217
126
|
}
|
|
@@ -230,43 +139,24 @@ function providerHostname(provider) {
|
|
|
230
139
|
return '';
|
|
231
140
|
}
|
|
232
141
|
}
|
|
233
|
-
/** True for localhost endpoints (Ollama / LM Studio / llama.cpp). */
|
|
234
142
|
function isLocalProvider(provider) {
|
|
235
143
|
const h = providerHostname(provider);
|
|
236
144
|
return h === 'localhost' || h === '127.0.0.1' || h === '0.0.0.0' || h === '::1';
|
|
237
145
|
}
|
|
238
|
-
/** Providers that natively implement the OpenAI Responses API (codex talks to them directly). */
|
|
239
146
|
function isResponsesNativeProvider(provider) {
|
|
240
147
|
return providerHost(provider).includes('openrouter');
|
|
241
148
|
}
|
|
242
|
-
/** codex's built-in local provider id for a localhost endpoint. */
|
|
243
149
|
function codexLocalProvider(provider) {
|
|
244
150
|
let port = '';
|
|
245
151
|
try {
|
|
246
152
|
port = new URL(provider.baseURL).port;
|
|
247
153
|
}
|
|
248
|
-
catch {
|
|
154
|
+
catch { }
|
|
249
155
|
if (port === '1234' || /lm\s*studio/i.test(provider.name))
|
|
250
156
|
return 'lmstudio';
|
|
251
157
|
return 'ollama';
|
|
252
158
|
}
|
|
253
|
-
/** Ollama keeps a prewarmed model resident for this long (its `keep_alive`). */
|
|
254
159
|
const PREWARM_KEEP_ALIVE = '30m';
|
|
255
|
-
/**
|
|
256
|
-
* Warm a localhost model backend so the user's first real turn doesn't pay the
|
|
257
|
-
* model cold-load (weights → memory). Fire-and-forget: never blocks the caller,
|
|
258
|
-
* never throws.
|
|
259
|
-
*
|
|
260
|
-
* - Ollama has a native load endpoint — `POST /api/generate {model, keep_alive}`
|
|
261
|
-
* with no prompt loads the weights and returns immediately; `keep_alive`
|
|
262
|
-
* keeps them resident across the seed + real turns of a session.
|
|
263
|
-
* - LM Studio JIT-loads on first request, so we nudge it with a 1-token
|
|
264
|
-
* completion against its OpenAI-compatible endpoint.
|
|
265
|
-
*
|
|
266
|
-
* Called when a local Profile is bound (warm while the user reads / types) and
|
|
267
|
-
* again at spawn (re-assert keep_alive). Measured: a cold gemma3:4b spent ~12s
|
|
268
|
-
* before its first token; prewarmed, generation starts in ~2s.
|
|
269
|
-
*/
|
|
270
160
|
export function prewarmLocalModel(provider, modelId) {
|
|
271
161
|
if (!modelId || !isLocalProvider(provider))
|
|
272
162
|
return;
|
|
@@ -290,15 +180,6 @@ export function prewarmLocalModel(provider, modelId) {
|
|
|
290
180
|
body: JSON.stringify({ model: modelId, keep_alive: PREWARM_KEEP_ALIVE }),
|
|
291
181
|
}).then(r => { writeScopedLog('model-prewarm', `ollama load ${modelId} → ${r.status}`); }, e => { writeScopedLog('model-prewarm', `ollama load ${modelId} failed: ${e?.message || e}`, { level: 'warn', stream: 'stderr' }); });
|
|
292
182
|
}
|
|
293
|
-
/**
|
|
294
|
-
* Decide how codex should reach a provider. Codex 0.140+ speaks ONLY the
|
|
295
|
-
* Responses API, so the route depends on what the provider implements:
|
|
296
|
-
* openai-native genuine OpenAI → built-in `openai` provider
|
|
297
|
-
* local-oss localhost Ollama/LMStudio → built-in `ollama`/`lmstudio` (responses)
|
|
298
|
-
* responses-native OpenRouter, … → custom provider, responses direct
|
|
299
|
-
* bridge chat-only (DeepSeek, Kimi, MiniMax, 豆包, Qwen, Zhipu, …)
|
|
300
|
-
* → local Responses↔Chat bridge
|
|
301
|
-
*/
|
|
302
183
|
function codexRoute(provider) {
|
|
303
184
|
if (provider.kind === 'openai')
|
|
304
185
|
return 'openai-native';
|
|
@@ -308,19 +189,6 @@ function codexRoute(provider) {
|
|
|
308
189
|
return 'responses-native';
|
|
309
190
|
return 'bridge';
|
|
310
191
|
}
|
|
311
|
-
/**
|
|
312
|
-
* Codex CLI honours `model_providers.<slug>` definitions in `config.toml` and
|
|
313
|
-
* binds the active one via `model_provider="<slug>"`. The credential lives in
|
|
314
|
-
* the env var named by `env_key`, picked host-aware (e.g. `DEEPSEEK_API_KEY`).
|
|
315
|
-
*
|
|
316
|
-
* Codex 0.140+ dropped Chat Completions (`wire_api = "chat"` is rejected at
|
|
317
|
-
* config load) — it speaks ONLY the Responses API. So this injector routes per
|
|
318
|
-
* `codexRoute()`: responses-capable providers (OpenAI, OpenRouter, local
|
|
319
|
-
* Ollama/LM Studio) are reached directly with the default `responses` wire;
|
|
320
|
-
* chat-only providers (DeepSeek and the domestic series) are routed through the
|
|
321
|
-
* in-process Responses↔Chat bridge, which codex sees as just another
|
|
322
|
-
* responses-speaking provider on localhost.
|
|
323
|
-
*/
|
|
324
192
|
const codexInjector = async (provider, profile, apiKey) => {
|
|
325
193
|
if (provider.kind !== 'openai' && provider.kind !== 'openai-compatible') {
|
|
326
194
|
return {
|
|
@@ -330,10 +198,6 @@ const codexInjector = async (provider, profile, apiKey) => {
|
|
|
330
198
|
}
|
|
331
199
|
const model = profile.modelId;
|
|
332
200
|
const route = codexRoute(provider);
|
|
333
|
-
// Local Ollama / LM Studio: codex's built-in provider already speaks the
|
|
334
|
-
// Responses API to the local server. Just select it — no custom provider, no
|
|
335
|
-
// API key. (Defining `model_providers.<built-in>` is rejected: "Built-in
|
|
336
|
-
// providers cannot be overridden.")
|
|
337
201
|
if (route === 'local-oss') {
|
|
338
202
|
const local = codexLocalProvider(provider);
|
|
339
203
|
prewarmLocalModel(provider, model);
|
|
@@ -344,7 +208,6 @@ const codexInjector = async (provider, profile, apiKey) => {
|
|
|
344
208
|
detail: `Codex local → ${provider.name} / ${model} (built-in ${local}, responses)`,
|
|
345
209
|
};
|
|
346
210
|
}
|
|
347
|
-
// Genuine OpenAI: use the built-in `openai` provider; inject the key (+ base).
|
|
348
211
|
if (route === 'openai-native') {
|
|
349
212
|
const env = { OPENAI_API_KEY: apiKey };
|
|
350
213
|
if (provider.baseURL)
|
|
@@ -358,9 +221,6 @@ const codexInjector = async (provider, profile, apiKey) => {
|
|
|
358
221
|
}
|
|
359
222
|
const slug = providerSlug(provider);
|
|
360
223
|
const envKey = codexEnvKey(provider);
|
|
361
|
-
// Chat-only providers: route through the local Responses↔Chat bridge. Codex
|
|
362
|
-
// forwards `Authorization: Bearer <key>` (from env_key) to the bridge, which
|
|
363
|
-
// relays it to the upstream chat endpoint — the bridge never stores secrets.
|
|
364
224
|
if (route === 'bridge') {
|
|
365
225
|
const port = await ensureResponsesBridge();
|
|
366
226
|
const base = `http://127.0.0.1:${port}/u/${upstreamToken(provider.baseURL)}`;
|
|
@@ -377,8 +237,6 @@ const codexInjector = async (provider, profile, apiKey) => {
|
|
|
377
237
|
detail: `Codex BYOK → ${provider.name} / ${model} via Responses↔Chat bridge (provider=${slug})`,
|
|
378
238
|
};
|
|
379
239
|
}
|
|
380
|
-
// responses-native (OpenRouter, …): point codex straight at the provider's
|
|
381
|
-
// Responses endpoint (wire_api omitted ⇒ codex default `responses`).
|
|
382
240
|
return {
|
|
383
241
|
env: { [envKey]: apiKey },
|
|
384
242
|
argvAppend: [],
|
|
@@ -392,7 +250,6 @@ const codexInjector = async (provider, profile, apiKey) => {
|
|
|
392
250
|
detail: `Codex BYOK → ${provider.name} / ${model} (provider=${slug}, native responses)`,
|
|
393
251
|
};
|
|
394
252
|
};
|
|
395
|
-
/** Gemini CLI accepts `GEMINI_API_KEY` but does not allow custom baseURL. */
|
|
396
253
|
const geminiInjector = (provider, profile, apiKey) => {
|
|
397
254
|
if (provider.kind !== 'google') {
|
|
398
255
|
return {
|
|
@@ -407,29 +264,9 @@ const geminiInjector = (provider, profile, apiKey) => {
|
|
|
407
264
|
detail: `Gemini BYOK → ${provider.name} / ${profile.modelId}`,
|
|
408
265
|
};
|
|
409
266
|
};
|
|
410
|
-
/**
|
|
411
|
-
* Hermes injector. Two channels:
|
|
412
|
-
* 1. Env vars carry the credential — `hermes acp` honours `OPENROUTER_API_KEY`,
|
|
413
|
-
* `ANTHROPIC_API_KEY`, etc. just like the top-level `hermes` CLI.
|
|
414
|
-
* 2. The model is bound *per-session* by the driver via the ACP
|
|
415
|
-
* `session/set_model` request — `hermes acp` does NOT accept `-m` /
|
|
416
|
-
* `--provider` (only `--accept-hooks`); appending `-m` here used to make
|
|
417
|
-
* every BYOK-bound spawn die with `unrecognized arguments`.
|
|
418
|
-
*
|
|
419
|
-
* The model is handed to the driver via `modelOverride` (an ACP-style
|
|
420
|
-
* `<provider>:<model>` string). The driver passes it to `session/set_model`
|
|
421
|
-
* after `session/new` returns; if the user has no Profile bound, no
|
|
422
|
-
* `set_model` call is made and Hermes uses its `~/.hermes/config.yaml`
|
|
423
|
-
* default.
|
|
424
|
-
*/
|
|
425
267
|
const hermesInjector = (provider, profile, apiKey) => {
|
|
426
268
|
const env = providerCredentialEnv(provider, apiKey);
|
|
427
269
|
const slug = providerSlug(provider);
|
|
428
|
-
// Only strip a leading `<slug>/` or `<slug>:` if the user accidentally
|
|
429
|
-
// stored a redundant provider prefix. Do NOT strip the *first segment* of
|
|
430
|
-
// a slash-separated model id wholesale — for OpenRouter the canonical
|
|
431
|
-
// model id is `vendor/model` (e.g. `deepseek/deepseek-v4-flash`), and
|
|
432
|
-
// dropping the `vendor/` part yields a non-existent model.
|
|
433
270
|
let bareModel = profile.modelId;
|
|
434
271
|
if (bareModel.startsWith(`${slug}/`) || bareModel.startsWith(`${slug}:`)) {
|
|
435
272
|
bareModel = bareModel.slice(slug.length + 1);
|
|
@@ -447,14 +284,6 @@ const AGENT_INJECT_TABLE = {
|
|
|
447
284
|
gemini: geminiInjector,
|
|
448
285
|
hermes: hermesInjector,
|
|
449
286
|
};
|
|
450
|
-
// ---------------------------------------------------------------------------
|
|
451
|
-
// Public API
|
|
452
|
-
// ---------------------------------------------------------------------------
|
|
453
|
-
/**
|
|
454
|
-
* Resolve the active Profile for an agent and return the spawn config to
|
|
455
|
-
* inject. Returns `null` when no Profile is bound (caller should fall back
|
|
456
|
-
* to the agent's native auth / default model).
|
|
457
|
-
*/
|
|
458
287
|
export async function resolveAgentInjection(agentId) {
|
|
459
288
|
const profile = getActiveProfile(agentId);
|
|
460
289
|
if (!profile)
|
|
@@ -465,9 +294,6 @@ export async function resolveAgentInjection(agentId) {
|
|
|
465
294
|
const injector = AGENT_INJECT_TABLE[agentId];
|
|
466
295
|
if (!injector)
|
|
467
296
|
return null;
|
|
468
|
-
// Local providers (Ollama / LM Studio / llama.cpp) need no credential — codex
|
|
469
|
-
// reaches them via its built-in localhost provider with no auth. Don't let a
|
|
470
|
-
// missing/placeholder key block an otherwise-valid local binding.
|
|
471
297
|
let apiKey = '';
|
|
472
298
|
try {
|
|
473
299
|
apiKey = await resolveCredential(provider.credential);
|
|
@@ -478,12 +304,7 @@ export async function resolveAgentInjection(agentId) {
|
|
|
478
304
|
}
|
|
479
305
|
}
|
|
480
306
|
const result = await injector(provider, profile, apiKey);
|
|
481
|
-
// Attach the provider display name so renders can surface "via <provider>"
|
|
482
|
-
// — this is what tells the user the turn is going through a BYOK route.
|
|
483
307
|
result.providerName = provider.name;
|
|
484
|
-
// Attach the real context window from the provider's cached /models listing.
|
|
485
|
-
// Sync peek — no network blocking; on miss we kick off a fetch so the *next*
|
|
486
|
-
// session has it, and this turn falls back to the CLI's advertised value.
|
|
487
308
|
const cached = peekProviderModelInfo(provider.id, profile.modelId);
|
|
488
309
|
if (cached?.contextLength && cached.contextLength > 0) {
|
|
489
310
|
result.contextWindow = cached.contextLength;
|
|
@@ -493,7 +314,6 @@ export async function resolveAgentInjection(agentId) {
|
|
|
493
314
|
}
|
|
494
315
|
return result;
|
|
495
316
|
}
|
|
496
|
-
/** Returns `true` if the given agent is bound to a Profile. */
|
|
497
317
|
export function isAgentBoundToProfile(agentId) {
|
|
498
318
|
return getActiveProfile(agentId) !== null;
|
|
499
319
|
}
|