Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/empty-hoops-tie.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"opencode-adaptive-thinking": patch
---

Preserve the active model when synthetic prompts adjust reasoning effort and when temporary overrides reset after a session becomes idle. This prevents OpenCode from falling back to the agent/provider default model instead of the model that was selected when the reasoning change was initiated.
11 changes: 8 additions & 3 deletions src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,8 @@ describe("AdaptiveThinkingPlugin", () => {
expect(system[0]).toContain("set_reasoning_effort");
});

test("preserves the calling agent when setting reasoning effort", async () => {
const sessionID = "preserve-agent";
test("preserves the calling agent and model when setting reasoning effort", async () => {
const sessionID = "preserve-agent-model";
const { client, toolContext } = createClient(sessionID, [createMessage("medium")]);
toolContext.agent = "custom-worker";
const plugin = await AdaptiveThinkingPlugin({ client } as never);
Expand All @@ -127,7 +127,11 @@ describe("AdaptiveThinkingPlugin", () => {

expect(client.session.promptAsync).toHaveBeenCalledWith(
expect.objectContaining({
body: expect.objectContaining({ agent: "custom-worker", variant: "high" }),
body: expect.objectContaining({
agent: "custom-worker",
model: { providerID: "provider", modelID: "model" },
variant: "high",
}),
}),
);
});
Expand Down Expand Up @@ -314,6 +318,7 @@ describe("AdaptiveThinkingPlugin", () => {
expect(secondPrompt).toMatchObject({
body: {
agent: "agent",
model: { providerID: "provider", modelID: "model" },
variant: "medium",
parts: [{ ignored: true, synthetic: true }],
},
Expand Down
48 changes: 38 additions & 10 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ type SessionState = {
persistedVariant?: string;
temporaryResetVariant?: string;
temporaryResetAgent?: string;
temporaryResetModel?: { providerID: string; modelID: string };
};

class SessionStateCache {
Expand Down Expand Up @@ -45,6 +46,7 @@ const state = new SessionStateCache(maxSessionStateSize);

type PromptAsyncOptions = Parameters<OpencodeClient["session"]["promptAsync"]>[0];
type PromptAsyncBody = NonNullable<PromptAsyncOptions["body"]> & { variant: string };
type ModelInfo = { providerID: string; modelID: string };

export const AdaptiveThinkingPlugin: Plugin = async ({ client }, options) => {
const configParseResult = ConfigSchema.safeParse(options);
Expand Down Expand Up @@ -85,6 +87,7 @@ export const AdaptiveThinkingPlugin: Plugin = async ({ client }, options) => {
variant: string,
text: string,
agent: string,
model?: ModelInfo,
ignored = true,
) => {
const body: PromptAsyncBody = {
Expand All @@ -101,6 +104,10 @@ export const AdaptiveThinkingPlugin: Plugin = async ({ client }, options) => {
variant,
};

if (model) {
body.model = model;
}

return client.session.promptAsync({
path: { id: sessionID },
body,
Expand All @@ -124,16 +131,7 @@ export const AdaptiveThinkingPlugin: Plugin = async ({ client }, options) => {
});
return [];
}
let modelInfo: { providerID: string; modelID: string } | undefined;
const messages = messagesResponse.data as Array<{ info: Message; parts: Array<Part> }>;
for (let i = messages.length - 1; i >= 0; i--) {
const message = messages[i]!;
if ("model" in message.info) {
modelInfo = message.info.model;
break;
}
}

const modelInfo = resolveLatestModelInfo(messagesResponse.data);
if (!modelInfo) return [];

const providers = await client.provider.list();
Expand All @@ -155,6 +153,22 @@ export const AdaptiveThinkingPlugin: Plugin = async ({ client }, options) => {
return getModelVariants(provider.models[modelInfo.modelID] as Model | undefined);
};

const resolveLatestModelInfo = (
data: unknown,
): { providerID: string; modelID: string } | undefined => {
const messages = data as Array<{ info: Message; parts: Array<Part> }>;
for (let i = messages.length - 1; i >= 0; i--) {
const message = messages[i]!;
if ("model" in message.info) {
const { providerID, modelID } = message.info.model;
if (providerID && modelID) {
return { providerID, modelID };
}
}
}
return;
};

const resolveCurrentVariant = async (sessionID: string): Promise<string | undefined> => {
const messagesResponse = await client.session.messages({
path: { id: sessionID },
Expand Down Expand Up @@ -228,12 +242,16 @@ export const AdaptiveThinkingPlugin: Plugin = async ({ client }, options) => {
const resetVariant = persist
? undefined
: (sessionState?.persistedVariant ?? (await resolveCurrentVariant(sessionID)));
const currentModel = resolveLatestModelInfo(
(await client.session.messages({ path: { id: sessionID } })).data,
);

const promptResponse = await sendVariantPrompt(
sessionID,
level,
`Reasoning effort set to ${level}`,
agent,
currentModel,
);
if (promptResponse.error) {
return `Failed to set reasoning effort: ${JSON.stringify(promptResponse.error.data)}`;
Expand All @@ -245,12 +263,19 @@ export const AdaptiveThinkingPlugin: Plugin = async ({ client }, options) => {
entry.persistedVariant = level;
delete entry.temporaryResetVariant;
delete entry.temporaryResetAgent;
delete entry.temporaryResetModel;
} else if (resetVariant && resetVariant !== level) {
entry.temporaryResetVariant = resetVariant;
entry.temporaryResetAgent = agent;
if (currentModel) {
entry.temporaryResetModel = currentModel;
} else {
delete entry.temporaryResetModel;
}
} else {
delete entry.temporaryResetVariant;
delete entry.temporaryResetAgent;
delete entry.temporaryResetModel;
}
});

Expand All @@ -264,13 +289,15 @@ export const AdaptiveThinkingPlugin: Plugin = async ({ client }, options) => {
const sessionState = state.get(sessionID);
const resetVariant = sessionState?.temporaryResetVariant;
const resetAgent = sessionState?.temporaryResetAgent;
const resetModel = sessionState?.temporaryResetModel;
if (!resetVariant || !resetAgent) return;

const promptResponse = await sendVariantPrompt(
sessionID,
resetVariant,
`Reasoning effort reset to ${resetVariant}.`,
resetAgent,
resetModel,
);
if (promptResponse.error) {
client.app.log({
Expand All @@ -287,6 +314,7 @@ export const AdaptiveThinkingPlugin: Plugin = async ({ client }, options) => {
entry.currentVariant = resetVariant;
delete entry.temporaryResetVariant;
delete entry.temporaryResetAgent;
delete entry.temporaryResetModel;
});
return;
}
Expand Down
Loading