pi2dsh 0.4.0 → 0.5.0

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.
@@ -1,13 +1,14 @@
1
1
 
2
2
  import { C as realBuiltinProvider, R as AssistantMessageEventStream, S as openAIResponsesApi, a as builtinProviders, i as anthropicMessagesApi, r as __setPiAiLlmBridge, x as openAICompletionsApi } from "./pi-ai-B_88Wb0r.mjs";
3
3
  import { r as normalizePath, t as getAgentDir } from "./pi-config-shim-CZ1wFzqM.mjs";
4
- import { E as __setSubagentSessionFactory, S as Theme, f as ExtensionRunner } from "./pi-coding-agent-COMX0KHL.mjs";
4
+ import { E as __setSubagentSessionFactory, S as Theme, f as ExtensionRunner } from "./pi-coding-agent-C9Y9ql5E.mjs";
5
5
  import { a as resolvePiProviderAuth, n as loginPiProvider, r as providerSupportsOAuth, t as FileCredentialStore } from "./oauth-bridge-C6fL1qSv.mjs";
6
6
  import { createRequire } from "node:module";
7
- import { access, readFile } from "node:fs/promises";
7
+ import { access, mkdir, readFile, writeFile } from "node:fs/promises";
8
8
  import { join } from "node:path";
9
9
  import { fileURLToPath } from "node:url";
10
10
  import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
11
+ import { tmpdir } from "node:os";
11
12
  import { EventEmitter } from "node:events";
12
13
  import { AsyncLocalStorage } from "node:async_hooks";
13
14
  import { createJiti } from "jiti";
@@ -494,6 +495,7 @@ async function createBridgedAgentSession(host, options) {
494
495
  meta: {
495
496
  cwd: typeof options.cwd === "string" ? options.cwd : host.cwd(),
496
497
  origin: "subagent",
498
+ delegationDepth: host.parentDelegationDepth() + 1,
497
499
  ...host.parentSessionId() !== void 0 ? { parentSession: host.parentSessionId() } : {}
498
500
  },
499
501
  ...typeof requestedModel?.id === "string" && requestedModel.id.length > 0 ? { agentOptions: {
@@ -1750,8 +1752,17 @@ function modelsJsonPath() {
1750
1752
  const emptySnapshot = () => ({
1751
1753
  models: [],
1752
1754
  providers: /* @__PURE__ */ new Map(),
1753
- errors: []
1755
+ errors: [],
1756
+ companions: /* @__PURE__ */ new Map()
1754
1757
  });
1758
+ function companionImageModels(provider) {
1759
+ if (provider.baseUrl !== void 0 || provider.models !== void 0 || provider.apiKey !== void 0) return void 0;
1760
+ const overrides = provider.modelOverrides;
1761
+ if (overrides === void 0) return void 0;
1762
+ const imageModels = /* @__PURE__ */ new Set();
1763
+ for (const [modelId, override] of Object.entries(overrides)) if (Array.isArray(override.input) && override.input.includes("image")) imageModels.add(modelId);
1764
+ return imageModels.size > 0 ? imageModels : void 0;
1765
+ }
1755
1766
  /**
1756
1767
  * One load of models.json projected to Pi Model objects. Mirrors Pi's
1757
1768
  * composeModelProvider composition order: base (builtin) models →
@@ -1770,6 +1781,11 @@ async function loadModelsJsonSnapshot(baseProviderOf) {
1770
1781
  for (const providerId of config.getProviderIds()) {
1771
1782
  const providerConfig = config.getProvider(providerId);
1772
1783
  if (providerConfig === void 0) continue;
1784
+ const imageModels = companionImageModels(providerConfig);
1785
+ if (imageModels !== void 0) {
1786
+ snapshot.companions.set(providerId, imageModels);
1787
+ continue;
1788
+ }
1773
1789
  try {
1774
1790
  const base = await baseProviderOf?.(providerId);
1775
1791
  const composed = applyModelsJson(providerId, typeof base?.getModels === "function" ? base.getModels() : [], providerConfig).map((model) => {
@@ -2002,6 +2018,77 @@ function piProviderDshAdapter(providerId, provider, host) {
2002
2018
  }
2003
2019
  };
2004
2020
  }
2021
+ const IMAGE_OMITTED_TEXT = "[image attachment omitted: the selected model reads text only]";
2022
+ const imageNoticeText = (path) => path === void 0 ? IMAGE_OMITTED_TEXT : `[image attached at ${path} — the selected model reads text only; use an image-capable tool to view it]`;
2023
+ async function textOnlyBlocks(blocks, materialize) {
2024
+ const out = [];
2025
+ for (const block of blocks) if (block.type === "image") {
2026
+ const attachment = block.attachment;
2027
+ const path = typeof attachment === "object" && attachment !== null ? await materialize(attachment) : void 0;
2028
+ out.push({
2029
+ type: "text",
2030
+ text: imageNoticeText(path)
2031
+ });
2032
+ } else if (block.type === "tool-result" && Array.isArray(block.content)) out.push({
2033
+ ...block,
2034
+ content: await textOnlyBlocks(block.content, materialize)
2035
+ });
2036
+ else out.push(block);
2037
+ return out;
2038
+ }
2039
+ async function textOnlyMessages(messages, materialize) {
2040
+ return Promise.all(messages.map(async (message) => Array.isArray(message.content) ? {
2041
+ ...message,
2042
+ content: await textOnlyBlocks(message.content, materialize)
2043
+ } : message));
2044
+ }
2045
+ /**
2046
+ * A DSH route that admits images on behalf of a text-only route — the
2047
+ * single-directory answer to models.json modelOverrides declaring
2048
+ * `input: ["text", "image"]` for models of a route this bridge does not own.
2049
+ * The companion honestly declares image input (so the host's admission and
2050
+ * model-switch checks pass), replaces image blocks with an explicit notice,
2051
+ * and forwards every call to the original route. It never carries a wire
2052
+ * transport of its own; the images themselves are served by whatever vision
2053
+ * extension handles the turn's entering messages.
2054
+ */
2055
+ function imageAdmissionCompanionAdapter(options) {
2056
+ const { originalId, imageModels, llm } = options;
2057
+ const materialize = options.materializeImage ?? (async () => void 0);
2058
+ const admitImage = (info, id) => {
2059
+ const modalities = Array.isArray(info.inputModalities) ? info.inputModalities.slice() : ["text"];
2060
+ if (!modalities.includes("image")) modalities.push("image");
2061
+ return {
2062
+ ...info,
2063
+ provider: id,
2064
+ inputModalities: modalities
2065
+ };
2066
+ };
2067
+ return {
2068
+ providerInfo: (id) => {
2069
+ return {
2070
+ id,
2071
+ name: `${llm.listProviders().find((provider) => provider.id === originalId)?.name ?? originalId} + Vision Bridge`
2072
+ };
2073
+ },
2074
+ providerRetryPolicy: () => void 0,
2075
+ listModels: async (id) => (await llm.listModels(originalId)).filter((model) => imageModels.has(String(model.id))).map((model) => admitImage(model, id)),
2076
+ resolveModel: async (id, modelId) => {
2077
+ const info = await llm.resolveModelInfo(originalId, modelId);
2078
+ return imageModels.has(modelId) ? admitImage(info, id) : {
2079
+ ...info,
2080
+ provider: id
2081
+ };
2082
+ },
2083
+ async *stream(streamOptions) {
2084
+ yield* llm.stream({
2085
+ ...streamOptions,
2086
+ provider: originalId,
2087
+ ...Array.isArray(streamOptions.messages) ? { messages: await textOnlyMessages(streamOptions.messages, materialize) } : {}
2088
+ });
2089
+ }
2090
+ };
2091
+ }
2005
2092
  /**
2006
2093
  * Register the provider as a live DSH route when an llm service is mounted.
2007
2094
  * A provider carrying its own transport streams through it; a config-only
@@ -2271,7 +2358,8 @@ function isSubagentOrigin(subject) {
2271
2358
  function currentPiModel(state, agent) {
2272
2359
  const override = state.modelOverrides.get(agent);
2273
2360
  const options = agent.options;
2274
- const provider = String(override?.provider ?? options?.provider ?? "");
2361
+ const selectedProvider = String(override?.provider ?? options?.provider ?? "");
2362
+ const provider = state.companionRoutes.get(selectedProvider) ?? selectedProvider;
2275
2363
  const id = String(override?.model ?? options?.model ?? "");
2276
2364
  if (id.length === 0) return override;
2277
2365
  return (provider.length > 0 ? state.modelCatalog?.find(provider, id) : void 0) ?? {
@@ -3090,6 +3178,7 @@ async function reloadModelsJson(ctx, state) {
3090
3178
  dispose();
3091
3179
  }
3092
3180
  state.modelsJsonProviders.clear();
3181
+ state.companionRoutes.clear();
3093
3182
  state.modelsJson = await loadModelsJsonSnapshot(realBuiltinProvider);
3094
3183
  for (const problem of state.modelsJson.errors) logger(ctx).warn(`[pi2dsh] models.json: ${problem}`);
3095
3184
  for (const [providerId, providerConfig] of state.modelsJson.providers) {
@@ -3118,6 +3207,57 @@ async function reloadModelsJson(ctx, state) {
3118
3207
  logger(ctx).info(`[pi2dsh] models.json provider ${JSON.stringify(providerId)} registered as a native DSH llm route (${models.length} models)`);
3119
3208
  }
3120
3209
  }
3210
+ registerCompanionRoutes(ctx, state);
3211
+ }
3212
+ function registerCompanionRoutes(ctx, state) {
3213
+ const companions = state.modelsJson?.companions;
3214
+ if (companions === void 0 || companions.size === 0) return;
3215
+ const llm = llmOf(ctx);
3216
+ if (llm === void 0) return;
3217
+ for (const [originalId, imageModels] of companions) {
3218
+ if (!llm.listProviders().some((provider) => provider.id === originalId)) {
3219
+ logger(ctx).warn(`[pi2dsh] models.json: modelOverrides for ${JSON.stringify(originalId)} declare image input, but no such llm route exists; no companion route was registered`);
3220
+ continue;
3221
+ }
3222
+ const companionId = `${originalId}-vision`;
3223
+ state.companionRoutes.set(companionId, originalId);
3224
+ try {
3225
+ const dispose = llm.registerAdapter([companionId], imageAdmissionCompanionAdapter({
3226
+ originalId,
3227
+ imageModels,
3228
+ llm,
3229
+ materializeImage: (attachment) => materializeAttachmentImage(ctx, attachment)
3230
+ }));
3231
+ state.providerRouteDisposers.set(`${MODELS_JSON_ROUTE_PREFIX}${companionId}`, dispose);
3232
+ logger(ctx).info(`[pi2dsh] image-admission companion route ${JSON.stringify(companionId)} registered for ${JSON.stringify(originalId)} (${imageModels.size} models)`);
3233
+ } catch (error) {
3234
+ logger(ctx).warn(`[pi2dsh] companion route ${JSON.stringify(companionId)} already has a live adapter in this host (${error instanceof Error ? error.message : String(error)}); reusing it`);
3235
+ }
3236
+ }
3237
+ }
3238
+ const IMAGE_EXTENSIONS = {
3239
+ "image/png": "png",
3240
+ "image/jpeg": "jpg",
3241
+ "image/webp": "webp",
3242
+ "image/gif": "gif"
3243
+ };
3244
+ async function materializeAttachmentImage(ctx, attachment) {
3245
+ const attachments = ctx.get("attachments");
3246
+ const id = typeof attachment.attachmentId === "string" ? attachment.attachmentId : void 0;
3247
+ if (attachments === void 0 || id === void 0) return void 0;
3248
+ const extension = IMAGE_EXTENSIONS[String(attachment.mediaType)] ?? "png";
3249
+ const dir = join(tmpdir(), "pi2dsh-attached-images");
3250
+ const filePath = join(dir, `${id}.${extension}`);
3251
+ try {
3252
+ if (!existsSync(filePath)) {
3253
+ const stored = await attachments.readImage(attachment);
3254
+ await mkdir(dir, { recursive: true });
3255
+ await writeFile(filePath, Buffer.from(stored.data));
3256
+ }
3257
+ return filePath;
3258
+ } catch {
3259
+ return;
3260
+ }
3121
3261
  }
3122
3262
  function ensureLoginCommand(ctx, state) {
3123
3263
  if (state.loginCommandRegistered === true) return;
@@ -3698,6 +3838,7 @@ async function applyPiPackage(ctx, options) {
3698
3838
  commands: /* @__PURE__ */ new Map(),
3699
3839
  commandDisposers: /* @__PURE__ */ new Map(),
3700
3840
  modelsJsonProviders: /* @__PURE__ */ new Map(),
3841
+ companionRoutes: /* @__PURE__ */ new Map(),
3701
3842
  flags: /* @__PURE__ */ new Map(),
3702
3843
  notifications: [],
3703
3844
  activeAgents: /* @__PURE__ */ new Set(),
@@ -3766,6 +3907,13 @@ async function applyPiPackage(ctx, options) {
3766
3907
  const session = agentSession(currentAgent(state));
3767
3908
  return session === void 0 ? void 0 : String(session.id ?? "") || void 0;
3768
3909
  },
3910
+ parentDelegationDepth: () => {
3911
+ const parent = currentAgent(state);
3912
+ const header = agentSession(parent)?.header;
3913
+ const fromHeader = typeof header?.delegationDepth === "number" ? header.delegationDepth : 0;
3914
+ const fromOptions = typeof parent?.options?.subagentDepth === "number" ? parent.options.subagentDepth : 0;
3915
+ return Math.max(fromHeader, fromOptions);
3916
+ },
3769
3917
  piContentToDsh: (content) => piToDshContent(ctx, content),
3770
3918
  deliver: (agent, message, mode) => deliverAgentMessage(agent, message, mode),
3771
3919
  messageFromSessionEvent,
@@ -3785,4 +3933,4 @@ const runtimeInternals = {
3785
3933
  //#endregion
3786
3934
  export { normalizeToolSchema as n, runtimeInternals as r, applyPiPackage as t };
3787
3935
 
3788
- //# sourceMappingURL=runtime-B7mEIRf-.mjs.map
3936
+ //# sourceMappingURL=runtime-VK4eVnYa.mjs.map