@desplega.ai/agent-swarm 1.92.2 → 1.94.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/README.md +2 -2
- package/openapi.json +242 -3
- package/package.json +5 -5
- package/src/be/db.ts +152 -11
- package/src/be/memory/boot-reembed.ts +0 -1
- package/src/be/memory/providers/sqlite-store.ts +42 -25
- package/src/be/memory/raters/llm-client.ts +12 -5
- package/src/be/memory/types.ts +3 -0
- package/src/be/migrations/088_script_runs_list_indexes.sql +10 -0
- package/src/be/migrations/089_harness_variant.sql +2 -0
- package/src/be/migrations/090_model_tiers.sql +2 -0
- package/src/be/migrations/091_seed_swarm_operations_metrics.sql +12 -0
- package/src/be/migrations/092_metrics_dashboard_combobox_filters.sql +68 -0
- package/src/be/migrations/093_slack_message_tracking.sql +6 -0
- package/src/be/migrations/runner.ts +52 -0
- package/src/be/modelsdev-cache.json +3264 -1166
- package/src/be/scripts/boot-reembed.ts +74 -0
- package/src/be/scripts/db.ts +19 -3
- package/src/be/seed/index.ts +1 -1
- package/src/be/seed/registry.ts +2 -2
- package/src/be/seed/runner.ts +5 -5
- package/src/be/seed/types.ts +6 -1
- package/src/be/seed-pricing.ts +2 -0
- package/src/be/seed-scripts/catalog/boot-triage.inline.ts +221 -0
- package/src/be/seed-scripts/catalog/catalog-report.inline.ts +457 -0
- package/src/be/seed-scripts/catalog/compound-insights.inline.ts +863 -0
- package/src/be/seed-scripts/catalog/ops-catalog-audit.inline.ts +506 -0
- package/src/be/seed-scripts/index.ts +8 -7
- package/src/be/skill-sync.ts +28 -179
- package/src/commands/runner.ts +197 -10
- package/src/http/api-keys.ts +42 -0
- package/src/http/index.ts +13 -2
- package/src/http/mcp-bridge.ts +1 -1
- package/src/http/memory.ts +23 -24
- package/src/http/metrics.ts +55 -6
- package/src/http/schedules.ts +16 -15
- package/src/http/script-runs.ts +7 -1
- package/src/http/scripts.ts +147 -1
- package/src/http/tasks.ts +17 -6
- package/src/model-tiers.ts +140 -0
- package/src/providers/claude-adapter.ts +33 -1
- package/src/providers/claude-managed-adapter.ts +3 -0
- package/src/providers/claude-managed-models.ts +16 -0
- package/src/providers/codex-adapter.ts +8 -1
- package/src/providers/codex-models.ts +1 -0
- package/src/providers/codex-oauth/auth-json.ts +1 -0
- package/src/providers/harness-version.ts +7 -0
- package/src/providers/opencode-adapter.ts +12 -4
- package/src/providers/pi-mono-adapter.ts +90 -8
- package/src/providers/types.ts +2 -0
- package/src/scheduler/scheduler.ts +22 -34
- package/src/scripts-runtime/egress-secrets.ts +83 -0
- package/src/scripts-runtime/eval-harness.ts +4 -0
- package/src/scripts-runtime/executors/types.ts +7 -0
- package/src/scripts-runtime/loader.ts +2 -0
- package/src/server-user.ts +8 -2
- package/src/slack/channel-join.ts +41 -0
- package/src/slack/responses.ts +39 -11
- package/src/slack/watcher.ts +121 -8
- package/src/tests/additive-buffer.test.ts +0 -1
- package/src/tests/agents-list-model-display.test.ts +13 -0
- package/src/tests/api-key-tracking.test.ts +113 -0
- package/src/tests/approval-requests.test.ts +0 -6
- package/src/tests/aws-error-classifier.test.ts +148 -0
- package/src/tests/claude-managed-adapter.test.ts +12 -0
- package/src/tests/claude-managed-setup.test.ts +0 -4
- package/src/tests/codex-pool.test.ts +2 -6
- package/src/tests/context-window.test.ts +7 -0
- package/src/tests/http-api-integration.test.ts +23 -6
- package/src/tests/memory-edges.test.ts +0 -2
- package/src/tests/memory-rate-endpoint.test.ts +0 -2
- package/src/tests/memory-rater-e2e.test.ts +0 -2
- package/src/tests/memory-store.test.ts +19 -1
- package/src/tests/memory.test.ts +51 -0
- package/src/tests/metrics-http.test.ts +137 -3
- package/src/tests/migration-046-budgets.test.ts +33 -0
- package/src/tests/migration-runner-regressions.test.ts +69 -0
- package/src/tests/model-control.test.ts +162 -46
- package/src/tests/opencode-adapter.test.ts +9 -0
- package/src/tests/pi-mono-adapter.test.ts +319 -0
- package/src/tests/providers/pi-cost.test.ts +9 -0
- package/src/tests/reload-config.test.ts +33 -17
- package/src/tests/runner-fallback-output.test.ts +50 -0
- package/src/tests/runner-skills-refresh.test.ts +216 -46
- package/src/tests/script-runs-http.test.ts +7 -1
- package/src/tests/scripts-boot-reembed.test.ts +163 -0
- package/src/tests/scripts-embeddings.test.ts +90 -0
- package/src/tests/scripts-runtime-secret-egress.test.ts +129 -0
- package/src/tests/seed-scripts.test.ts +13 -1
- package/src/tests/seed.test.ts +26 -1
- package/src/tests/session-attach.test.ts +6 -6
- package/src/tests/session-costs-model-key-normalize.test.ts +2 -0
- package/src/tests/skill-fs-writer.test.ts +250 -0
- package/src/tests/slack-attachments-block.test.ts +0 -1
- package/src/tests/slack-blocks.test.ts +0 -1
- package/src/tests/slack-channel-join.test.ts +80 -0
- package/src/tests/slack-identity-resolution.test.ts +0 -1
- package/src/tests/slack-watcher.test.ts +66 -0
- package/src/tests/structured-output.test.ts +0 -2
- package/src/tests/use-dismissible-card.test.ts +0 -4
- package/src/tests/workflow-agent-task.test.ts +5 -2
- package/src/tests/workflow-validation-port-routing.test.ts +181 -0
- package/src/tools/memory-get.ts +11 -0
- package/src/tools/memory-search.ts +18 -0
- package/src/tools/schedules/create-schedule.ts +71 -70
- package/src/tools/schedules/update-schedule.ts +43 -31
- package/src/tools/send-task.ts +16 -5
- package/src/tools/slack-post.ts +18 -15
- package/src/tools/slack-read.ts +9 -11
- package/src/tools/slack-reply.ts +18 -15
- package/src/tools/slack-start-thread.ts +17 -14
- package/src/tools/task-action.ts +11 -3
- package/src/types.ts +40 -0
- package/src/utils/aws-error-classifier.ts +97 -0
- package/src/utils/context-window.ts +5 -0
- package/src/utils/credentials.test.ts +68 -0
- package/src/utils/credentials.ts +66 -5
- package/src/utils/pretty-print.ts +25 -10
- package/src/utils/skill-fs-writer.ts +220 -0
- package/src/utils/skills-refresh.ts +123 -40
- package/src/workflows/engine.ts +3 -2
- package/src/workflows/executors/agent-task.ts +3 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
2
|
import { unlink } from "node:fs/promises";
|
|
3
3
|
import { closeDb, getDb, initDb } from "../be/db";
|
|
4
|
+
import { seedPricingFromModelsDev } from "../be/seed-pricing";
|
|
4
5
|
import { CODEX_MODEL_PRICING } from "../providers/codex-models";
|
|
5
6
|
|
|
6
7
|
const TEST_DB_PATH = "./test-migration-046.sqlite";
|
|
@@ -173,6 +174,38 @@ describe("migration 046 — budgets and pricing", () => {
|
|
|
173
174
|
}
|
|
174
175
|
});
|
|
175
176
|
|
|
177
|
+
test("models.dev seed includes Claude Mythos 5 pricing rows", () => {
|
|
178
|
+
const db = getDb();
|
|
179
|
+
const result = seedPricingFromModelsDev({ quiet: true });
|
|
180
|
+
expect(result.modelsdevFound).toBe(true);
|
|
181
|
+
|
|
182
|
+
const expectedPrices = {
|
|
183
|
+
input: 10,
|
|
184
|
+
cached_input: 1,
|
|
185
|
+
cache_write: 12.5,
|
|
186
|
+
output: 50,
|
|
187
|
+
} as const;
|
|
188
|
+
const seededKeys = [
|
|
189
|
+
["claude", "claude-mythos-5"],
|
|
190
|
+
["claude-managed", "claude-mythos-5"],
|
|
191
|
+
["claude", "mythos"],
|
|
192
|
+
["claude-managed", "mythos"],
|
|
193
|
+
["pi", "mythos"],
|
|
194
|
+
] as const;
|
|
195
|
+
|
|
196
|
+
for (const [provider, model] of seededKeys) {
|
|
197
|
+
for (const [tokenClass, price] of Object.entries(expectedPrices)) {
|
|
198
|
+
const row = db
|
|
199
|
+
.prepare<PricingRow, [string, string, string]>(
|
|
200
|
+
`SELECT * FROM pricing
|
|
201
|
+
WHERE provider = ? AND model = ? AND token_class = ? AND effective_from = 0`,
|
|
202
|
+
)
|
|
203
|
+
.get(provider, model, tokenClass);
|
|
204
|
+
expect(row?.price_per_million_usd).toBe(price);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
|
|
176
209
|
test("idx_pricing_lookup index exists", () => {
|
|
177
210
|
const db = getDb();
|
|
178
211
|
const idx = db
|
|
@@ -2,9 +2,11 @@ import { Database } from "bun:sqlite";
|
|
|
2
2
|
import { afterEach, describe, expect, test } from "bun:test";
|
|
3
3
|
import { unlink } from "node:fs/promises";
|
|
4
4
|
import { closeDb, initDb } from "../be/db";
|
|
5
|
+
import { runMigrations } from "../be/migrations/runner";
|
|
5
6
|
|
|
6
7
|
const INCOMPLETE_DB_PATH = "./test-migration-incomplete.sqlite";
|
|
7
8
|
const FRESH_DB_PATH = "./test-migration-fresh.sqlite";
|
|
9
|
+
const REPAIR_DB_PATH = "./test-migration-repair.sqlite";
|
|
8
10
|
|
|
9
11
|
async function removeDbFiles(dbPath: string): Promise<void> {
|
|
10
12
|
for (const suffix of ["", "-wal", "-shm"]) {
|
|
@@ -22,6 +24,7 @@ afterEach(async () => {
|
|
|
22
24
|
closeDb();
|
|
23
25
|
await removeDbFiles(INCOMPLETE_DB_PATH);
|
|
24
26
|
await removeDbFiles(FRESH_DB_PATH);
|
|
27
|
+
await removeDbFiles(REPAIR_DB_PATH);
|
|
25
28
|
});
|
|
26
29
|
|
|
27
30
|
describe("migration regressions", () => {
|
|
@@ -98,4 +101,70 @@ describe("migration regressions", () => {
|
|
|
98
101
|
expect(requestedByFk?.table).toBe("users");
|
|
99
102
|
expect(requestedByFk?.to).toBe("id");
|
|
100
103
|
});
|
|
104
|
+
|
|
105
|
+
test("repairs seed-as-090 history so 090_model_tiers is never skipped", () => {
|
|
106
|
+
// 2026-06-10 incident: PR #722 shipped the metrics seed as migration 090
|
|
107
|
+
// and production applied it; PR #719 then renumbered the seed to 091 and
|
|
108
|
+
// took 090 for model tiers. The runner keys applied migrations on version,
|
|
109
|
+
// so those databases skipped 090_model_tiers and crashed on the missing
|
|
110
|
+
// modelTier column. repairRenumberedModelTiers() in the runner must detect
|
|
111
|
+
// that history and fix it on boot.
|
|
112
|
+
const SEED_NAME = "090_seed_swarm_operations_metrics";
|
|
113
|
+
const SEED_CHECKSUM = "8ca4a05263b42d115b419f468bf5113caa5b7ee4363177568897513549224b01";
|
|
114
|
+
|
|
115
|
+
// Raw Database + runMigrations directly: initDb()'s test-template fast
|
|
116
|
+
// path skips the runner entirely, and the repair lives in the runner.
|
|
117
|
+
const database = new Database(REPAIR_DB_PATH, { create: true });
|
|
118
|
+
runMigrations(database);
|
|
119
|
+
|
|
120
|
+
// Reconstruct the divergent history: modelTier columns absent, version 90
|
|
121
|
+
// recorded as the seed migration.
|
|
122
|
+
database.run("ALTER TABLE agent_tasks DROP COLUMN modelTier");
|
|
123
|
+
database.run("ALTER TABLE scheduled_tasks DROP COLUMN modelTier");
|
|
124
|
+
database.run("UPDATE _migrations SET name = ?, checksum = ? WHERE version = 90", [
|
|
125
|
+
SEED_NAME,
|
|
126
|
+
SEED_CHECKSUM,
|
|
127
|
+
]);
|
|
128
|
+
|
|
129
|
+
// Next boot repairs the history.
|
|
130
|
+
runMigrations(database);
|
|
131
|
+
|
|
132
|
+
for (const table of ["agent_tasks", "scheduled_tasks"]) {
|
|
133
|
+
const columns = database
|
|
134
|
+
.prepare<{ name: string }, []>(`PRAGMA table_info(${table})`)
|
|
135
|
+
.all()
|
|
136
|
+
.map((column) => column.name);
|
|
137
|
+
expect(columns).toContain("modelTier");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const row = database
|
|
141
|
+
.prepare<{ name: string; checksum: string }, []>(
|
|
142
|
+
"SELECT name, checksum FROM _migrations WHERE version = 90",
|
|
143
|
+
)
|
|
144
|
+
.get();
|
|
145
|
+
expect(row?.name).toBe("090_model_tiers");
|
|
146
|
+
expect(row?.checksum).not.toBe(SEED_CHECKSUM);
|
|
147
|
+
|
|
148
|
+
// The original failure mode: inserting a task with a modelTier value.
|
|
149
|
+
const now = new Date().toISOString();
|
|
150
|
+
expect(() => {
|
|
151
|
+
database.run(
|
|
152
|
+
`INSERT INTO agent_tasks (id, task, status, source, modelTier, createdAt, lastUpdatedAt)
|
|
153
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
154
|
+
[crypto.randomUUID(), "boot triage", "pending", "system", "regular", now, now],
|
|
155
|
+
);
|
|
156
|
+
}).not.toThrow();
|
|
157
|
+
|
|
158
|
+
// Healthy histories are untouched: booting again is a no-op.
|
|
159
|
+
runMigrations(database);
|
|
160
|
+
const rowAfter = database
|
|
161
|
+
.prepare<{ name: string; checksum: string }, []>(
|
|
162
|
+
"SELECT name, checksum FROM _migrations WHERE version = 90",
|
|
163
|
+
)
|
|
164
|
+
.get();
|
|
165
|
+
expect(rowAfter?.name).toBe("090_model_tiers");
|
|
166
|
+
expect(rowAfter?.checksum).toBe(row?.checksum);
|
|
167
|
+
|
|
168
|
+
database.close();
|
|
169
|
+
});
|
|
101
170
|
});
|
|
@@ -12,7 +12,17 @@ import {
|
|
|
12
12
|
updateScheduledTask,
|
|
13
13
|
upsertSwarmConfig,
|
|
14
14
|
} from "../be/db";
|
|
15
|
+
import {
|
|
16
|
+
parseModelTier,
|
|
17
|
+
resolveModelTier,
|
|
18
|
+
resolveTaskModelSelection,
|
|
19
|
+
splitLegacyModelAlias,
|
|
20
|
+
} from "../model-tiers";
|
|
15
21
|
import { runScheduleNow } from "../scheduler";
|
|
22
|
+
import { createScheduleInputSchema } from "../tools/schedules/create-schedule";
|
|
23
|
+
import { updateScheduleInputSchema } from "../tools/schedules/update-schedule";
|
|
24
|
+
import { sendTaskInputSchema } from "../tools/send-task";
|
|
25
|
+
import { taskActionInputSchema } from "../tools/task-action";
|
|
16
26
|
|
|
17
27
|
const TEST_DB_PATH = "./test-model-control.sqlite";
|
|
18
28
|
|
|
@@ -84,6 +94,24 @@ describe("Model Control - Task Creation", () => {
|
|
|
84
94
|
expect(task.model).toBe("sonnet");
|
|
85
95
|
expect(task.status).toBe("offered");
|
|
86
96
|
});
|
|
97
|
+
|
|
98
|
+
test("should store modelTier when creating a task with portable tier", () => {
|
|
99
|
+
const task = createTaskExtended("Test task with tier", { modelTier: "smart" });
|
|
100
|
+
expect(task.model).toBeUndefined();
|
|
101
|
+
expect(task.modelTier).toBe("smart");
|
|
102
|
+
|
|
103
|
+
const retrieved = getTaskById(task.id);
|
|
104
|
+
expect(retrieved?.modelTier).toBe("smart");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("should preserve freeform concrete model strings", () => {
|
|
108
|
+
const task = createTaskExtended("Test task with freeform model", {
|
|
109
|
+
model: "openrouter/anthropic/claude-sonnet-4.6",
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
expect(task.model).toBe("openrouter/anthropic/claude-sonnet-4.6");
|
|
113
|
+
expect(task.modelTier).toBeUndefined();
|
|
114
|
+
});
|
|
87
115
|
});
|
|
88
116
|
|
|
89
117
|
describe("Model Control - Schedule Creation", () => {
|
|
@@ -102,7 +130,7 @@ describe("Model Control - Schedule Creation", () => {
|
|
|
102
130
|
});
|
|
103
131
|
|
|
104
132
|
test("should store all valid model values on schedules", () => {
|
|
105
|
-
for (const model of ["haiku", "sonnet", "opus"] as const) {
|
|
133
|
+
for (const model of ["haiku", "sonnet", "opus", "fable", "gpt-5.5"] as const) {
|
|
106
134
|
const schedule = createScheduledTask({
|
|
107
135
|
name: `model-schedule-all-${model}-${Date.now()}`,
|
|
108
136
|
intervalMs: 60000,
|
|
@@ -123,6 +151,21 @@ describe("Model Control - Schedule Creation", () => {
|
|
|
123
151
|
|
|
124
152
|
expect(schedule.model).toBeUndefined();
|
|
125
153
|
});
|
|
154
|
+
|
|
155
|
+
test("should store modelTier on scheduled task creation", () => {
|
|
156
|
+
const schedule = createScheduledTask({
|
|
157
|
+
name: "model-schedule-tier",
|
|
158
|
+
intervalMs: 60000,
|
|
159
|
+
taskTemplate: "Scheduled with portable tier",
|
|
160
|
+
modelTier: "regular",
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
expect(schedule.model).toBeUndefined();
|
|
164
|
+
expect(schedule.modelTier).toBe("regular");
|
|
165
|
+
|
|
166
|
+
const retrieved = getScheduledTaskById(schedule.id);
|
|
167
|
+
expect(retrieved?.modelTier).toBe("regular");
|
|
168
|
+
});
|
|
126
169
|
});
|
|
127
170
|
|
|
128
171
|
describe("Model Control - Schedule Update", () => {
|
|
@@ -169,6 +212,23 @@ describe("Model Control - Schedule Update", () => {
|
|
|
169
212
|
expect(updated?.model).toBe("haiku");
|
|
170
213
|
expect(updated?.priority).toBe(90);
|
|
171
214
|
});
|
|
215
|
+
|
|
216
|
+
test("should update and clear modelTier on existing schedule", () => {
|
|
217
|
+
const schedule = createScheduledTask({
|
|
218
|
+
name: "model-tier-update-test",
|
|
219
|
+
intervalMs: 60000,
|
|
220
|
+
taskTemplate: "Update model tier test",
|
|
221
|
+
modelTier: "regular",
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
expect(schedule.modelTier).toBe("regular");
|
|
225
|
+
|
|
226
|
+
const updated = updateScheduledTask(schedule.id, { modelTier: "ultra" });
|
|
227
|
+
expect(updated?.modelTier).toBe("ultra");
|
|
228
|
+
|
|
229
|
+
const cleared = updateScheduledTask(schedule.id, { modelTier: null });
|
|
230
|
+
expect(cleared?.modelTier).toBeUndefined();
|
|
231
|
+
});
|
|
172
232
|
});
|
|
173
233
|
|
|
174
234
|
describe("Model Control - Schedule to Task Propagation", () => {
|
|
@@ -213,6 +273,28 @@ describe("Model Control - Schedule to Task Propagation", () => {
|
|
|
213
273
|
const task = getTaskById(row!.id);
|
|
214
274
|
expect(task?.model).toBeUndefined();
|
|
215
275
|
});
|
|
276
|
+
|
|
277
|
+
test("should propagate modelTier from schedule to task on manual run", async () => {
|
|
278
|
+
const schedule = createScheduledTask({
|
|
279
|
+
name: "model-tier-propagate-manual",
|
|
280
|
+
intervalMs: 60000,
|
|
281
|
+
taskTemplate: "Propagated model tier task (manual)",
|
|
282
|
+
modelTier: "smart",
|
|
283
|
+
enabled: true,
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
await runScheduleNow(schedule.id);
|
|
287
|
+
|
|
288
|
+
const { getDb } = await import("../be/db");
|
|
289
|
+
const row = getDb()
|
|
290
|
+
.query("SELECT id FROM agent_tasks WHERE task = ? ORDER BY createdAt DESC LIMIT 1")
|
|
291
|
+
.get("Propagated model tier task (manual)") as { id: string } | null;
|
|
292
|
+
|
|
293
|
+
expect(row).not.toBeNull();
|
|
294
|
+
const task = getTaskById(row!.id);
|
|
295
|
+
expect(task?.model).toBeUndefined();
|
|
296
|
+
expect(task?.modelTier).toBe("smart");
|
|
297
|
+
});
|
|
216
298
|
});
|
|
217
299
|
|
|
218
300
|
describe("Model Control - Config MODEL_OVERRIDE Resolution", () => {
|
|
@@ -268,71 +350,105 @@ describe("Model Control - Config MODEL_OVERRIDE Resolution", () => {
|
|
|
268
350
|
});
|
|
269
351
|
|
|
270
352
|
describe("Model Control - Priority Resolution Logic", () => {
|
|
271
|
-
// The runner resolves model as: task.model || freshEnv.MODEL_OVERRIDE || "opus"
|
|
272
|
-
// We test the same logic pattern here to ensure correctness
|
|
273
|
-
|
|
274
|
-
function resolveModel(taskModel?: string, configOverride?: string): string {
|
|
275
|
-
return taskModel || configOverride || "opus";
|
|
276
|
-
}
|
|
277
|
-
|
|
278
353
|
test("task.model takes highest priority", () => {
|
|
279
|
-
expect(
|
|
354
|
+
expect(
|
|
355
|
+
resolveTaskModelSelection({
|
|
356
|
+
model: "gpt-5.5",
|
|
357
|
+
modelTier: "smol",
|
|
358
|
+
harnessProvider: "codex",
|
|
359
|
+
}).model,
|
|
360
|
+
).toBe("gpt-5.5");
|
|
280
361
|
});
|
|
281
362
|
|
|
282
|
-
test("
|
|
283
|
-
expect(
|
|
363
|
+
test("task.modelTier resolves using the claiming worker harness", () => {
|
|
364
|
+
expect(resolveModelTier({ tier: "smol", harnessProvider: "claude" })).toBe("haiku");
|
|
365
|
+
expect(resolveModelTier({ tier: "smol", harnessProvider: "codex" })).toBe("gpt-5.4-mini");
|
|
366
|
+
expect(resolveModelTier({ tier: "smart", harnessProvider: "opencode" })).toBe(
|
|
367
|
+
"openrouter/deepseek/deepseek-v4-pro",
|
|
368
|
+
);
|
|
369
|
+
expect(resolveModelTier({ tier: "ultra", harnessProvider: "pi" })).toBe(
|
|
370
|
+
"openrouter/anthropic/claude-opus-4.8",
|
|
371
|
+
);
|
|
284
372
|
});
|
|
285
373
|
|
|
286
|
-
test("
|
|
287
|
-
expect(
|
|
374
|
+
test("task.modelTier supports env map and direct tier overrides", () => {
|
|
375
|
+
expect(
|
|
376
|
+
resolveModelTier({
|
|
377
|
+
tier: "regular",
|
|
378
|
+
harnessProvider: "codex",
|
|
379
|
+
env: { MODEL_TIER_MAP: JSON.stringify({ regular: "gpt-5.3-codex" }) },
|
|
380
|
+
}),
|
|
381
|
+
).toBe("gpt-5.3-codex");
|
|
382
|
+
expect(
|
|
383
|
+
resolveModelTier({
|
|
384
|
+
tier: "regular",
|
|
385
|
+
harnessProvider: "codex",
|
|
386
|
+
env: {
|
|
387
|
+
MODEL_TIER_MAP: JSON.stringify({ regular: "gpt-5.3-codex" }),
|
|
388
|
+
MODEL_TIER_REGULAR: "gpt-5.5",
|
|
389
|
+
},
|
|
390
|
+
}),
|
|
391
|
+
).toBe("gpt-5.5");
|
|
288
392
|
});
|
|
289
393
|
|
|
290
|
-
test("
|
|
291
|
-
expect(
|
|
394
|
+
test("legacy model aliases parse as tiers", () => {
|
|
395
|
+
expect(parseModelTier("haiku")).toBe("smol");
|
|
396
|
+
expect(parseModelTier("sonnet")).toBe("regular");
|
|
397
|
+
expect(parseModelTier("opus")).toBe("smart");
|
|
398
|
+
expect(parseModelTier("fable")).toBe("ultra");
|
|
399
|
+
expect(splitLegacyModelAlias({ model: "opus" })).toEqual({ modelTier: "smart" });
|
|
292
400
|
});
|
|
293
401
|
|
|
294
|
-
test("
|
|
295
|
-
expect(
|
|
402
|
+
test("freeform concrete model strings stay concrete", () => {
|
|
403
|
+
expect(splitLegacyModelAlias({ model: "gpt-5.5" })).toEqual({
|
|
404
|
+
model: "gpt-5.5",
|
|
405
|
+
modelTier: undefined,
|
|
406
|
+
});
|
|
296
407
|
});
|
|
297
408
|
|
|
298
|
-
test("
|
|
299
|
-
expect(
|
|
300
|
-
|
|
409
|
+
test("missing task model selection falls through to adapter/config", () => {
|
|
410
|
+
expect(
|
|
411
|
+
resolveTaskModelSelection({ model: "", modelTier: undefined, harnessProvider: "codex" }),
|
|
412
|
+
).toEqual({ source: "none" });
|
|
301
413
|
});
|
|
302
414
|
});
|
|
303
415
|
|
|
304
416
|
describe("Model Control - Zod Validation Schema", () => {
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
expect(modelSchema.parse("sonnet")).toBe("sonnet");
|
|
314
|
-
expect(modelSchema.parse("opus")).toBe("opus");
|
|
315
|
-
expect(modelSchema.parse(undefined)).toBeUndefined();
|
|
417
|
+
test("task tools accept freeform concrete models and model tiers", () => {
|
|
418
|
+
expect(
|
|
419
|
+
sendTaskInputSchema.parse({ agentId: crypto.randomUUID(), task: "x", model: "gpt-5.5" })
|
|
420
|
+
.model,
|
|
421
|
+
).toBe("gpt-5.5");
|
|
422
|
+
expect(
|
|
423
|
+
taskActionInputSchema.parse({ action: "create", task: "x", modelTier: "ultra" }).modelTier,
|
|
424
|
+
).toBe("ultra");
|
|
316
425
|
});
|
|
317
426
|
|
|
318
|
-
test("
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
expect(() =>
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
expect(() => modelSchema.parse("")).toThrow();
|
|
326
|
-
expect(() => modelSchema.parse(123)).toThrow();
|
|
327
|
-
expect(() => modelSchema.parse(null)).toThrow();
|
|
427
|
+
test("task tools reject empty model strings and invalid tiers", () => {
|
|
428
|
+
expect(() =>
|
|
429
|
+
sendTaskInputSchema.parse({ agentId: crypto.randomUUID(), task: "x", model: "" }),
|
|
430
|
+
).toThrow();
|
|
431
|
+
expect(() =>
|
|
432
|
+
taskActionInputSchema.parse({ action: "create", task: "x", modelTier: "massive" }),
|
|
433
|
+
).toThrow();
|
|
328
434
|
});
|
|
329
435
|
|
|
330
436
|
test("nullable model schema (update-schedule) should accept null", async () => {
|
|
331
|
-
|
|
332
|
-
|
|
437
|
+
expect(updateScheduleInputSchema.shape.model.parse(null)).toBeNull();
|
|
438
|
+
expect(updateScheduleInputSchema.shape.model.parse("gpt-5.5")).toBe("gpt-5.5");
|
|
439
|
+
expect(updateScheduleInputSchema.shape.modelTier.parse(null)).toBeNull();
|
|
440
|
+
expect(updateScheduleInputSchema.shape.modelTier.parse("smol")).toBe("smol");
|
|
441
|
+
});
|
|
333
442
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
443
|
+
test("create schedule schema accepts freeform model and modelTier", () => {
|
|
444
|
+
const parsed = createScheduleInputSchema.parse({
|
|
445
|
+
name: "schema-model-tier",
|
|
446
|
+
taskTemplate: "x",
|
|
447
|
+
intervalMs: 60000,
|
|
448
|
+
model: "openrouter/openai/gpt-5.5",
|
|
449
|
+
modelTier: "smart",
|
|
450
|
+
});
|
|
451
|
+
expect(parsed.model).toBe("openrouter/openai/gpt-5.5");
|
|
452
|
+
expect(parsed.modelTier).toBe("smart");
|
|
337
453
|
});
|
|
338
454
|
});
|
|
@@ -156,6 +156,15 @@ describe("OpencodeSession — SSE→ProviderEvent mapping", () => {
|
|
|
156
156
|
];
|
|
157
157
|
const { emitted, result, serverCloseCalls } = await driveSession(events);
|
|
158
158
|
|
|
159
|
+
const sessionInit = emitted.find((e) => e.type === "session_init");
|
|
160
|
+
expect(sessionInit).toBeDefined();
|
|
161
|
+
if (sessionInit?.type === "session_init") {
|
|
162
|
+
expect(sessionInit.provider).toBe("opencode");
|
|
163
|
+
expect(sessionInit.harnessVariant).toBe("stock");
|
|
164
|
+
expect(typeof sessionInit.harnessVariantMeta?.version).toBe("string");
|
|
165
|
+
expect((sessionInit.harnessVariantMeta?.version as string).length).toBeGreaterThan(0);
|
|
166
|
+
}
|
|
167
|
+
|
|
159
168
|
const resultEvent = emitted.find((e) => e.type === "result");
|
|
160
169
|
expect(resultEvent).toBeDefined();
|
|
161
170
|
if (resultEvent?.type === "result") {
|