@desplega.ai/agent-swarm 1.166.0 → 1.168.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.
- package/dist/acp-adapter-zhejfecx.js +34 -0
- package/dist/{actions-qz3m692w.js → actions-7jg840nr.js} +25 -25
- package/dist/{amp-adapter-j1xbmsqz.js → amp-adapter-ye6rnetm.js} +4 -4
- package/dist/{anthropic-messages-hst62ws9.js → anthropic-messages-13yn502s.js} +4 -4
- package/dist/{app-43vrdmv8.js → app-ffes2kyf.js} +10 -9
- package/dist/{assistant-ertffmka.js → assistant-fhm5smyz.js} +26 -26
- package/dist/{azure-openai-responses-mh8tvf78.js → azure-openai-responses-9st34pfz.js} +5 -5
- package/dist/{boot-reembed-998jtpdg.js → boot-reembed-20q3nzvg.js} +9 -8
- package/dist/{boot-reembed-s7k7c6v2.js → boot-reembed-jvfadhsk.js} +9 -8
- package/dist/{boot-scrub-sweep-nepsk4t1.js → boot-scrub-sweep-jtfajzve.js} +10 -9
- package/dist/{boot-seeds-vvzs1zhk.js → boot-seeds-9qt0j8j2.js} +12 -11
- package/dist/{claude-adapter-2zvmekrp.js → claude-adapter-a8znhthz.js} +24 -22
- package/dist/{claude-managed-adapter-s49wbk8t.js → claude-managed-adapter-tzwxy803.js} +4 -4
- package/dist/{claude-managed-setup-cvhqq8qn.js → claude-managed-setup-nrhfx5b0.js} +3 -3
- package/dist/{claude-sdk-session-kkbxtw5q.js → claude-sdk-session-7x7chdrs.js} +7608 -7596
- package/dist/{cli-zsh093hk.js → cli-014vns2y.js} +63 -1
- package/dist/{cli-8kgvmkak.js → cli-0jngabtb.js} +1 -1
- package/dist/{cli-qvfk83fy.js → cli-13xwcqvj.js} +2 -2
- package/dist/{cli-0nhdwxd5.js → cli-43jg9xdw.js} +3 -3
- package/dist/{cli-cpfmqz3n.js → cli-47zfzshm.js} +5 -5
- package/dist/{cli-zy7vwsne.js → cli-5qkcdv8c.js} +16 -9
- package/dist/{cli-cwxhbf43.js → cli-5t5my0h8.js} +85 -2
- package/dist/{cli-y59h2pk9.js → cli-69egj654.js} +1 -1
- package/dist/{cli-68r1j4sg.js → cli-6bpvv8h4.js} +3 -3
- package/dist/{cli-yshaazs1.js → cli-74ez9b66.js} +63 -20
- package/dist/{cli-32bca7nr.js → cli-7avy4v8w.js} +1 -1
- package/dist/{cli-f2n17zhp.js → cli-7mhy3ths.js} +21 -18
- package/dist/{cli-q38n4bq0.js → cli-95zs84qw.js} +3 -3
- package/dist/{cli-fn697bnt.js → cli-9q0bh411.js} +21 -8
- package/dist/{cli-rggdq8wq.js → cli-9q24yyvs.js} +3 -3
- package/dist/cli-a35hzdve.js +33 -0
- package/dist/{cli-3qn4r334.js → cli-aj1e0h0d.js} +20 -14
- package/dist/{cli-rsy933yz.js → cli-bcspch6h.js} +30 -19
- package/dist/{cli-x3x0qwky.js → cli-bxqez1p0.js} +51 -23
- package/dist/{cli-ng97fjq7.js → cli-cmxkbzzs.js} +33 -7
- package/dist/{cli-df56tqdw.js → cli-ee0402n1.js} +198 -70
- package/dist/{cli-mwegs4af.js → cli-ew0c3a1j.js} +3 -3
- package/dist/{cli-ysnyhws9.js → cli-ewhwpyrb.js} +54 -51
- package/dist/{cli-j2jbr8af.js → cli-fhj4y16f.js} +1 -1
- package/dist/{cli-8t229cmr.js → cli-fqq06kkb.js} +30 -17
- package/dist/{cli-jvj6a0cv.js → cli-g6gx2dn1.js} +54 -19
- package/dist/{cli-nexwh8zg.js → cli-g7k7zbcz.js} +12 -5
- package/dist/{cli-ecknp7p1.js → cli-j05x7pnb.js} +10 -10
- package/dist/{cli-gt8azyet.js → cli-jb931ba2.js} +1 -1
- package/dist/{cli-nvvfd8bq.js → cli-jbzbr36n.js} +4091 -3793
- package/dist/{cli-3ddybbbx.js → cli-jvmtxqtc.js} +26 -14
- package/dist/{cli-pkke6geq.js → cli-km4x66j0.js} +83 -65
- package/dist/{cli-b0f70z24.js → cli-mn8qkd8v.js} +1 -1
- package/dist/{cli-0qjcsxgz.js → cli-nctfv2yy.js} +99 -9
- package/dist/{cli-4r1sjfc4.js → cli-ng1pmdx6.js} +10 -10
- package/dist/cli-nkqjj42q.js +104 -0
- package/dist/{cli-aqcey741.js → cli-p5jqk8fr.js} +1 -1
- package/dist/{cli-66zh26sp.js → cli-pgehz8e1.js} +57 -17
- package/dist/cli-q1vnz8wv.js +96 -0
- package/dist/{cli-fj86xr3c.js → cli-qd97v6z2.js} +2 -2
- package/dist/{cli-p4ztb80h.js → cli-r7eeddkq.js} +13 -8
- package/dist/{cli-8ssjez7e.js → cli-rhy7z1rp.js} +22 -11
- package/dist/{cli-vzc73wet.js → cli-rr5z1j4p.js} +87 -69
- package/dist/{cli-4mevd1hf.js → cli-s9y81s7j.js} +5 -5
- package/dist/{cli-dw2q9f7g.js → cli-svah6b6w.js} +4 -4
- package/dist/{cli-h3x613ze.js → cli-tc5zec7v.js} +6 -6
- package/dist/{cli-ttbsse3f.js → cli-tmhsxhb8.js} +1 -1
- package/dist/{cli-feh3dkah.js → cli-v1w7pbd8.js} +2 -1
- package/dist/{cli-10zmp1p7.js → cli-vr5avem2.js} +27 -32
- package/dist/{cli-bzr0ftht.js → cli-wnb9b2g3.js} +19 -0
- package/dist/{cli-krqm5f11.js → cli-xezn9nkh.js} +16 -13
- package/dist/{cli-8kt0bmgx.js → cli-xf713zz0.js} +2 -2
- package/dist/{cli-6zcfw0x7.js → cli-y195hv1a.js} +1 -1
- package/dist/{cli-x7kpr72a.js → cli-ytmmyj7f.js} +2 -2
- package/dist/{cli-v2a8x45d.js → cli-z48ggfyq.js} +11 -6
- package/dist/{cli-qbksc292.js → cli-zy39d433.js} +2 -0
- package/dist/cli.js +28 -28
- package/dist/{cloudflare-workers-ai-system-one-tt484t61.js → cloudflare-workers-ai-system-one-sta45jf9.js} +7 -5
- package/dist/{codex-adapter-dw1wxqt8.js → codex-adapter-n63k33d3.js} +18 -18
- package/dist/{codex-login-terbdsqq.js → codex-login-g1cyt5zm.js} +1 -1
- package/dist/{codex-session-runner-3cgb8y1g.js → codex-session-runner-xkxfd81a.js} +18 -18
- package/dist/{commands-mhxam6z2.js → commands-q4mfhn51.js} +9 -8
- package/dist/{cursor-adapter-2qda3st7.js → cursor-adapter-v8mm6bp5.js} +5 -5
- package/dist/{db-sjks418k.js → db-1t0bj3pc.js} +9 -8
- package/dist/{dispatcher-1es759rn.js → dispatcher-k1fhavcf.js} +24 -24
- package/dist/{dsh-adapter-bek4cyw9.js → dsh-adapter-5423p70m.js} +5 -5
- package/dist/{easter-egg-3d-mtpnw06s.js → easter-egg-3d-66zxervv.js} +2 -2
- package/dist/{execute-s4ccjq3n.js → execute-h4jgevyw.js} +61 -11
- package/dist/{google-generative-ai-d6nphg3q.js → google-generative-ai-b8zxrpax.js} +5 -5
- package/dist/{google-vertex-cpshkref.js → google-vertex-nrdd697t.js} +5 -5
- package/dist/{grok-adapter-ar9cw9jg.js → grok-adapter-phrndeb8.js} +18 -8
- package/dist/{handlers-1gkbhktr.js → handlers-59ywqwzh.js} +27 -27
- package/dist/{hook-vxa3tcsy.js → hook-atj0swc2.js} +14 -14
- package/dist/{hook-ds2zxq9v.js → hook-mew1chq9.js} +42 -31
- package/dist/{http-3h21z9md.js → http-ef2fnn9v.js} +953 -98
- package/dist/{index-n99fycq7.js → index-48pb9erw.js} +24 -24
- package/dist/{index-bn6cy8ps.js → index-9fc2h7ww.js} +24 -24
- package/dist/{index-9b65j1m5.js → index-gr69c43q.js} +9 -8
- package/dist/{index-f8xnqrye.js → index-h26wjcfy.js} +43 -24
- package/dist/{index-dkfxpkmm.js → index-w2yhye23.js} +25 -25
- package/dist/{keepalive-3xspa9jw.js → keepalive-qhcqz56v.js} +11 -10
- package/dist/{lead-6qphc124.js → lead-hpk5r3b1.js} +47 -47
- package/dist/{llama-cpp-classify-tgj35k8d.js → llama-cpp-classify-6erb7wwv.js} +3 -1
- package/dist/{maintenance-0teem1bb.js → maintenance-nvkyp2xe.js} +9 -8
- package/dist/{mistral-conversations-8445ddjn.js → mistral-conversations-059y51zt.js} +4 -4
- package/dist/{node-9sg7ajby.js → node-qm9eb6z0.js} +1 -1
- package/dist/{oauth-refresh-sweep-sh9q25xp.js → oauth-refresh-sweep-ngskthwh.js} +10 -9
- package/dist/{onboard-54qmnqxe.js → onboard-yfnkqhxc.js} +2 -2
- package/dist/{openai-codex-responses-9ynm88zy.js → openai-codex-responses-jzxkta9b.js} +8 -7
- package/dist/{openai-completions-nhqwjmaw.js → openai-completions-663yncfq.js} +4 -4
- package/dist/openai-decisions-vj9nkpf8.js +162 -0
- package/dist/{openai-responses-xx1c4kgm.js → openai-responses-m3g43rrt.js} +5 -5
- package/dist/{opencode-adapter-wjfa2wvd.js → opencode-adapter-yjyqynzf.js} +7 -7
- package/dist/{openrouter-images-8yx3487g.js → openrouter-images-kr60n6b9.js} +1 -1
- package/dist/{otel-impl-ycqzp2a1.js → otel-impl-hhm64r6m.js} +1 -1
- package/dist/{pi-messages-vxdt8v7j.js → pi-messages-xszk613n.js} +1 -1
- package/dist/{pi-mono-adapter-e77e9zst.js → pi-mono-adapter-0eg8kw2c.js} +68 -51
- package/dist/{pricing-refresh-atnkngqs.js → pricing-refresh-35f91nx2.js} +11 -10
- package/dist/{rbac-roles-j78wdksm.js → rbac-roles-97w370qj.js} +10 -9
- package/dist/{rbac-roles-9pjphh9q.js → rbac-roles-z6sje3gy.js} +9 -8
- package/dist/{render-v2-pxjs49fx.js → render-v2-n4v66bj3.js} +10 -9
- package/dist/{runtime-v2t1r79e.js → runtime-h9ha1mq6.js} +4 -4
- package/dist/{setup-ymej0xtx.js → setup-m3hhyv5f.js} +2 -2
- package/dist/{typesafe-system-one-datfr82p.js → typesafe-system-one-e988vty7.js} +7 -5
- package/dist/{virtual-modules-wm8mxdxx.js → virtual-modules-ftr9kwe8.js} +11 -11
- package/dist/{work-objects-3nh4ymzb.js → work-objects-mxf2c842.js} +27 -27
- package/dist/{worker-qa9fkz5s.js → worker-9t07z1gq.js} +47 -47
- package/openapi.json +72 -1
- package/package.json +10 -10
- package/src/be/memory/providers/openai-embedding.ts +7 -2
- package/src/be/memory/raters/llm-summarizer.ts +7 -2
- package/src/be/modelsdev-cache.json +61 -0
- package/src/be/script-connections.ts +40 -6
- package/src/be/seed-pricing.ts +84 -53
- package/src/be/swarm-config-guard.ts +14 -0
- package/src/commands/provider-credentials.ts +28 -5
- package/src/commands/runner.ts +7 -12
- package/src/extensions/catalog.generated.json +17 -0
- package/src/heartbeat/heartbeat.ts +91 -19
- package/src/hooks/hook.ts +72 -27
- package/src/http/mcp.ts +5 -0
- package/src/http/onboarding.ts +2 -0
- package/src/http/script-connections.ts +35 -0
- package/src/providers/acp-targets.ts +10 -5
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/claude-adapter.ts +32 -0
- package/src/providers/claude-background-keepalive.ts +100 -0
- package/src/providers/claude-sdk-session.ts +6 -0
- package/src/providers/codex-adapter.ts +30 -3
- package/src/providers/codex-oauth/auth-json.ts +21 -4
- package/src/providers/codex-oauth/types.ts +2 -0
- package/src/providers/dsh-adapter.ts +11 -2
- package/src/providers/grok-adapter.ts +18 -2
- package/src/providers/opencode-adapter.ts +21 -5
- package/src/providers/pi-mono-extension.ts +56 -25
- package/src/providers/pricing-sources.md +2 -0
- package/src/scheduler/scheduler.ts +82 -13
- package/src/scripts-runtime/executors/registry.ts +1 -1
- package/src/server.ts +63 -3
- package/src/tests/acp-adapter.test.ts +152 -12
- package/src/tests/ci-unit-test-report.test.ts +73 -0
- package/src/tests/claude-background-keepalive-wiring.test.ts +220 -0
- package/src/tests/claude-background-keepalive.fixture.ts +162 -0
- package/src/tests/claude-background-keepalive.test.ts +139 -0
- package/src/tests/claude-default-route.test.ts +75 -0
- package/src/tests/claude-hook-output.test.ts +233 -0
- package/src/tests/codex-adapter-pool-auth-revalidate.test.ts +4 -2
- package/src/tests/codex-adapter.test.ts +166 -26
- package/src/tests/codex-oauth-auth-timestamps.test.ts +76 -0
- package/src/tests/codex-oauth.test.ts +4 -1
- package/src/tests/connection-embedded-auth.test.ts +51 -0
- package/src/tests/context-window.test.ts +2 -2
- package/src/tests/credential-check.test.ts +51 -1
- package/src/tests/docs-index-footprint.test.js +3 -3
- package/src/tests/dsh-adapter.test.ts +24 -2
- package/src/tests/enabled-tools.test.ts +269 -0
- package/src/tests/entrypoint-codex-oauth-seed.test.ts +12 -0
- package/src/tests/extensions-catalog.test.ts +1 -0
- package/src/tests/extensions-example-slack-reply-gate.test.ts +756 -0
- package/src/tests/grok-adapter.test.ts +13 -0
- package/src/tests/haiku-5-5-pricing.test.ts +38 -0
- package/src/tests/heartbeat-checklist.test.ts +158 -1
- package/src/tests/onboarding.test.ts +44 -0
- package/src/tests/opencode-adapter.test.ts +66 -3
- package/src/tests/opencode-plugin.test.ts +37 -0
- package/src/tests/openrouter-attribution-call-sites.test.ts +204 -0
- package/src/tests/openrouter-base-url.test.ts +30 -39
- package/src/tests/pi-mono-extension.test.ts +161 -2
- package/src/tests/pricing-refresh.test.ts +70 -1
- package/src/tests/reasoning-effort.test.ts +3 -1
- package/src/tests/schedule-target-type.test.ts +136 -0
- package/src/tests/script-connections-http.test.ts +45 -0
- package/src/tests/script-connections-mcp.test.ts +41 -0
- package/src/tests/script-executor-registry.test.ts +18 -0
- package/src/tests/send-task-self-send.test.ts +107 -0
- package/src/tests/telemetry-trigger.test.ts +12 -1
- package/src/tests/workflow-llm.test.ts +14 -0
- package/src/tests/workflow-system-one-decision-providers.test.ts +33 -0
- package/src/tools/request-human-input.ts +7 -2
- package/src/tools/script-connections/tool.ts +35 -3
- package/src/tools/send-task.ts +9 -1
- package/src/utils/enabled-tools.ts +27 -0
- package/src/utils/internal-ai/complete-structured.ts +11 -3
- package/src/utils/openrouter-base-url.ts +23 -0
- package/src/workflows/executors/raw-llm.ts +1 -0
- package/src/workflows/executors/system-one-decision.ts +2 -0
- package/src/workflows/executors/validate.ts +1 -0
- package/src/workflows/executors/workflow-llm.ts +9 -2
- package/templates/extensions/README.md +1 -0
- package/templates/extensions/slack-reply-gate/README.md +98 -0
- package/templates/extensions/slack-reply-gate/hooks.ts +582 -0
- package/templates/extensions/slack-reply-gate/manifest.json +8 -0
- package/templates/skills/agent-browser/SKILL.md +2 -0
- package/templates/skills/agent-browser/content.md +2 -0
- package/templates/skills/code-quality/content.md +1 -0
- package/templates/skills/heartbeat-runbook/content.md +1 -1
- package/templates/skills/swarm-scripts/SKILL.md +10 -0
- package/templates/skills/swarm-scripts/content.md +10 -0
- package/templates/skills/work-on-task/SKILL.md +4 -0
- package/templates/skills/work-on-task/content.md +4 -0
- package/templates/skills/workflow-iterate/content.md +9 -0
- package/dist/acp-adapter-yzfxn8vj.js +0 -23
- package/dist/cli-3dpj7atn.js +0 -11
- package/dist/cli-gvcgmx69.js +0 -183
- package/dist/{cli-jhm6hzjn.js → cli-mxvnnv5q.js} +7 -7
- package/dist/{codex-hook-d2rkvf39.js → codex-hook-15dgc4cb.js} +1 -1
|
@@ -167,6 +167,66 @@ function makeFakeAppServer(
|
|
|
167
167
|
};
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
+
// Cost JSON (minus durationMs) the pre-split adapter (origin/main 97deb6c6)
|
|
171
|
+
// produced for the notification sequence in the "unchanged by the context
|
|
172
|
+
// split" test.
|
|
173
|
+
const PRE_SPLIT_COST_JSON =
|
|
174
|
+
'{"sessionId":"","taskId":"","agentId":"agent-test","totalCostUsd":0.3675,"inputTokens":390000,"outputTokens":7500,"reasoningOutputTokens":1900,"cacheReadTokens":320000,"cacheWriteTokens":3000,"numTurns":2,"model":"gpt-5.4","isError":false,"provider":"codex"}';
|
|
175
|
+
|
|
176
|
+
type AppServerTokenUsage = {
|
|
177
|
+
total: Record<string, number>;
|
|
178
|
+
last?: Record<string, number>;
|
|
179
|
+
modelContextWindow?: number | null;
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
function breakdown(input: number, cached: number, output: number, reasoning = 0, cacheWrite = 0) {
|
|
183
|
+
return {
|
|
184
|
+
inputTokens: input,
|
|
185
|
+
cachedInputTokens: cached,
|
|
186
|
+
cacheWriteInputTokens: cacheWrite,
|
|
187
|
+
outputTokens: output,
|
|
188
|
+
reasoningOutputTokens: reasoning,
|
|
189
|
+
totalTokens: input + output,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Run an app-server session where each turn sends one
|
|
195
|
+
* `thread/tokenUsage/updated` notification per model request. The first turn
|
|
196
|
+
* is the initial prompt; later turns are queued follow-ups.
|
|
197
|
+
*/
|
|
198
|
+
async function runAppServerTurns(
|
|
199
|
+
turns: AppServerTokenUsage[][],
|
|
200
|
+
): Promise<{ emitted: ProviderEvent[]; result: ProviderResult }> {
|
|
201
|
+
const threadId = "thread-token-usage";
|
|
202
|
+
let turnNumber = 0;
|
|
203
|
+
const fake = makeFakeAppServer((method, _params, emit) => {
|
|
204
|
+
if (method === "thread/start") return { thread: { id: threadId } };
|
|
205
|
+
if (method !== "turn/start") throw new Error(`Unexpected method: ${method}`);
|
|
206
|
+
const turnId = `turn-${++turnNumber}`;
|
|
207
|
+
const updates = turns[turnNumber - 1] ?? [];
|
|
208
|
+
queueMicrotask(() => {
|
|
209
|
+
emit("turn/started", { threadId, turn: { id: turnId, status: "inProgress" } });
|
|
210
|
+
for (const tokenUsage of updates) {
|
|
211
|
+
emit("thread/tokenUsage/updated", { threadId, turnId, tokenUsage });
|
|
212
|
+
}
|
|
213
|
+
emit("turn/completed", { threadId, turn: { id: turnId, status: "completed", error: null } });
|
|
214
|
+
});
|
|
215
|
+
return { turn: { id: turnId } };
|
|
216
|
+
});
|
|
217
|
+
const session = await new CodexAdapter({
|
|
218
|
+
bypassSubprocess: true,
|
|
219
|
+
appServerFactory: () => fake.client as never,
|
|
220
|
+
}).createSession(testConfig({ taskId: "", apiUrl: "", apiKey: "" }));
|
|
221
|
+
const emitted: ProviderEvent[] = [];
|
|
222
|
+
session.onEvent((event) => emitted.push(event));
|
|
223
|
+
for (let i = 1; i < turns.length; i++) {
|
|
224
|
+
await session.deliverSteering?.({ mode: "queue", text: `follow-up ${i}` });
|
|
225
|
+
}
|
|
226
|
+
const result = await session.waitForCompletion();
|
|
227
|
+
return { emitted, result };
|
|
228
|
+
}
|
|
229
|
+
|
|
170
230
|
/**
|
|
171
231
|
* Because `CodexSession` is not exported, we load the module source and
|
|
172
232
|
* instantiate it via `eval` of a small helper module. This is brittle but
|
|
@@ -431,6 +491,88 @@ describe("Codex app-server session", () => {
|
|
|
431
491
|
).toMatchObject({ toolName: "Write" });
|
|
432
492
|
});
|
|
433
493
|
|
|
494
|
+
test("context follows the latest request, not the turn's summed usage", async () => {
|
|
495
|
+
// Two requests in one turn. The cumulative total (1.3M input) exceeds the
|
|
496
|
+
// 1.05M gpt-5.4 window; neither single request does.
|
|
497
|
+
const { emitted } = await runAppServerTurns([
|
|
498
|
+
[
|
|
499
|
+
{
|
|
500
|
+
total: breakdown(600_000, 500_000, 1_000),
|
|
501
|
+
last: breakdown(600_000, 500_000, 1_000),
|
|
502
|
+
},
|
|
503
|
+
{
|
|
504
|
+
total: breakdown(1_300_000, 1_100_000, 3_000),
|
|
505
|
+
last: breakdown(700_000, 600_000, 2_000),
|
|
506
|
+
},
|
|
507
|
+
],
|
|
508
|
+
]);
|
|
509
|
+
|
|
510
|
+
const snapshots = emitted.filter((event) => event.type === "context_usage");
|
|
511
|
+
expect(snapshots.map((event) => event.contextUsedTokens)).toEqual([601_000, 702_000]);
|
|
512
|
+
for (const snapshot of snapshots) {
|
|
513
|
+
expect(snapshot.contextUsedTokens).toBeLessThanOrEqual(702_000);
|
|
514
|
+
expect(snapshot.contextTotalTokens).toBe(1_050_000);
|
|
515
|
+
expect(snapshot.contextPercent).toBeLessThan(100);
|
|
516
|
+
expect(snapshot.contextFormula).toBe("input-cache-output");
|
|
517
|
+
}
|
|
518
|
+
expect(snapshots.at(-1)?.contextPercent).toBeCloseTo((702_000 / 1_050_000) * 100, 6);
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
test("per-turn cost and usage totals are unchanged by the context split", async () => {
|
|
522
|
+
// `last` and `modelContextWindow` must not move cost or usage totals.
|
|
523
|
+
const { result } = await runAppServerTurns([
|
|
524
|
+
[
|
|
525
|
+
{
|
|
526
|
+
total: breakdown(120_000, 90_000, 2_000, 500, 1_000),
|
|
527
|
+
last: breakdown(120_000, 90_000, 2_000, 500, 1_000),
|
|
528
|
+
modelContextWindow: 258_400,
|
|
529
|
+
},
|
|
530
|
+
{
|
|
531
|
+
total: breakdown(250_000, 200_000, 5_000, 1_200, 2_500),
|
|
532
|
+
last: breakdown(130_000, 110_000, 3_000, 700, 1_500),
|
|
533
|
+
modelContextWindow: 258_400,
|
|
534
|
+
},
|
|
535
|
+
],
|
|
536
|
+
[
|
|
537
|
+
{
|
|
538
|
+
total: breakdown(390_000, 320_000, 7_500, 1_900, 3_000),
|
|
539
|
+
last: breakdown(140_000, 120_000, 2_500, 700, 500),
|
|
540
|
+
modelContextWindow: 258_400,
|
|
541
|
+
},
|
|
542
|
+
],
|
|
543
|
+
]);
|
|
544
|
+
|
|
545
|
+
const { durationMs: _durationMs, ...cost } = result.cost ?? ({} as Record<string, unknown>);
|
|
546
|
+
expect(JSON.stringify(cost)).toBe(PRE_SPLIT_COST_JSON);
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
test("modelContextWindow from the app-server wins over the models.dev fallback", async () => {
|
|
550
|
+
const { emitted } = await runAppServerTurns([
|
|
551
|
+
[
|
|
552
|
+
{
|
|
553
|
+
total: breakdown(50_000, 40_000, 1_000),
|
|
554
|
+
last: breakdown(50_000, 40_000, 1_000),
|
|
555
|
+
modelContextWindow: 258_400,
|
|
556
|
+
},
|
|
557
|
+
{
|
|
558
|
+
total: breakdown(110_000, 90_000, 2_000),
|
|
559
|
+
last: breakdown(60_000, 50_000, 1_000),
|
|
560
|
+
modelContextWindow: null,
|
|
561
|
+
},
|
|
562
|
+
],
|
|
563
|
+
]);
|
|
564
|
+
|
|
565
|
+
const snapshots = emitted.filter((event) => event.type === "context_usage");
|
|
566
|
+
expect(snapshots).toHaveLength(2);
|
|
567
|
+
expect(snapshots[0]).toMatchObject({ contextUsedTokens: 51_000, contextTotalTokens: 258_400 });
|
|
568
|
+
expect(snapshots[0]?.contextPercent).toBeCloseTo((51_000 / 258_400) * 100, 6);
|
|
569
|
+
// A null window falls back to the models.dev window for the model.
|
|
570
|
+
expect(snapshots[1]).toMatchObject({
|
|
571
|
+
contextUsedTokens: 61_000,
|
|
572
|
+
contextTotalTokens: 1_050_000,
|
|
573
|
+
});
|
|
574
|
+
});
|
|
575
|
+
|
|
434
576
|
test("abort uses turn/interrupt and scrubs complete provider events", async () => {
|
|
435
577
|
const requests: string[] = [];
|
|
436
578
|
const fake = makeFakeAppServer((method, _params, emit) => {
|
|
@@ -550,6 +692,13 @@ describe("Codex app-server session", () => {
|
|
|
550
692
|
outputTokens: 9,
|
|
551
693
|
reasoningOutputTokens: 6,
|
|
552
694
|
},
|
|
695
|
+
last: {
|
|
696
|
+
inputTokens: 30,
|
|
697
|
+
cachedInputTokens: 11,
|
|
698
|
+
cacheWriteInputTokens: 4,
|
|
699
|
+
outputTokens: 4,
|
|
700
|
+
reasoningOutputTokens: 2,
|
|
701
|
+
},
|
|
553
702
|
},
|
|
554
703
|
});
|
|
555
704
|
emit("turn/completed", {
|
|
@@ -581,9 +730,10 @@ describe("Codex app-server session", () => {
|
|
|
581
730
|
expect(
|
|
582
731
|
emitted.find((event) => event.type === "custom" && event.name === "codex.reasoning"),
|
|
583
732
|
).toMatchObject({ data: { text: "First thought\nSecond thought" } });
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
733
|
+
// Context comes from `last` (30 + 4), not the turn total (44 + 9).
|
|
734
|
+
expect(emitted.filter((event) => event.type === "context_usage")).toEqual([
|
|
735
|
+
expect.objectContaining({ contextUsedTokens: 34 }),
|
|
736
|
+
]);
|
|
587
737
|
});
|
|
588
738
|
|
|
589
739
|
test("keeps failed-turn usage and treats retrying errors as diagnostics", async () => {
|
|
@@ -701,6 +851,10 @@ describe("CodexSession event mapping", () => {
|
|
|
701
851
|
{ type: "thread.started", thread_id: "thread-abc" },
|
|
702
852
|
{ type: "turn.started" },
|
|
703
853
|
{ type: "item.completed", item: agentMsg as ThreadItem },
|
|
854
|
+
{
|
|
855
|
+
type: "context.updated",
|
|
856
|
+
usage: { input_tokens: 100, cached_input_tokens: 25, output_tokens: 50 },
|
|
857
|
+
},
|
|
704
858
|
{
|
|
705
859
|
type: "turn.completed",
|
|
706
860
|
usage: { input_tokens: 100, cached_input_tokens: 25, output_tokens: 50 },
|
|
@@ -808,15 +962,9 @@ describe("CodexSession event mapping", () => {
|
|
|
808
962
|
}
|
|
809
963
|
});
|
|
810
964
|
|
|
811
|
-
test("
|
|
812
|
-
//
|
|
813
|
-
//
|
|
814
|
-
// shared with every other provider. The trade-off: a chatty Codex turn
|
|
815
|
-
// — where `input_tokens` is the SUM across every model call in the turn
|
|
816
|
-
// — over-reports compared to the peak-proxy variant. The clamp at 100%
|
|
817
|
-
// keeps the gauge sensible; downstream consumers reading the new
|
|
818
|
-
// `contextFormula='input-cache-output'` tag know it's apples-to-apples
|
|
819
|
-
// across providers. Numbers below are from the verify-plan transcript.
|
|
965
|
+
test("a chatty turn's summed usage is billed but never reported as context", async () => {
|
|
966
|
+
// A turn's usage sums every model request in it, so it can exceed the
|
|
967
|
+
// window. Only `context.updated` (one request) may become context_usage.
|
|
820
968
|
const agentMsg: AgentMessageItem = {
|
|
821
969
|
id: "msg-1",
|
|
822
970
|
type: "agent_message",
|
|
@@ -829,9 +977,9 @@ describe("CodexSession event mapping", () => {
|
|
|
829
977
|
{
|
|
830
978
|
type: "turn.completed",
|
|
831
979
|
usage: {
|
|
832
|
-
input_tokens:
|
|
833
|
-
cached_input_tokens:
|
|
834
|
-
output_tokens:
|
|
980
|
+
input_tokens: 4_850_000, // summed across the turn, above the 1.05M window
|
|
981
|
+
cached_input_tokens: 4_700_000,
|
|
982
|
+
output_tokens: 54_343,
|
|
835
983
|
},
|
|
836
984
|
},
|
|
837
985
|
];
|
|
@@ -843,22 +991,14 @@ describe("CodexSession event mapping", () => {
|
|
|
843
991
|
|
|
844
992
|
const { emitted } = await runSessionWithFakeThread(events, config);
|
|
845
993
|
|
|
846
|
-
|
|
847
|
-
expect(contextUsage).toBeDefined();
|
|
848
|
-
if (contextUsage && contextUsage.type === "context_usage") {
|
|
849
|
-
// Phase 9 unified: input + output = 357142 + 2156 = 359298.
|
|
850
|
-
expect(contextUsage.contextUsedTokens).toBe(359298);
|
|
851
|
-
expect(contextUsage.contextTotalTokens).toBe(1_050_000);
|
|
852
|
-
expect(contextUsage.contextPercent).toBeCloseTo((359298 / 1_050_000) * 100, 6);
|
|
853
|
-
expect(contextUsage.contextFormula).toBe("input-cache-output");
|
|
854
|
-
}
|
|
994
|
+
expect(emitted.some((e) => e.type === "context_usage")).toBe(false);
|
|
855
995
|
|
|
856
996
|
// Cost still uses the full input_tokens — billing semantics are
|
|
857
997
|
// preserved (cached portion gets the cached rate, uncached gets full).
|
|
858
998
|
const resultEvent = emitted.findLast((e) => e.type === "result");
|
|
859
999
|
if (resultEvent && resultEvent.type === "result") {
|
|
860
|
-
expect(resultEvent.cost.inputTokens).toBe(
|
|
861
|
-
expect(resultEvent.cost.cacheReadTokens).toBe(
|
|
1000
|
+
expect(resultEvent.cost.inputTokens).toBe(4_850_000);
|
|
1001
|
+
expect(resultEvent.cost.cacheReadTokens).toBe(4_700_000);
|
|
862
1002
|
expect(resultEvent.cost.totalCostUsd).toBeGreaterThan(0);
|
|
863
1003
|
}
|
|
864
1004
|
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { codexOAuthAuthJson } from "../../scripts/e2e/harness";
|
|
3
|
+
import { authJsonToCredentials, credentialsToAuthJson } from "../providers/codex-oauth/auth-json";
|
|
4
|
+
import type { CodexOAuthCredentials } from "../providers/codex-oauth/types";
|
|
5
|
+
|
|
6
|
+
const credentials: CodexOAuthCredentials = {
|
|
7
|
+
access: "example-access-token",
|
|
8
|
+
refresh: "example-refresh-token",
|
|
9
|
+
accountId: "example-account",
|
|
10
|
+
expires: Date.UTC(2099, 0, 1),
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
describe("Codex auth.json timestamps", () => {
|
|
14
|
+
test("records the write time separately from the access-token expiry", () => {
|
|
15
|
+
const before = Date.now();
|
|
16
|
+
const auth = credentialsToAuthJson(credentials);
|
|
17
|
+
const after = Date.now();
|
|
18
|
+
|
|
19
|
+
expect(Date.parse(auth.last_refresh)).toBeGreaterThanOrEqual(before);
|
|
20
|
+
expect(Date.parse(auth.last_refresh)).toBeLessThanOrEqual(after);
|
|
21
|
+
expect(authJsonToCredentials(auth).expires).toBe(credentials.expires);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("reads expiry from a native CLI JWT instead of last_refresh", () => {
|
|
25
|
+
const expiresSeconds = 1_800_000_000;
|
|
26
|
+
const access = `header.${btoa(JSON.stringify({ exp: expiresSeconds }))}.signature`;
|
|
27
|
+
const auth = {
|
|
28
|
+
auth_mode: "chatgpt" as const,
|
|
29
|
+
OPENAI_API_KEY: null,
|
|
30
|
+
tokens: {
|
|
31
|
+
id_token: access,
|
|
32
|
+
access_token: access,
|
|
33
|
+
refresh_token: credentials.refresh,
|
|
34
|
+
account_id: credentials.accountId,
|
|
35
|
+
},
|
|
36
|
+
last_refresh: "2026-10-01T00:00:00.000Z",
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
expect(authJsonToCredentials(auth).expires).toBe(expiresSeconds * 1000);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("prefers the current JWT over metadata left from an older token", () => {
|
|
43
|
+
const auth = credentialsToAuthJson(credentials);
|
|
44
|
+
auth.tokens.access_token = `header.${btoa(JSON.stringify({ exp: 1_800_000_000 }))}.signature`;
|
|
45
|
+
expect(authJsonToCredentials(auth).expires).toBe(1_800_000_000_000);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("retains the explicit expiry when a JWT has no usable exp claim", () => {
|
|
49
|
+
for (const exp of [undefined, "1800000000", null, -1]) {
|
|
50
|
+
const access = `header.${btoa(JSON.stringify({ exp }))}.signature`;
|
|
51
|
+
const auth = credentialsToAuthJson({ ...credentials, access });
|
|
52
|
+
expect(authJsonToCredentials(auth).expires).toBe(credentials.expires);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("keeps the legacy expiry-in-last_refresh fallback for opaque tokens", () => {
|
|
57
|
+
const auth = credentialsToAuthJson(credentials);
|
|
58
|
+
delete auth.expires;
|
|
59
|
+
auth.last_refresh = new Date(credentials.expires).toISOString();
|
|
60
|
+
expect(authJsonToCredentials(auth).expires).toBe(credentials.expires);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("preserves expiry without exposing a pool refresh token", () => {
|
|
64
|
+
const auth = credentialsToAuthJson(credentials, { includeRefreshToken: false });
|
|
65
|
+
expect(auth.tokens.refresh_token).toBe("");
|
|
66
|
+
expect(authJsonToCredentials(auth).expires).toBe(credentials.expires);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("seeds the black-box harness with separate refresh and expiry timestamps", () => {
|
|
70
|
+
const before = Date.now();
|
|
71
|
+
const auth = JSON.parse(codexOAuthAuthJson(JSON.stringify(credentials)));
|
|
72
|
+
expect(Date.parse(auth.last_refresh)).toBeGreaterThanOrEqual(before);
|
|
73
|
+
expect(Date.parse(auth.last_refresh)).toBeLessThanOrEqual(Date.now());
|
|
74
|
+
expect(authJsonToCredentials(auth)).toEqual(credentials);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
@@ -355,6 +355,7 @@ describe("credentialsToAuthJson", () => {
|
|
|
355
355
|
accountId: "c724a178-abc",
|
|
356
356
|
};
|
|
357
357
|
|
|
358
|
+
const before = Date.now();
|
|
358
359
|
const authJson = credentialsToAuthJson(creds);
|
|
359
360
|
expect(authJson.auth_mode).toBe("chatgpt");
|
|
360
361
|
expect(authJson.OPENAI_API_KEY).toBeNull();
|
|
@@ -362,7 +363,9 @@ describe("credentialsToAuthJson", () => {
|
|
|
362
363
|
expect(authJson.tokens.refresh_token).toBe(creds.refresh);
|
|
363
364
|
expect(authJson.tokens.account_id).toBe(creds.accountId);
|
|
364
365
|
expect(authJson.tokens.id_token).toBe(creds.access);
|
|
365
|
-
expect(authJson.last_refresh)
|
|
366
|
+
expect(Date.parse(authJson.last_refresh)).toBeGreaterThanOrEqual(before);
|
|
367
|
+
expect(Date.parse(authJson.last_refresh)).toBeLessThanOrEqual(Date.now());
|
|
368
|
+
expect(authJson.expires).toBe(creds.expires);
|
|
366
369
|
});
|
|
367
370
|
});
|
|
368
371
|
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
} from "../be/db";
|
|
10
10
|
import { getOAuthApp, upsertAuthorization, upsertOAuthApp } from "../be/db-queries/oauth";
|
|
11
11
|
import {
|
|
12
|
+
deleteScriptConnection,
|
|
12
13
|
getScriptApiConnectionDescriptors,
|
|
13
14
|
getScriptConnectionById,
|
|
14
15
|
listRelationalCredentialBindings,
|
|
@@ -718,3 +719,53 @@ describe("embedded connection auth", () => {
|
|
|
718
719
|
}
|
|
719
720
|
});
|
|
720
721
|
});
|
|
722
|
+
|
|
723
|
+
describe("deleteScriptConnection", () => {
|
|
724
|
+
test("removes the connection, its managed binding, and its derived inline secret", async () => {
|
|
725
|
+
createdConfigKeys.push("connection.deleteVendor.secret");
|
|
726
|
+
const connection = await upsertScriptConnection({
|
|
727
|
+
slug: "deleteVendor",
|
|
728
|
+
kind: "graphql",
|
|
729
|
+
baseUrl: "https://api.vendor.test/graphql",
|
|
730
|
+
allowedHosts: ["api.vendor.test"],
|
|
731
|
+
auth: { type: "bearer", secret: "example-delete-tok-123" },
|
|
732
|
+
});
|
|
733
|
+
createdConnectionIds.push(connection.id);
|
|
734
|
+
expect(await managedBindingFor(connection.id)).not.toBeNull();
|
|
735
|
+
expect(await getSwarmConfigs({ key: "connection.deleteVendor.secret" })).toHaveLength(1);
|
|
736
|
+
|
|
737
|
+
const deleted = await deleteScriptConnection(connection.id);
|
|
738
|
+
|
|
739
|
+
expect(deleted?.id).toBe(connection.id);
|
|
740
|
+
expect(await getScriptConnectionById(connection.id)).toBeNull();
|
|
741
|
+
expect(await managedBindingFor(connection.id)).toBeNull();
|
|
742
|
+
expect(await getSwarmConfigs({ key: "connection.deleteVendor.secret" })).toHaveLength(0);
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
test("keeps a shared configKey secret the connection does not own", async () => {
|
|
746
|
+
createdConfigKeys.push("SHARED_DELETE_VENDOR_KEY");
|
|
747
|
+
await upsertSwarmConfig({
|
|
748
|
+
scope: "global",
|
|
749
|
+
key: "SHARED_DELETE_VENDOR_KEY",
|
|
750
|
+
value: "example-shared-tok",
|
|
751
|
+
isSecret: true,
|
|
752
|
+
});
|
|
753
|
+
const connection = await upsertScriptConnection({
|
|
754
|
+
slug: "sharedDeleteVendor",
|
|
755
|
+
kind: "graphql",
|
|
756
|
+
baseUrl: "https://api.vendor.test/graphql",
|
|
757
|
+
allowedHosts: ["api.vendor.test"],
|
|
758
|
+
auth: { type: "bearer", configKey: "SHARED_DELETE_VENDOR_KEY" },
|
|
759
|
+
});
|
|
760
|
+
createdConnectionIds.push(connection.id);
|
|
761
|
+
|
|
762
|
+
expect((await deleteScriptConnection(connection.id))?.id).toBe(connection.id);
|
|
763
|
+
|
|
764
|
+
expect(await managedBindingFor(connection.id)).toBeNull();
|
|
765
|
+
expect(await getSwarmConfigs({ key: "SHARED_DELETE_VENDOR_KEY" })).toHaveLength(1);
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
test("returns null for an unknown id", async () => {
|
|
769
|
+
expect(await deleteScriptConnection("00000000-0000-4000-8000-000000000000")).toBeNull();
|
|
770
|
+
});
|
|
771
|
+
});
|
|
@@ -32,9 +32,9 @@ describe("getContextWindowSize", () => {
|
|
|
32
32
|
expect(getContextWindowSize("sonnet")).toBe(1_000_000);
|
|
33
33
|
});
|
|
34
34
|
|
|
35
|
-
test("returns 200K for haiku
|
|
35
|
+
test("returns 200K for Haiku 4.5 and 1M for the haiku shortname (Haiku 5.5)", () => {
|
|
36
36
|
expect(getContextWindowSize("claude-haiku-4-5")).toBe(200_000);
|
|
37
|
-
expect(getContextWindowSize("haiku")).toBe(
|
|
37
|
+
expect(getContextWindowSize("haiku")).toBe(1_000_000);
|
|
38
38
|
});
|
|
39
39
|
|
|
40
40
|
test("reads OpenAI windows from the catalog", () => {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, expect, mock, test } from "bun:test";
|
|
2
2
|
import {
|
|
3
3
|
buildCredStatusReport,
|
|
4
|
+
CREDENTIAL_PROVIDER_CHECKERS,
|
|
4
5
|
checkProviderCredentials,
|
|
5
6
|
isBedrockMode,
|
|
6
7
|
isCredCheckDisabled,
|
|
@@ -8,6 +9,7 @@ import {
|
|
|
8
9
|
shouldRefreshBedrockStatus,
|
|
9
10
|
validateProviderCredentials,
|
|
10
11
|
} from "../commands/provider-credentials";
|
|
12
|
+
import { ACP_TARGET_IDS } from "../providers/acp-target-catalog";
|
|
11
13
|
import { checkClaudeCredentials } from "../providers/claude-adapter";
|
|
12
14
|
import { checkClaudeManagedCredentials } from "../providers/claude-managed-adapter";
|
|
13
15
|
import { checkCodexCredentials } from "../providers/codex-adapter";
|
|
@@ -660,6 +662,54 @@ describe("checkOpencodeCredentials", () => {
|
|
|
660
662
|
});
|
|
661
663
|
});
|
|
662
664
|
|
|
665
|
+
describe("ACP readiness", () => {
|
|
666
|
+
test.each(["typo", "", " \t "])("rejects unsupported target %j", async (target) => {
|
|
667
|
+
const status = await checkProviderCredentials("acp", {
|
|
668
|
+
ACP_TARGET: target,
|
|
669
|
+
ACP_TARGET_COMMAND: "custom-acp --stdio",
|
|
670
|
+
});
|
|
671
|
+
expect(status.ready).toBe(false);
|
|
672
|
+
expect(status.missing).toEqual(["ACP_TARGET"]);
|
|
673
|
+
expect(status.hint).toContain("ACP_TARGET");
|
|
674
|
+
for (const supported of ACP_TARGET_IDS) expect(status.hint).toContain(supported);
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
test.each([undefined, "custom"])("requires a command for target %s", async (target) => {
|
|
678
|
+
const status = await CREDENTIAL_PROVIDER_CHECKERS.acp({ ACP_TARGET: target });
|
|
679
|
+
expect(status.ready).toBe(false);
|
|
680
|
+
expect(status.missing).toEqual(["ACP_TARGET_COMMAND"]);
|
|
681
|
+
expect(status.hint).toContain("ACP_TARGET_COMMAND");
|
|
682
|
+
expect(status.hint).toContain("ACP_COMMAND");
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
test.each(["", " \t "])("rejects a blank custom command %j", async (command) => {
|
|
686
|
+
const status = await checkProviderCredentials("acp", {
|
|
687
|
+
ACP_TARGET: "custom",
|
|
688
|
+
ACP_TARGET_COMMAND: command,
|
|
689
|
+
ACP_COMMAND: "legacy-acp",
|
|
690
|
+
});
|
|
691
|
+
expect(status.ready).toBe(false);
|
|
692
|
+
expect(status.missing).toEqual(["ACP_TARGET_COMMAND"]);
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
test.each(["ACP_TARGET_COMMAND", "ACP_COMMAND"])("accepts a command from %s", async (key) => {
|
|
696
|
+
for (const target of [undefined, "custom"]) {
|
|
697
|
+
const status = await checkProviderCredentials("acp", {
|
|
698
|
+
ACP_TARGET: target,
|
|
699
|
+
[key]: "custom-acp --stdio",
|
|
700
|
+
});
|
|
701
|
+
expect(status).toEqual({ ready: true, missing: [], satisfiedBy: "sdk-delegated" });
|
|
702
|
+
}
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
test.each(
|
|
706
|
+
ACP_TARGET_IDS.filter((target) => target !== "custom"),
|
|
707
|
+
)("keeps preset %s ready without a custom command", async (target) => {
|
|
708
|
+
const status = await checkProviderCredentials("acp", { ACP_TARGET: target });
|
|
709
|
+
expect(status).toEqual({ ready: true, missing: [], satisfiedBy: "sdk-delegated" });
|
|
710
|
+
});
|
|
711
|
+
});
|
|
712
|
+
|
|
663
713
|
// ─── dispatcher ──────────────────────────────────────────────────────────────
|
|
664
714
|
|
|
665
715
|
describe("checkProviderCredentials dispatcher", () => {
|
|
@@ -720,7 +770,7 @@ describe("checkProviderCredentials dispatcher", () => {
|
|
|
720
770
|
).ready,
|
|
721
771
|
).toBe(true);
|
|
722
772
|
|
|
723
|
-
const acpStatus = await checkProviderCredentials("acp", {});
|
|
773
|
+
const acpStatus = await checkProviderCredentials("acp", { ACP_TARGET: "opencode" });
|
|
724
774
|
expect(acpStatus.ready).toBe(true);
|
|
725
775
|
expect(acpStatus.satisfiedBy).toBe("sdk-delegated");
|
|
726
776
|
});
|
|
@@ -78,7 +78,7 @@ describe("docs API index footprint", () => {
|
|
|
78
78
|
expect(tasks).toContain('"path":"/api/task-templates","method":"get"');
|
|
79
79
|
});
|
|
80
80
|
|
|
81
|
-
test("OpenAPI and generated pages keep all
|
|
81
|
+
test("OpenAPI and generated pages keep all 400 operations", async () => {
|
|
82
82
|
const spec = await Bun.file(resolve(repoRoot, "openapi.json")).json();
|
|
83
83
|
const operationKeys = [];
|
|
84
84
|
const tags = new Set();
|
|
@@ -103,8 +103,8 @@ describe("docs API index footprint", () => {
|
|
|
103
103
|
expect(spec.paths["/api/agents/{id}/runtime"]?.get).toBeDefined();
|
|
104
104
|
|
|
105
105
|
const generatedKeys = await generatedOperationKeys();
|
|
106
|
-
expect(operationKeys).toHaveLength(
|
|
107
|
-
expect(generatedKeys).toHaveLength(
|
|
106
|
+
expect(operationKeys).toHaveLength(400);
|
|
107
|
+
expect(generatedKeys).toHaveLength(400);
|
|
108
108
|
expect(new Set(generatedKeys)).toEqual(new Set(operationKeys));
|
|
109
109
|
});
|
|
110
110
|
|
|
@@ -9,7 +9,10 @@ import type { ProviderEvent, ProviderSessionConfig } from "../providers/types";
|
|
|
9
9
|
import { DEFAULT_MODEL_TIER_MAP } from "../types";
|
|
10
10
|
import { getModelAwareCredentialVars } from "../utils/credentials";
|
|
11
11
|
import { resolveHarnessProvider } from "../utils/harness-provider";
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
DEFAULT_OPENROUTER_BASE_URL,
|
|
14
|
+
OPENROUTER_APP_ATTRIBUTION_HEADERS,
|
|
15
|
+
} from "../utils/openrouter-base-url";
|
|
13
16
|
|
|
14
17
|
const directories: string[] = [];
|
|
15
18
|
afterEach(async () => {
|
|
@@ -166,6 +169,7 @@ describe("dsh harness", () => {
|
|
|
166
169
|
openrouter: {
|
|
167
170
|
apiKeyEnv: "OPENROUTER_API_KEY",
|
|
168
171
|
baseURL: DEFAULT_OPENROUTER_BASE_URL,
|
|
172
|
+
headers: OPENROUTER_APP_ATTRIBUTION_HEADERS,
|
|
169
173
|
api: "openai-completions",
|
|
170
174
|
models: [{ id: modelId }],
|
|
171
175
|
},
|
|
@@ -316,6 +320,24 @@ describe("dsh harness", () => {
|
|
|
316
320
|
model: "deepseek/deepseek-v4.1-flash",
|
|
317
321
|
});
|
|
318
322
|
expect(patch[2].config.providers.openrouter.baseURL).toBe("https://gateway.example/proxy/v1");
|
|
323
|
+
// A gateway gets no OpenRouter app attribution.
|
|
324
|
+
expect(patch[2].config.providers.openrouter).not.toHaveProperty("headers");
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
test("OPENROUTER_APP_ATTRIBUTION=false drops the attribution headers", async () => {
|
|
328
|
+
const config = await fixture();
|
|
329
|
+
config.model = "openrouter/vendor/new-model";
|
|
330
|
+
config.env = {
|
|
331
|
+
...config.env,
|
|
332
|
+
DEEPSEEK_API_KEY: "",
|
|
333
|
+
OPENROUTER_API_KEY: "openrouter-test-key",
|
|
334
|
+
OPENROUTER_APP_ATTRIBUTION: "false",
|
|
335
|
+
};
|
|
336
|
+
const session = await new DshAdapter().createSession(config);
|
|
337
|
+
expect((await session.waitForCompletion()).isError).toBe(false);
|
|
338
|
+
const { patch } = await Bun.file(join(config.cwd, "invocation.json")).json();
|
|
339
|
+
expect(patch[2].config.providers.openrouter.baseURL).toBe(DEFAULT_OPENROUTER_BASE_URL);
|
|
340
|
+
expect(patch[2].config.providers.openrouter).not.toHaveProperty("headers");
|
|
319
341
|
});
|
|
320
342
|
|
|
321
343
|
test("bare models keep direct DeepSeek when both keys exist", async () => {
|
|
@@ -354,7 +376,7 @@ describe("dsh harness", () => {
|
|
|
354
376
|
await rename(join(config.cwd, "dsh"), join(config.cwd, "npx"));
|
|
355
377
|
config.env = { ...config.env, DSH_BINARY: "", PATH: config.cwd };
|
|
356
378
|
await expect(new DshAdapter().createSession(config)).rejects.toThrow(
|
|
357
|
-
"dsh CLI not found. Install @deepseek-ai/dsh@0.2.1-alpha.
|
|
379
|
+
"dsh CLI not found. Install @deepseek-ai/dsh@0.2.1-alpha.2",
|
|
358
380
|
);
|
|
359
381
|
expect(await Bun.file(join(config.cwd, "invocation.json")).exists()).toBe(false);
|
|
360
382
|
});
|