@@ -40,10 +40,10 @@ Both installers use `~/.config/opencode`, respect `XDG_CONFIG_HOME`, and accept
|
|||||||
|
|
||||||
See [GPT configuration](examples/gpt.opencode.jsonc) and [custom model assignments](examples/custom.opencode.jsonc).
|
See [GPT configuration](examples/gpt.opencode.jsonc) and [custom model assignments](examples/custom.opencode.jsonc).
|
||||||
|
|
||||||
For an installed plugin, use `./plugins/stanp-opencode` as the plugin package path. Set `lunaPriority` to `false` to avoid requesting priority pricing.
|
For an installed plugin, use `./plugins/stanp-opencode` as the plugin package path. In GPT mode with the `openai` provider, set top-level `lunaPriority` to `false` to avoid requesting priority pricing. Other providers do not accept this option. Custom top-level and per-agent `provider` and `lunaPriority` fields are invalid.
|
||||||
Custom configurations require the six worker assignments. The legacy optional `coordinate`
|
Custom configurations require the six worker assignments and use `provider/model` model names.
|
||||||
assignment is accepted for compatibility but deprecated and ignored; orchestration inherits
|
The legacy optional `coordinate` assignment is accepted for compatibility but ignored. Orchestration
|
||||||
`deep-lo`, or `good-lo` when the configured `deep-lo` model is unavailable.
|
inherits `deep-lo`, or `good-lo` when the configured `deep-lo` model is unavailable.
|
||||||
|
|
||||||
## Multi-work
|
## Multi-work
|
||||||
|
|
||||||
|
|||||||
@@ -3,22 +3,20 @@
|
|||||||
// Required for multi-work -> coordinate -> workers. Apply in worktrees too.
|
// Required for multi-work -> coordinate -> workers. Apply in worktrees too.
|
||||||
// Multi-work and coordinate use deep-lo, falling back to good-lo when unavailable.
|
// Multi-work and coordinate use deep-lo, falling back to good-lo when unavailable.
|
||||||
"experimental": { "subagent_depth": 2 },
|
"experimental": { "subagent_depth": 2 },
|
||||||
// All six worker assignments are required. Model IDs are local to the provider.
|
// All six worker assignments are required. Models use provider/model names.
|
||||||
// An unavailable deep-lo omits deep workers and uses good-lo for orchestration.
|
// An unavailable deep-lo omits deep workers and uses good-lo for orchestration.
|
||||||
"plugins": [
|
"plugins": [
|
||||||
{
|
{
|
||||||
"package": "stanp-opencode",
|
"package": "stanp-opencode",
|
||||||
"options": {
|
"options": {
|
||||||
"mode": "custom",
|
"mode": "custom",
|
||||||
"provider": "openai",
|
|
||||||
"lunaPriority": false,
|
|
||||||
"agents": {
|
"agents": {
|
||||||
"fast-lo": { "model": "gpt-5.6-luna", "effort": "low" },
|
"fast-lo": { "model": "openai/gpt-5.6-luna", "effort": "low" },
|
||||||
"fast-hi": { "model": "gpt-5.6-luna", "effort": "high" },
|
"fast-hi": { "model": "openai/gpt-5.6-luna", "effort": "high" },
|
||||||
"good-lo": { "model": "gpt-5.6-sol", "effort": "low" },
|
"good-lo": { "model": "openai/gpt-5.6-sol", "effort": "low" },
|
||||||
"good-hi": { "model": "gpt-5.6-sol", "effort": "high" },
|
"good-hi": { "model": "openai/gpt-5.6-sol", "effort": "high" },
|
||||||
"deep-lo": { "model": "gpt-6-astra", "effort": "low" },
|
"deep-lo": { "model": "openai/gpt-6-astra", "effort": "low" },
|
||||||
"deep-hi": { "model": "gpt-6-astra", "effort": "high" },
|
"deep-hi": { "model": "openai/gpt-6-astra", "effort": "high" },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
+39
-12
@@ -9,31 +9,58 @@ export const workerNames = [
|
|||||||
"deep-hi",
|
"deep-hi",
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
const assignment = z.strictObject({
|
const gptAssignment = z.strictObject({
|
||||||
model: z.string().trim().min(1),
|
model: z.string().trim().min(1),
|
||||||
effort: z.enum(["none", "minimal", "low", "medium", "high", "xhigh", "max"]),
|
effort: z.enum(["none", "minimal", "low", "medium", "high", "xhigh", "max"]),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const customAssignment = gptAssignment.extend({
|
||||||
|
model: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.refine((model) => {
|
||||||
|
const slash = model.indexOf("/");
|
||||||
|
return slash > 0 && slash < model.length - 1;
|
||||||
|
}, "model must use the provider/model convention"),
|
||||||
|
});
|
||||||
|
|
||||||
const common = {
|
const common = {
|
||||||
provider: z.string().trim().min(1).default("openai"),
|
|
||||||
lunaPriority: z.boolean().default(true),
|
|
||||||
playwright: z.boolean().default(false),
|
playwright: z.boolean().default(false),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const settings = z.discriminatedUnion("mode", [
|
export const settings = z.discriminatedUnion("mode", [
|
||||||
z.strictObject({ ...common, mode: z.literal("gpt") }),
|
z
|
||||||
|
.strictObject({
|
||||||
|
...common,
|
||||||
|
mode: z.literal("gpt"),
|
||||||
|
provider: z.string().trim().min(1).default("openai"),
|
||||||
|
lunaPriority: z.boolean().optional(),
|
||||||
|
})
|
||||||
|
.transform((value, context) => {
|
||||||
|
if (value.provider !== "openai" && value.lunaPriority !== undefined) {
|
||||||
|
context.addIssue({
|
||||||
|
code: "custom",
|
||||||
|
path: ["lunaPriority"],
|
||||||
|
message: "lunaPriority is only supported by the openai provider",
|
||||||
|
});
|
||||||
|
return z.NEVER;
|
||||||
|
}
|
||||||
|
return value.provider === "openai"
|
||||||
|
? { ...value, lunaPriority: value.lunaPriority ?? true }
|
||||||
|
: value;
|
||||||
|
}),
|
||||||
z.strictObject({
|
z.strictObject({
|
||||||
...common,
|
...common,
|
||||||
mode: z.literal("custom"),
|
mode: z.literal("custom"),
|
||||||
agents: z.strictObject({
|
agents: z.strictObject({
|
||||||
"fast-lo": assignment,
|
"fast-lo": customAssignment,
|
||||||
"fast-hi": assignment,
|
"fast-hi": customAssignment,
|
||||||
"good-lo": assignment,
|
"good-lo": customAssignment,
|
||||||
"good-hi": assignment,
|
"good-hi": customAssignment,
|
||||||
"deep-lo": assignment,
|
"deep-lo": customAssignment,
|
||||||
"deep-hi": assignment,
|
"deep-hi": customAssignment,
|
||||||
// Deprecated compatibility field. Coordinate is derived from deep-lo/good-lo.
|
// Deprecated compatibility field. Coordinate is derived from deep-lo/good-lo.
|
||||||
coordinate: assignment.optional(),
|
coordinate: customAssignment.optional(),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
@@ -45,4 +72,4 @@ export const gptAgents = {
|
|||||||
"good-hi": { model: "gpt-5.6-sol", effort: "high" },
|
"good-hi": { model: "gpt-5.6-sol", effort: "high" },
|
||||||
"deep-lo": { model: "gpt-6-astra", effort: "low" },
|
"deep-lo": { model: "gpt-6-astra", effort: "low" },
|
||||||
"deep-hi": { model: "gpt-6-astra", effort: "high" },
|
"deep-hi": { model: "gpt-6-astra", effort: "high" },
|
||||||
} satisfies Record<(typeof workerNames)[number], z.infer<typeof assignment>>;
|
} satisfies Record<(typeof workerNames)[number], z.infer<typeof gptAssignment>>;
|
||||||
|
|||||||
+41
-9
@@ -3,7 +3,7 @@ import type { RpcRegistration } from "@opencode/plugin/promise/rpc";
|
|||||||
import type { ModelInfo } from "@opencode/client";
|
import type { ModelInfo } from "@opencode/client";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { gptAgents, settings, workerNames } from "./config.js";
|
import { gptAgents, settings, workerNames } from "./config.js";
|
||||||
import { discover } from "./provider.js";
|
import { discover, modelKey } from "./provider.js";
|
||||||
import { descriptions, routingGuide } from "./routing.js";
|
import { descriptions, routingGuide } from "./routing.js";
|
||||||
import { QuotaTracker } from "./quota.js";
|
import { QuotaTracker } from "./quota.js";
|
||||||
import { QuotaRpc } from "./quota-rpc.js";
|
import { QuotaRpc } from "./quota-rpc.js";
|
||||||
@@ -50,6 +50,10 @@ const makeBasePlugin = (permissionsChanged: (permissions: Agent.Info["permission
|
|||||||
id: "stanp-opencode",
|
id: "stanp-opencode",
|
||||||
async setup(context) {
|
async setup(context) {
|
||||||
const selected = settings.parse({ mode: "gpt", ...context.options });
|
const selected = settings.parse({ mode: "gpt", ...context.options });
|
||||||
|
const lunaPriority =
|
||||||
|
selected.mode === "gpt" &&
|
||||||
|
selected.provider === "openai" &&
|
||||||
|
selected.lunaPriority === true;
|
||||||
let models = new Map<string, { model: ModelInfo; serviceTier: string | undefined }>();
|
let models = new Map<string, { model: ModelInfo; serviceTier: string | undefined }>();
|
||||||
let inherited: Agent.Info["permissions"] = Agent.Info.default(
|
let inherited: Agent.Info["permissions"] = Agent.Info.default(
|
||||||
Agent.ID.make("build"),
|
Agent.ID.make("build"),
|
||||||
@@ -83,7 +87,7 @@ const makeBasePlugin = (permissionsChanged: (permissions: Agent.Info["permission
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (event.type === "model.updated") {
|
if (event.type === "model.updated") {
|
||||||
models = await discover(context, selected.provider);
|
models = await discover(context, lunaPriority);
|
||||||
await context.agent.reload();
|
await context.agent.reload();
|
||||||
} else if (event.type === "agent.updated") {
|
} else if (event.type === "agent.updated") {
|
||||||
// The configured build transform may run after ours. Read the final
|
// The configured build transform may run after ours. Read the final
|
||||||
@@ -118,13 +122,24 @@ const makeBasePlugin = (permissionsChanged: (permissions: Agent.Info["permission
|
|||||||
if (!controller.signal.aborted)
|
if (!controller.signal.aborted)
|
||||||
console.error("stanp-opencode model update failed", error);
|
console.error("stanp-opencode model update failed", error);
|
||||||
});
|
});
|
||||||
models = await discover(context, selected.provider);
|
models = await discover(context, lunaPriority);
|
||||||
const { data: initialAgents } = await context.agent.list();
|
const { data: initialAgents } = await context.agent.list();
|
||||||
inherited =
|
inherited =
|
||||||
initialAgents.find((agent) => agent.id === "build")?.permissions ?? inherited;
|
initialAgents.find((agent) => agent.id === "build")?.permissions ?? inherited;
|
||||||
permissionsChanged(inherited);
|
permissionsChanged(inherited);
|
||||||
permissionKey = JSON.stringify(inherited);
|
permissionKey = JSON.stringify(inherited);
|
||||||
const assignments = selected.mode === "gpt" ? gptAgents : selected.agents;
|
const assignments =
|
||||||
|
selected.mode === "gpt"
|
||||||
|
? Object.fromEntries(
|
||||||
|
workerNames.map((name) => [
|
||||||
|
name,
|
||||||
|
{
|
||||||
|
...gptAgents[name],
|
||||||
|
provider: selected.provider,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
: selected.agents;
|
||||||
let workers: (typeof workerNames)[number][] = [];
|
let workers: (typeof workerNames)[number][] = [];
|
||||||
const active = new Set<string>();
|
const active = new Set<string>();
|
||||||
const playwrightCommand = selected.playwright
|
const playwrightCommand = selected.playwright
|
||||||
@@ -163,11 +178,26 @@ const makeBasePlugin = (permissionsChanged: (permissions: Agent.Info["permission
|
|||||||
});
|
});
|
||||||
|
|
||||||
await context.agent.transform((editor) => {
|
await context.agent.transform((editor) => {
|
||||||
const deep = models.has(assignments["deep-lo"].model);
|
const coordinates = (assignment: { model: string }) => {
|
||||||
|
if (selected.mode === "gpt")
|
||||||
|
return { provider: selected.provider, model: assignment.model };
|
||||||
|
const slash = assignment.model.indexOf("/");
|
||||||
|
return {
|
||||||
|
provider: assignment.model.slice(0, slash),
|
||||||
|
model: assignment.model.slice(slash + 1),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
const availableFor = (
|
||||||
|
assignment: (typeof assignments)[(typeof workerNames)[number]],
|
||||||
|
) => {
|
||||||
|
const { provider, model } = coordinates(assignment);
|
||||||
|
return models.get(modelKey(provider, model));
|
||||||
|
};
|
||||||
|
const deep = availableFor(assignments["deep-lo"]) !== undefined;
|
||||||
const candidates = workerNames.filter((name) => deep || !name.startsWith("deep-"));
|
const candidates = workerNames.filter((name) => deep || !name.startsWith("deep-"));
|
||||||
const coordinate = assignments[deep ? "deep-lo" : "good-lo"];
|
const coordinate = assignments[deep ? "deep-lo" : "good-lo"];
|
||||||
workers = [coordinate, ...candidates.map((name) => assignments[name])].every(
|
workers = [coordinate, ...candidates.map((name) => assignments[name])].every(
|
||||||
({ model }) => models.has(model),
|
(assignment) => availableFor(assignment) !== undefined,
|
||||||
)
|
)
|
||||||
? candidates
|
? candidates
|
||||||
: [];
|
: [];
|
||||||
@@ -176,7 +206,7 @@ const makeBasePlugin = (permissionsChanged: (permissions: Agent.Info["permission
|
|||||||
if (workers.length === 0) return;
|
if (workers.length === 0) return;
|
||||||
for (const name of workers) {
|
for (const name of workers) {
|
||||||
const assignment = assignments[name];
|
const assignment = assignments[name];
|
||||||
const available = models.get(assignment.model)!;
|
const available = availableFor(assignment)!;
|
||||||
editor.update(name, (agent) => {
|
editor.update(name, (agent) => {
|
||||||
const hasVariant = available.model.variants.some(
|
const hasVariant = available.model.variants.some(
|
||||||
({ id }) => id === assignment.effort,
|
({ id }) => id === assignment.effort,
|
||||||
@@ -196,7 +226,7 @@ const makeBasePlugin = (permissionsChanged: (permissions: Agent.Info["permission
|
|||||||
headers: {},
|
headers: {},
|
||||||
body: {},
|
body: {},
|
||||||
};
|
};
|
||||||
if (selected.lunaPriority && available.serviceTier)
|
if (lunaPriority && available.serviceTier)
|
||||||
agent.request.settings.serviceTier = available.serviceTier;
|
agent.request.settings.serviceTier = available.serviceTier;
|
||||||
agent.permissions = [
|
agent.permissions = [
|
||||||
...structuredClone(inherited),
|
...structuredClone(inherited),
|
||||||
@@ -205,7 +235,7 @@ const makeBasePlugin = (permissionsChanged: (permissions: Agent.Info["permission
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
editor.update("coordinate", (agent) => {
|
editor.update("coordinate", (agent) => {
|
||||||
const available = models.get(coordinate.model)!;
|
const available = availableFor(coordinate)!;
|
||||||
const hasVariant = available.model.variants.some(
|
const hasVariant = available.model.variants.some(
|
||||||
({ id }) => id === coordinate.effort,
|
({ id }) => id === coordinate.effort,
|
||||||
);
|
);
|
||||||
@@ -224,6 +254,8 @@ const makeBasePlugin = (permissionsChanged: (permissions: Agent.Info["permission
|
|||||||
headers: {},
|
headers: {},
|
||||||
body: {},
|
body: {},
|
||||||
};
|
};
|
||||||
|
if (lunaPriority && available.serviceTier)
|
||||||
|
agent.request.settings.serviceTier = available.serviceTier;
|
||||||
agent.system = routingGuide(deep);
|
agent.system = routingGuide(deep);
|
||||||
agent.permissions = [
|
agent.permissions = [
|
||||||
{ action: "*", resource: "*", effect: "deny" },
|
{ action: "*", resource: "*", effect: "deny" },
|
||||||
|
|||||||
+10
-3
@@ -20,13 +20,20 @@ function priority(model: ModelInfo) {
|
|||||||
: undefined;
|
: undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function discover(context: Plugin.Context, providerID: string) {
|
export function modelKey(providerID: string, modelID: string) {
|
||||||
|
return `${providerID}\0${modelID}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function discover(context: Plugin.Context, detectPriority: boolean) {
|
||||||
// Native lists already apply activation, integrations, configuration, and model transforms.
|
// Native lists already apply activation, integrations, configuration, and model transforms.
|
||||||
const { data } = await context.model.list();
|
const { data } = await context.model.list();
|
||||||
const models = new Map<string, { model: ModelInfo; serviceTier: string | undefined }>();
|
const models = new Map<string, { model: ModelInfo; serviceTier: string | undefined }>();
|
||||||
for (const model of data) {
|
for (const model of data) {
|
||||||
if (model.providerID === providerID && model.enabled)
|
if (model.enabled)
|
||||||
models.set(model.id, { model, serviceTier: priority(model) });
|
models.set(modelKey(model.providerID, model.id), {
|
||||||
|
model,
|
||||||
|
serviceTier: detectPriority ? priority(model) : undefined,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return models;
|
return models;
|
||||||
}
|
}
|
||||||
|
|||||||
+176
-28
@@ -9,7 +9,7 @@ import type { Info, ToolContext, ToolEditor } from "@opencode/plugin/promise/too
|
|||||||
import type { JsonValue, ModelInfo, SessionGetInput } from "@opencode/client";
|
import type { JsonValue, ModelInfo, SessionGetInput } from "@opencode/client";
|
||||||
import type { SessionContext } from "@opencode/plugin/promise/session";
|
import type { SessionContext } from "@opencode/plugin/promise/session";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { gptAgents } from "../src/config.js";
|
import { gptAgents, settings, workerNames } from "../src/config.js";
|
||||||
import { basePlugin as plugin } from "../src/index.js";
|
import { basePlugin as plugin } from "../src/index.js";
|
||||||
import { playwrightMcpCommand } from "../src/playwright.js";
|
import { playwrightMcpCommand } from "../src/playwright.js";
|
||||||
import { bindingKey } from "../src/multi-work.js";
|
import { bindingKey } from "../src/multi-work.js";
|
||||||
@@ -27,7 +27,31 @@ function reasoningModel(id: string): ModelInfo {
|
|||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
function providerModel(provider: string, id: string): ModelInfo {
|
||||||
|
return {
|
||||||
|
...reasoningModel(id),
|
||||||
|
providerID: Provider.ID.make(provider),
|
||||||
|
};
|
||||||
|
}
|
||||||
const allModels = ["gpt-5.6-luna", "gpt-5.6-sol", "gpt-6-astra"].map(reasoningModel);
|
const allModels = ["gpt-5.6-luna", "gpt-5.6-sol", "gpt-6-astra"].map(reasoningModel);
|
||||||
|
type CustomAssignment = {
|
||||||
|
model: string;
|
||||||
|
effort: "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
|
||||||
|
};
|
||||||
|
const customAssignments = (provider = "test") => {
|
||||||
|
const assignment = (name: (typeof workerNames)[number]): CustomAssignment => ({
|
||||||
|
...gptAgents[name],
|
||||||
|
model: `${provider}/${gptAgents[name].model}`,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
"fast-lo": assignment("fast-lo"),
|
||||||
|
"fast-hi": assignment("fast-hi"),
|
||||||
|
"good-lo": assignment("good-lo"),
|
||||||
|
"good-hi": assignment("good-hi"),
|
||||||
|
"deep-lo": assignment("deep-lo"),
|
||||||
|
"deep-hi": assignment("deep-hi"),
|
||||||
|
};
|
||||||
|
};
|
||||||
const cleanups: Plugin.Cleanup[] = [];
|
const cleanups: Plugin.Cleanup[] = [];
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
for (const cleanup of cleanups.splice(0)) await cleanup();
|
for (const cleanup of cleanups.splice(0)) await cleanup();
|
||||||
@@ -352,15 +376,20 @@ test("orchestration inherits deep-lo and falls back to good-lo with available wo
|
|||||||
expect((await host(allModels.slice(1))).agents.has("coordinate")).toBe(false);
|
expect((await host(allModels.slice(1))).agents.has("coordinate")).toBe(false);
|
||||||
|
|
||||||
const customAgents = {
|
const customAgents = {
|
||||||
...gptAgents,
|
...customAssignments(),
|
||||||
"good-lo": { model: "custom-good", effort: "high" as const },
|
"good-lo": {
|
||||||
"deep-lo": { model: "custom-deep", effort: "medium" as const },
|
model: "test/custom-good",
|
||||||
|
effort: "high" as const,
|
||||||
|
},
|
||||||
|
"deep-lo": {
|
||||||
|
model: "test/custom-deep",
|
||||||
|
effort: "medium" as const,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
const custom = await host(
|
const custom = await host(
|
||||||
[...allModels, reasoningModel("custom-good"), reasoningModel("custom-deep")],
|
[...allModels, reasoningModel("custom-good"), reasoningModel("custom-deep")],
|
||||||
{
|
{
|
||||||
mode: "custom",
|
mode: "custom",
|
||||||
provider: "test",
|
|
||||||
agents: customAgents,
|
agents: customAgents,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -370,7 +399,6 @@ test("orchestration inherits deep-lo and falls back to good-lo with available wo
|
|||||||
});
|
});
|
||||||
const customFallback = await host([...allModels, reasoningModel("custom-good")], {
|
const customFallback = await host([...allModels, reasoningModel("custom-good")], {
|
||||||
mode: "custom",
|
mode: "custom",
|
||||||
provider: "test",
|
|
||||||
agents: customAgents,
|
agents: customAgents,
|
||||||
});
|
});
|
||||||
expect(customFallback.agents.get("coordinate")?.model).toMatchObject({
|
expect(customFallback.agents.get("coordinate")?.model).toMatchObject({
|
||||||
@@ -382,13 +410,106 @@ test("orchestration inherits deep-lo and falls back to good-lo with available wo
|
|||||||
(
|
(
|
||||||
await host(allModels, {
|
await host(allModels, {
|
||||||
mode: "custom",
|
mode: "custom",
|
||||||
provider: "test",
|
|
||||||
agents: customAgents,
|
agents: customAgents,
|
||||||
})
|
})
|
||||||
).agents.has("coordinate"),
|
).agents.has("coordinate"),
|
||||||
).toBe(false);
|
).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("custom assignments resolve duplicate model IDs by provider and refresh provider-aware fallback", async () => {
|
||||||
|
const agents = customAssignments("alpha");
|
||||||
|
agents["fast-lo"] = { ...agents["fast-lo"], model: "beta/shared" };
|
||||||
|
agents["fast-hi"] = { ...agents["fast-hi"], model: "alpha/shared" };
|
||||||
|
agents["deep-lo"] = { ...agents["deep-lo"], model: "beta/deep" };
|
||||||
|
agents["deep-hi"] = { ...agents["deep-hi"], model: "beta/deep" };
|
||||||
|
const base = [
|
||||||
|
providerModel("alpha", "shared"),
|
||||||
|
providerModel("beta", "shared"),
|
||||||
|
providerModel("alpha", "gpt-5.6-sol"),
|
||||||
|
providerModel("alpha", "gpt-6-astra"),
|
||||||
|
];
|
||||||
|
const result = await host(base, { mode: "custom", agents });
|
||||||
|
expect(String(result.agents.get("fast-lo")?.model?.providerID)).toBe("beta");
|
||||||
|
expect(String(result.agents.get("fast-hi")?.model?.providerID)).toBe("alpha");
|
||||||
|
expect(String(result.agents.get("coordinate")?.model?.id)).toBe("gpt-5.6-sol");
|
||||||
|
expect(result.agents.has("deep-lo")).toBe(false);
|
||||||
|
|
||||||
|
await result.refresh([...base, providerModel("beta", "deep")]);
|
||||||
|
expect(result.agents.get("coordinate")?.model).toMatchObject({
|
||||||
|
providerID: "beta",
|
||||||
|
id: "deep",
|
||||||
|
});
|
||||||
|
expect(result.agents.has("deep-lo")).toBe(true);
|
||||||
|
|
||||||
|
const nested = customAssignments("alpha");
|
||||||
|
nested["fast-lo"] = { ...nested["fast-lo"], model: "alpha/group/shared" };
|
||||||
|
const nestedResult = await host(
|
||||||
|
[...base, providerModel("alpha", "gpt-5.6-luna"), providerModel("alpha", "group/shared")],
|
||||||
|
{ mode: "custom", agents: nested },
|
||||||
|
);
|
||||||
|
expect(String(nestedResult.agents.get("fast-lo")?.model?.id)).toBe("group/shared");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("custom configuration requires qualified models and rejects obsolete fields", () => {
|
||||||
|
expect(() => settings.parse({ mode: "custom", agents: customAssignments() })).not.toThrow();
|
||||||
|
for (const model of ["bare", "/model", "provider/"])
|
||||||
|
expect(() =>
|
||||||
|
settings.parse({
|
||||||
|
mode: "custom",
|
||||||
|
agents: {
|
||||||
|
...customAssignments(),
|
||||||
|
"fast-lo": { model, effort: "low" },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).toThrow();
|
||||||
|
const nested = customAssignments();
|
||||||
|
nested["fast-lo"] = { model: "provider/group/model", effort: "low" };
|
||||||
|
expect(() => settings.parse({ mode: "custom", agents: nested })).not.toThrow();
|
||||||
|
expect(() =>
|
||||||
|
settings.parse({ mode: "custom", provider: "test", agents: customAssignments() }),
|
||||||
|
).toThrow();
|
||||||
|
expect(() =>
|
||||||
|
settings.parse({ mode: "custom", lunaPriority: false, agents: customAssignments() }),
|
||||||
|
).toThrow();
|
||||||
|
expect(() =>
|
||||||
|
settings.parse({
|
||||||
|
mode: "custom",
|
||||||
|
agents: {
|
||||||
|
...customAssignments(),
|
||||||
|
"fast-lo": { ...customAssignments()["fast-lo"], provider: "test" },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).toThrow();
|
||||||
|
expect(() =>
|
||||||
|
settings.parse({
|
||||||
|
mode: "custom",
|
||||||
|
agents: {
|
||||||
|
...customAssignments(),
|
||||||
|
"fast-lo": { ...customAssignments()["fast-lo"], lunaPriority: true },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).toThrow();
|
||||||
|
expect(() =>
|
||||||
|
settings.parse({
|
||||||
|
mode: "custom",
|
||||||
|
agents: { ...customAssignments(), coordinate: customAssignments()["deep-lo"] },
|
||||||
|
}),
|
||||||
|
).not.toThrow();
|
||||||
|
for (const obsolete of [{ provider: "test" }, { lunaPriority: true }])
|
||||||
|
expect(() =>
|
||||||
|
settings.parse({
|
||||||
|
mode: "custom",
|
||||||
|
agents: {
|
||||||
|
...customAssignments(),
|
||||||
|
coordinate: {
|
||||||
|
...customAssignments()["deep-lo"],
|
||||||
|
...obsolete,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
test("worker inheritance and coordinator isolation preserve build and default", async () => {
|
test("worker inheritance and coordinator isolation preserve build and default", async () => {
|
||||||
const result = await host();
|
const result = await host();
|
||||||
expect(result.defaultAgent).toBe("build");
|
expect(result.defaultAgent).toBe("build");
|
||||||
@@ -689,33 +810,58 @@ test("coordinate prompt admission requires owned ready placement; local and move
|
|||||||
|
|
||||||
test("reasoning settings and Luna priority use native advertised or official support", async () => {
|
test("reasoning settings and Luna priority use native advertised or official support", async () => {
|
||||||
const luna = {
|
const luna = {
|
||||||
...model("gpt-5.6-luna"),
|
...providerModel("openai", "gpt-5.6-luna"),
|
||||||
package: "@opencode/ai/providers/openai",
|
package: "@opencode/ai/providers/openai",
|
||||||
settings: { baseURL: "https://api.openai.com/v1" },
|
settings: { baseURL: "https://api.openai.com/v1" },
|
||||||
};
|
};
|
||||||
const ordinary = await host();
|
const openaiModels = [
|
||||||
|
luna,
|
||||||
|
providerModel("openai", "gpt-5.6-sol"),
|
||||||
|
providerModel("openai", "gpt-6-astra"),
|
||||||
|
];
|
||||||
|
const ordinary = await host(
|
||||||
|
[providerModel("openai", "gpt-5.6-luna"), ...openaiModels.slice(1)],
|
||||||
|
{ provider: "openai" },
|
||||||
|
);
|
||||||
expect(ordinary.agents.get("fast-lo")?.request.settings).toEqual({});
|
expect(ordinary.agents.get("fast-lo")?.request.settings).toEqual({});
|
||||||
expect(String(ordinary.agents.get("fast-lo")?.model?.variant)).toBe("low");
|
expect(String(ordinary.agents.get("fast-lo")?.model?.variant)).toBe("low");
|
||||||
const official = await host([luna, ...allModels.slice(1)]);
|
const official = await host(openaiModels, { provider: "openai" });
|
||||||
expect(official.agents.get("fast-hi")?.request.settings).toEqual({
|
expect(official.agents.get("fast-hi")?.request.settings).toEqual({ serviceTier: "priority" });
|
||||||
reasoningEffort: "high",
|
const proxy = await host(
|
||||||
serviceTier: "priority",
|
[{ ...luna, settings: { baseURL: "https://proxy.example/v1" } }, ...openaiModels.slice(1)],
|
||||||
});
|
{ provider: "openai" },
|
||||||
const proxy = await host([
|
);
|
||||||
{ ...luna, settings: { baseURL: "https://proxy.example/v1" } },
|
expect(proxy.agents.get("fast-lo")?.request.settings).toEqual({});
|
||||||
...allModels.slice(1),
|
const advertised = await host(
|
||||||
]);
|
[
|
||||||
expect(proxy.agents.get("fast-lo")?.request.settings).toEqual({ reasoningEffort: "low" });
|
{ ...luna, package: "custom", settings: { serviceTier: "fast" } },
|
||||||
const advertised = await host([
|
...openaiModels.slice(1),
|
||||||
{ ...luna, package: "custom", settings: { serviceTier: "fast" } },
|
],
|
||||||
...allModels.slice(1),
|
{ provider: "openai" },
|
||||||
]);
|
);
|
||||||
expect(advertised.agents.get("fast-lo")?.request.settings.serviceTier).toBe("fast");
|
expect(advertised.agents.get("fast-lo")?.request.settings.serviceTier).toBe("fast");
|
||||||
const disabled = await host([luna, ...allModels.slice(1)], {
|
const sdkAlias = await host(
|
||||||
provider: "test",
|
[{ ...luna, package: "@ai-sdk/openai" }, ...openaiModels.slice(1)],
|
||||||
|
{ provider: "openai" },
|
||||||
|
);
|
||||||
|
expect(sdkAlias.agents.get("fast-lo")?.request.settings.serviceTier).toBe("priority");
|
||||||
|
const disabled = await host(openaiModels, {
|
||||||
|
provider: "openai",
|
||||||
lunaPriority: false,
|
lunaPriority: false,
|
||||||
});
|
});
|
||||||
expect(disabled.agents.get("fast-lo")?.request.settings).toEqual({ reasoningEffort: "low" });
|
expect(disabled.agents.get("fast-lo")?.request.settings).toEqual({});
|
||||||
|
|
||||||
|
const custom = customAssignments("openai");
|
||||||
|
const customResult = await host(openaiModels, {
|
||||||
|
mode: "custom",
|
||||||
|
agents: custom,
|
||||||
|
});
|
||||||
|
expect(customResult.agents.get("fast-lo")?.request.settings.serviceTier).toBeUndefined();
|
||||||
|
expect(customResult.agents.get("fast-hi")?.request.settings.serviceTier).toBeUndefined();
|
||||||
|
|
||||||
|
const other = await host(allModels, { provider: "test" });
|
||||||
|
expect(other.agents.get("fast-lo")?.request.settings.serviceTier).toBeUndefined();
|
||||||
|
expect(() => settings.parse({ mode: "gpt", provider: "test", lunaPriority: true })).toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("coordinator uses inherited effort and ignores the legacy custom assignment", async () => {
|
test("coordinator uses inherited effort and ignores the legacy custom assignment", async () => {
|
||||||
@@ -737,8 +883,10 @@ test("coordinator uses inherited effort and ignores the legacy custom assignment
|
|||||||
|
|
||||||
const custom = await host([...allModels, model("local")], {
|
const custom = await host([...allModels, model("local")], {
|
||||||
mode: "custom",
|
mode: "custom",
|
||||||
provider: "test",
|
agents: {
|
||||||
agents: { ...gptAgents, coordinate: { model: "local", effort: "xhigh" } },
|
...customAssignments(),
|
||||||
|
coordinate: { model: "test/local", effort: "xhigh" },
|
||||||
|
},
|
||||||
});
|
});
|
||||||
expect(custom.agents.get("coordinate")?.model).toMatchObject({
|
expect(custom.agents.get("coordinate")?.model).toMatchObject({
|
||||||
id: "gpt-6-astra",
|
id: "gpt-6-astra",
|
||||||
|
|||||||
Reference in New Issue
Block a user