Fix OpenCode default model and model-unavailable diagnostics
This commit is contained in:
@@ -1,17 +1,12 @@
|
|||||||
export const type = "opencode_local";
|
export const type = "opencode_local";
|
||||||
export const label = "OpenCode (local)";
|
export const label = "OpenCode (local)";
|
||||||
export const DEFAULT_OPENCODE_LOCAL_MODEL = "openai/gpt-5.3-codex";
|
export const DEFAULT_OPENCODE_LOCAL_MODEL = "openai/gpt-5.2-codex";
|
||||||
|
|
||||||
export const models = [
|
export const models = [
|
||||||
{ id: DEFAULT_OPENCODE_LOCAL_MODEL, label: DEFAULT_OPENCODE_LOCAL_MODEL },
|
{ id: DEFAULT_OPENCODE_LOCAL_MODEL, label: DEFAULT_OPENCODE_LOCAL_MODEL },
|
||||||
{ id: "openai/gpt-5.3-codex-spark", label: "openai/gpt-5.3-codex-spark" },
|
{ id: "openai/gpt-5.2", label: "openai/gpt-5.2" },
|
||||||
{ id: "openai/gpt-5.2-codex", label: "openai/gpt-5.2-codex" },
|
{ id: "openai/gpt-5.1-codex-max", label: "openai/gpt-5.1-codex-max" },
|
||||||
{ id: "openai/gpt-5.1-codex", label: "openai/gpt-5.1-codex" },
|
{ id: "openai/gpt-5.1-codex-mini", label: "openai/gpt-5.1-codex-mini" },
|
||||||
{ id: "openai/gpt-5-codex", label: "openai/gpt-5-codex" },
|
|
||||||
{ id: "openai/codex-mini-latest", label: "openai/codex-mini-latest" },
|
|
||||||
{ id: "openai/gpt-5", label: "openai/gpt-5" },
|
|
||||||
{ id: "openai/o3", label: "openai/o3" },
|
|
||||||
{ id: "openai/o4-mini", label: "openai/o4-mini" },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
export const agentConfigurationDoc = `# opencode_local agent configuration
|
export const agentConfigurationDoc = `# opencode_local agent configuration
|
||||||
@@ -31,7 +26,7 @@ Don't use when:
|
|||||||
Core fields:
|
Core fields:
|
||||||
- cwd (string, optional): default absolute working directory fallback for the agent process (created if missing when possible)
|
- cwd (string, optional): default absolute working directory fallback for the agent process (created if missing when possible)
|
||||||
- instructionsFilePath (string, optional): absolute path to a markdown instructions file prepended to the run prompt
|
- instructionsFilePath (string, optional): absolute path to a markdown instructions file prepended to the run prompt
|
||||||
- model (string, optional): OpenCode model id in provider/model format (for example openai/gpt-5.3-codex)
|
- model (string, optional): OpenCode model id in provider/model format (for example openai/gpt-5.2-codex)
|
||||||
- variant (string, optional): provider-specific reasoning/profile variant passed as --variant
|
- variant (string, optional): provider-specific reasoning/profile variant passed as --variant
|
||||||
- promptTemplate (string, optional): run prompt template
|
- promptTemplate (string, optional): run prompt template
|
||||||
- command (string, optional): defaults to "opencode"
|
- command (string, optional): defaults to "opencode"
|
||||||
|
|||||||
@@ -59,28 +59,58 @@ function resolveProviderFromModel(model: string): string | null {
|
|||||||
return trimmed.slice(0, slash).toLowerCase();
|
return trimmed.slice(0, slash).toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveProviderCredentialKey(provider: string | null): string | null {
|
|
||||||
if (!provider) return null;
|
|
||||||
switch (provider) {
|
|
||||||
case "openai":
|
|
||||||
return "OPENAI_API_KEY";
|
|
||||||
case "anthropic":
|
|
||||||
return "ANTHROPIC_API_KEY";
|
|
||||||
case "openrouter":
|
|
||||||
return "OPENROUTER_API_KEY";
|
|
||||||
case "google":
|
|
||||||
case "gemini":
|
|
||||||
return "GEMINI_API_KEY";
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function isProviderModelNotFoundFailure(stdout: string, stderr: string): boolean {
|
function isProviderModelNotFoundFailure(stdout: string, stderr: string): boolean {
|
||||||
const haystack = `${stdout}\n${stderr}`;
|
const haystack = `${stdout}\n${stderr}`;
|
||||||
return /ProviderModelNotFoundError|provider model not found/i.test(haystack);
|
return /ProviderModelNotFoundError|provider model not found/i.test(haystack);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ProviderModelNotFoundDetails = {
|
||||||
|
providerId: string | null;
|
||||||
|
modelId: string | null;
|
||||||
|
suggestions: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
function parseProviderModelNotFoundDetails(
|
||||||
|
stdout: string,
|
||||||
|
stderr: string,
|
||||||
|
): ProviderModelNotFoundDetails | null {
|
||||||
|
if (!isProviderModelNotFoundFailure(stdout, stderr)) return null;
|
||||||
|
const haystack = `${stdout}\n${stderr}`;
|
||||||
|
|
||||||
|
const providerMatch = haystack.match(/providerID:\s*"([^"]+)"/i);
|
||||||
|
const modelMatch = haystack.match(/modelID:\s*"([^"]+)"/i);
|
||||||
|
const suggestionsMatch = haystack.match(/suggestions:\s*\[([^\]]*)\]/i);
|
||||||
|
const suggestions = suggestionsMatch
|
||||||
|
? Array.from(
|
||||||
|
suggestionsMatch[1].matchAll(/"([^"]+)"/g),
|
||||||
|
(match) => match[1].trim(),
|
||||||
|
).filter((value) => value.length > 0)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
return {
|
||||||
|
providerId: providerMatch?.[1]?.trim().toLowerCase() || null,
|
||||||
|
modelId: modelMatch?.[1]?.trim() || null,
|
||||||
|
suggestions,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatModelNotFoundError(
|
||||||
|
model: string,
|
||||||
|
providerFromModel: string | null,
|
||||||
|
details: ProviderModelNotFoundDetails | null,
|
||||||
|
): string {
|
||||||
|
const provider = details?.providerId || providerFromModel || "unknown";
|
||||||
|
const missingModel = details?.modelId || model;
|
||||||
|
const suggestions = details?.suggestions ?? [];
|
||||||
|
const suggestionText =
|
||||||
|
suggestions.length > 0 ? ` Suggested models: ${suggestions.map((value) => `\`${value}\``).join(", ")}.` : "";
|
||||||
|
return (
|
||||||
|
`OpenCode model \`${missingModel}\` is unavailable for provider \`${provider}\`.` +
|
||||||
|
` Run \`opencode models ${provider}\` and set adapterConfig.model to a supported value.` +
|
||||||
|
suggestionText
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function claudeSkillsHome(): string {
|
function claudeSkillsHome(): string {
|
||||||
return path.join(os.homedir(), ".claude", "skills");
|
return path.join(os.homedir(), ".claude", "skills");
|
||||||
}
|
}
|
||||||
@@ -372,13 +402,9 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||||||
: null;
|
: null;
|
||||||
const parsedError = typeof attempt.parsed.errorMessage === "string" ? attempt.parsed.errorMessage.trim() : "";
|
const parsedError = typeof attempt.parsed.errorMessage === "string" ? attempt.parsed.errorMessage.trim() : "";
|
||||||
const stderrLine = firstNonEmptyLine(attempt.proc.stderr);
|
const stderrLine = firstNonEmptyLine(attempt.proc.stderr);
|
||||||
const providerCredentialKey = resolveProviderCredentialKey(providerFromModel);
|
const modelNotFound = parseProviderModelNotFoundDetails(attempt.proc.stdout, attempt.proc.stderr);
|
||||||
const missingProviderCredential =
|
const fallbackErrorMessage = modelNotFound
|
||||||
providerCredentialKey !== null &&
|
? formatModelNotFoundError(model, providerFromModel, modelNotFound)
|
||||||
!hasEffectiveEnvValue(env, providerCredentialKey) &&
|
|
||||||
isProviderModelNotFoundFailure(attempt.proc.stdout, attempt.proc.stderr);
|
|
||||||
const fallbackErrorMessage = missingProviderCredential
|
|
||||||
? `OpenCode provider "${providerFromModel}" is not configured. Set ${providerCredentialKey} or authenticate with \`opencode auth login\`.`
|
|
||||||
: parsedError ||
|
: parsedError ||
|
||||||
stderrLine ||
|
stderrLine ||
|
||||||
`OpenCode exited with code ${attempt.proc.exitCode ?? -1}`;
|
`OpenCode exited with code ${attempt.proc.exitCode ?? -1}`;
|
||||||
|
|||||||
@@ -58,7 +58,8 @@ function summarizeProbeDetail(stdout: string, stderr: string, parsedError: strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
const OPENCODE_AUTH_REQUIRED_RE =
|
const OPENCODE_AUTH_REQUIRED_RE =
|
||||||
/(?:not\s+authenticated|authentication\s+required|unauthorized|forbidden|api(?:[_\s-]?key)?(?:\s+is)?\s+required|missing\s+api(?:[_\s-]?key)?|openai[_\s-]?api[_\s-]?key|provider\s+credentials|provider\s+model\s+not\s+found|ProviderModelNotFoundError|login\s+required)/i;
|
/(?:not\s+authenticated|authentication\s+required|unauthorized|forbidden|api(?:[_\s-]?key)?(?:\s+is)?\s+required|missing\s+api(?:[_\s-]?key)?|openai[_\s-]?api[_\s-]?key|provider\s+credentials|login\s+required)/i;
|
||||||
|
const OPENCODE_MODEL_NOT_FOUND_RE = /ProviderModelNotFoundError|provider\s+model\s+not\s+found/i;
|
||||||
|
|
||||||
export async function testEnvironment(
|
export async function testEnvironment(
|
||||||
ctx: AdapterEnvironmentTestContext,
|
ctx: AdapterEnvironmentTestContext,
|
||||||
@@ -123,7 +124,7 @@ export async function testEnvironment(
|
|||||||
message: "OPENAI_API_KEY is not set. OpenCode runs may fail until authentication is configured.",
|
message: "OPENAI_API_KEY is not set. OpenCode runs may fail until authentication is configured.",
|
||||||
hint: configDefinesOpenAiKey
|
hint: configDefinesOpenAiKey
|
||||||
? "adapterConfig.env defines OPENAI_API_KEY but it is empty. Set a non-empty value or remove the override."
|
? "adapterConfig.env defines OPENAI_API_KEY but it is empty. Set a non-empty value or remove the override."
|
||||||
: "Set OPENAI_API_KEY in adapter env or shell environment.",
|
: "Set OPENAI_API_KEY in adapter env/shell, or authenticate with `opencode auth login`.",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,6 +169,12 @@ export async function testEnvironment(
|
|||||||
const parsed = parseOpenCodeJsonl(probe.stdout);
|
const parsed = parseOpenCodeJsonl(probe.stdout);
|
||||||
const detail = summarizeProbeDetail(probe.stdout, probe.stderr, parsed.errorMessage);
|
const detail = summarizeProbeDetail(probe.stdout, probe.stderr, parsed.errorMessage);
|
||||||
const authEvidence = `${parsed.errorMessage ?? ""}\n${probe.stdout}\n${probe.stderr}`.trim();
|
const authEvidence = `${parsed.errorMessage ?? ""}\n${probe.stdout}\n${probe.stderr}`.trim();
|
||||||
|
const modelNotFound = OPENCODE_MODEL_NOT_FOUND_RE.test(authEvidence);
|
||||||
|
const modelProvider = (() => {
|
||||||
|
const slash = model.indexOf("/");
|
||||||
|
if (slash <= 0) return "openai";
|
||||||
|
return model.slice(0, slash).toLowerCase();
|
||||||
|
})();
|
||||||
|
|
||||||
if (probe.timedOut) {
|
if (probe.timedOut) {
|
||||||
checks.push({
|
checks.push({
|
||||||
@@ -192,6 +199,14 @@ export async function testEnvironment(
|
|||||||
hint: "Try `opencode run --format json \"Respond with hello\"` manually to inspect full output.",
|
hint: "Try `opencode run --format json \"Respond with hello\"` manually to inspect full output.",
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
} else if (modelNotFound) {
|
||||||
|
checks.push({
|
||||||
|
code: "opencode_hello_probe_model_unavailable",
|
||||||
|
level: "warn",
|
||||||
|
message: `OpenCode could not run model \`${model}\`.`,
|
||||||
|
...(detail ? { detail } : {}),
|
||||||
|
hint: `Run \`opencode models ${modelProvider}\` and set adapterConfig.model to one of the available models.`,
|
||||||
|
});
|
||||||
} 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",
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ describe("opencode_local environment diagnostics", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("classifies ProviderModelNotFoundError probe output as auth-required warning", async () => {
|
it("classifies ProviderModelNotFoundError probe output as model-unavailable warning", async () => {
|
||||||
const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-opencode-env-probe-cwd-"));
|
const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-opencode-env-probe-cwd-"));
|
||||||
const binDir = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-opencode-env-probe-bin-"));
|
const binDir = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-opencode-env-probe-bin-"));
|
||||||
const fakeOpencode = path.join(binDir, "opencode");
|
const fakeOpencode = path.join(binDir, "opencode");
|
||||||
@@ -86,9 +86,9 @@ describe("opencode_local environment diagnostics", () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const authCheck = result.checks.find((check) => check.code === "opencode_hello_probe_auth_required");
|
const modelCheck = result.checks.find((check) => check.code === "opencode_hello_probe_model_unavailable");
|
||||||
expect(authCheck).toBeTruthy();
|
expect(modelCheck).toBeTruthy();
|
||||||
expect(authCheck?.level).toBe("warn");
|
expect(modelCheck?.level).toBe("warn");
|
||||||
expect(result.status).toBe("warn");
|
expect(result.status).toBe("warn");
|
||||||
} finally {
|
} finally {
|
||||||
await fs.rm(cwd, { recursive: true, force: true });
|
await fs.rm(cwd, { recursive: true, force: true });
|
||||||
|
|||||||
Reference in New Issue
Block a user