Skip to content
Merged
38 changes: 38 additions & 0 deletions packages/provider-opencode/src/server/OpenCodeRuntime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,44 @@ export interface ParsedOpenCodeModelSlug {
readonly modelID: string;
}

export type OpenCodeVariantCache = Map<string, ReadonlySet<string> | undefined>;

export const resolveOpenCodeVariant = Effect.fnUntraced(function* (
client: OpencodeClient,
model: ParsedOpenCodeModelSlug | undefined,
variant: string | undefined,
abortSignal?: AbortSignal,
cache?: OpenCodeVariantCache,
) {
if (!variant || !model) return variant;

const key = `${model.providerID}/${model.modelID}`;
const cached = cache?.get(key);
if (cache?.has(key)) {
return cached !== undefined && !cached.has(variant) ? undefined : variant;
}

// Only a confirmed catalog mismatch can invalidate a persisted selection.
const response = yield* runOpenCodeSdk("provider.list", (signal) =>
client.provider.list(undefined, {
signal: abortSignal ? AbortSignal.any([signal, abortSignal]) : signal,
}),
).pipe(
Effect.timeout("10 seconds"),
Effect.orElseSucceed(() => undefined),
);
if (response?.data === undefined) return variant;

const catalogModel = response.data.all.find((provider) => provider.id === model.providerID)
?.models[model.modelID];
const variants = catalogModel ? new Set(Object.keys(catalogModel.variants ?? {})) : undefined;
if (cache !== undefined) {
// Keep only this model's names; provider.list can return several megabytes.
cache.set(key, variants);
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
Resolved
}
return variants !== undefined && !variants.has(variant) ? undefined : variant;
});

export interface OpenCodeSkill {
readonly name?: string | null;
readonly description?: string | null;
Expand Down
264 changes: 263 additions & 1 deletion packages/provider-opencode/src/server/adapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import * as Layer from "effect/Layer";
import * as Option from "effect/Option";
import * as Schema from "effect/Schema";
import * as Stream from "effect/Stream";
import * as TestClock from "effect/testing/TestClock";

import * as ProviderHost from "@t3tools/provider-core/server/ProviderHost";
import * as TestProviderHost from "@t3tools/provider-testing/TestProviderHost";
Expand Down Expand Up @@ -154,6 +155,7 @@ const makeOpenCodeRuntimeHarness = Effect.fn("makeOpenCodeRuntimeHarness")(funct
client: object,
host: Partial<ProviderHost.ProviderHost["Service"]> = {},
turnSettings: {
readonly model?: string;
readonly options?: ReadonlyArray<ProviderOptionSelection>;
readonly interactionMode?: ProviderAdapter.ProviderAdapterV2RuntimePolicy["interactionMode"];
} = {},
Expand All @@ -162,7 +164,7 @@ const makeOpenCodeRuntimeHarness = Effect.fn("makeOpenCodeRuntimeHarness")(funct
const threadId = ThreadId.make(`thread-opencode-${suffix}`);
const modelSelection = {
instanceId,
model: "anthropic/claude-sonnet",
model: turnSettings.model ?? "anthropic/claude-sonnet",
options: turnSettings.options ?? [],
};
const policy = runtimePolicy("full-access", {
Expand Down Expand Up @@ -255,6 +257,266 @@ const makeOpenCodeRuntimeHarness = Effect.fn("makeOpenCodeRuntimeHarness")(funct
});

describe("OpenCodeAdapterV2", () => {
it.effect.each([
["empty", "prompt"],
["unsupported", "command"],
["supported", "command"],
["unknown", "prompt"],
["failure", "prompt"],
["unset", "prompt"],
] as const)("normalizes reasoning variants for %s inventory at %s dispatch", ([kind, request]) =>
Effect.gen(function* () {
const suffix = `variant-${kind}-${request}`;
const nativeEvents = asyncEventStream();
const inputs: Array<Record<string, unknown>> = [];
let inventoryCalls = 0;
const receive = async (input: Record<string, unknown>) => {
inputs.push(input);
return { data: true };
};
const harness = yield* makeOpenCodeRuntimeHarness(
suffix,
suffix,
{
event: {
subscribe: async (_input: unknown, options: { signal?: AbortSignal }) => {
options.signal?.addEventListener("abort", () => nativeEvents.close(), { once: true });
return { stream: nativeEvents.stream };
},
},
provider: {
list: async () => {
inventoryCalls++;
if (kind === "failure") throw new Error("inventory unavailable");
return {
data: {
all: [
{
id: "opencode",
models:
kind === "unknown"
? {}
: {
"big-pickle": {
id: "big-pickle",
variants:
kind === "supported"
? { medium: {} }
: kind === "unsupported"
? { high: {} }
: {},
},
},
},
],
},
};
},
},
command: { list: async () => ({ data: [{ name: "review" }] }) },
session: {
create: async () => ({ data: { id: suffix, time: { created: 1, updated: 1 } } }),
promptAsync: receive,
command: receive,
},
},
{},
{
model: "opencode/big-pickle",
options: [
...(kind === "unset" ? [] : [{ id: "variant", value: "medium" }]),
{ id: "agent", value: "plan" },
],
},
);
yield* harness.startTurn(request === "command" ? "/review staged changes" : "hello");
assert.equal(inputs.length, 1);
const payload = inputs[0]!;
assert.equal(payload.agent, "plan");
if (kind === "supported" || kind === "unknown" || kind === "failure") {
assert.equal(payload.variant, "medium");
} else {
assert.equal(Object.hasOwn(payload, "variant"), false);
}
if (kind === "unset") assert.equal(inventoryCalls, 0);
}).pipe(
Effect.provide(Layer.merge(IdAllocator.layer, McpProviderSessions.layer)),
Effect.scoped,
),
);

it.effect("reuses successful model variants for the session and retries failed inventories", () =>
Effect.gen(function* () {
const sessionId = "variant-cache";
const nativeEvents = asyncEventStream();
const inputs: Array<Record<string, unknown>> = [];
let inventoryCalls = 0;
let variants: Record<string, object> = {};
const harness = yield* makeOpenCodeRuntimeHarness(
sessionId,
sessionId,
{
event: {
subscribe: async (_input: unknown, options: { signal?: AbortSignal }) => {
options.signal?.addEventListener("abort", () => nativeEvents.close(), { once: true });
return { stream: nativeEvents.stream };
},
},
provider: {
list: async () => {
inventoryCalls++;
if (inventoryCalls === 1) throw new Error("inventory unavailable");
return {
data: {
all: [{ id: "opencode", models: { "big-pickle": { variants } } }],
},
};
},
},
session: {
create: async () => ({ data: { id: sessionId, time: { created: 1, updated: 1 } } }),
promptAsync: async (input: Record<string, unknown>) => {
inputs.push(input);
return { data: true };
},
},
},
{},
{ model: "opencode/big-pickle", options: [{ id: "variant", value: "medium" }] },
);
const submit = Effect.fnUntraced(function* () {
yield* harness.startTurn();
yield* Effect.promise(() =>
nativeEvents.push({
type: "message.updated",
properties: {
sessionID: sessionId,
info: {
id: inputs.at(-1)!.messageID,
sessionID: sessionId,
role: "user",
time: { created: 1 },
},
},
}),
);
yield* Effect.promise(() =>
nativeEvents.push({ type: "session.idle", properties: { sessionID: sessionId } }),
);
yield* harness.runtime.events.pipe(
Stream.filter((event) => event.type === "turn.terminal"),
Stream.take(1),
Stream.runDrain,
);
return inputs.at(-1)!;
});

assert.equal((yield* submit()).variant, "medium");
assert.equal(inventoryCalls, 1);
assert.isFalse(Object.hasOwn(yield* submit(), "variant"));
assert.equal(inventoryCalls, 2);
variants = { medium: {} };
assert.isFalse(Object.hasOwn(yield* submit(), "variant"));
assert.equal(inventoryCalls, 2);
}).pipe(
Effect.provide(Layer.merge(IdAllocator.layer, McpProviderSessions.layer)),
Effect.scoped,
),
);

it.effect("keeps queued steers behind a cold catalog lookup and cancels them on stop", () =>
Effect.gen(function* () {
const sessionId = "catalog-admission";
const nativeEvents = asyncEventStream();
const catalogStarted = promiseGate<void>();
const calls: string[] = [];
const harness = yield* makeOpenCodeRuntimeHarness(
sessionId,
sessionId,
{
event: {
subscribe: async (_input: unknown, options: { signal?: AbortSignal }) => {
options.signal?.addEventListener("abort", () => nativeEvents.close(), { once: true });
return { stream: nativeEvents.stream };
},
},
provider: {
list: async (_input: unknown, { signal }: { signal: AbortSignal }) => {
calls.push("catalog");
catalogStarted.resolve();
await new Promise<void>((_resolve, reject) => {
signal.addEventListener(
"abort",
() => {
calls.push("catalog-aborted");
reject(signal.reason);
},
{ once: true },
);
});
return { data: { all: [] } };
},
},
session: {
create: async () => ({ data: { id: sessionId, time: { created: 1, updated: 1 } } }),
get: async () => ({ data: { id: sessionId, time: { created: 1, updated: 1 } } }),
messages: async () => ({ data: [] }),
children: async () => ({ data: [] }),
promptAsync: async (_input: unknown, { signal }: { signal: AbortSignal }) => {
signal.throwIfAborted();
calls.push("prompt");
return { data: true };
},
abort: async () => {
calls.push("abort");
return { data: true };
},
},
},
{},
{ model: "opencode/big-pickle", options: [{ id: "variant", value: "medium" }] },
);
const start = yield* harness.startTurn().pipe(Effect.forkScoped);
yield* Effect.promise(() => catalogStarted.promise);
const snapshot = yield* harness.runtime.readThreadSnapshot({
providerThread: harness.providerThread,
});
const activeTurn = snapshot.providerTurns.at(-1)!;
const steer = (message: string) =>
harness.runtime
.steerTurn({
threadId: harness.threadId,
runId: harness.runId,
providerThread: harness.providerThread,
providerTurnId: activeTurn.id,
message: {
messageId: MessageId.make(message),
text: message,
attachments: [],
createdBy: "user",
creationSource: "web",
},
})
.pipe(Effect.exit, Effect.forkScoped({ startImmediately: true }));
const firstSteer = yield* steer("first follow-up");
const secondSteer = yield* steer("second follow-up");
yield* TestClock.adjust("5 seconds");
assert.deepEqual(calls, ["catalog"]);

yield* harness.runtime.interruptTurn({
providerThread: harness.providerThread,
providerTurnId: activeTurn.id,
});
yield* Fiber.join(start);
assert.isTrue(Exit.isFailure(yield* Fiber.join(firstSteer)));
assert.isTrue(Exit.isFailure(yield* Fiber.join(secondSteer)));
assert.deepEqual(calls, ["catalog", "catalog-aborted", "abort"]);
}).pipe(
Effect.provide(Layer.merge(IdAllocator.layer, McpProviderSessions.layer)),
Effect.scoped,
),
);

it.effect.each([
{
message: "<none>",
Expand Down
Loading
Loading