mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-25 02:32:47 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
78cc05516a | ||
|
|
4e21643e3a |
@@ -1,4 +1,4 @@
|
|||||||
import type { SessionMessageInfo } from "@opencode/client";
|
import type { ModelInfo, SessionMessageInfo } from "@opencode/client";
|
||||||
import { describe, expect, test } from "vitest";
|
import { describe, expect, test } from "vitest";
|
||||||
|
|
||||||
import { createTestLogger } from "../../../../../test-utils/test-logger.js";
|
import { createTestLogger } from "../../../../../test-utils/test-logger.js";
|
||||||
@@ -535,4 +535,85 @@ describe("OpenCode v2 session lifecycle", () => {
|
|||||||
await session.close();
|
await session.close();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("switching to a model without the current variant sends the next prompt without it", async () => {
|
||||||
|
const harness = offeringModels({
|
||||||
|
"opencode/space-bunny-free": ["low", "medium", "high"],
|
||||||
|
"opencode/longcat-2.5-preview-free": [],
|
||||||
|
"opencode/muse-spark-free": ["minimal", "medium"],
|
||||||
|
});
|
||||||
|
const client = new OpenCodeV2AgentClient({
|
||||||
|
logger: createTestLogger(),
|
||||||
|
runtime: harness.runtime,
|
||||||
|
});
|
||||||
|
const session = await client.createSession({
|
||||||
|
provider: "opencode",
|
||||||
|
cwd: "/tmp/project",
|
||||||
|
model: "opencode/space-bunny-free",
|
||||||
|
thinkingOptionId: "medium",
|
||||||
|
});
|
||||||
|
const thinkingChanges: Array<string | null> = [];
|
||||||
|
session.subscribe((event) => {
|
||||||
|
if (event.type === "thinking_option_changed") thinkingChanges.push(event.thinkingOptionId);
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await session.setModel!("opencode/muse-spark-free");
|
||||||
|
expect(await session.getRuntimeInfo!()).toMatchObject({ thinkingOptionId: "medium" });
|
||||||
|
|
||||||
|
await session.setModel!("opencode/longcat-2.5-preview-free");
|
||||||
|
await session.run("hello");
|
||||||
|
expect(harness.prompts).toEqual(["hello"]);
|
||||||
|
expect(await session.getRuntimeInfo!()).toMatchObject({
|
||||||
|
model: "opencode/longcat-2.5-preview-free",
|
||||||
|
thinkingOptionId: null,
|
||||||
|
});
|
||||||
|
expect(thinkingChanges).toEqual([null]);
|
||||||
|
} finally {
|
||||||
|
await session.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Behaves like an OpenCode v2 server: switching to any listed model succeeds,
|
||||||
|
// and a prompt fails when the session's variant is not one its model offers.
|
||||||
|
function offeringModels(variantsByModel: Record<string, string[]>): V2Harness {
|
||||||
|
const harness = new V2Harness();
|
||||||
|
const catalog: ModelInfo[] = Object.entries(variantsByModel).map(([ref, variants]) => {
|
||||||
|
const [providerID, id] = ref.split("/");
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
modelID: id,
|
||||||
|
providerID,
|
||||||
|
name: id,
|
||||||
|
capabilities: { tools: true, input: ["text"], output: ["text"] },
|
||||||
|
variants: variants.map((variant) => ({ id: variant })),
|
||||||
|
time: { released: 0 },
|
||||||
|
cost: [],
|
||||||
|
status: "active",
|
||||||
|
enabled: true,
|
||||||
|
limit: { context: 1000, output: 1000 },
|
||||||
|
};
|
||||||
|
});
|
||||||
|
harness.api.model.list = async (input) => ({
|
||||||
|
location: { directory: input?.location?.directory ?? harness.info.location.directory },
|
||||||
|
data: catalog,
|
||||||
|
});
|
||||||
|
harness.api.session.create = async (input = {}) => {
|
||||||
|
harness.creates.push(input);
|
||||||
|
harness.info.model = input.model;
|
||||||
|
return harness.info;
|
||||||
|
};
|
||||||
|
harness.api.session.switchModel = async ({ model }) => {
|
||||||
|
harness.info.model = model;
|
||||||
|
};
|
||||||
|
harness.prompt = async (input) => {
|
||||||
|
const model = harness.info.model;
|
||||||
|
const offered = catalog.find(
|
||||||
|
(entry) => entry.providerID === model?.providerID && entry.id === model?.id,
|
||||||
|
);
|
||||||
|
if (model?.variant && !offered?.variants.some((variant) => variant.id === model.variant))
|
||||||
|
throw new Error(`Variant unavailable for ${model.providerID}/${model.id}: ${model.variant}`);
|
||||||
|
harness.prompts.push(input.text);
|
||||||
|
};
|
||||||
|
return harness;
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { SessionTurns } from "./turns.js";
|
|||||||
import { V2Timeline } from "./timeline.js";
|
import { V2Timeline } from "./timeline.js";
|
||||||
import { waitForLocationReady, awaitPaseoPlugin } from "./readiness.js";
|
import { waitForLocationReady, awaitPaseoPlugin } from "./readiness.js";
|
||||||
|
|
||||||
import type { SessionInfo, SessionMessageInfo } from "@opencode/client";
|
import type { ModelRef, SessionInfo, SessionMessageInfo } from "@opencode/client";
|
||||||
|
|
||||||
import { setTimeout as delay } from "node:timers/promises";
|
import { setTimeout as delay } from "node:timers/promises";
|
||||||
import type { Logger } from "pino";
|
import type { Logger } from "pino";
|
||||||
@@ -277,24 +277,35 @@ export class OpenCodeV2Session implements AgentSession {
|
|||||||
async setModel(model: string | null) {
|
async setModel(model: string | null) {
|
||||||
await this.reconnectIfExited();
|
await this.reconnectIfExited();
|
||||||
const selected = model
|
const selected = model
|
||||||
? modelRef(model, this.config.thinkingOptionId)
|
? modelRef(model)
|
||||||
: (await this.client.model.default({ location: { directory: this.config.cwd } })).data;
|
: (await this.client.model.default({ location: { directory: this.config.cwd } })).data;
|
||||||
if (!selected) throw new Error("OpenCode has no default model");
|
if (!selected) throw new Error("OpenCode has no default model");
|
||||||
|
// OpenCode rejects a prompt whose variant the model does not offer, so the
|
||||||
|
// current variant only carries over to a model that has it.
|
||||||
|
const variant = await this.offeredVariant(selected, this.config.thinkingOptionId);
|
||||||
await this.client.session.switchModel({
|
await this.client.session.switchModel({
|
||||||
sessionID: this.id,
|
sessionID: this.id,
|
||||||
model: {
|
model: { id: selected.id, providerID: selected.providerID, variant },
|
||||||
id: selected.id,
|
|
||||||
providerID: selected.providerID,
|
|
||||||
variant: this.config.thinkingOptionId,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
this.config.model = model ?? undefined;
|
this.config.model = model ?? undefined;
|
||||||
|
if (variant !== this.config.thinkingOptionId) {
|
||||||
|
this.config.thinkingOptionId = variant;
|
||||||
|
this.emit({ type: "thinking_option_changed", provider: "opencode", thinkingOptionId: null });
|
||||||
|
}
|
||||||
this.emit({
|
this.emit({
|
||||||
type: "model_changed",
|
type: "model_changed",
|
||||||
provider: "opencode",
|
provider: "opencode",
|
||||||
runtimeInfo: await this.getRuntimeInfo(),
|
runtimeInfo: await this.getRuntimeInfo(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
private async offeredVariant(model: ModelRef, variant: string | undefined) {
|
||||||
|
if (!variant) return undefined;
|
||||||
|
const { data } = await this.client.model.list({ location: { directory: this.config.cwd } });
|
||||||
|
const offered = data.find(
|
||||||
|
(entry) => entry.providerID === model.providerID && entry.id === model.id,
|
||||||
|
);
|
||||||
|
return offered?.variants.some((entry) => entry.id === variant) ? variant : undefined;
|
||||||
|
}
|
||||||
async setThinkingOption(variant: string | null) {
|
async setThinkingOption(variant: string | null) {
|
||||||
await this.reconnectIfExited();
|
await this.reconnectIfExited();
|
||||||
const model = this.info.model;
|
const model = this.info.model;
|
||||||
|
|||||||
Reference in New Issue
Block a user