@desplega.ai/agent-swarm 1.121.0 → 1.121.1
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/dist/{actions-mwshtkf7.js → actions-99h2dh5e.js} +5 -5
- package/dist/{app-skbty91x.js → app-9gfdk9me.js} +3 -3
- package/dist/{assistant-tg7dhfj8.js → assistant-mabq9kz7.js} +8 -8
- package/dist/{boot-reembed-q5x0xpcp.js → boot-reembed-3vr2amzp.js} +4 -4
- package/dist/{boot-reembed-ntjmeyh2.js → boot-reembed-ya33nge3.js} +3 -3
- package/dist/{boot-scrub-logs-5gznsfbe.js → boot-scrub-logs-2nmz7fzw.js} +2 -2
- package/dist/{cli-vp9r4ap4.js → cli-02m8v96c.js} +2 -2
- package/dist/{cli-6s3ytc5x.js → cli-16dv125e.js} +27 -2
- package/dist/{cli-0df2g86x.js → cli-2846g99z.js} +1 -1
- package/dist/cli-3dpj7atn.js +11 -0
- package/dist/{cli-vg3zrw6h.js → cli-3t4nvx1x.js} +4 -4
- package/dist/{cli-1728qh70.js → cli-5jd04m23.js} +1 -1
- package/dist/{cli-srjqn7fd.js → cli-7bk7s2sx.js} +1 -1
- package/dist/{cli-3fxqwfh3.js → cli-9hewdgzb.js} +5 -5
- package/dist/{cli-95rbe11h.js → cli-avxs2rp9.js} +12 -9
- package/dist/{cli-46etyprn.js → cli-dn5ycmtw.js} +1 -1
- package/dist/{cli-3b1ayz83.js → cli-dwqakg2b.js} +5 -5
- package/dist/{cli-072t49hf.js → cli-e0zbndab.js} +3 -3
- package/dist/{cli-p9w7kb3h.js → cli-fxdhzkqx.js} +1 -1
- package/dist/{cli-nf7sadev.js → cli-gahmct71.js} +2 -2
- package/dist/{cli-7pzae66m.js → cli-kwjx3f1v.js} +1 -1
- package/dist/{cli-g9vva2qh.js → cli-kx7vsvg0.js} +1 -1
- package/dist/{cli-avw9ev3x.js → cli-msyrqt1f.js} +14 -11
- package/dist/{cli-b2mqkg1h.js → cli-ng0gr9rz.js} +2 -2
- package/dist/{cli-s8bdcke2.js → cli-p8qcm631.js} +1 -1
- package/dist/{cli-tv7vekye.js → cli-qfh6pmvm.js} +1 -1
- package/dist/{cli-agdrxyex.js → cli-rjn06qph.js} +1 -1
- package/dist/{cli-dmzv0986.js → cli-vh3h1grg.js} +10 -1
- package/dist/{cli-ghq3v4c9.js → cli-w4crd8gx.js} +1 -1
- package/dist/{cli-gdpx70nc.js → cli-yhwn2vjm.js} +19 -19
- package/dist/{cli-4pfweq4x.js → cli-yy9sm7c1.js} +5 -5
- package/dist/{cli-5hpvd15x.js → cli-yyqpn0gc.js} +2 -2
- package/dist/{cli-ky8esb8t.js → cli-zm78xp1y.js} +3 -3
- package/dist/{cli-7pt9sc83.js → cli-zweca3q3.js} +3 -2
- package/dist/cli.js +10 -10
- package/dist/{codex-adapter-mmy741zd.js → codex-adapter-0kv2meed.js} +3 -2
- package/dist/{codex-session-runner-sazmx9zt.js → codex-session-runner-m1e1v4r0.js} +3 -2
- package/dist/{commands-cxxye5e4.js → commands-hty4156r.js} +2 -2
- package/dist/{db-ab1954b7.js → db-kyeq3t7h.js} +2 -2
- package/dist/{handlers-fmfe2343.js → handlers-fxvqnp6s.js} +8 -8
- package/dist/{hook-ygnw3cvt.js → hook-j4ksm2fy.js} +3 -2
- package/dist/{http-kactcc0z.js → http-m7fq6kbc.js} +35 -34
- package/dist/{index-g86j2yt5.js → index-62g7ym5y.js} +7 -7
- package/dist/{index-6erq0wvk.js → index-mbaa4yn7.js} +11 -10
- package/dist/{index-gmzs6a2q.js → index-r2tmg4ts.js} +10 -9
- package/dist/{index-hmp82wn0.js → index-rqxt0515.js} +12 -11
- package/dist/{keepalive-3cvt26dw.js → keepalive-acgfgbk8.js} +5 -5
- package/dist/{lead-ysf26cjr.js → lead-r3pnbky3.js} +23 -22
- package/dist/{maintenance-9npwptbb.js → maintenance-4tqr6va0.js} +4 -4
- package/dist/{oauth-refresh-sweep-0r2v76vw.js → oauth-refresh-sweep-5714hq77.js} +4 -4
- package/dist/{onboard-ddb1674a.js → onboard-s5x8zjqd.js} +2 -2
- package/dist/{opencode-adapter-h6rkxwrk.js → opencode-adapter-s3nhtp9m.js} +4 -1
- package/dist/{otel-impl-b43bkq8e.js → otel-impl-8g0741yr.js} +1 -1
- package/dist/{pi-mono-adapter-ke789738.js → pi-mono-adapter-k3z6e5fn.js} +102 -4
- package/dist/{pricing-refresh-2rqf114z.js → pricing-refresh-09ty42xr.js} +4 -4
- package/dist/{rbac-roles-5wkhc8xv.js → rbac-roles-2hjvj3q0.js} +3 -3
- package/dist/{rbac-roles-6vtafvzb.js → rbac-roles-q38w8rne.js} +2 -2
- package/dist/{seed-pricing-23wrb873.js → seed-pricing-wzx03625.js} +3 -3
- package/dist/{setup-6rny4fk6.js → setup-4hmnc9fz.js} +2 -2
- package/dist/{worker-yapewe14.js → worker-efef96pc.js} +23 -22
- package/openapi.json +1 -1
- package/package.json +1 -1
- package/src/be/memory/constants.ts +7 -6
- package/src/be/memory/graph-expansion.ts +3 -3
- package/src/be/memory/raters/llm-summarizer.ts +10 -2
- package/src/commands/provider-credentials.ts +2 -1
- package/src/http/memory.ts +2 -1
- package/src/providers/codex-adapter.ts +3 -0
- package/src/providers/opencode-adapter.ts +40 -0
- package/src/providers/pi-mono-adapter.ts +179 -1
- package/src/providers/pi-mono-extension.ts +9 -0
- package/src/tests/agent-avatar-resolution.test.ts +170 -0
- package/src/tests/memory-feature-flags.test.ts +45 -0
- package/src/tests/memory-rater-llm-summarizer.test.ts +24 -0
- package/src/tests/opencode-plugin.test.ts +43 -0
- package/src/tests/openrouter-base-url.test.ts +256 -0
- package/src/tests/pi-mono-adapter.test.ts +90 -0
- package/src/tools/memory-search.ts +2 -1
- package/src/utils/internal-ai/complete-structured.ts +19 -1
- package/src/utils/internal-ai/summarize-session.ts +7 -0
- package/src/utils/openrouter-base-url.ts +28 -0
- package/src/workflows/executors/raw-llm.ts +2 -1
- package/src/workflows/executors/validate.ts +2 -1
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OPENROUTER_BASE_URL gateway routing — unit tests.
|
|
3
|
+
*
|
|
4
|
+
* Covers the shared resolver (`src/utils/openrouter-base-url.ts`), the pi
|
|
5
|
+
* models.json override writer (`ensureOpenRouterModelsOverride`) including
|
|
6
|
+
* its composition through pi-coding-agent's ModelRuntime, and the opencode
|
|
7
|
+
* per-task config injection (`applyOpenRouterBaseUrlOverride`).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
11
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
12
|
+
import { tmpdir } from "node:os";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import { ModelRuntime } from "@earendil-works/pi-coding-agent";
|
|
15
|
+
import type { Config } from "@opencode-ai/sdk";
|
|
16
|
+
import { applyOpenRouterBaseUrlOverride } from "../providers/opencode-adapter";
|
|
17
|
+
import { ensureOpenRouterModelsOverride } from "../providers/pi-mono-adapter";
|
|
18
|
+
import { DEFAULT_OPENROUTER_BASE_URL, getOpenRouterBaseUrl } from "../utils/openrouter-base-url";
|
|
19
|
+
|
|
20
|
+
const GATEWAY = "https://control-plane.example/proxy/v1";
|
|
21
|
+
|
|
22
|
+
describe("getOpenRouterBaseUrl", () => {
|
|
23
|
+
test("defaults to openrouter.ai when unset", () => {
|
|
24
|
+
expect(getOpenRouterBaseUrl({} as NodeJS.ProcessEnv)).toBe(DEFAULT_OPENROUTER_BASE_URL);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("blank / whitespace-only values fall back to the default", () => {
|
|
28
|
+
expect(getOpenRouterBaseUrl({ OPENROUTER_BASE_URL: "" } as NodeJS.ProcessEnv)).toBe(
|
|
29
|
+
DEFAULT_OPENROUTER_BASE_URL,
|
|
30
|
+
);
|
|
31
|
+
expect(getOpenRouterBaseUrl({ OPENROUTER_BASE_URL: " " } as NodeJS.ProcessEnv)).toBe(
|
|
32
|
+
DEFAULT_OPENROUTER_BASE_URL,
|
|
33
|
+
);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("returns the override verbatim (trimmed)", () => {
|
|
37
|
+
expect(getOpenRouterBaseUrl({ OPENROUTER_BASE_URL: ` ${GATEWAY} ` } as NodeJS.ProcessEnv)).toBe(
|
|
38
|
+
GATEWAY,
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("strips trailing slashes so call sites can append paths", () => {
|
|
43
|
+
expect(getOpenRouterBaseUrl({ OPENROUTER_BASE_URL: `${GATEWAY}//` } as NodeJS.ProcessEnv)).toBe(
|
|
44
|
+
GATEWAY,
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe("ensureOpenRouterModelsOverride (pi)", () => {
|
|
50
|
+
let agentDir: string;
|
|
51
|
+
|
|
52
|
+
beforeAll(() => {
|
|
53
|
+
agentDir = mkdtempSync(join(tmpdir(), "pi-or-override-"));
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
afterAll(() => {
|
|
57
|
+
rmSync(agentDir, { recursive: true, force: true });
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("no-op when OPENROUTER_BASE_URL is unset", async () => {
|
|
61
|
+
const dir = mkdtempSync(join(tmpdir(), "pi-or-noop-"));
|
|
62
|
+
try {
|
|
63
|
+
await ensureOpenRouterModelsOverride(dir, {});
|
|
64
|
+
expect(await Bun.file(join(dir, "models.json")).exists()).toBe(false);
|
|
65
|
+
} finally {
|
|
66
|
+
rmSync(dir, { recursive: true, force: true });
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("writes providers.openrouter.baseUrl when the env is set", async () => {
|
|
71
|
+
await ensureOpenRouterModelsOverride(agentDir, { OPENROUTER_BASE_URL: GATEWAY });
|
|
72
|
+
const written = JSON.parse(await Bun.file(join(agentDir, "models.json")).text());
|
|
73
|
+
expect(written).toEqual({ providers: { openrouter: { baseUrl: GATEWAY } } });
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("reverts a written override when the env returns to default (file we created is removed)", async () => {
|
|
77
|
+
const dir = mkdtempSync(join(tmpdir(), "pi-or-revert-"));
|
|
78
|
+
try {
|
|
79
|
+
await ensureOpenRouterModelsOverride(dir, { OPENROUTER_BASE_URL: GATEWAY });
|
|
80
|
+
expect(await Bun.file(join(dir, "models.json")).exists()).toBe(true);
|
|
81
|
+
await ensureOpenRouterModelsOverride(dir, {});
|
|
82
|
+
// The override was ALL the file contained — both it and the marker go.
|
|
83
|
+
expect(await Bun.file(join(dir, "models.json")).exists()).toBe(false);
|
|
84
|
+
expect(await Bun.file(join(dir, ".agent-swarm-openrouter-override.json")).exists()).toBe(
|
|
85
|
+
false,
|
|
86
|
+
);
|
|
87
|
+
} finally {
|
|
88
|
+
rmSync(dir, { recursive: true, force: true });
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("revert restores a displaced user baseUrl and keeps sibling keys", async () => {
|
|
93
|
+
const dir = mkdtempSync(join(tmpdir(), "pi-or-restore-"));
|
|
94
|
+
try {
|
|
95
|
+
await Bun.write(
|
|
96
|
+
join(dir, "models.json"),
|
|
97
|
+
JSON.stringify({
|
|
98
|
+
providers: {
|
|
99
|
+
openrouter: { baseUrl: "https://user.example/v1", compat: { supportsTools: true } },
|
|
100
|
+
custom: { baseUrl: "https://custom.example/v1", api: "openai-completions" },
|
|
101
|
+
},
|
|
102
|
+
}),
|
|
103
|
+
);
|
|
104
|
+
await ensureOpenRouterModelsOverride(dir, { OPENROUTER_BASE_URL: GATEWAY });
|
|
105
|
+
// Re-run with the gateway still set — displaced value must survive.
|
|
106
|
+
await ensureOpenRouterModelsOverride(dir, { OPENROUTER_BASE_URL: GATEWAY });
|
|
107
|
+
await ensureOpenRouterModelsOverride(dir, {});
|
|
108
|
+
const restored = JSON.parse(await Bun.file(join(dir, "models.json")).text());
|
|
109
|
+
expect(restored.providers.openrouter).toEqual({
|
|
110
|
+
baseUrl: "https://user.example/v1",
|
|
111
|
+
compat: { supportsTools: true },
|
|
112
|
+
});
|
|
113
|
+
expect(restored.providers.custom).toEqual({
|
|
114
|
+
baseUrl: "https://custom.example/v1",
|
|
115
|
+
api: "openai-completions",
|
|
116
|
+
});
|
|
117
|
+
} finally {
|
|
118
|
+
rmSync(dir, { recursive: true, force: true });
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("revert leaves a user-edited baseUrl alone (marker mismatch) and drops the marker", async () => {
|
|
123
|
+
const dir = mkdtempSync(join(tmpdir(), "pi-or-user-edit-"));
|
|
124
|
+
try {
|
|
125
|
+
await ensureOpenRouterModelsOverride(dir, { OPENROUTER_BASE_URL: GATEWAY });
|
|
126
|
+
// User hand-edits the file after we wrote it.
|
|
127
|
+
await Bun.write(
|
|
128
|
+
join(dir, "models.json"),
|
|
129
|
+
JSON.stringify({ providers: { openrouter: { baseUrl: "https://mine.example/v1" } } }),
|
|
130
|
+
);
|
|
131
|
+
await ensureOpenRouterModelsOverride(dir, {});
|
|
132
|
+
const kept = JSON.parse(await Bun.file(join(dir, "models.json")).text());
|
|
133
|
+
expect(kept.providers.openrouter.baseUrl).toBe("https://mine.example/v1");
|
|
134
|
+
expect(await Bun.file(join(dir, ".agent-swarm-openrouter-override.json")).exists()).toBe(
|
|
135
|
+
false,
|
|
136
|
+
);
|
|
137
|
+
} finally {
|
|
138
|
+
rmSync(dir, { recursive: true, force: true });
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("default env never touches a hand-authored models.json (no marker)", async () => {
|
|
143
|
+
const dir = mkdtempSync(join(tmpdir(), "pi-or-hands-off-"));
|
|
144
|
+
try {
|
|
145
|
+
const userConfig = {
|
|
146
|
+
providers: { openrouter: { baseUrl: "https://mine.example/v1" } },
|
|
147
|
+
};
|
|
148
|
+
await Bun.write(join(dir, "models.json"), JSON.stringify(userConfig));
|
|
149
|
+
await ensureOpenRouterModelsOverride(dir, {});
|
|
150
|
+
expect(JSON.parse(await Bun.file(join(dir, "models.json")).text())).toEqual(userConfig);
|
|
151
|
+
} finally {
|
|
152
|
+
rmSync(dir, { recursive: true, force: true });
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("merge-preserves existing providers and openrouter keys", async () => {
|
|
157
|
+
const dir = mkdtempSync(join(tmpdir(), "pi-or-merge-"));
|
|
158
|
+
try {
|
|
159
|
+
await Bun.write(
|
|
160
|
+
join(dir, "models.json"),
|
|
161
|
+
JSON.stringify({
|
|
162
|
+
providers: {
|
|
163
|
+
openrouter: { baseUrl: "https://old.example/v1", compat: { supportsTools: true } },
|
|
164
|
+
custom: { baseUrl: "https://custom.example/v1", api: "openai-completions" },
|
|
165
|
+
},
|
|
166
|
+
}),
|
|
167
|
+
);
|
|
168
|
+
await ensureOpenRouterModelsOverride(dir, { OPENROUTER_BASE_URL: GATEWAY });
|
|
169
|
+
const written = JSON.parse(await Bun.file(join(dir, "models.json")).text());
|
|
170
|
+
expect(written.providers.openrouter.baseUrl).toBe(GATEWAY);
|
|
171
|
+
expect(written.providers.openrouter.compat).toEqual({ supportsTools: true });
|
|
172
|
+
expect(written.providers.custom).toEqual({
|
|
173
|
+
baseUrl: "https://custom.example/v1",
|
|
174
|
+
api: "openai-completions",
|
|
175
|
+
});
|
|
176
|
+
} finally {
|
|
177
|
+
rmSync(dir, { recursive: true, force: true });
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test("overwrites a corrupt models.json with just the override", async () => {
|
|
182
|
+
const dir = mkdtempSync(join(tmpdir(), "pi-or-corrupt-"));
|
|
183
|
+
try {
|
|
184
|
+
await Bun.write(join(dir, "models.json"), "{not json");
|
|
185
|
+
await ensureOpenRouterModelsOverride(dir, { OPENROUTER_BASE_URL: GATEWAY });
|
|
186
|
+
const written = JSON.parse(await Bun.file(join(dir, "models.json")).text());
|
|
187
|
+
expect(written).toEqual({ providers: { openrouter: { baseUrl: GATEWAY } } });
|
|
188
|
+
} finally {
|
|
189
|
+
rmSync(dir, { recursive: true, force: true });
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test("ModelRuntime composes the override onto built-in OpenRouter models", async () => {
|
|
194
|
+
// The whole point of the lever: built-in model IDs resolved through the
|
|
195
|
+
// runtime's composed store carry the gateway baseUrl.
|
|
196
|
+
const runtime = await ModelRuntime.create({
|
|
197
|
+
authPath: join(agentDir, "auth.json"),
|
|
198
|
+
modelsPath: join(agentDir, "models.json"),
|
|
199
|
+
});
|
|
200
|
+
expect(runtime.getError()).toBeUndefined();
|
|
201
|
+
const model = runtime.getModel("openrouter", "anthropic/claude-sonnet-5");
|
|
202
|
+
expect(model?.baseUrl).toBe(GATEWAY);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test("ModelRuntime keeps the default baseUrl when no models.json exists", async () => {
|
|
206
|
+
const dir = mkdtempSync(join(tmpdir(), "pi-or-default-"));
|
|
207
|
+
try {
|
|
208
|
+
const runtime = await ModelRuntime.create({
|
|
209
|
+
authPath: join(dir, "auth.json"),
|
|
210
|
+
modelsPath: join(dir, "models.json"),
|
|
211
|
+
});
|
|
212
|
+
const model = runtime.getModel("openrouter", "anthropic/claude-sonnet-5");
|
|
213
|
+
expect(model?.baseUrl).toBe(DEFAULT_OPENROUTER_BASE_URL);
|
|
214
|
+
} finally {
|
|
215
|
+
rmSync(dir, { recursive: true, force: true });
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
describe("applyOpenRouterBaseUrlOverride (opencode)", () => {
|
|
221
|
+
test("no-op when OPENROUTER_BASE_URL is unset", () => {
|
|
222
|
+
const config: Config = { model: "openrouter/google/gemini-3-flash-preview" };
|
|
223
|
+
applyOpenRouterBaseUrlOverride(config, {});
|
|
224
|
+
expect(config.provider).toBeUndefined();
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test("sets provider.openrouter.options.baseURL when the env is set", () => {
|
|
228
|
+
const config: Config = { model: "openrouter/google/gemini-3-flash-preview" };
|
|
229
|
+
applyOpenRouterBaseUrlOverride(config, { OPENROUTER_BASE_URL: GATEWAY });
|
|
230
|
+
expect(config.provider?.openrouter?.options?.baseURL).toBe(GATEWAY);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test("preserves existing provider config (e.g. reasoning model options)", () => {
|
|
234
|
+
const config: Config = {
|
|
235
|
+
model: "openrouter/google/gemini-3-flash-preview",
|
|
236
|
+
provider: {
|
|
237
|
+
openrouter: {
|
|
238
|
+
models: {
|
|
239
|
+
"google/gemini-3-flash-preview": { options: { reasoning: { effort: "low" } } },
|
|
240
|
+
},
|
|
241
|
+
options: { apiKey: "sk-keep" },
|
|
242
|
+
},
|
|
243
|
+
anthropic: { options: { apiKey: "sk-ant" } },
|
|
244
|
+
},
|
|
245
|
+
};
|
|
246
|
+
applyOpenRouterBaseUrlOverride(config, { OPENROUTER_BASE_URL: GATEWAY });
|
|
247
|
+
expect(config.provider?.openrouter?.options).toEqual({
|
|
248
|
+
apiKey: "sk-keep",
|
|
249
|
+
baseURL: GATEWAY,
|
|
250
|
+
});
|
|
251
|
+
expect(config.provider?.openrouter?.models).toEqual({
|
|
252
|
+
"google/gemini-3-flash-preview": { options: { reasoning: { effort: "low" } } },
|
|
253
|
+
});
|
|
254
|
+
expect(config.provider?.anthropic).toEqual({ options: { apiKey: "sk-ant" } });
|
|
255
|
+
});
|
|
256
|
+
});
|
|
@@ -81,6 +81,96 @@ describe("PiMonoAdapter.createSession — reasoning_effort", () => {
|
|
|
81
81
|
});
|
|
82
82
|
});
|
|
83
83
|
|
|
84
|
+
// ─── OPENROUTER_BASE_URL gateway routing (session model composition) ─────────
|
|
85
|
+
|
|
86
|
+
describe("PiMonoAdapter.createSession — OPENROUTER_BASE_URL gateway", () => {
|
|
87
|
+
const GATEWAY = "https://control-plane.example/proxy/v1";
|
|
88
|
+
const agentDir = `/tmp/pi-or-gateway-test-${Date.now()}`;
|
|
89
|
+
const origAgentDirEnv = process.env.PI_CODING_AGENT_DIR;
|
|
90
|
+
|
|
91
|
+
let createAgentSessionSpy: ReturnType<typeof spyOn>;
|
|
92
|
+
let capturedOptions: Record<string, unknown> | undefined;
|
|
93
|
+
|
|
94
|
+
beforeAll(() => {
|
|
95
|
+
mkdirSync(agentDir, { recursive: true });
|
|
96
|
+
// getAgentDir() reads process.env directly (config.env doesn't reach it).
|
|
97
|
+
process.env.PI_CODING_AGENT_DIR = agentDir;
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
afterAll(() => {
|
|
101
|
+
if (origAgentDirEnv === undefined) delete process.env.PI_CODING_AGENT_DIR;
|
|
102
|
+
else process.env.PI_CODING_AGENT_DIR = origAgentDirEnv;
|
|
103
|
+
rmSync(agentDir, { recursive: true, force: true });
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
afterEach(() => {
|
|
107
|
+
createAgentSessionSpy?.mockRestore();
|
|
108
|
+
capturedOptions = undefined;
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
function makeConfig(env: Record<string, string>): ProviderSessionConfig {
|
|
112
|
+
return {
|
|
113
|
+
prompt: "hello",
|
|
114
|
+
systemPrompt: "",
|
|
115
|
+
model: "openrouter/google/gemini-3-flash-preview",
|
|
116
|
+
role: "worker",
|
|
117
|
+
agentId: "test-agent",
|
|
118
|
+
taskId: "test-task",
|
|
119
|
+
apiUrl: "",
|
|
120
|
+
apiKey: "",
|
|
121
|
+
cwd: "/tmp",
|
|
122
|
+
logFile: `/tmp/pi-or-gateway-test-${Date.now()}-${Math.random().toString(36).slice(2)}.log`,
|
|
123
|
+
env: { OPENROUTER_API_KEY: "sk-or-test", ...env },
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function spyOnCreateAgentSession() {
|
|
128
|
+
createAgentSessionSpy = spyOn(piCodingAgent, "createAgentSession").mockImplementation((async (
|
|
129
|
+
opts: Record<string, unknown>,
|
|
130
|
+
) => {
|
|
131
|
+
capturedOptions = opts;
|
|
132
|
+
return {
|
|
133
|
+
session: {
|
|
134
|
+
sessionId: "fake-session",
|
|
135
|
+
isStreaming: false,
|
|
136
|
+
model: undefined,
|
|
137
|
+
subscribe: () => () => {},
|
|
138
|
+
dispose: () => {},
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
}) as typeof piCodingAgent.createAgentSession);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
test("session model carries the gateway baseUrl when OPENROUTER_BASE_URL is set", async () => {
|
|
145
|
+
spyOnCreateAgentSession();
|
|
146
|
+
const adapter = new PiMonoAdapter();
|
|
147
|
+
await adapter.createSession(makeConfig({ OPENROUTER_BASE_URL: GATEWAY }));
|
|
148
|
+
const model = capturedOptions?.model as { baseUrl?: string; id?: string } | undefined;
|
|
149
|
+
expect(model?.id).toBe("google/gemini-3-flash-preview");
|
|
150
|
+
expect(model?.baseUrl).toBe(GATEWAY);
|
|
151
|
+
// The override was materialized for the runtime to load.
|
|
152
|
+
expect(existsSync(join(agentDir, "models.json"))).toBe(true);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("session model keeps the default openrouter.ai baseUrl when env is unset", async () => {
|
|
156
|
+
// Fresh agent dir so the previous test's models.json doesn't leak in.
|
|
157
|
+
const cleanDir = `${agentDir}-clean`;
|
|
158
|
+
mkdirSync(cleanDir, { recursive: true });
|
|
159
|
+
process.env.PI_CODING_AGENT_DIR = cleanDir;
|
|
160
|
+
try {
|
|
161
|
+
spyOnCreateAgentSession();
|
|
162
|
+
const adapter = new PiMonoAdapter();
|
|
163
|
+
await adapter.createSession(makeConfig({}));
|
|
164
|
+
const model = capturedOptions?.model as { baseUrl?: string } | undefined;
|
|
165
|
+
expect(model?.baseUrl).toBe("https://openrouter.ai/api/v1");
|
|
166
|
+
expect(existsSync(join(cleanDir, "models.json"))).toBe(false);
|
|
167
|
+
} finally {
|
|
168
|
+
process.env.PI_CODING_AGENT_DIR = agentDir;
|
|
169
|
+
rmSync(cleanDir, { recursive: true, force: true });
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
84
174
|
describe("AGENTS.md symlink management", () => {
|
|
85
175
|
const tmpDir = `/tmp/pi-mono-test-${Date.now()}`;
|
|
86
176
|
|
|
@@ -93,7 +93,8 @@ export const registerMemorySearchTool = (server: McpServer) => {
|
|
|
93
93
|
isLead,
|
|
94
94
|
queryText: query,
|
|
95
95
|
});
|
|
96
|
-
// 1-hop memory_link neighbor expansion (
|
|
96
|
+
// Default-on 1-hop memory_link neighbor expansion (disable with
|
|
97
|
+
// MEMORY_GRAPH_EXPANSION=0|false).
|
|
97
98
|
const expanded = expandCandidatesWithGraph(candidates, requestInfo.agentId, {
|
|
98
99
|
scope: scope as "agent" | "swarm" | "all",
|
|
99
100
|
source,
|
|
@@ -20,6 +20,7 @@ import { complete } from "@earendil-works/pi-ai/compat";
|
|
|
20
20
|
import { getBuiltinModel as getModel } from "@earendil-works/pi-ai/providers/all";
|
|
21
21
|
import type { TSchema } from "typebox";
|
|
22
22
|
import { z } from "zod";
|
|
23
|
+
import { DEFAULT_OPENROUTER_BASE_URL, getOpenRouterBaseUrl } from "../openrouter-base-url.js";
|
|
23
24
|
import { type ResolvedCredential, resolveCredential } from "./credentials.js";
|
|
24
25
|
import { parseModelStr } from "./models.js";
|
|
25
26
|
|
|
@@ -40,6 +41,14 @@ export interface CompleteStructuredOptions<TZod extends z.ZodTypeAny> {
|
|
|
40
41
|
*/
|
|
41
42
|
apiUrl?: string;
|
|
42
43
|
apiKey?: string;
|
|
44
|
+
/**
|
|
45
|
+
* Per-task resolved environment (`ProviderSessionConfig.env` /
|
|
46
|
+
* `fetchResolvedEnv` output). Used for credential lookup AND the
|
|
47
|
+
* `OPENROUTER_BASE_URL` gateway override — swarm_config-sourced values
|
|
48
|
+
* never reach `process.env`, so callers with a session env must pass it.
|
|
49
|
+
* Defaults to `process.env`.
|
|
50
|
+
*/
|
|
51
|
+
env?: Record<string, string | undefined>;
|
|
43
52
|
/** Default: 3. */
|
|
44
53
|
retries?: number;
|
|
45
54
|
signal?: AbortSignal;
|
|
@@ -186,7 +195,7 @@ export async function completeStructured<TZod extends z.ZodTypeAny>(
|
|
|
186
195
|
} else {
|
|
187
196
|
const resolver = opts._resolveCredential ?? resolveCredential;
|
|
188
197
|
cred = await resolver({
|
|
189
|
-
env: process.env,
|
|
198
|
+
env: (opts.env ?? process.env) as NodeJS.ProcessEnv,
|
|
190
199
|
apiUrl: opts.apiUrl,
|
|
191
200
|
apiKey: opts.apiKey,
|
|
192
201
|
callerTag: opts.callerTag,
|
|
@@ -259,6 +268,15 @@ export async function completeStructured<TZod extends z.ZodTypeAny>(
|
|
|
259
268
|
return null;
|
|
260
269
|
}
|
|
261
270
|
|
|
271
|
+
// The builtin catalog hardcodes openrouter.ai; OPENROUTER_BASE_URL reroutes
|
|
272
|
+
// through a gateway (pi-ai's stream path uses `model.baseUrl` verbatim).
|
|
273
|
+
// Resolved from the caller's session env when provided — swarm_config
|
|
274
|
+
// overlays don't reach `process.env`.
|
|
275
|
+
const openRouterBaseUrl = getOpenRouterBaseUrl((opts.env ?? process.env) as NodeJS.ProcessEnv);
|
|
276
|
+
if (provider === "openrouter" && openRouterBaseUrl !== DEFAULT_OPENROUTER_BASE_URL) {
|
|
277
|
+
model = { ...model, baseUrl: openRouterBaseUrl };
|
|
278
|
+
}
|
|
279
|
+
|
|
262
280
|
const completeFn = opts._complete ?? complete;
|
|
263
281
|
let userPrompt = opts.userPrompt;
|
|
264
282
|
let lastErr: unknown = null;
|
|
@@ -35,6 +35,12 @@ export interface SummarizeSessionOptions {
|
|
|
35
35
|
/** Passed through to `completeStructured` for codex-OAuth probing. */
|
|
36
36
|
apiUrl: string;
|
|
37
37
|
apiKey: string;
|
|
38
|
+
/**
|
|
39
|
+
* Per-task resolved env — forwarded to `completeStructured` so credential
|
|
40
|
+
* lookup and the `OPENROUTER_BASE_URL` gateway override see swarm_config
|
|
41
|
+
* overlays that never reach `process.env`.
|
|
42
|
+
*/
|
|
43
|
+
env?: Record<string, string | undefined>;
|
|
38
44
|
signal?: AbortSignal;
|
|
39
45
|
/**
|
|
40
46
|
* Bypass `resolveCredential` entirely — opencode auth path (and tests) pass
|
|
@@ -94,6 +100,7 @@ export async function summarizeSession(
|
|
|
94
100
|
callerTag: `session-summary:${opts.harness}`,
|
|
95
101
|
apiUrl: opts.apiUrl,
|
|
96
102
|
apiKey: opts.apiKey,
|
|
103
|
+
env: opts.env,
|
|
97
104
|
signal: opts.signal,
|
|
98
105
|
retries: 3,
|
|
99
106
|
...(opts._credentialOverride ? { _credentialOverride: opts._credentialOverride } : {}),
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenRouter base-URL resolution.
|
|
3
|
+
*
|
|
4
|
+
* `OPENROUTER_BASE_URL` lets a deployment route all OpenRouter traffic
|
|
5
|
+
* through an OpenAI-compatible gateway (e.g. the cloud control plane's
|
|
6
|
+
* `/proxy/v1` endpoint) instead of `openrouter.ai` directly, so the raw
|
|
7
|
+
* OpenRouter key never has to live on the box. When the env var is unset
|
|
8
|
+
* the default OpenRouter URL is used and behavior is unchanged.
|
|
9
|
+
*
|
|
10
|
+
* Worker-safe: pure env read, no `src/be/db` / `bun:sqlite` imports.
|
|
11
|
+
* The opencode summarize plugin (`plugin/opencode-plugins/lib/summarize.ts`)
|
|
12
|
+
* vendors this logic — keep the two in sync.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export const DEFAULT_OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Resolve the OpenRouter base URL (no trailing slash). Reads
|
|
19
|
+
* `OPENROUTER_BASE_URL` from `env`; blank values fall back to the default.
|
|
20
|
+
* Call sites append paths like `/chat/completions` or `/models`.
|
|
21
|
+
*/
|
|
22
|
+
export function getOpenRouterBaseUrl(env: NodeJS.ProcessEnv = process.env): string {
|
|
23
|
+
const raw = env.OPENROUTER_BASE_URL;
|
|
24
|
+
if (typeof raw === "string" && raw.trim().length > 0) {
|
|
25
|
+
return raw.trim().replace(/\/+$/, "");
|
|
26
|
+
}
|
|
27
|
+
return DEFAULT_OPENROUTER_BASE_URL;
|
|
28
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import type { ExecutorMeta } from "../../types";
|
|
3
|
+
import { getOpenRouterBaseUrl } from "../../utils/openrouter-base-url";
|
|
3
4
|
import { BaseExecutor, type ExecutorResult } from "./base";
|
|
4
5
|
|
|
5
6
|
// ─── Schemas ────────────────────────────────────────────────
|
|
@@ -27,7 +28,7 @@ export async function executeRawLlm(
|
|
|
27
28
|
try {
|
|
28
29
|
const { createOpenAI } = await import("@ai-sdk/openai");
|
|
29
30
|
const openrouter = createOpenAI({
|
|
30
|
-
baseURL:
|
|
31
|
+
baseURL: getOpenRouterBaseUrl(),
|
|
31
32
|
apiKey: process.env.OPENROUTER_API_KEY,
|
|
32
33
|
});
|
|
33
34
|
const model = openrouter(modelName);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import type { ExecutorMeta } from "../../types";
|
|
3
|
+
import { getOpenRouterBaseUrl } from "../../utils/openrouter-base-url";
|
|
3
4
|
import { BaseExecutor, type ExecutorResult } from "./base";
|
|
4
5
|
|
|
5
6
|
// ─── Schemas ────────────────────────────────────────────────
|
|
@@ -115,7 +116,7 @@ export class ValidateExecutor extends BaseExecutor<
|
|
|
115
116
|
const { generateObject, jsonSchema } = await import("ai");
|
|
116
117
|
|
|
117
118
|
const openrouter = createOpenAI({
|
|
118
|
-
baseURL:
|
|
119
|
+
baseURL: getOpenRouterBaseUrl(),
|
|
119
120
|
apiKey: process.env.OPENROUTER_API_KEY,
|
|
120
121
|
});
|
|
121
122
|
|