Compare commits
4
Commits
v2.4.0
..
0b6f39b6d9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0b6f39b6d9 | ||
|
|
1fdc558ff7 | ||
|
|
5175a93c47 | ||
|
|
7da94c9223 |
@@ -41,7 +41,7 @@ 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. 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.
|
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 and use `provider/model` model names.
|
Custom configurations require the four `fast-*` and `good-*` assignments; `deep-lo` and `deep-hi` are optional. Assignments use `provider/model` model names and require both `model` and `effort` when supplied. If either deep assignment is omitted, both deep workers are disabled and orchestration uses `good-lo`.
|
||||||
The legacy optional `coordinate` assignment is accepted for compatibility but ignored. Orchestration
|
The legacy optional `coordinate` assignment is accepted for compatibility but ignored. Orchestration
|
||||||
inherits `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.
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -3,7 +3,7 @@
|
|||||||
`multi-work` is a **primary-only** agent for coordinating independent work in
|
`multi-work` is a **primary-only** agent for coordinating independent work in
|
||||||
the current Herdr root checkout or new Herdr-managed Git worktrees. Like
|
the current Herdr root checkout or new Herdr-managed Git worktrees. Like
|
||||||
`coordinate`, it uses the configured `deep-lo` model and effort, falling back
|
`coordinate`, it uses the configured `deep-lo` model and effort, falling back
|
||||||
to `good-lo` when `deep-lo` is unavailable. No extra custom model assignment is
|
to `good-lo` when `deep-lo` is unavailable or either custom deep assignment is omitted. No extra custom model assignment is
|
||||||
required.
|
required.
|
||||||
|
|
||||||
```text
|
```text
|
||||||
|
|||||||
@@ -3,8 +3,9 @@
|
|||||||
// 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. Models use provider/model names.
|
// Fast and good assignments are required; both deep assignments are optional.
|
||||||
// An unavailable deep-lo omits deep workers and uses good-lo for orchestration.
|
// Omitting either deep assignment disables both deep workers and uses good-lo.
|
||||||
|
// Models use provider/model names; each assignment needs model and effort.
|
||||||
"plugins": [
|
"plugins": [
|
||||||
{
|
{
|
||||||
"package": "stanp-opencode",
|
"package": "stanp-opencode",
|
||||||
|
|||||||
+4
-4
@@ -57,8 +57,8 @@ export const settings = z.discriminatedUnion("mode", [
|
|||||||
"fast-hi": customAssignment,
|
"fast-hi": customAssignment,
|
||||||
"good-lo": customAssignment,
|
"good-lo": customAssignment,
|
||||||
"good-hi": customAssignment,
|
"good-hi": customAssignment,
|
||||||
"deep-lo": customAssignment,
|
"deep-lo": customAssignment.optional(),
|
||||||
"deep-hi": customAssignment,
|
"deep-hi": customAssignment.optional(),
|
||||||
// Deprecated compatibility field. Coordinate is derived from deep-lo/good-lo.
|
// Deprecated compatibility field. Coordinate is derived from deep-lo/good-lo.
|
||||||
coordinate: customAssignment.optional(),
|
coordinate: customAssignment.optional(),
|
||||||
}),
|
}),
|
||||||
@@ -68,8 +68,8 @@ export const settings = z.discriminatedUnion("mode", [
|
|||||||
export const gptAgents = {
|
export const gptAgents = {
|
||||||
"fast-lo": { model: "gpt-6-luna", effort: "low" },
|
"fast-lo": { model: "gpt-6-luna", effort: "low" },
|
||||||
"fast-hi": { model: "gpt-6-luna", effort: "high" },
|
"fast-hi": { model: "gpt-6-luna", effort: "high" },
|
||||||
"good-lo": { model: "gpt-6-sol", effort: "low" },
|
"good-lo": { model: "gpt-6.1-sol", effort: "low" },
|
||||||
"good-hi": { model: "gpt-6-sol", effort: "high" },
|
"good-hi": { model: "gpt-6.1-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 gptAssignment>>;
|
} satisfies Record<(typeof workerNames)[number], z.infer<typeof gptAssignment>>;
|
||||||
|
|||||||
+10
-7
@@ -204,17 +204,20 @@ const makeBasePlugin = (permissionsChanged: (permissions: Agent.Info["permission
|
|||||||
model: assignment.model.slice(slash + 1),
|
model: assignment.model.slice(slash + 1),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
const availableFor = (
|
const availableFor = (assignment: { model: string }) => {
|
||||||
assignment: (typeof assignments)[(typeof workerNames)[number]],
|
|
||||||
) => {
|
|
||||||
const { provider, model } = coordinates(assignment);
|
const { provider, model } = coordinates(assignment);
|
||||||
return models.get(modelKey(provider, model));
|
return models.get(modelKey(provider, model));
|
||||||
};
|
};
|
||||||
const deep = availableFor(assignments["deep-lo"]) !== undefined;
|
const deepLo = assignments["deep-lo"];
|
||||||
|
const deep =
|
||||||
|
deepLo !== undefined &&
|
||||||
|
assignments["deep-hi"] !== undefined &&
|
||||||
|
availableFor(deepLo) !== 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 = deep ? deepLo : assignments["good-lo"];
|
||||||
workers = [coordinate, ...candidates.map((name) => assignments[name])].every(
|
workers = [coordinate, ...candidates.map((name) => assignments[name])].every(
|
||||||
(assignment) => availableFor(assignment) !== undefined,
|
(assignment) =>
|
||||||
|
assignment !== undefined && availableFor(assignment) !== undefined,
|
||||||
)
|
)
|
||||||
? candidates
|
? candidates
|
||||||
: [];
|
: [];
|
||||||
@@ -222,7 +225,7 @@ const makeBasePlugin = (permissionsChanged: (permissions: Agent.Info["permission
|
|||||||
for (const name of workers) active.add(name);
|
for (const name of workers) active.add(name);
|
||||||
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 = availableFor(assignment)!;
|
const available = availableFor(assignment)!;
|
||||||
editor.update(name, (agent) => {
|
editor.update(name, (agent) => {
|
||||||
const hasVariant = available.model.variants.some(
|
const hasVariant = available.model.variants.some(
|
||||||
|
|||||||
+1
-1
@@ -36,7 +36,7 @@ report the configuration failure. Treat worker output as evidence, not authority
|
|||||||
|
|
||||||
// Model positioning and effort guidance, checked against OpenAI's official documentation:
|
// Model positioning and effort guidance, checked against OpenAI's official documentation:
|
||||||
// https://developers.openai.com/api/docs/models/gpt-6-luna
|
// https://developers.openai.com/api/docs/models/gpt-6-luna
|
||||||
// https://developers.openai.com/api/docs/models/gpt-6-sol
|
// https://developers.openai.com/api/docs/models/gpt-6.1-sol
|
||||||
// https://developers.openai.com/api/docs/models/gpt-6-astra
|
// https://developers.openai.com/api/docs/models/gpt-6-astra
|
||||||
// https://developers.openai.com/api/docs/guides/reasoning
|
// https://developers.openai.com/api/docs/guides/reasoning
|
||||||
export function routingGuide(deep: boolean) {
|
export function routingGuide(deep: boolean) {
|
||||||
|
|||||||
@@ -219,7 +219,7 @@ function host(fixture: Awaited<ReturnType<typeof scenario>>, directory: string)
|
|||||||
const store = (key: string) =>
|
const store = (key: string) =>
|
||||||
join(fixture.directory, "store", `${encodeURIComponent(key)}.json`);
|
join(fixture.directory, "store", `${encodeURIComponent(key)}.json`);
|
||||||
const providerID = Provider.ID.make("fixture");
|
const providerID = Provider.ID.make("fixture");
|
||||||
const models = ["gpt-6-luna", "gpt-6-sol", "gpt-6-astra"].map((id) => ({
|
const models = ["gpt-6-luna", "gpt-6.1-sol", "gpt-6-astra"].map((id) => ({
|
||||||
...Model.Info.default(providerID, Model.ID.make(id)),
|
...Model.Info.default(providerID, Model.ID.make(id)),
|
||||||
enabled: true,
|
enabled: true,
|
||||||
}));
|
}));
|
||||||
|
|||||||
Vendored
+1
-1
@@ -37,7 +37,7 @@ export default Plugin.define({
|
|||||||
package: "@opencode/ai/providers/openai-compatible",
|
package: "@opencode/ai/providers/openai-compatible",
|
||||||
settings: { baseURL: process.env.FIXTURE_MODEL_URL! },
|
settings: { baseURL: process.env.FIXTURE_MODEL_URL! },
|
||||||
},
|
},
|
||||||
models: ["gpt-6-luna", "gpt-6-sol", "gpt-6-astra"].map((id) =>
|
models: ["gpt-6-luna", "gpt-6.1-sol", "gpt-6-astra"].map((id) =>
|
||||||
Model.Info.default(providerID, Model.ID.make(id)),
|
Model.Info.default(providerID, Model.ID.make(id)),
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
|
|||||||
+53
-8
@@ -35,7 +35,7 @@ function providerModel(provider: string, id: string): ModelInfo {
|
|||||||
providerID: Provider.ID.make(provider),
|
providerID: Provider.ID.make(provider),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const allModels = ["gpt-6-luna", "gpt-6-sol", "gpt-6-astra"].map(reasoningModel);
|
const allModels = ["gpt-6-luna", "gpt-6.1-sol", "gpt-6-astra"].map(reasoningModel);
|
||||||
type CustomAssignment = {
|
type CustomAssignment = {
|
||||||
model: string;
|
model: string;
|
||||||
effort: "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
|
effort: "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
|
||||||
@@ -387,12 +387,20 @@ async function host(
|
|||||||
|
|
||||||
test("orchestration inherits deep-lo and falls back to good-lo with available worker sets", async () => {
|
test("orchestration inherits deep-lo and falls back to good-lo with available worker sets", async () => {
|
||||||
const full = await host();
|
const full = await host();
|
||||||
|
expect(full.agents.get("good-lo")?.model).toMatchObject({
|
||||||
|
id: "gpt-6.1-sol",
|
||||||
|
variant: "low",
|
||||||
|
});
|
||||||
|
expect(full.agents.get("good-hi")?.model).toMatchObject({
|
||||||
|
id: "gpt-6.1-sol",
|
||||||
|
variant: "high",
|
||||||
|
});
|
||||||
expect(full.agents.has("deep-hi")).toBe(true);
|
expect(full.agents.has("deep-hi")).toBe(true);
|
||||||
expect(String(full.agents.get("coordinate")?.model?.id)).toBe("gpt-6-astra");
|
expect(String(full.agents.get("coordinate")?.model?.id)).toBe("gpt-6-astra");
|
||||||
expect(String(full.agents.get("coordinate")?.model?.variant)).toBe("low");
|
expect(String(full.agents.get("coordinate")?.model?.variant)).toBe("low");
|
||||||
const reduced = await host(allModels.slice(0, 2));
|
const reduced = await host(allModels.slice(0, 2));
|
||||||
expect(reduced.agents.has("deep-hi")).toBe(false);
|
expect(reduced.agents.has("deep-hi")).toBe(false);
|
||||||
expect(String(reduced.agents.get("coordinate")?.model?.id)).toBe("gpt-6-sol");
|
expect(String(reduced.agents.get("coordinate")?.model?.id)).toBe("gpt-6.1-sol");
|
||||||
expect(String(reduced.agents.get("coordinate")?.model?.variant)).toBe("low");
|
expect(String(reduced.agents.get("coordinate")?.model?.variant)).toBe("low");
|
||||||
expect(reduced.agents.get("coordinate")?.system).toContain("Deep agents are unavailable");
|
expect(reduced.agents.get("coordinate")?.system).toContain("Deep agents are unavailable");
|
||||||
expect((await host([])).agents.has("coordinate")).toBe(false);
|
expect((await host([])).agents.has("coordinate")).toBe(false);
|
||||||
@@ -439,6 +447,26 @@ test("orchestration inherits deep-lo and falls back to good-lo with available wo
|
|||||||
).toBe(false);
|
).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("custom deep assignments are optional and a partial deep tier falls back to good-lo", async () => {
|
||||||
|
for (const omitted of [["deep-lo", "deep-hi"], ["deep-lo"], ["deep-hi"]] as const) {
|
||||||
|
const agents: Partial<ReturnType<typeof customAssignments>> = customAssignments();
|
||||||
|
for (const name of omitted) delete agents[name];
|
||||||
|
const result = await host(allModels, { mode: "custom", agents });
|
||||||
|
expect(result.agents.get("coordinate")?.model).toMatchObject({
|
||||||
|
id: "gpt-6.1-sol",
|
||||||
|
variant: "low",
|
||||||
|
});
|
||||||
|
expect(result.agents.get("coordinate")?.system).toContain("Deep agents are unavailable");
|
||||||
|
for (const name of ["fast-lo", "fast-hi", "good-lo", "good-hi"])
|
||||||
|
expect(result.agents.has(name)).toBe(true);
|
||||||
|
expect(result.agents.has("deep-lo")).toBe(false);
|
||||||
|
expect(result.agents.has("deep-hi")).toBe(false);
|
||||||
|
expect((await result.tools("coordinate")).subagent?.input.properties).toMatchObject({
|
||||||
|
agent: { type: "string", enum: ["fast-lo", "fast-hi", "good-lo", "good-hi"] },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test("registers the embedded anti-overengineering skill and makes it available to coordinate", async () => {
|
test("registers the embedded anti-overengineering skill and makes it available to coordinate", async () => {
|
||||||
const result = await host();
|
const result = await host();
|
||||||
const skill = result.skills.get("anti-overengineering")!;
|
const skill = result.skills.get("anti-overengineering")!;
|
||||||
@@ -472,13 +500,13 @@ test("custom assignments resolve duplicate model IDs by provider and refresh pro
|
|||||||
const base = [
|
const base = [
|
||||||
providerModel("alpha", "shared"),
|
providerModel("alpha", "shared"),
|
||||||
providerModel("beta", "shared"),
|
providerModel("beta", "shared"),
|
||||||
providerModel("alpha", "gpt-6-sol"),
|
providerModel("alpha", "gpt-6.1-sol"),
|
||||||
providerModel("alpha", "gpt-6-astra"),
|
providerModel("alpha", "gpt-6-astra"),
|
||||||
];
|
];
|
||||||
const result = await host(base, { mode: "custom", agents });
|
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-lo")?.model?.providerID)).toBe("beta");
|
||||||
expect(String(result.agents.get("fast-hi")?.model?.providerID)).toBe("alpha");
|
expect(String(result.agents.get("fast-hi")?.model?.providerID)).toBe("alpha");
|
||||||
expect(String(result.agents.get("coordinate")?.model?.id)).toBe("gpt-6-sol");
|
expect(String(result.agents.get("coordinate")?.model?.id)).toBe("gpt-6.1-sol");
|
||||||
expect(result.agents.has("deep-lo")).toBe(false);
|
expect(result.agents.has("deep-lo")).toBe(false);
|
||||||
|
|
||||||
await result.refresh([...base, providerModel("beta", "deep")]);
|
await result.refresh([...base, providerModel("beta", "deep")]);
|
||||||
@@ -499,6 +527,23 @@ test("custom assignments resolve duplicate model IDs by provider and refresh pro
|
|||||||
|
|
||||||
test("custom configuration requires qualified models and rejects obsolete fields", () => {
|
test("custom configuration requires qualified models and rejects obsolete fields", () => {
|
||||||
expect(() => settings.parse({ mode: "custom", agents: customAssignments() })).not.toThrow();
|
expect(() => settings.parse({ mode: "custom", agents: customAssignments() })).not.toThrow();
|
||||||
|
for (const omitted of [["deep-lo", "deep-hi"], ["deep-lo"], ["deep-hi"]] as const) {
|
||||||
|
const agents: Partial<ReturnType<typeof customAssignments>> = customAssignments();
|
||||||
|
for (const name of omitted) delete agents[name];
|
||||||
|
expect(() => settings.parse({ mode: "custom", agents })).not.toThrow();
|
||||||
|
}
|
||||||
|
for (const name of ["fast-lo", "fast-hi", "good-lo", "good-hi"] as const) {
|
||||||
|
const agents: Partial<ReturnType<typeof customAssignments>> = customAssignments();
|
||||||
|
delete agents[name];
|
||||||
|
expect(() => settings.parse({ mode: "custom", agents })).toThrow();
|
||||||
|
}
|
||||||
|
for (const name of ["deep-lo", "deep-hi"] as const)
|
||||||
|
expect(() =>
|
||||||
|
settings.parse({
|
||||||
|
mode: "custom",
|
||||||
|
agents: { ...customAssignments(), [name]: { model: "test/model" } },
|
||||||
|
}),
|
||||||
|
).toThrow();
|
||||||
for (const model of ["bare", "/model", "provider/"])
|
for (const model of ["bare", "/model", "provider/"])
|
||||||
expect(() =>
|
expect(() =>
|
||||||
settings.parse({
|
settings.parse({
|
||||||
@@ -863,7 +908,7 @@ test("reasoning settings and Luna priority use native advertised or official sup
|
|||||||
};
|
};
|
||||||
const openaiModels = [
|
const openaiModels = [
|
||||||
luna,
|
luna,
|
||||||
providerModel("openai", "gpt-6-sol"),
|
providerModel("openai", "gpt-6.1-sol"),
|
||||||
providerModel("openai", "gpt-6-astra"),
|
providerModel("openai", "gpt-6-astra"),
|
||||||
];
|
];
|
||||||
const ordinary = await host([providerModel("openai", "gpt-6-luna"), ...openaiModels.slice(1)], {
|
const ordinary = await host([providerModel("openai", "gpt-6-luna"), ...openaiModels.slice(1)], {
|
||||||
@@ -922,7 +967,7 @@ test("coordinator uses inherited effort and ignores the legacy custom assignment
|
|||||||
|
|
||||||
const fallback = await host(allModels.slice(0, 2));
|
const fallback = await host(allModels.slice(0, 2));
|
||||||
expect(fallback.agents.get("coordinate")?.model).toMatchObject({
|
expect(fallback.agents.get("coordinate")?.model).toMatchObject({
|
||||||
id: "gpt-6-sol",
|
id: "gpt-6.1-sol",
|
||||||
variant: "low",
|
variant: "low",
|
||||||
});
|
});
|
||||||
expect(fallback.agents.get("coordinate")?.request.settings.reasoningEffort).toBeUndefined();
|
expect(fallback.agents.get("coordinate")?.request.settings.reasoningEffort).toBeUndefined();
|
||||||
@@ -943,7 +988,7 @@ test("coordinator uses inherited effort and ignores the legacy custom assignment
|
|||||||
|
|
||||||
test("the current host model API filters discovery and reloads changed availability", async () => {
|
test("the current host model API filters discovery and reloads changed availability", async () => {
|
||||||
const disabled = { ...reasoningModel("gpt-6-luna"), enabled: false };
|
const disabled = { ...reasoningModel("gpt-6-luna"), enabled: false };
|
||||||
const other = { ...reasoningModel("gpt-6-sol"), providerID: Provider.ID.make("other") };
|
const other = { ...reasoningModel("gpt-6.1-sol"), providerID: Provider.ID.make("other") };
|
||||||
expect((await host([disabled, other, ...allModels.slice(1)])).agents.has("coordinate")).toBe(
|
expect((await host([disabled, other, ...allModels.slice(1)])).agents.has("coordinate")).toBe(
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
@@ -954,7 +999,7 @@ test("the current host model API filters discovery and reloads changed availabil
|
|||||||
expect(result.agents.get("coordinate")?.request.settings.reasoningEffort).toBeUndefined();
|
expect(result.agents.get("coordinate")?.request.settings.reasoningEffort).toBeUndefined();
|
||||||
await result.refresh(allModels.slice(0, 2));
|
await result.refresh(allModels.slice(0, 2));
|
||||||
expect(result.agents.has("deep-lo")).toBe(false);
|
expect(result.agents.has("deep-lo")).toBe(false);
|
||||||
expect(String(result.agents.get("coordinate")?.model?.id)).toBe("gpt-6-sol");
|
expect(String(result.agents.get("coordinate")?.model?.id)).toBe("gpt-6.1-sol");
|
||||||
expect(String(result.agents.get("coordinate")?.model?.variant)).toBe("low");
|
expect(String(result.agents.get("coordinate")?.model?.variant)).toBe("low");
|
||||||
expect(result.agents.get("coordinate")?.request.settings.reasoningEffort).toBeUndefined();
|
expect(result.agents.get("coordinate")?.request.settings.reasoningEffort).toBeUndefined();
|
||||||
await result.refresh([]);
|
await result.refresh([]);
|
||||||
|
|||||||
Reference in New Issue
Block a user