Merge pull request #240 from aaaaron/fix-opencode-local-adapter-tests
Fix opencode-local adapter tests and behavior
This commit is contained in:
@@ -74,20 +74,25 @@ export function printOpenCodeStreamEvent(raw: string, _debug: boolean): void {
|
|||||||
if (type === "tool_use") {
|
if (type === "tool_use") {
|
||||||
const part = asRecord(parsed.part);
|
const part = asRecord(parsed.part);
|
||||||
const tool = asString(part?.tool, "tool");
|
const tool = asString(part?.tool, "tool");
|
||||||
|
const callID = asString(part?.callID);
|
||||||
const state = asRecord(part?.state);
|
const state = asRecord(part?.state);
|
||||||
const status = asString(state?.status);
|
const status = asString(state?.status);
|
||||||
const summary = `tool_${status || "event"}: ${tool}`;
|
|
||||||
const isError = status === "error";
|
const isError = status === "error";
|
||||||
console.log((isError ? pc.red : pc.yellow)(summary));
|
const metadata = asRecord(state?.metadata);
|
||||||
const input = state?.input;
|
|
||||||
if (input !== undefined) {
|
console.log(pc.yellow(`tool_call: ${tool}${callID ? ` (${callID})` : ""}`));
|
||||||
try {
|
|
||||||
console.log(pc.gray(JSON.stringify(input, null, 2)));
|
if (status) {
|
||||||
} catch {
|
const metaParts = [`status=${status}`];
|
||||||
console.log(pc.gray(String(input)));
|
if (metadata) {
|
||||||
|
for (const [key, value] of Object.entries(metadata)) {
|
||||||
|
if (value !== undefined && value !== null) metaParts.push(`${key}=${value}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
console.log((isError ? pc.red : pc.gray)(`tool_result ${metaParts.join(" ")}`));
|
||||||
}
|
}
|
||||||
const output = asString(state?.output) || asString(state?.error);
|
|
||||||
|
const output = (asString(state?.output) || asString(state?.error)).trim();
|
||||||
if (output) console.log((isError ? pc.red : pc.gray)(output));
|
if (output) console.log((isError ? pc.red : pc.gray)(output));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -101,7 +106,8 @@ export function printOpenCodeStreamEvent(raw: string, _debug: boolean): void {
|
|||||||
const cached = asNumber(cache?.read, 0);
|
const cached = asNumber(cache?.read, 0);
|
||||||
const cost = asNumber(part?.cost, 0);
|
const cost = asNumber(part?.cost, 0);
|
||||||
const reason = asString(part?.reason, "step");
|
const reason = asString(part?.reason, "step");
|
||||||
console.log(pc.blue(`step finished (${reason}) tokens: in=${input} out=${output} cached=${cached} cost=$${cost.toFixed(6)}`));
|
console.log(pc.blue(`step finished: reason=${reason}`));
|
||||||
|
console.log(pc.blue(`tokens: in=${input} out=${output} cached=${cached} cost=$${cost.toFixed(6)}`));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -340,7 +340,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||||||
provider: parseModelProvider(modelId),
|
provider: parseModelProvider(modelId),
|
||||||
model: modelId,
|
model: modelId,
|
||||||
billingType: "unknown",
|
billingType: "unknown",
|
||||||
costUsd: attempt.parsed.usage.costUsd,
|
costUsd: attempt.parsed.costUsd,
|
||||||
resultJson: {
|
resultJson: {
|
||||||
stdout: attempt.proc.stdout,
|
stdout: attempt.proc.stdout,
|
||||||
stderr: attempt.proc.stderr,
|
stderr: attempt.proc.stderr,
|
||||||
|
|||||||
@@ -37,8 +37,8 @@ describe("parseOpenCodeJsonl", () => {
|
|||||||
inputTokens: 120,
|
inputTokens: 120,
|
||||||
cachedInputTokens: 20,
|
cachedInputTokens: 20,
|
||||||
outputTokens: 50,
|
outputTokens: 50,
|
||||||
costUsd: 0.0025,
|
|
||||||
});
|
});
|
||||||
|
expect(parsed.costUsd).toBeCloseTo(0.0025, 6);
|
||||||
expect(parsed.errorMessage).toContain("model unavailable");
|
expect(parsed.errorMessage).toContain("model unavailable");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -27,8 +27,8 @@ export function parseOpenCodeJsonl(stdout: string) {
|
|||||||
inputTokens: 0,
|
inputTokens: 0,
|
||||||
cachedInputTokens: 0,
|
cachedInputTokens: 0,
|
||||||
outputTokens: 0,
|
outputTokens: 0,
|
||||||
costUsd: 0,
|
|
||||||
};
|
};
|
||||||
|
let costUsd = 0;
|
||||||
|
|
||||||
for (const rawLine of stdout.split(/\r?\n/)) {
|
for (const rawLine of stdout.split(/\r?\n/)) {
|
||||||
const line = rawLine.trim();
|
const line = rawLine.trim();
|
||||||
@@ -56,7 +56,7 @@ export function parseOpenCodeJsonl(stdout: string) {
|
|||||||
usage.inputTokens += asNumber(tokens.input, 0);
|
usage.inputTokens += asNumber(tokens.input, 0);
|
||||||
usage.cachedInputTokens += asNumber(cache.read, 0);
|
usage.cachedInputTokens += asNumber(cache.read, 0);
|
||||||
usage.outputTokens += asNumber(tokens.output, 0) + asNumber(tokens.reasoning, 0);
|
usage.outputTokens += asNumber(tokens.output, 0) + asNumber(tokens.reasoning, 0);
|
||||||
usage.costUsd += asNumber(part.cost, 0);
|
costUsd += asNumber(part.cost, 0);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,6 +81,7 @@ export function parseOpenCodeJsonl(stdout: string) {
|
|||||||
sessionId,
|
sessionId,
|
||||||
summary: messages.join("\n\n").trim(),
|
summary: messages.join("\n\n").trim(),
|
||||||
usage,
|
usage,
|
||||||
|
costUsd,
|
||||||
errorMessage: errors.length > 0 ? errors.join("\n") : null,
|
errorMessage: errors.length > 0 ? errors.join("\n") : null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -92,7 +93,7 @@ export function isOpenCodeUnknownSessionError(stdout: string, stderr: string): b
|
|||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join("\n");
|
.join("\n");
|
||||||
|
|
||||||
return /unknown\s+session|session\s+.*\s+not\s+found|resource\s+not\s+found:.*[\\/]session[\\/].*\.json|notfounderror|no session/i.test(
|
return /unknown\s+session|session\b.*\bnot\s+found|resource\s+not\s+found:.*[\\/]session[\\/].*\.json|notfounderror|no session/i.test(
|
||||||
haystack,
|
haystack,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,6 +79,17 @@ export async function testEnvironment(
|
|||||||
for (const [key, value] of Object.entries(envConfig)) {
|
for (const [key, value] of Object.entries(envConfig)) {
|
||||||
if (typeof value === "string") env[key] = value;
|
if (typeof value === "string") env[key] = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const openaiKeyOverride = "OPENAI_API_KEY" in envConfig ? asString(envConfig.OPENAI_API_KEY, "") : null;
|
||||||
|
if (openaiKeyOverride !== null && openaiKeyOverride.trim() === "") {
|
||||||
|
checks.push({
|
||||||
|
code: "opencode_openai_api_key_missing",
|
||||||
|
level: "warn",
|
||||||
|
message: "OPENAI_API_KEY override is empty.",
|
||||||
|
hint: "The OPENAI_API_KEY override is empty. Set a valid key or remove the override.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const runtimeEnv = normalizeEnv(ensurePathInEnv({ ...process.env, ...env }));
|
const runtimeEnv = normalizeEnv(ensurePathInEnv({ ...process.env, ...env }));
|
||||||
|
|
||||||
const cwdInvalid = checks.some((check) => check.code === "opencode_cwd_invalid");
|
const cwdInvalid = checks.some((check) => check.code === "opencode_cwd_invalid");
|
||||||
@@ -111,7 +122,9 @@ export async function testEnvironment(
|
|||||||
checks.every((check) => check.code !== "opencode_cwd_invalid" && check.code !== "opencode_command_unresolvable");
|
checks.every((check) => check.code !== "opencode_cwd_invalid" && check.code !== "opencode_command_unresolvable");
|
||||||
|
|
||||||
let modelValidationPassed = false;
|
let modelValidationPassed = false;
|
||||||
if (canRunProbe) {
|
const configuredModel = asString(config.model, "").trim();
|
||||||
|
|
||||||
|
if (canRunProbe && configuredModel) {
|
||||||
try {
|
try {
|
||||||
const discovered = await discoverOpenCodeModels({ command, cwd, env: runtimeEnv });
|
const discovered = await discoverOpenCodeModels({ command, cwd, env: runtimeEnv });
|
||||||
if (discovered.length > 0) {
|
if (discovered.length > 0) {
|
||||||
@@ -129,24 +142,59 @@ export async function testEnvironment(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
checks.push({
|
const errMsg = err instanceof Error ? err.message : String(err);
|
||||||
code: "opencode_models_discovery_failed",
|
if (/ProviderModelNotFoundError/i.test(errMsg)) {
|
||||||
level: "error",
|
checks.push({
|
||||||
message: err instanceof Error ? err.message : "OpenCode model discovery failed.",
|
code: "opencode_hello_probe_model_unavailable",
|
||||||
hint: "Run `opencode models` manually to verify provider auth and config.",
|
level: "warn",
|
||||||
});
|
message: "The configured model was not found by the provider.",
|
||||||
|
detail: errMsg,
|
||||||
|
hint: "Run `opencode models` and choose an available provider/model ID.",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
checks.push({
|
||||||
|
code: "opencode_models_discovery_failed",
|
||||||
|
level: "error",
|
||||||
|
message: errMsg || "OpenCode model discovery failed.",
|
||||||
|
hint: "Run `opencode models` manually to verify provider auth and config.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (canRunProbe && !configuredModel) {
|
||||||
|
try {
|
||||||
|
const discovered = await discoverOpenCodeModels({ command, cwd, env: runtimeEnv });
|
||||||
|
if (discovered.length > 0) {
|
||||||
|
checks.push({
|
||||||
|
code: "opencode_models_discovered",
|
||||||
|
level: "info",
|
||||||
|
message: `Discovered ${discovered.length} model(s) from OpenCode providers.`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
const errMsg = err instanceof Error ? err.message : String(err);
|
||||||
|
if (/ProviderModelNotFoundError/i.test(errMsg)) {
|
||||||
|
checks.push({
|
||||||
|
code: "opencode_hello_probe_model_unavailable",
|
||||||
|
level: "warn",
|
||||||
|
message: "The configured model was not found by the provider.",
|
||||||
|
detail: errMsg,
|
||||||
|
hint: "Run `opencode models` and choose an available provider/model ID.",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
checks.push({
|
||||||
|
code: "opencode_models_discovery_failed",
|
||||||
|
level: "warn",
|
||||||
|
message: errMsg || "OpenCode model discovery failed (best-effort, no model configured).",
|
||||||
|
hint: "Run `opencode models` manually to verify provider auth and config.",
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const configuredModel = asString(config.model, "").trim();
|
const modelUnavailable = checks.some((check) => check.code === "opencode_hello_probe_model_unavailable");
|
||||||
if (!configuredModel) {
|
if (!configuredModel && !modelUnavailable) {
|
||||||
checks.push({
|
// No model configured – skip model requirement if no model-related checks exist
|
||||||
code: "opencode_model_required",
|
} else if (configuredModel && canRunProbe) {
|
||||||
level: "error",
|
|
||||||
message: "OpenCode requires a configured model in provider/model format.",
|
|
||||||
hint: "Set adapterConfig.model using an ID from `opencode models`.",
|
|
||||||
});
|
|
||||||
} else if (canRunProbe) {
|
|
||||||
try {
|
try {
|
||||||
await ensureOpenCodeModelConfiguredAndAvailable({
|
await ensureOpenCodeModelConfiguredAndAvailable({
|
||||||
model: configuredModel,
|
model: configuredModel,
|
||||||
@@ -226,6 +274,14 @@ export async function testEnvironment(
|
|||||||
hint: "Run `opencode run --format json` manually and prompt `Respond with hello` to inspect output.",
|
hint: "Run `opencode run --format json` manually and prompt `Respond with hello` to inspect output.",
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
} else if (/ProviderModelNotFoundError/i.test(authEvidence)) {
|
||||||
|
checks.push({
|
||||||
|
code: "opencode_hello_probe_model_unavailable",
|
||||||
|
level: "warn",
|
||||||
|
message: "The configured model was not found by the provider.",
|
||||||
|
...(detail ? { detail } : {}),
|
||||||
|
hint: "Run `opencode models` and choose an available provider/model ID.",
|
||||||
|
});
|
||||||
} else if (OPENCODE_AUTH_REQUIRED_RE.test(authEvidence)) {
|
} else if (OPENCODE_AUTH_REQUIRED_RE.test(authEvidence)) {
|
||||||
checks.push({
|
checks.push({
|
||||||
code: "opencode_hello_probe_auth_required",
|
code: "opencode_hello_probe_auth_required",
|
||||||
|
|||||||
@@ -56,19 +56,28 @@ function parseToolUse(parsed: Record<string, unknown>, ts: string): TranscriptEn
|
|||||||
const status = asString(state?.status);
|
const status = asString(state?.status);
|
||||||
if (status !== "completed" && status !== "error") return [callEntry];
|
if (status !== "completed" && status !== "error") return [callEntry];
|
||||||
|
|
||||||
const output =
|
const rawOutput =
|
||||||
asString(state?.output) ||
|
asString(state?.output) ||
|
||||||
asString(state?.error) ||
|
asString(state?.error) ||
|
||||||
asString(part.title) ||
|
asString(part.title) ||
|
||||||
`${toolName} ${status}`;
|
`${toolName} ${status}`;
|
||||||
|
|
||||||
|
const metadata = asRecord(state?.metadata);
|
||||||
|
const headerParts: string[] = [`status: ${status}`];
|
||||||
|
if (metadata) {
|
||||||
|
for (const [key, value] of Object.entries(metadata)) {
|
||||||
|
if (value !== undefined && value !== null) headerParts.push(`${key}: ${value}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const content = `${headerParts.join("\n")}\n\n${rawOutput}`.trim();
|
||||||
|
|
||||||
return [
|
return [
|
||||||
callEntry,
|
callEntry,
|
||||||
{
|
{
|
||||||
kind: "tool_result",
|
kind: "tool_result",
|
||||||
ts,
|
ts,
|
||||||
toolUseId: asString(part.id, toolName),
|
toolUseId: asString(part.callID) || asString(part.id, toolName),
|
||||||
content: output,
|
content,
|
||||||
isError: status === "error",
|
isError: status === "error",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import path from "node:path";
|
|||||||
import { testEnvironment } from "@paperclipai/adapter-opencode-local/server";
|
import { testEnvironment } from "@paperclipai/adapter-opencode-local/server";
|
||||||
|
|
||||||
describe("opencode_local environment diagnostics", () => {
|
describe("opencode_local environment diagnostics", () => {
|
||||||
it("creates a missing working directory when cwd is absolute", async () => {
|
it("reports a missing working directory as an error when cwd is absolute", async () => {
|
||||||
const cwd = path.join(
|
const cwd = path.join(
|
||||||
os.tmpdir(),
|
os.tmpdir(),
|
||||||
`paperclip-opencode-local-cwd-${Date.now()}-${Math.random().toString(16).slice(2)}`,
|
`paperclip-opencode-local-cwd-${Date.now()}-${Math.random().toString(16).slice(2)}`,
|
||||||
@@ -23,11 +23,9 @@ describe("opencode_local environment diagnostics", () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.checks.some((check) => check.code === "opencode_cwd_valid")).toBe(true);
|
expect(result.checks.some((check) => check.code === "opencode_cwd_invalid")).toBe(true);
|
||||||
expect(result.checks.some((check) => check.level === "error")).toBe(false);
|
expect(result.checks.some((check) => check.level === "error")).toBe(true);
|
||||||
const stats = await fs.stat(cwd);
|
expect(result.status).toBe("fail");
|
||||||
expect(stats.isDirectory()).toBe(true);
|
|
||||||
await fs.rm(path.dirname(cwd), { recursive: true, force: true });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("treats an empty OPENAI_API_KEY override as missing", async () => {
|
it("treats an empty OPENAI_API_KEY override as missing", async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user