Fix opencode-local adapter: parser, UI, CLI, and environment tests

- Move costUsd to top-level return field in parseOpenCodeJsonl (out of usage)
- Fix session-not-found regex to match "Session not found" pattern
- Use callID for toolUseId in UI stdout parser, add status/metadata header
- Fix CLI formatter: separate tool_call/tool_result lines, split step_finish
- Enable createIfMissing for cwd validation in environment tests
- Add empty OPENAI_API_KEY override detection
- Classify ProviderModelNotFoundError as warning during model discovery
- Make model discovery best-effort when no model is configured

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Aaron
2026-03-07 19:01:04 +00:00
parent 671a8ae554
commit 17058dd751
6 changed files with 100 additions and 35 deletions

View File

@@ -340,7 +340,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
provider: parseModelProvider(modelId),
model: modelId,
billingType: "unknown",
costUsd: attempt.parsed.usage.costUsd,
costUsd: attempt.parsed.costUsd,
resultJson: {
stdout: attempt.proc.stdout,
stderr: attempt.proc.stderr,

View File

@@ -37,8 +37,8 @@ describe("parseOpenCodeJsonl", () => {
inputTokens: 120,
cachedInputTokens: 20,
outputTokens: 50,
costUsd: 0.0025,
});
expect(parsed.costUsd).toBeCloseTo(0.0025, 6);
expect(parsed.errorMessage).toContain("model unavailable");
});

View File

@@ -27,8 +27,8 @@ export function parseOpenCodeJsonl(stdout: string) {
inputTokens: 0,
cachedInputTokens: 0,
outputTokens: 0,
costUsd: 0,
};
let costUsd = 0;
for (const rawLine of stdout.split(/\r?\n/)) {
const line = rawLine.trim();
@@ -56,7 +56,7 @@ export function parseOpenCodeJsonl(stdout: string) {
usage.inputTokens += asNumber(tokens.input, 0);
usage.cachedInputTokens += asNumber(cache.read, 0);
usage.outputTokens += asNumber(tokens.output, 0) + asNumber(tokens.reasoning, 0);
usage.costUsd += asNumber(part.cost, 0);
costUsd += asNumber(part.cost, 0);
continue;
}
@@ -81,6 +81,7 @@ export function parseOpenCodeJsonl(stdout: string) {
sessionId,
summary: messages.join("\n\n").trim(),
usage,
costUsd,
errorMessage: errors.length > 0 ? errors.join("\n") : null,
};
}
@@ -92,7 +93,7 @@ export function isOpenCodeUnknownSessionError(stdout: string, stderr: string): b
.filter(Boolean)
.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,
);
}

View File

@@ -59,7 +59,7 @@ export async function testEnvironment(
const cwd = asString(config.cwd, process.cwd());
try {
await ensureAbsoluteDirectory(cwd, { createIfMissing: false });
await ensureAbsoluteDirectory(cwd, { createIfMissing: true });
checks.push({
code: "opencode_cwd_valid",
level: "info",
@@ -79,6 +79,17 @@ export async function testEnvironment(
for (const [key, value] of Object.entries(envConfig)) {
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 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");
let modelValidationPassed = false;
if (canRunProbe) {
const configuredModel = asString(config.model, "").trim();
if (canRunProbe && configuredModel) {
try {
const discovered = await discoverOpenCodeModels({ command, cwd, env: runtimeEnv });
if (discovered.length > 0) {
@@ -129,24 +142,52 @@ export async function testEnvironment(
});
}
} catch (err) {
checks.push({
code: "opencode_models_discovery_failed",
level: "error",
message: err instanceof Error ? err.message : "OpenCode model discovery failed.",
hint: "Run `opencode models` manually to verify provider auth and config.",
});
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: "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.",
});
}
}
}
const configuredModel = asString(config.model, "").trim();
if (!configuredModel) {
checks.push({
code: "opencode_model_required",
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) {
const modelUnavailable = checks.some((check) => check.code === "opencode_hello_probe_model_unavailable");
if (!configuredModel && !modelUnavailable) {
// No model configured skip model requirement if no model-related checks exist
} else if (configuredModel && canRunProbe) {
try {
await ensureOpenCodeModelConfiguredAndAvailable({
model: configuredModel,
@@ -226,6 +267,14 @@ export async function testEnvironment(
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)) {
checks.push({
code: "opencode_hello_probe_auth_required",