pikiloom 0.4.13 → 0.4.15

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.
Files changed (56) hide show
  1. package/dashboard/dist/assets/AgentTab-CKoy_-w4.js +1 -0
  2. package/dashboard/dist/assets/{DirBrowser-Du91b-sn.js → DirBrowser-DpbuN0OL.js} +1 -1
  3. package/dashboard/dist/assets/{ExtensionsTab-CV0rbtj2.js → ExtensionsTab-ymr7K8dU.js} +1 -1
  4. package/dashboard/dist/assets/{IMAccessTab-BevAFdq9.js → IMAccessTab-CaTtCn3l.js} +1 -1
  5. package/dashboard/dist/assets/{Modal-DK1MkhKX.js → Modal-DA-9kJxp.js} +1 -1
  6. package/dashboard/dist/assets/Modals-BkLIRnNK.js +1 -0
  7. package/dashboard/dist/assets/Select-B0pZtuzF.js +1 -0
  8. package/dashboard/dist/assets/SessionPanel-CYQtZZNX.js +1 -0
  9. package/dashboard/dist/assets/{SystemTab-jafqMUsq.js → SystemTab-B9TcGMzc.js} +1 -1
  10. package/dashboard/dist/assets/codex-C6EwIzap.png +0 -0
  11. package/dashboard/dist/assets/deepseek-DOQzDJ-4.ico +0 -0
  12. package/dashboard/dist/assets/hermes-ClPe1RPI.png +0 -0
  13. package/dashboard/dist/assets/index-BCYshErN.js +3 -0
  14. package/dashboard/dist/assets/index-C5irxzzD.js +23 -0
  15. package/dashboard/dist/assets/logo-wordmark-B0Z6VgSZ.png +0 -0
  16. package/dashboard/dist/assets/logo-wordmark-light-D9FCWeOH.png +0 -0
  17. package/dashboard/dist/assets/playwright-GP3HuCap.ico +0 -0
  18. package/dashboard/dist/assets/qwen-DKVAROae.png +0 -0
  19. package/dashboard/dist/assets/shared-i_XUH0xm.js +1 -0
  20. package/dashboard/dist/index.html +1 -1
  21. package/dashboard/dist/logo.png +0 -0
  22. package/dist/agent/auto-update.js +99 -4
  23. package/dist/agent/drivers/claude.js +6 -26
  24. package/dist/agent/drivers/codex.js +4 -26
  25. package/dist/agent/drivers/gemini.js +4 -26
  26. package/dist/agent/drivers/hermes.js +4 -26
  27. package/dist/agent/index.js +1 -1
  28. package/dist/agent/mcp/bridge.js +53 -2
  29. package/dist/agent/session.js +16 -3
  30. package/dist/agent/stream.js +37 -3
  31. package/dist/bot/bot.js +18 -5
  32. package/dist/channels/telegram/bot.js +2 -2
  33. package/dist/channels/telegram/render.js +47 -1
  34. package/dist/core/constants.js +8 -0
  35. package/dist/dashboard/routes/extensions.js +6 -0
  36. package/dist/dashboard/routes/models.js +9 -1
  37. package/dist/dashboard/routes/sessions.js +25 -0
  38. package/dist/dashboard/server.js +8 -0
  39. package/dist/model/index.js +1 -1
  40. package/dist/model/injector.js +209 -28
  41. package/dist/model/responses-bridge.js +407 -0
  42. package/package.json +1 -1
  43. package/dashboard/dist/assets/AgentTab-DJ2MSY9m.js +0 -1
  44. package/dashboard/dist/assets/Modals-UEF0H1UN.js +0 -1
  45. package/dashboard/dist/assets/Select-YrnugZXH.js +0 -1
  46. package/dashboard/dist/assets/SessionPanel-DbSdD2Jt.js +0 -1
  47. package/dashboard/dist/assets/codex-DYadqqp0.png +0 -0
  48. package/dashboard/dist/assets/deepseek-BeYNZEk0.ico +0 -0
  49. package/dashboard/dist/assets/hermes-BAarh-tH.png +0 -0
  50. package/dashboard/dist/assets/index-BnTrNACS.js +0 -23
  51. package/dashboard/dist/assets/index-SkDflrDp.js +0 -3
  52. package/dashboard/dist/assets/logo-wordmark-FzeBAUsd.png +0 -0
  53. package/dashboard/dist/assets/logo-wordmark-light-snSpARTN.png +0 -0
  54. package/dashboard/dist/assets/playwright-BldPFZgC.ico +0 -0
  55. package/dashboard/dist/assets/qwen-xykkX0_y.png +0 -0
  56. package/dashboard/dist/assets/shared-BpcXDkDP.js +0 -1
@@ -7,8 +7,10 @@
7
7
  * = adding one entry to AGENT_INJECT_TABLE.
8
8
  */
9
9
  import { resolveCredential } from '../core/secrets/index.js';
10
+ import { writeScopedLog } from '../core/logging.js';
10
11
  import { getActiveProfile, getProvider } from './store.js';
11
12
  import { peekProviderModelInfo, prefetchProviderModels } from './provider-models.js';
13
+ import { ensureResponsesBridge, upstreamToken } from './responses-bridge.js';
12
14
  const EMPTY = { env: {}, argvAppend: [], detail: '' };
13
15
  // ---------------------------------------------------------------------------
14
16
  // Shared host-based provider identification
@@ -53,7 +55,13 @@ function providerSlug(provider) {
53
55
  return 'doubao';
54
56
  if (host.includes('openrouter'))
55
57
  return 'openrouter';
56
- 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
+ const label = host.replace(/:\d+$/, '').replace(/^(www|api)\./, '').split('.')[0].replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
64
+ return label || 'byok';
57
65
  }
58
66
  /**
59
67
  * Canonical env-var name(s) carrying the credential for a provider. Returned
@@ -152,6 +160,24 @@ function claudeAnthropicBaseURL(provider) {
152
160
  }
153
161
  return raw.replace(/\/v1$/, '');
154
162
  }
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
+ function isFirstPartyAnthropic(baseURL) {
172
+ let host;
173
+ try {
174
+ host = new URL(baseURL).hostname.toLowerCase();
175
+ }
176
+ catch {
177
+ return false;
178
+ }
179
+ return host === 'anthropic.com' || host.endsWith('.anthropic.com');
180
+ }
155
181
  /**
156
182
  * Claude Code respects `ANTHROPIC_BASE_URL` + `ANTHROPIC_API_KEY` (or
157
183
  * `ANTHROPIC_AUTH_TOKEN`) as a BYOK route. The CLI itself is unchanged.
@@ -170,50 +196,200 @@ const claudeInjector = (provider, profile, apiKey) => {
170
196
  detail: `Claude BYOK requires Anthropic or OpenAI-compatible (Anthropic-API-shaped) provider; got ${provider.kind}.`,
171
197
  };
172
198
  }
199
+ const baseURL = claudeAnthropicBaseURL(provider);
200
+ const env = {
201
+ ANTHROPIC_BASE_URL: baseURL,
202
+ ANTHROPIC_API_KEY: apiKey,
203
+ ANTHROPIC_AUTH_TOKEN: apiKey,
204
+ };
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
+ if (!isFirstPartyAnthropic(baseURL)) {
216
+ env.CLAUDE_CODE_ATTRIBUTION_HEADER = '0';
217
+ }
173
218
  return {
174
- env: {
175
- ANTHROPIC_BASE_URL: claudeAnthropicBaseURL(provider),
176
- ANTHROPIC_API_KEY: apiKey,
177
- ANTHROPIC_AUTH_TOKEN: apiKey,
178
- },
219
+ env,
179
220
  argvAppend: [],
180
221
  modelOverride: profile.modelId,
181
222
  detail: `Claude BYOK → ${provider.name} / ${profile.modelId}`,
182
223
  };
183
224
  };
225
+ function providerHostname(provider) {
226
+ try {
227
+ return new URL(provider.baseURL).hostname.toLowerCase();
228
+ }
229
+ catch {
230
+ return '';
231
+ }
232
+ }
233
+ /** True for localhost endpoints (Ollama / LM Studio / llama.cpp). */
234
+ function isLocalProvider(provider) {
235
+ const h = providerHostname(provider);
236
+ return h === 'localhost' || h === '127.0.0.1' || h === '0.0.0.0' || h === '::1';
237
+ }
238
+ /** Providers that natively implement the OpenAI Responses API (codex talks to them directly). */
239
+ function isResponsesNativeProvider(provider) {
240
+ return providerHost(provider).includes('openrouter');
241
+ }
242
+ /** codex's built-in local provider id for a localhost endpoint. */
243
+ function codexLocalProvider(provider) {
244
+ let port = '';
245
+ try {
246
+ port = new URL(provider.baseURL).port;
247
+ }
248
+ catch { /* ignore */ }
249
+ if (port === '1234' || /lm\s*studio/i.test(provider.name))
250
+ return 'lmstudio';
251
+ return 'ollama';
252
+ }
253
+ /** Ollama keeps a prewarmed model resident for this long (its `keep_alive`). */
254
+ 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
+ export function prewarmLocalModel(provider, modelId) {
271
+ if (!modelId || !isLocalProvider(provider))
272
+ return;
273
+ let origin;
274
+ try {
275
+ origin = new URL(provider.baseURL).origin;
276
+ }
277
+ catch {
278
+ return;
279
+ }
280
+ const swallow = () => { };
281
+ if (codexLocalProvider(provider) === 'lmstudio') {
282
+ void fetch(`${origin}/v1/chat/completions`, {
283
+ method: 'POST', headers: { 'content-type': 'application/json' },
284
+ body: JSON.stringify({ model: modelId, max_tokens: 1, messages: [{ role: 'user', content: 'hi' }] }),
285
+ }).then(swallow, swallow);
286
+ return;
287
+ }
288
+ void fetch(`${origin}/api/generate`, {
289
+ method: 'POST', headers: { 'content-type': 'application/json' },
290
+ body: JSON.stringify({ model: modelId, keep_alive: PREWARM_KEEP_ALIVE }),
291
+ }).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
+ }
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
+ function codexRoute(provider) {
303
+ if (provider.kind === 'openai')
304
+ return 'openai-native';
305
+ if (isLocalProvider(provider))
306
+ return 'local-oss';
307
+ if (isResponsesNativeProvider(provider))
308
+ return 'responses-native';
309
+ return 'bridge';
310
+ }
184
311
  /**
185
- * Codex CLI honours `model_providers.<slug>` definitions in `config.toml`.
186
- * Setting `OPENAI_BASE_URL` alone is not enough Codex still routes through
187
- * the default `openai` provider's auth flow. The robust path is to declare a
188
- * one-shot `model_providers.<slug>` via `-c` overrides and bind it via
189
- * `model_provider="<slug>"`. The credential lives in the env var named by
190
- * `env_key`, picked host-aware (e.g. `OPENROUTER_API_KEY` for openrouter.ai).
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`).
191
315
  *
192
- * Note on `wire_api`: codex 0.130 dropped `"chat"` ("no longer supported"); we
193
- * omit the field entirely so codex picks its current default (`responses`),
194
- * which OpenRouter and other major OpenAI-compatible providers accept.
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.
195
323
  */
196
- const codexInjector = (provider, profile, apiKey) => {
324
+ const codexInjector = async (provider, profile, apiKey) => {
197
325
  if (provider.kind !== 'openai' && provider.kind !== 'openai-compatible') {
198
326
  return {
199
327
  ...EMPTY,
200
- detail: `Codex BYOK requires OpenAI-compatible provider; got ${provider.kind}.`,
328
+ detail: `Codex BYOK requires an OpenAI-compatible provider; got ${provider.kind}.`,
329
+ };
330
+ }
331
+ const model = profile.modelId;
332
+ 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
+ if (route === 'local-oss') {
338
+ const local = codexLocalProvider(provider);
339
+ prewarmLocalModel(provider, model);
340
+ return {
341
+ env: {}, argvAppend: [],
342
+ codexConfigOverrides: [`model_provider="${local}"`],
343
+ modelOverride: model,
344
+ detail: `Codex local → ${provider.name} / ${model} (built-in ${local}, responses)`,
345
+ };
346
+ }
347
+ // Genuine OpenAI: use the built-in `openai` provider; inject the key (+ base).
348
+ if (route === 'openai-native') {
349
+ const env = { OPENAI_API_KEY: apiKey };
350
+ if (provider.baseURL)
351
+ env.OPENAI_BASE_URL = provider.baseURL;
352
+ return {
353
+ env, argvAppend: [],
354
+ codexConfigOverrides: ['model_provider="openai"'],
355
+ modelOverride: model,
356
+ detail: `Codex BYOK → OpenAI / ${model}`,
201
357
  };
202
358
  }
203
359
  const slug = providerSlug(provider);
204
360
  const envKey = codexEnvKey(provider);
205
- const overrides = [
206
- `model_providers.${slug}.name="${tomlEscape(provider.name)}"`,
207
- `model_providers.${slug}.base_url="${tomlEscape(provider.baseURL)}"`,
208
- `model_providers.${slug}.env_key="${envKey}"`,
209
- `model_provider="${slug}"`,
210
- ];
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
+ if (route === 'bridge') {
365
+ const port = await ensureResponsesBridge();
366
+ const base = `http://127.0.0.1:${port}/u/${upstreamToken(provider.baseURL)}`;
367
+ return {
368
+ env: { [envKey]: apiKey },
369
+ argvAppend: [],
370
+ codexConfigOverrides: [
371
+ `model_providers.${slug}.name="${tomlEscape(provider.name)}"`,
372
+ `model_providers.${slug}.base_url="${tomlEscape(base)}"`,
373
+ `model_providers.${slug}.env_key="${envKey}"`,
374
+ `model_provider="${slug}"`,
375
+ ],
376
+ modelOverride: model,
377
+ detail: `Codex BYOK → ${provider.name} / ${model} via Responses↔Chat bridge (provider=${slug})`,
378
+ };
379
+ }
380
+ // responses-native (OpenRouter, …): point codex straight at the provider's
381
+ // Responses endpoint (wire_api omitted ⇒ codex default `responses`).
211
382
  return {
212
383
  env: { [envKey]: apiKey },
213
384
  argvAppend: [],
214
- codexConfigOverrides: overrides,
215
- modelOverride: profile.modelId,
216
- detail: `Codex BYOK → ${provider.name} / ${profile.modelId} (provider=${slug})`,
385
+ codexConfigOverrides: [
386
+ `model_providers.${slug}.name="${tomlEscape(provider.name)}"`,
387
+ `model_providers.${slug}.base_url="${tomlEscape(provider.baseURL)}"`,
388
+ `model_providers.${slug}.env_key="${envKey}"`,
389
+ `model_provider="${slug}"`,
390
+ ],
391
+ modelOverride: model,
392
+ detail: `Codex BYOK → ${provider.name} / ${model} (provider=${slug}, native responses)`,
217
393
  };
218
394
  };
219
395
  /** Gemini CLI accepts `GEMINI_API_KEY` but does not allow custom baseURL. */
@@ -289,12 +465,17 @@ export async function resolveAgentInjection(agentId) {
289
465
  const injector = AGENT_INJECT_TABLE[agentId];
290
466
  if (!injector)
291
467
  return null;
292
- let apiKey;
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
+ let apiKey = '';
293
472
  try {
294
473
  apiKey = await resolveCredential(provider.credential);
295
474
  }
296
475
  catch (e) {
297
- throw new Error(`Failed to resolve credential for ${provider.name}: ${e?.message || e}`);
476
+ if (!isLocalProvider(provider)) {
477
+ throw new Error(`Failed to resolve credential for ${provider.name}: ${e?.message || e}`);
478
+ }
298
479
  }
299
480
  const result = await injector(provider, profile, apiKey);
300
481
  // Attach the provider display name so renders can surface "via <provider>"