Merge pull request #141 from aaaaron/integrate-opencode-pr62
Integrate opencode pr62 & pr104
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { models as codexFallbackModels } from "@paperclipai/adapter-codex-local";
|
||||
import { models as cursorFallbackModels } from "@paperclipai/adapter-cursor-local";
|
||||
import { resetOpenCodeModelsCacheForTests } from "@paperclipai/adapter-opencode-local/server";
|
||||
import { listAdapterModels } from "../adapters/index.js";
|
||||
import { resetCodexModelsCacheForTests } from "../adapters/codex-models.js";
|
||||
import { resetCursorModelsCacheForTests, setCursorModelsRunnerForTests } from "../adapters/cursor-models.js";
|
||||
@@ -8,9 +9,11 @@ import { resetCursorModelsCacheForTests, setCursorModelsRunnerForTests } from ".
|
||||
describe("adapter model listing", () => {
|
||||
beforeEach(() => {
|
||||
delete process.env.OPENAI_API_KEY;
|
||||
delete process.env.PAPERCLIP_OPENCODE_COMMAND;
|
||||
resetCodexModelsCacheForTests();
|
||||
resetCursorModelsCacheForTests();
|
||||
setCursorModelsRunnerForTests(null);
|
||||
resetOpenCodeModelsCacheForTests();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
@@ -60,6 +63,7 @@ describe("adapter model listing", () => {
|
||||
expect(models).toEqual(codexFallbackModels);
|
||||
});
|
||||
|
||||
|
||||
it("returns cursor fallback models when CLI discovery is unavailable", async () => {
|
||||
setCursorModelsRunnerForTests(() => ({
|
||||
status: null,
|
||||
@@ -90,4 +94,11 @@ describe("adapter model listing", () => {
|
||||
expect(first.some((model) => model.id === "gpt-5.3-codex-high")).toBe(true);
|
||||
expect(first.some((model) => model.id === "composer-1")).toBe(true);
|
||||
});
|
||||
|
||||
it("returns no opencode models when opencode command is unavailable", async () => {
|
||||
process.env.PAPERCLIP_OPENCODE_COMMAND = "__paperclip_missing_opencode_command__";
|
||||
|
||||
const models = await listAdapterModels("opencode_local");
|
||||
expect(models).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,11 +18,14 @@ import {
|
||||
} from "@paperclipai/adapter-cursor-local/server";
|
||||
import { agentConfigurationDoc as cursorAgentConfigurationDoc, models as cursorModels } from "@paperclipai/adapter-cursor-local";
|
||||
import {
|
||||
execute as opencodeExecute,
|
||||
testEnvironment as opencodeTestEnvironment,
|
||||
sessionCodec as opencodeSessionCodec,
|
||||
execute as openCodeExecute,
|
||||
testEnvironment as openCodeTestEnvironment,
|
||||
sessionCodec as openCodeSessionCodec,
|
||||
listOpenCodeModels,
|
||||
} from "@paperclipai/adapter-opencode-local/server";
|
||||
import { agentConfigurationDoc as opencodeAgentConfigurationDoc, models as opencodeModels } from "@paperclipai/adapter-opencode-local";
|
||||
import {
|
||||
agentConfigurationDoc as openCodeAgentConfigurationDoc,
|
||||
} from "@paperclipai/adapter-opencode-local";
|
||||
import {
|
||||
execute as openclawExecute,
|
||||
testEnvironment as openclawTestEnvironment,
|
||||
@@ -58,16 +61,6 @@ const codexLocalAdapter: ServerAdapterModule = {
|
||||
agentConfigurationDoc: codexAgentConfigurationDoc,
|
||||
};
|
||||
|
||||
const opencodeLocalAdapter: ServerAdapterModule = {
|
||||
type: "opencode_local",
|
||||
execute: opencodeExecute,
|
||||
testEnvironment: opencodeTestEnvironment,
|
||||
sessionCodec: opencodeSessionCodec,
|
||||
models: opencodeModels,
|
||||
supportsLocalAgentJwt: true,
|
||||
agentConfigurationDoc: opencodeAgentConfigurationDoc,
|
||||
};
|
||||
|
||||
const cursorLocalAdapter: ServerAdapterModule = {
|
||||
type: "cursor",
|
||||
execute: cursorExecute,
|
||||
@@ -89,8 +82,19 @@ const openclawAdapter: ServerAdapterModule = {
|
||||
agentConfigurationDoc: openclawAgentConfigurationDoc,
|
||||
};
|
||||
|
||||
const openCodeLocalAdapter: ServerAdapterModule = {
|
||||
type: "opencode_local",
|
||||
execute: openCodeExecute,
|
||||
testEnvironment: openCodeTestEnvironment,
|
||||
sessionCodec: openCodeSessionCodec,
|
||||
models: [],
|
||||
listModels: listOpenCodeModels,
|
||||
supportsLocalAgentJwt: true,
|
||||
agentConfigurationDoc: openCodeAgentConfigurationDoc,
|
||||
};
|
||||
|
||||
const adaptersByType = new Map<string, ServerAdapterModule>(
|
||||
[claudeLocalAdapter, codexLocalAdapter, opencodeLocalAdapter, cursorLocalAdapter, openclawAdapter, processAdapter, httpAdapter].map((a) => [a.type, a]),
|
||||
[claudeLocalAdapter, codexLocalAdapter, openCodeLocalAdapter, cursorLocalAdapter, openclawAdapter, processAdapter, httpAdapter].map((a) => [a.type, a]),
|
||||
);
|
||||
|
||||
export function getServerAdapter(type: string): ServerAdapterModule {
|
||||
|
||||
@@ -37,7 +37,7 @@ import {
|
||||
DEFAULT_CODEX_LOCAL_MODEL,
|
||||
} from "@paperclipai/adapter-codex-local";
|
||||
import { DEFAULT_CURSOR_LOCAL_MODEL } from "@paperclipai/adapter-cursor-local";
|
||||
import { DEFAULT_OPENCODE_LOCAL_MODEL } from "@paperclipai/adapter-opencode-local";
|
||||
import { ensureOpenCodeModelConfiguredAndAvailable } from "@paperclipai/adapter-opencode-local/server";
|
||||
|
||||
export function agentRoutes(db: Db) {
|
||||
const DEFAULT_INSTRUCTIONS_PATH_KEYS: Record<string, string> = {
|
||||
@@ -198,15 +198,34 @@ export function agentRoutes(db: Db) {
|
||||
}
|
||||
return next;
|
||||
}
|
||||
if (adapterType === "opencode_local" && !asNonEmptyString(next.model)) {
|
||||
next.model = DEFAULT_OPENCODE_LOCAL_MODEL;
|
||||
}
|
||||
// OpenCode requires explicit model selection — no default
|
||||
if (adapterType === "cursor" && !asNonEmptyString(next.model)) {
|
||||
next.model = DEFAULT_CURSOR_LOCAL_MODEL;
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
async function assertAdapterConfigConstraints(
|
||||
companyId: string,
|
||||
adapterType: string | null | undefined,
|
||||
adapterConfig: Record<string, unknown>,
|
||||
) {
|
||||
if (adapterType !== "opencode_local") return;
|
||||
const runtimeConfig = await secretsSvc.resolveAdapterConfigForRuntime(companyId, adapterConfig);
|
||||
const runtimeEnv = asRecord(runtimeConfig.env) ?? {};
|
||||
try {
|
||||
await ensureOpenCodeModelConfiguredAndAvailable({
|
||||
model: runtimeConfig.model,
|
||||
command: runtimeConfig.command,
|
||||
cwd: runtimeConfig.cwd,
|
||||
env: runtimeEnv,
|
||||
});
|
||||
} catch (err) {
|
||||
const reason = err instanceof Error ? err.message : String(err);
|
||||
throw unprocessable(`Invalid opencode_local adapterConfig: ${reason}`);
|
||||
}
|
||||
}
|
||||
|
||||
function resolveInstructionsFilePath(candidatePath: string, adapterConfig: Record<string, unknown>) {
|
||||
const trimmed = candidatePath.trim();
|
||||
if (path.isAbsolute(trimmed)) return trimmed;
|
||||
@@ -338,7 +357,9 @@ export function agentRoutes(db: Db) {
|
||||
}
|
||||
});
|
||||
|
||||
router.get("/adapters/:type/models", async (req, res) => {
|
||||
router.get("/companies/:companyId/adapters/:type/models", async (req, res) => {
|
||||
const companyId = req.params.companyId as string;
|
||||
assertCompanyAccess(req, companyId);
|
||||
const type = req.params.type as string;
|
||||
const models = await listAdapterModels(type);
|
||||
res.json(models);
|
||||
@@ -592,6 +613,11 @@ export function agentRoutes(db: Db) {
|
||||
requestedAdapterConfig,
|
||||
{ strictMode: strictSecretsMode },
|
||||
);
|
||||
await assertAdapterConfigConstraints(
|
||||
companyId,
|
||||
hireInput.adapterType,
|
||||
normalizedAdapterConfig,
|
||||
);
|
||||
const normalizedHireInput = {
|
||||
...hireInput,
|
||||
adapterConfig: normalizedAdapterConfig,
|
||||
@@ -727,6 +753,11 @@ export function agentRoutes(db: Db) {
|
||||
requestedAdapterConfig,
|
||||
{ strictMode: strictSecretsMode },
|
||||
);
|
||||
await assertAdapterConfigConstraints(
|
||||
companyId,
|
||||
req.body.adapterType,
|
||||
normalizedAdapterConfig,
|
||||
);
|
||||
|
||||
const agent = await svc.create(companyId, {
|
||||
...req.body,
|
||||
@@ -906,6 +937,27 @@ export function agentRoutes(db: Db) {
|
||||
);
|
||||
}
|
||||
|
||||
const requestedAdapterType =
|
||||
typeof patchData.adapterType === "string" ? patchData.adapterType : existing.adapterType;
|
||||
const touchesAdapterConfiguration =
|
||||
Object.prototype.hasOwnProperty.call(patchData, "adapterType") ||
|
||||
Object.prototype.hasOwnProperty.call(patchData, "adapterConfig");
|
||||
if (touchesAdapterConfiguration && requestedAdapterType === "opencode_local") {
|
||||
const rawEffectiveAdapterConfig = Object.prototype.hasOwnProperty.call(patchData, "adapterConfig")
|
||||
? (asRecord(patchData.adapterConfig) ?? {})
|
||||
: (asRecord(existing.adapterConfig) ?? {});
|
||||
const effectiveAdapterConfig = await secretsSvc.normalizeAdapterConfigForPersistence(
|
||||
existing.companyId,
|
||||
rawEffectiveAdapterConfig,
|
||||
{ strictMode: strictSecretsMode },
|
||||
);
|
||||
await assertAdapterConfigConstraints(
|
||||
existing.companyId,
|
||||
requestedAdapterType,
|
||||
effectiveAdapterConfig,
|
||||
);
|
||||
}
|
||||
|
||||
const actor = getActorInfo(req);
|
||||
const agent = await svc.update(id, patchData, {
|
||||
recordRevision: {
|
||||
|
||||
Reference in New Issue
Block a user