fix: fix configurations #23
@@ -12,7 +12,7 @@ Choose any plugin subset; Multi-work requires Agents first.
|
|||||||
|
|
||||||
Quota shows ChatGPT limits for the connected OpenAI account and, when Claude Code is logged in, Claude subscription limits read from `${CLAUDE_CONFIG_DIR:-~/.claude}/.credentials.json` (read-only; Claude Code refreshes the token).
|
Quota shows ChatGPT limits for the connected OpenAI account and, when Claude Code is logged in, Claude subscription limits read from `${CLAUDE_CONFIG_DIR:-~/.claude}/.credentials.json` (read-only; Claude Code refreshes the token).
|
||||||
|
|
||||||
Optchat implements [VictorTaelin's optchat recipe](https://gist.github.com/VictorTaelin/91837951a5ce5b38f341ec1ba1df6449). It is off by default; run `/optchat on`, `/optchat off`, or `/optchat status` in a root session. In an enabled session, each turn sees a constant-size view of the whole chat, made of one-line summaries, plus the new message. The model can open any line with the `zoom` tool and read message times with `date`. Optchat does not apply to subagent sessions. Memory is stored in `~/.local/share/opencode/optchat/<sessionID>/`. Compactions run in a child session named "optchat compactions". The plugin accepts two options: `compactionModel` (default `opencode/claude-haiku-5-5#xhigh`) and `dataDir`.
|
Optchat implements [VictorTaelin's optchat recipe](https://gist.github.com/VictorTaelin/91837951a5ce5b38f341ec1ba1df6449). It is off by default; run `/optchat on`, `/optchat off`, or `/optchat status` in a root session. In an enabled session, each turn sees a constant-size view of the whole chat, made of one-line summaries, plus the new message. The model can open any line with the `zoom` tool and read message times with `date`. Optchat does not apply to subagent sessions. Memory is stored in `~/.local/share/opencode/optchat/<sessionID>/`. Compactions run in a child session named "optchat compactions". Optchat activates only when the `compactionModel` option is set, in the format `provider/model[#variant]` (for example `opencode/claude-haiku-5-5#xhigh`); without it (or when empty) the plugin registers nothing. A non-empty `dataDir` overrides the memory location. The installer writes both options as empty strings for you to fill in.
|
||||||
|
|
||||||
## Local installation
|
## Local installation
|
||||||
|
|
||||||
@@ -21,12 +21,11 @@ From a local checkout, run:
|
|||||||
```sh
|
```sh
|
||||||
bash /path/to/stanp-opencode/install-local.sh --plugins agents,multi-work,quota
|
bash /path/to/stanp-opencode/install-local.sh --plugins agents,multi-work,quota
|
||||||
opencode service restart
|
opencode service restart
|
||||||
# Connect OpenAI and select coordinate in OpenCode.
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## GPT configuration
|
## Configuration
|
||||||
|
|
||||||
Merge this into `~/.config/opencode/opencode.json`, preserving unrelated settings and plugins; update existing plugin entries instead of appending duplicates.
|
Agents registers nothing until its `agents` option is set. The installer writes every missing option with an empty value (`""`) for you to fill in; an empty value means unset, so a fresh install stays inert. Each assignment is `provider/model[#variant]`; the variant selects the model's reasoning variant, falling back to a `reasoningEffort` request setting when the model does not advertise it. Merge this into `~/.config/opencode/opencode.json`, preserving unrelated settings and plugins; update existing plugin entries instead of appending duplicates. The `deep-lo` and `deep-hi` assignments are optional as a pair; if either is omitted, both deep workers are disabled and orchestration uses `good-lo`.
|
||||||
|
|
||||||
```jsonc
|
```jsonc
|
||||||
{
|
{
|
||||||
@@ -37,39 +36,13 @@ Merge this into `~/.config/opencode/opencode.json`, preserving unrelated setting
|
|||||||
{
|
{
|
||||||
"package": "./plugins/stanp-opencode/agents",
|
"package": "./plugins/stanp-opencode/agents",
|
||||||
"options": {
|
"options": {
|
||||||
"mode": "gpt",
|
|
||||||
"provider": "openai",
|
|
||||||
// Priority may cost more; set false to opt out.
|
|
||||||
"lunaPriority": true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ "package": "./plugins/stanp-opencode/multi-work" },
|
|
||||||
{ "package": "./plugins/stanp-opencode/quota" },
|
|
||||||
],
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Custom model configuration
|
|
||||||
|
|
||||||
Merge this into `~/.config/opencode/opencode.json`, preserving unrelated settings and plugins; update existing plugin entries instead of appending duplicates. The `deep-lo` and `deep-hi` assignments are optional as a pair; if either is omitted, both deep workers are disabled and orchestration uses `good-lo`.
|
|
||||||
|
|
||||||
```jsonc
|
|
||||||
{
|
|
||||||
"$schema": "https://opencode.ai/config.json",
|
|
||||||
// Required at the root and in worktrees.
|
|
||||||
"experimental": { "subagent_depth": 2 },
|
|
||||||
"plugins": [
|
|
||||||
{
|
|
||||||
"package": "./plugins/stanp-opencode/agents",
|
|
||||||
"options": {
|
|
||||||
"mode": "custom",
|
|
||||||
"agents": {
|
"agents": {
|
||||||
"fast-lo": { "model": "openai/gpt-6-luna", "effort": "low" },
|
"fast-lo": "openai/gpt-6-luna#low",
|
||||||
"fast-hi": { "model": "openai/gpt-6-luna", "effort": "high" },
|
"fast-hi": "openai/gpt-6-luna#high",
|
||||||
"good-lo": { "model": "openai/gpt-6.1-sol", "effort": "low" },
|
"good-lo": "openai/gpt-6.1-sol#low",
|
||||||
"good-hi": { "model": "openai/gpt-6.1-sol", "effort": "high" },
|
"good-hi": "openai/gpt-6.1-sol#high",
|
||||||
"deep-lo": { "model": "openai/gpt-6-astra", "effort": "low" },
|
"deep-lo": "openai/gpt-6-astra#low",
|
||||||
"deep-hi": { "model": "openai/gpt-6-astra", "effort": "high" },
|
"deep-hi": "openai/gpt-6-astra#high",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
+30
-2
@@ -9,6 +9,35 @@ const entrySchema = z
|
|||||||
.object({ package: z.string(), options: z.record(z.string(), z.json()).optional() })
|
.object({ package: z.string(), options: z.record(z.string(), z.json()).optional() })
|
||||||
.catchall(z.json());
|
.catchall(z.json());
|
||||||
type Entry = z.infer<typeof entrySchema>;
|
type Entry = z.infer<typeof entrySchema>;
|
||||||
|
type Options = NonNullable<Entry["options"]>;
|
||||||
|
|
||||||
|
// Every option the plugins read, empty so users can fill them in; plugins treat "" as unset.
|
||||||
|
function skeleton(name: (typeof names)[number]): Options {
|
||||||
|
if (name === "agents")
|
||||||
|
return {
|
||||||
|
agents: Object.fromEntries(
|
||||||
|
["fast-lo", "fast-hi", "good-lo", "good-hi", "deep-lo", "deep-hi"].map((worker) => [
|
||||||
|
worker,
|
||||||
|
"",
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
if (name === "optchat") return { compactionModel: "", dataDir: "" };
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
const nested = z.record(z.string(), z.json());
|
||||||
|
/** Adds missing keys from `defaults` without overwriting existing values. */
|
||||||
|
function fill(target: Options, defaults: Options) {
|
||||||
|
for (const [key, value] of Object.entries(defaults)) {
|
||||||
|
const existing = nested.safeParse(target[key]);
|
||||||
|
const missing = nested.safeParse(value);
|
||||||
|
if (target[key] === undefined) target[key] = structuredClone(value);
|
||||||
|
else if (existing.success && missing.success) {
|
||||||
|
fill(existing.data, missing.data);
|
||||||
|
target[key] = existing.data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
const pluginSchema = z.union([z.string(), entrySchema]);
|
const pluginSchema = z.union([z.string(), entrySchema]);
|
||||||
const registrationSchema = z
|
const registrationSchema = z
|
||||||
.union([z.string().transform((packageName) => ({ package: packageName })), entrySchema])
|
.union([z.string().transform((packageName) => ({ package: packageName })), entrySchema])
|
||||||
@@ -105,10 +134,9 @@ export async function install(source: string, config: string, selection?: string
|
|||||||
(name === "agents" && monolith(entry.package)),
|
(name === "agents" && monolith(entry.package)),
|
||||||
);
|
);
|
||||||
const options: NonNullable<Entry["options"]> = {};
|
const options: NonNullable<Entry["options"]> = {};
|
||||||
if (name === "agents" && !prior?.options)
|
|
||||||
Object.assign(options, { mode: "gpt", provider: "openai", lunaPriority: true });
|
|
||||||
Object.assign(options, prior?.options);
|
Object.assign(options, prior?.options);
|
||||||
delete options.playwright;
|
delete options.playwright;
|
||||||
|
fill(options, skeleton(name));
|
||||||
const entry = entrySchema.parse({ ...prior, package: `${base}/${name}` });
|
const entry = entrySchema.parse({ ...prior, package: `${base}/${name}` });
|
||||||
delete entry.options;
|
delete entry.options;
|
||||||
if (Object.keys(options).length) entry.options = options;
|
if (Object.keys(options).length) entry.options = options;
|
||||||
|
|||||||
+37
-53
@@ -1,7 +1,8 @@
|
|||||||
import { Agent, Model, Plugin, Provider } from "@opencode/plugin";
|
import { Agent, Model, Plugin, Provider } from "@opencode/plugin";
|
||||||
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 { settings, workerNames } from "./config.js";
|
||||||
|
import { model } from "./model.js";
|
||||||
import { discover, modelKey } from "./provider.js";
|
import { discover, modelKey } from "./provider.js";
|
||||||
import { descriptions, routingGuide } from "./routing.js";
|
import { descriptions, routingGuide } from "./routing.js";
|
||||||
import { Skill } from "@opencode/schema/skill";
|
import { Skill } from "@opencode/schema/skill";
|
||||||
@@ -37,12 +38,16 @@ const delegation = z.object({ agent: z.string() });
|
|||||||
export default Plugin.define({
|
export default Plugin.define({
|
||||||
id: "stanp-opencode.agents",
|
id: "stanp-opencode.agents",
|
||||||
async setup(context) {
|
async setup(context) {
|
||||||
const selected = settings.parse({ mode: "gpt", ...context.options });
|
const { agents } = settings.parse(context.options);
|
||||||
const lunaPriority =
|
if (!workerNames.some((name) => agents?.[name])) return;
|
||||||
selected.mode === "gpt" &&
|
const assignments: Partial<Record<(typeof workerNames)[number], ReturnType<typeof model>>> =
|
||||||
selected.provider === "openai" &&
|
{};
|
||||||
selected.lunaPriority === true;
|
for (const name of workerNames) {
|
||||||
let models = new Map<string, { model: ModelInfo; serviceTier: string | undefined }>();
|
const reference = agents?.[name];
|
||||||
|
if (reference) assignments[name] = model(reference, `agents ${name}`);
|
||||||
|
else if (!name.startsWith("deep-")) throw new Error(`agents ${name} is required`);
|
||||||
|
}
|
||||||
|
let models = new Map<string, ModelInfo>();
|
||||||
let inherited: Agent.Info["permissions"] = Agent.Info.default(
|
let inherited: Agent.Info["permissions"] = Agent.Info.default(
|
||||||
Agent.ID.make("build"),
|
Agent.ID.make("build"),
|
||||||
).permissions;
|
).permissions;
|
||||||
@@ -64,7 +69,7 @@ export default Plugin.define({
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (event.type === "model.updated") {
|
if (event.type === "model.updated") {
|
||||||
models = await discover(context, lunaPriority);
|
models = await discover(context);
|
||||||
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
|
||||||
@@ -84,25 +89,13 @@ export default Plugin.define({
|
|||||||
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, lunaPriority);
|
models = await discover(context);
|
||||||
const { data: initialAgents } = await context.agent.list();
|
const { data: initialAgents } = await context.agent.list();
|
||||||
inherited = initialAgents.find((agent) => agent.id === "build")?.permissions ?? inherited;
|
inherited = initialAgents.find((agent) => agent.id === "build")?.permissions ?? inherited;
|
||||||
permissionKey = JSON.stringify(inherited);
|
permissionKey = JSON.stringify(inherited);
|
||||||
const registration = await context.rpc.register(AgentsRpc, {
|
const registration = await context.rpc.register(AgentsRpc, {
|
||||||
permissions: async () => inherited.map((rule) => ({ ...rule })),
|
permissions: async () => inherited.map((rule) => ({ ...rule })),
|
||||||
});
|
});
|
||||||
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>();
|
||||||
|
|
||||||
@@ -136,19 +129,8 @@ export default Plugin.define({
|
|||||||
});
|
});
|
||||||
|
|
||||||
await context.agent.transform((editor) => {
|
await context.agent.transform((editor) => {
|
||||||
const coordinates = (assignment: { model: string }) => {
|
const availableFor = (assignment: { providerID: string; id: string }) =>
|
||||||
if (selected.mode === "gpt")
|
models.get(modelKey(assignment.providerID, assignment.id));
|
||||||
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: { model: string }) => {
|
|
||||||
const { provider, model } = coordinates(assignment);
|
|
||||||
return models.get(modelKey(provider, model));
|
|
||||||
};
|
|
||||||
const deepLo = assignments["deep-lo"];
|
const deepLo = assignments["deep-lo"];
|
||||||
const deep =
|
const deep =
|
||||||
deepLo !== undefined &&
|
deepLo !== undefined &&
|
||||||
@@ -163,30 +145,31 @@ export default Plugin.define({
|
|||||||
: [];
|
: [];
|
||||||
active.clear();
|
active.clear();
|
||||||
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 || coordinate === undefined) 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 =
|
||||||
({ id }) => id === assignment.effort,
|
assignment.variant !== undefined &&
|
||||||
);
|
available.variants.some(({ id }) => id === assignment.variant);
|
||||||
agent.name = Agent.Name.make(name);
|
agent.name = Agent.Name.make(name);
|
||||||
agent.mode = "subagent";
|
agent.mode = "subagent";
|
||||||
agent.hidden = false;
|
agent.hidden = false;
|
||||||
agent.description = descriptions[name];
|
agent.description = descriptions[name];
|
||||||
agent.model = {
|
agent.model = {
|
||||||
providerID: Provider.ID.make(available.model.providerID),
|
providerID: Provider.ID.make(available.providerID),
|
||||||
id: Model.ID.make(available.model.id),
|
id: Model.ID.make(available.id),
|
||||||
};
|
};
|
||||||
if (hasVariant) agent.model.variant = Model.VariantID.make(assignment.effort);
|
if (hasVariant) agent.model.variant = Model.VariantID.make(assignment.variant!);
|
||||||
agent.request = {
|
agent.request = {
|
||||||
settings: hasVariant ? {} : { reasoningEffort: assignment.effort },
|
settings:
|
||||||
|
hasVariant || assignment.variant === undefined
|
||||||
|
? {}
|
||||||
|
: { reasoningEffort: assignment.variant },
|
||||||
headers: {},
|
headers: {},
|
||||||
body: {},
|
body: {},
|
||||||
};
|
};
|
||||||
if (lunaPriority && available.serviceTier)
|
|
||||||
agent.request.settings.serviceTier = available.serviceTier;
|
|
||||||
agent.permissions = [
|
agent.permissions = [
|
||||||
...structuredClone(inherited),
|
...structuredClone(inherited),
|
||||||
{ action: "subagent", resource: "*", effect: "deny" },
|
{ action: "subagent", resource: "*", effect: "deny" },
|
||||||
@@ -195,26 +178,27 @@ export default Plugin.define({
|
|||||||
}
|
}
|
||||||
editor.update("coordinate", (agent) => {
|
editor.update("coordinate", (agent) => {
|
||||||
const available = availableFor(coordinate)!;
|
const available = availableFor(coordinate)!;
|
||||||
const hasVariant = available.model.variants.some(
|
const hasVariant =
|
||||||
({ id }) => id === coordinate.effort,
|
coordinate.variant !== undefined &&
|
||||||
);
|
available.variants.some(({ id }) => id === coordinate.variant);
|
||||||
agent.name = Agent.Name.make("coordinate");
|
agent.name = Agent.Name.make("coordinate");
|
||||||
agent.mode = "primary";
|
agent.mode = "primary";
|
||||||
agent.hidden = false;
|
agent.hidden = false;
|
||||||
agent.description =
|
agent.description =
|
||||||
"Read, plan, and coordinate specialist workers; never implement directly.";
|
"Read, plan, and coordinate specialist workers; never implement directly.";
|
||||||
agent.model = {
|
agent.model = {
|
||||||
providerID: Provider.ID.make(available.model.providerID),
|
providerID: Provider.ID.make(available.providerID),
|
||||||
id: Model.ID.make(available.model.id),
|
id: Model.ID.make(available.id),
|
||||||
};
|
};
|
||||||
if (hasVariant) agent.model.variant = Model.VariantID.make(coordinate.effort);
|
if (hasVariant) agent.model.variant = Model.VariantID.make(coordinate.variant!);
|
||||||
agent.request = {
|
agent.request = {
|
||||||
settings: hasVariant ? {} : { reasoningEffort: coordinate.effort },
|
settings:
|
||||||
|
hasVariant || coordinate.variant === undefined
|
||||||
|
? {}
|
||||||
|
: { reasoningEffort: coordinate.variant },
|
||||||
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" },
|
||||||
|
|||||||
+6
-57
@@ -9,61 +9,10 @@ export const workerNames = [
|
|||||||
"deep-hi",
|
"deep-hi",
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
const gptAssignment = z.strictObject({
|
// Each assignment is provider/model[#variant]; an empty string means unset. Without any
|
||||||
model: z.string().trim().min(1),
|
// assignment the plugin registers nothing.
|
||||||
effort: z.enum(["none", "minimal", "low", "medium", "high", "xhigh", "max"]),
|
export const settings = z.object({
|
||||||
|
agents: z
|
||||||
|
.strictObject(Object.fromEntries(workerNames.map((name) => [name, z.string().optional()])))
|
||||||
|
.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
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"),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const settings = z.discriminatedUnion("mode", [
|
|
||||||
z
|
|
||||||
.strictObject({
|
|
||||||
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({
|
|
||||||
mode: z.literal("custom"),
|
|
||||||
agents: z.strictObject({
|
|
||||||
"fast-lo": customAssignment,
|
|
||||||
"fast-hi": customAssignment,
|
|
||||||
"good-lo": customAssignment,
|
|
||||||
"good-hi": customAssignment,
|
|
||||||
"deep-lo": customAssignment.optional(),
|
|
||||||
"deep-hi": customAssignment.optional(),
|
|
||||||
// Deprecated compatibility field. Coordinate is derived from deep-lo/good-lo.
|
|
||||||
coordinate: customAssignment.optional(),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
export const gptAgents = {
|
|
||||||
"fast-lo": { model: "gpt-6-luna", effort: "low" },
|
|
||||||
"fast-hi": { model: "gpt-6-luna", effort: "high" },
|
|
||||||
"good-lo": { model: "gpt-6.1-sol", effort: "low" },
|
|
||||||
"good-hi": { model: "gpt-6.1-sol", effort: "high" },
|
|
||||||
"deep-lo": { model: "gpt-6-astra", effort: "low" },
|
|
||||||
"deep-hi": { model: "gpt-6-astra", effort: "high" },
|
|
||||||
} satisfies Record<(typeof workerNames)[number], z.infer<typeof gptAssignment>>;
|
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
/** Parses `provider/model[#variant]`; `name` prefixes the error for the offending option. */
|
||||||
|
export function model(reference: string, name: string) {
|
||||||
|
const [path = "", variant] = reference.split("#");
|
||||||
|
const slash = path.indexOf("/");
|
||||||
|
if (slash <= 0) throw new Error(`${name} must be provider/model[#variant]: ${reference}`);
|
||||||
|
const providerID = path.slice(0, slash);
|
||||||
|
const id = path.slice(slash + 1);
|
||||||
|
return variant ? { providerID, id, variant } : { providerID, id };
|
||||||
|
}
|
||||||
+15
-19
@@ -34,18 +34,23 @@ import {
|
|||||||
type Line,
|
type Line,
|
||||||
} from "./optchat-core.js";
|
} from "./optchat-core.js";
|
||||||
import { Memory } from "./optchat-memory.js";
|
import { Memory } from "./optchat-memory.js";
|
||||||
|
import { model } from "./model.js";
|
||||||
|
|
||||||
const settings = z.object({
|
const settings = z.object({
|
||||||
/** provider/model#variant used for compactions. */
|
/** provider/model[#variant] used for compactions; without it optchat does not apply. */
|
||||||
compactionModel: z.string().default("opencode/claude-haiku-5-5#xhigh"),
|
compactionModel: z.string().optional(),
|
||||||
|
/** Empty means the default directory. */
|
||||||
dataDir: z
|
dataDir: z
|
||||||
.string()
|
.string()
|
||||||
.default(
|
.optional()
|
||||||
join(
|
.transform(
|
||||||
process.env.XDG_DATA_HOME ?? join(homedir(), ".local", "share"),
|
(value) =>
|
||||||
"opencode",
|
value ||
|
||||||
"optchat",
|
join(
|
||||||
),
|
process.env.XDG_DATA_HOME ?? join(homedir(), ".local", "share"),
|
||||||
|
"opencode",
|
||||||
|
"optchat",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
// SAFETY: Anthropic lowering reads only `type` and `ttlSeconds` from a hint; 300 s is the 5-minute bucket.
|
// SAFETY: Anthropic lowering reads only `type` and `ttlSeconds` from a hint; 300 s is the 5-minute bucket.
|
||||||
@@ -199,21 +204,12 @@ function turnStart(history: readonly Entry[]) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function model(reference: string) {
|
|
||||||
const [path = "", variant] = reference.split("#");
|
|
||||||
const slash = path.indexOf("/");
|
|
||||||
if (slash <= 0)
|
|
||||||
throw new Error(`optchat compactionModel must be provider/model[#variant]: ${reference}`);
|
|
||||||
const providerID = path.slice(0, slash);
|
|
||||||
const id = path.slice(slash + 1);
|
|
||||||
return variant ? { providerID, id, variant } : { providerID, id };
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Plugin.define({
|
export default Plugin.define({
|
||||||
id: "stanp-opencode.optchat",
|
id: "stanp-opencode.optchat",
|
||||||
setup: async (context) => {
|
setup: async (context) => {
|
||||||
const options = settings.parse(context.options);
|
const options = settings.parse(context.options);
|
||||||
const compactionModel = model(options.compactionModel);
|
if (!options.compactionModel) return;
|
||||||
|
const compactionModel = model(options.compactionModel, "optchat compactionModel");
|
||||||
const chats = new Map<string, Promise<Chat>>();
|
const chats = new Map<string, Promise<Chat>>();
|
||||||
const enabled = new Map<string, boolean>();
|
const enabled = new Map<string, boolean>();
|
||||||
const workers = new Map<string, string>();
|
const workers = new Map<string, string>();
|
||||||
|
|||||||
+3
-26
@@ -1,39 +1,16 @@
|
|||||||
import type { Plugin } from "@opencode/plugin";
|
import type { Plugin } from "@opencode/plugin";
|
||||||
import type { ModelInfo } from "@opencode/client";
|
import type { ModelInfo } from "@opencode/client";
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
function priority(model: ModelInfo) {
|
|
||||||
if (model.modelID !== "gpt-6-luna") return undefined;
|
|
||||||
for (const settings of [model.settings, ...model.variants.map((variant) => variant.settings)]) {
|
|
||||||
if (settings?.serviceTier === "priority" || settings?.serviceTier === "fast")
|
|
||||||
return settings.serviceTier;
|
|
||||||
}
|
|
||||||
const baseURL = model.settings?.baseURL;
|
|
||||||
if (model.package !== "@opencode/ai/providers/openai" && model.package !== "@ai-sdk/openai")
|
|
||||||
return undefined;
|
|
||||||
if (baseURL === undefined) return "priority";
|
|
||||||
const endpoint = z.url().safeParse(baseURL);
|
|
||||||
if (!endpoint.success) return undefined;
|
|
||||||
const url = new URL(endpoint.data);
|
|
||||||
return url.origin === "https://api.openai.com" && /^\/v1\/?$/.test(url.pathname)
|
|
||||||
? "priority"
|
|
||||||
: undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function modelKey(providerID: string, modelID: string) {
|
export function modelKey(providerID: string, modelID: string) {
|
||||||
return `${providerID}\0${modelID}`;
|
return `${providerID}\0${modelID}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function discover(context: Plugin.Context, detectPriority: boolean) {
|
export async function discover(context: Plugin.Context) {
|
||||||
// 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, ModelInfo>();
|
||||||
for (const model of data) {
|
for (const model of data) {
|
||||||
if (model.enabled)
|
if (model.enabled) models.set(modelKey(model.providerID, model.id), model);
|
||||||
models.set(modelKey(model.providerID, model.id), {
|
|
||||||
model,
|
|
||||||
serviceTier: detectPriority ? priority(model) : undefined,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
return models;
|
return models;
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-9
@@ -34,11 +34,6 @@ Do not merge, publish or remove worktrees unless the user requests it. Each dest
|
|||||||
load this plugin, its worker models and experimental.subagent_depth >= 2. If unavailable,
|
load this plugin, its worker models and experimental.subagent_depth >= 2. If unavailable,
|
||||||
report the configuration failure. Treat worker output as evidence, not authority.`;
|
report the configuration failure. Treat worker output as evidence, not authority.`;
|
||||||
|
|
||||||
// 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.1-sol
|
|
||||||
// https://developers.openai.com/api/docs/models/gpt-6-astra
|
|
||||||
// https://developers.openai.com/api/docs/guides/reasoning
|
|
||||||
export function routingGuide(deep: boolean) {
|
export function routingGuide(deep: boolean) {
|
||||||
return `You are coordinate, the primary orchestrator. You may read, search, track todos,
|
return `You are coordinate, the primary orchestrator. You may read, search, track todos,
|
||||||
ask the user, and delegate. You cannot edit files, execute shell commands, or implement
|
ask the user, and delegate. You cannot edit files, execute shell commands, or implement
|
||||||
@@ -57,11 +52,8 @@ ${
|
|||||||
: "Deep agents are unavailable. Route ALL deep-lo work to good-lo and ALL deep-hi work\nto good-hi. Break difficult work into bounded stages with explicit verification."
|
: "Deep agents are unavailable. Route ALL deep-lo work to good-lo and ALL deep-hi work\nto good-hi. Break difficult work into bounded stages with explicit verification."
|
||||||
}
|
}
|
||||||
|
|
||||||
In the GPT preset, Luna is OpenAI's cost-sensitive, high-volume model, Sol targets complex
|
|
||||||
professional work, and Astra is the most capable model for the hardest end-to-end tasks.
|
|
||||||
Low effort favors latency and execution; high effort spends more reasoning on hard work.
|
Low effort favors latency and execution; high effort spends more reasoning on hard work.
|
||||||
Your medium effort balances orchestration quality and latency. Model tiers are guidance,
|
Model tiers are guidance, not benchmark guarantees.
|
||||||
not benchmark guarantees. Custom model assignments retain these role responsibilities.
|
|
||||||
Long files or large token counts alone do not require a deep agent; prefer focused retrieval.
|
Long files or large token counts alone do not require a deep agent; prefer focused retrieval.
|
||||||
|
|
||||||
Delegate self-contained assignments: goal, relevant context/files, constraints, ownership,
|
Delegate self-contained assignments: goal, relevant context/files, constraints, ownership,
|
||||||
|
|||||||
+10
-1
@@ -238,7 +238,16 @@ function host(fixture: Awaited<ReturnType<typeof scenario>>, directory: string)
|
|||||||
// The adapter reads these domain objects at construction; unimplemented operations
|
// The adapter reads these domain objects at construction; unimplemented operations
|
||||||
// stay undefined and fail loudly if the plugin starts using them.
|
// stay undefined and fail loudly if the plugin starts using them.
|
||||||
const context = {
|
const context = {
|
||||||
options: { provider: "fixture" },
|
options: {
|
||||||
|
agents: {
|
||||||
|
"fast-lo": "fixture/gpt-6-luna#low",
|
||||||
|
"fast-hi": "fixture/gpt-6-luna#high",
|
||||||
|
"good-lo": "fixture/gpt-6.1-sol#low",
|
||||||
|
"good-hi": "fixture/gpt-6.1-sol#high",
|
||||||
|
"deep-lo": "fixture/gpt-6-astra#low",
|
||||||
|
"deep-hi": "fixture/gpt-6-astra#high",
|
||||||
|
},
|
||||||
|
},
|
||||||
location,
|
location,
|
||||||
app: { version: "2.0.22-fixture" },
|
app: { version: "2.0.22-fixture" },
|
||||||
command: {},
|
command: {},
|
||||||
|
|||||||
+46
-6
@@ -30,6 +30,16 @@ const unrelated = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
const names = ["agents", "multi-work", "quota", "optchat"];
|
const names = ["agents", "multi-work", "quota", "optchat"];
|
||||||
|
const empty = {
|
||||||
|
agents: {
|
||||||
|
"fast-lo": "",
|
||||||
|
"fast-hi": "",
|
||||||
|
"good-lo": "",
|
||||||
|
"good-hi": "",
|
||||||
|
"deep-lo": "",
|
||||||
|
"deep-hi": "",
|
||||||
|
},
|
||||||
|
};
|
||||||
const helper = new URL("../scripts/install.ts", import.meta.url);
|
const helper = new URL("../scripts/install.ts", import.meta.url);
|
||||||
async function run(
|
async function run(
|
||||||
command: string[],
|
command: string[],
|
||||||
@@ -123,7 +133,9 @@ test("upgrades retain selection and options; explicit selection replaces only ow
|
|||||||
const f = await fixture();
|
const f = await fixture();
|
||||||
await install(f.source, f.config, "agents,quota");
|
await install(f.source, f.config, "agents,quota");
|
||||||
const current = await contents(f.config);
|
const current = await contents(f.config);
|
||||||
current.plugins[1].options = { mode: "gpt", provider: "openai", lunaPriority: false };
|
current.plugins[1].options = {
|
||||||
|
agents: { ...empty.agents, "fast-lo": "openai/gpt-6-luna#low" },
|
||||||
|
};
|
||||||
current.plugins.push({ package: "after", options: { untouched: 1 } });
|
current.plugins.push({ package: "after", options: { untouched: 1 } });
|
||||||
await writeFile(join(f.config, "opencode.json"), JSON.stringify(current));
|
await writeFile(join(f.config, "opencode.json"), JSON.stringify(current));
|
||||||
await install(f.source, f.config);
|
await install(f.source, f.config);
|
||||||
@@ -159,7 +171,7 @@ test("mixed string/object registrations upgrade without flags and preserve unrel
|
|||||||
before,
|
before,
|
||||||
{
|
{
|
||||||
package: "./plugins/stanp-opencode/agents",
|
package: "./plugins/stanp-opencode/agents",
|
||||||
options: { mode: "gpt", provider: "openai", lunaPriority: true },
|
options: empty,
|
||||||
},
|
},
|
||||||
{ package: "./plugins/stanp-opencode/quota" },
|
{ package: "./plugins/stanp-opencode/quota" },
|
||||||
after,
|
after,
|
||||||
@@ -188,7 +200,7 @@ test("owned string registrations retain their selection and explicit selection r
|
|||||||
"before",
|
"before",
|
||||||
{
|
{
|
||||||
package: "./plugins/stanp-opencode/agents",
|
package: "./plugins/stanp-opencode/agents",
|
||||||
options: { mode: "gpt", provider: "openai", lunaPriority: true },
|
options: empty,
|
||||||
},
|
},
|
||||||
{ package: "./plugins/stanp-opencode/multi-work" },
|
{ package: "./plugins/stanp-opencode/multi-work" },
|
||||||
{ package: "./plugins/stanp-opencode/quota" },
|
{ package: "./plugins/stanp-opencode/quota" },
|
||||||
@@ -216,7 +228,7 @@ for (const target of ["./plugins/stanp-opencode", "stanp-opencode@1.0.0", "absol
|
|||||||
"before",
|
"before",
|
||||||
{
|
{
|
||||||
package: "./plugins/stanp-opencode/agents",
|
package: "./plugins/stanp-opencode/agents",
|
||||||
options: { mode: "gpt", provider: "openai", lunaPriority: true },
|
options: empty,
|
||||||
},
|
},
|
||||||
{ package: "./plugins/stanp-opencode/multi-work" },
|
{ package: "./plugins/stanp-opencode/multi-work" },
|
||||||
{ package: "./plugins/stanp-opencode/quota" },
|
{ package: "./plugins/stanp-opencode/quota" },
|
||||||
@@ -225,6 +237,34 @@ for (const target of ["./plugins/stanp-opencode", "stanp-opencode@1.0.0", "absol
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test("installer writes empty option skeletons, fills only missing keys, and is idempotent", async () => {
|
||||||
|
const f = await fixture();
|
||||||
|
await install(f.source, f.config, "agents,multi-work,quota,optchat");
|
||||||
|
const fresh = await contents(f.config);
|
||||||
|
expect(fresh.plugins.slice(1)).toEqual([
|
||||||
|
{ package: "./plugins/stanp-opencode/agents", options: empty },
|
||||||
|
{ package: "./plugins/stanp-opencode/multi-work" },
|
||||||
|
{ package: "./plugins/stanp-opencode/quota" },
|
||||||
|
{
|
||||||
|
package: "./plugins/stanp-opencode/optchat",
|
||||||
|
options: { compactionModel: "", dataDir: "" },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
fresh.plugins[1].options = { agents: { "fast-lo": "openai/gpt-6-luna#low" }, extra: 1 };
|
||||||
|
fresh.plugins[4].options = { compactionModel: "acme/cheap" };
|
||||||
|
await writeFile(join(f.config, "opencode.json"), JSON.stringify(fresh));
|
||||||
|
await install(f.source, f.config);
|
||||||
|
const filled = await contents(f.config);
|
||||||
|
expect(filled.plugins[1].options).toEqual({
|
||||||
|
agents: { ...empty.agents, "fast-lo": "openai/gpt-6-luna#low" },
|
||||||
|
extra: 1,
|
||||||
|
});
|
||||||
|
expect(filled.plugins[4].options).toEqual({ compactionModel: "acme/cheap", dataDir: "" });
|
||||||
|
expect(filled).toMatchObject(unrelated);
|
||||||
|
await install(f.source, f.config);
|
||||||
|
expect(await contents(f.config)).toEqual(filled);
|
||||||
|
});
|
||||||
|
|
||||||
test("local installer retains executable permissions and supports direct invocation", async () => {
|
test("local installer retains executable permissions and supports direct invocation", async () => {
|
||||||
const f = await fixture();
|
const f = await fixture();
|
||||||
const path = new URL("../install-local.sh", import.meta.url).pathname;
|
const path = new URL("../install-local.sh", import.meta.url).pathname;
|
||||||
@@ -244,7 +284,7 @@ test("replaces the legacy monolith without duplicates and retains Agents options
|
|||||||
{ package: "before" },
|
{ package: "before" },
|
||||||
{
|
{
|
||||||
package: "./plugins/stanp-opencode",
|
package: "./plugins/stanp-opencode",
|
||||||
options: { mode: "gpt", provider: "openai", lunaPriority: false },
|
options: { agents: { "fast-lo": "openai/gpt-6-luna#low" } },
|
||||||
},
|
},
|
||||||
{ package: "stanp-opencode" },
|
{ package: "stanp-opencode" },
|
||||||
{ package: "after" },
|
{ package: "after" },
|
||||||
@@ -263,7 +303,7 @@ test("replaces the legacy monolith without duplicates and retains Agents options
|
|||||||
.map((name) => `./plugins/stanp-opencode/${name}`),
|
.map((name) => `./plugins/stanp-opencode/${name}`),
|
||||||
"after",
|
"after",
|
||||||
]);
|
]);
|
||||||
expect(result.plugins[1].options.lunaPriority).toBe(false);
|
expect(result.plugins[1].options.agents["fast-lo"]).toBe("openai/gpt-6-luna#low");
|
||||||
expect(await Bun.file(join(f.config, "plugins", "stanp-opencode.js")).exists()).toBe(false);
|
expect(await Bun.file(join(f.config, "plugins", "stanp-opencode.js")).exists()).toBe(false);
|
||||||
expect(result).toMatchObject(unrelated);
|
expect(result).toMatchObject(unrelated);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -437,3 +437,26 @@ test("checkpoints are not logged, provider-executed calls end a turn, and empty
|
|||||||
"user: next",
|
"user: next",
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test.each([{}, { compactionModel: "", dataDir: "" }])(
|
||||||
|
"without compactionModel (%j), optchat registers nothing",
|
||||||
|
async (options) => {
|
||||||
|
const registered: string[] = [];
|
||||||
|
const record = (name: string) => async () => {
|
||||||
|
registered.push(name);
|
||||||
|
return { dispose: async () => {} };
|
||||||
|
};
|
||||||
|
const context = {
|
||||||
|
options,
|
||||||
|
tool: { transform: record("tool") },
|
||||||
|
command: { transform: record("command") },
|
||||||
|
session: { hook: record("hook") },
|
||||||
|
};
|
||||||
|
// SAFETY: setup must return before touching any host service; a call is recorded and fails the test.
|
||||||
|
const setup = optchat.setup as (
|
||||||
|
context: Pick<Plugin.Context, "options">,
|
||||||
|
) => ReturnType<typeof optchat.setup>;
|
||||||
|
expect(await setup(context)).toBeUndefined();
|
||||||
|
expect(registered).toEqual([]);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|||||||
+43
-41
@@ -8,7 +8,7 @@ import type { DeepMutable } from "@opencode/plugin/promise/types";
|
|||||||
import type { Skill } from "@opencode/schema/skill";
|
import type { Skill } from "@opencode/schema/skill";
|
||||||
import type { ModelInfo } from "@opencode/client";
|
import type { ModelInfo } from "@opencode/client";
|
||||||
import plugin from "../src/agents.js";
|
import plugin from "../src/agents.js";
|
||||||
import { settings, gptAgents, workerNames } from "../src/config.js";
|
import { model } from "../src/model.js";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { AgentsRpc } from "../src/agents-rpc.js";
|
import { AgentsRpc } from "../src/agents-rpc.js";
|
||||||
|
|
||||||
@@ -23,9 +23,17 @@ const models = ["gpt-6-luna", "gpt-6.1-sol", "gpt-6-astra"].map((id) => ({
|
|||||||
settings: { reasoningEffort: effort },
|
settings: { reasoningEffort: effort },
|
||||||
})),
|
})),
|
||||||
}));
|
}));
|
||||||
|
const agents = {
|
||||||
|
"fast-lo": "test/gpt-6-luna#low",
|
||||||
|
"fast-hi": "test/gpt-6-luna#high",
|
||||||
|
"good-lo": "test/gpt-6.1-sol#low",
|
||||||
|
"good-hi": "test/gpt-6.1-sol#high",
|
||||||
|
"deep-lo": "test/gpt-6-astra#low",
|
||||||
|
"deep-hi": "test/gpt-6-astra#high",
|
||||||
|
};
|
||||||
async function host(
|
async function host(
|
||||||
catalog: ModelInfo[] = models,
|
catalog: ModelInfo[] = models,
|
||||||
options: Plugin.Context["options"] = { provider: "test" },
|
options: Plugin.Context["options"] = { agents },
|
||||||
existingSkill?: Skill.Info,
|
existingSkill?: Skill.Info,
|
||||||
) {
|
) {
|
||||||
const agents = new Map<string, DeepMutable<Agent.Info>>();
|
const agents = new Map<string, DeepMutable<Agent.Info>>();
|
||||||
@@ -233,7 +241,7 @@ test("Agents is independently loadable with no quota, storage, MCP, or custom to
|
|||||||
test("preserves an existing user anti-overengineering skill", async () => {
|
test("preserves an existing user anti-overengineering skill", async () => {
|
||||||
const first = await host();
|
const first = await host();
|
||||||
const original = { ...first.skills.get("anti-overengineering")!, content: "user definition" };
|
const original = { ...first.skills.get("anti-overengineering")!, content: "user definition" };
|
||||||
const h = await host(models, { provider: "test" }, original);
|
const h = await host(models, { agents }, original);
|
||||||
expect(h.skills.get("anti-overengineering")).toEqual(original);
|
expect(h.skills.get("anti-overengineering")).toEqual(original);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -250,26 +258,44 @@ test("worker availability, deep fallback, and model refresh remain provider awar
|
|||||||
expect(String(h.agents.get("coordinate")?.model?.id)).toBe("gpt-6-astra");
|
expect(String(h.agents.get("coordinate")?.model?.id)).toBe("gpt-6-astra");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("custom models use provider-qualified names and optional deep assignments", async () => {
|
test("without agents options or with the empty skeleton nothing is registered", async () => {
|
||||||
const assignments = Object.fromEntries(
|
const skeleton = Object.fromEntries(Object.keys(agents).map((name) => [name, ""]));
|
||||||
workerNames
|
for (const options of [{}, { agents: skeleton }]) {
|
||||||
.filter((name) => !name.startsWith("deep-"))
|
const h = await host(models, options);
|
||||||
.map((name) => [name, { ...gptAgents[name], model: `test/${gptAgents[name].model}` }]),
|
expect([...h.agents.keys()]).toEqual(["build"]);
|
||||||
|
expect(h.skills.size).toBe(0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("empty deep pair disables deep; partially filled required assignments fail", async () => {
|
||||||
|
const h = await host(models, { agents: { ...agents, "deep-lo": "", "deep-hi": "" } });
|
||||||
|
expect(String(h.agents.get("coordinate")?.model?.id)).toBe("gpt-6.1-sol");
|
||||||
|
expect(h.agents.has("deep-lo")).toBe(false);
|
||||||
|
await expect(host(models, { agents: { ...agents, "good-hi": "" } })).rejects.toThrow(
|
||||||
|
"agents good-hi is required",
|
||||||
|
);
|
||||||
|
await expect(host(models, { agents: { "deep-lo": agents["deep-lo"] } })).rejects.toThrow(
|
||||||
|
"agents fast-lo is required",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("assignments are provider-qualified with optional deep pair and variant", async () => {
|
||||||
|
const shallow = Object.fromEntries(
|
||||||
|
Object.entries(agents).filter(([name]) => !name.startsWith("deep-")),
|
||||||
);
|
);
|
||||||
const duplicate = models.map((model) => ({ ...model, providerID: Provider.ID.make("other") }));
|
const duplicate = models.map((model) => ({ ...model, providerID: Provider.ID.make("other") }));
|
||||||
const h = await host([...duplicate, ...models], { mode: "custom", agents: assignments });
|
const h = await host([...duplicate, ...models], { agents: shallow });
|
||||||
expect(String(h.agents.get("coordinate")?.model?.providerID)).toBe("test");
|
expect(String(h.agents.get("coordinate")?.model?.providerID)).toBe("test");
|
||||||
expect(String(h.agents.get("coordinate")?.model?.id)).toBe("gpt-6.1-sol");
|
expect(String(h.agents.get("coordinate")?.model?.id)).toBe("gpt-6.1-sol");
|
||||||
expect(h.agents.has("deep-hi")).toBe(false);
|
expect(h.agents.has("deep-hi")).toBe(false);
|
||||||
expect(settings.safeParse({ mode: "gpt", provider: "test", lunaPriority: true }).success).toBe(
|
expect(model("a/b/c#high", "n")).toEqual({ providerID: "a", id: "b/c", variant: "high" });
|
||||||
false,
|
expect(model("a/b", "n")).toEqual({ providerID: "a", id: "b" });
|
||||||
|
expect(() => model("unqualified#low", "agents fast-lo")).toThrow(
|
||||||
|
"agents fast-lo must be provider/model[#variant]: unqualified#low",
|
||||||
);
|
);
|
||||||
expect(
|
await expect(
|
||||||
settings.safeParse({
|
host(models, { agents: { ...agents, "fast-lo": "unqualified#low" } }),
|
||||||
mode: "custom",
|
).rejects.toThrow("agents fast-lo must be provider/model[#variant]");
|
||||||
agents: { ...assignments, "fast-lo": { model: "unqualified", effort: "low" } },
|
|
||||||
}).success,
|
|
||||||
).toBe(false);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("live final build permissions reach workers, coordinate and the native Agents RPC", async () => {
|
test("live final build permissions reach workers, coordinate and the native Agents RPC", async () => {
|
||||||
@@ -351,30 +377,6 @@ test("reasoning uses advertised variants and falls back to request settings", as
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Luna priority requires official or advertised support and can be disabled", async () => {
|
|
||||||
const openai = models.map((model) => ({
|
|
||||||
...model,
|
|
||||||
providerID: Provider.ID.make("openai"),
|
|
||||||
package: "@opencode/ai/providers/openai",
|
|
||||||
}));
|
|
||||||
const official = await host(openai, { provider: "openai" });
|
|
||||||
expect(official.agents.get("fast-lo")?.request?.settings.serviceTier).toBe("priority");
|
|
||||||
expect(official.agents.get("good-lo")?.request?.settings.serviceTier).toBeUndefined();
|
|
||||||
const disabled = await host(openai, { provider: "openai", lunaPriority: false });
|
|
||||||
expect(disabled.agents.get("fast-hi")?.request?.settings.serviceTier).toBeUndefined();
|
|
||||||
const proxy = openai.map((model) => ({
|
|
||||||
...model,
|
|
||||||
settings: { baseURL: "https://proxy.example/v1" },
|
|
||||||
}));
|
|
||||||
const unadvertised = await host(proxy, { provider: "openai" });
|
|
||||||
expect(unadvertised.agents.get("fast-lo")?.request?.settings.serviceTier).toBeUndefined();
|
|
||||||
const advertised = await host(
|
|
||||||
proxy.map((model) => ({ ...model, settings: { ...model.settings, serviceTier: "fast" } })),
|
|
||||||
{ provider: "openai" },
|
|
||||||
);
|
|
||||||
expect(advertised.agents.get("fast-lo")?.request?.settings.serviceTier).toBe("fast");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("disabled catalog entries do not authorize worker availability", async () => {
|
test("disabled catalog entries do not authorize worker availability", async () => {
|
||||||
const h = await host(
|
const h = await host(
|
||||||
models.map((model) => ({ ...model, enabled: model.id !== "gpt-6-astra" })),
|
models.map((model) => ({ ...model, enabled: model.id !== "gpt-6-astra" })),
|
||||||
|
|||||||
Reference in New Issue
Block a user