Improve OpenCode auth diagnostics for model lookup failures

This commit is contained in:
Dotta
2026-03-05 07:29:31 -06:00
parent 9454f76c0c
commit 3ae112acff
3 changed files with 127 additions and 14 deletions

View File

@@ -34,13 +34,21 @@ function firstNonEmptyLine(text: string): string {
);
}
function hasNonEmptyEnvValue(env: Record<string, string>, key: string): boolean {
const raw = env[key];
return typeof raw === "string" && raw.trim().length > 0;
function getEffectiveEnvValue(envOverrides: Record<string, string>, key: string): string {
if (Object.prototype.hasOwnProperty.call(envOverrides, key)) {
const raw = envOverrides[key];
return typeof raw === "string" ? raw : "";
}
const raw = process.env[key];
return typeof raw === "string" ? raw : "";
}
function hasEffectiveEnvValue(envOverrides: Record<string, string>, key: string): boolean {
return getEffectiveEnvValue(envOverrides, key).trim().length > 0;
}
function resolveOpenCodeBillingType(env: Record<string, string>): "api" | "subscription" {
return hasNonEmptyEnvValue(env, "OPENAI_API_KEY") ? "api" : "subscription";
return hasEffectiveEnvValue(env, "OPENAI_API_KEY") ? "api" : "subscription";
}
function resolveProviderFromModel(model: string): string | null {
@@ -51,6 +59,28 @@ function resolveProviderFromModel(model: string): string | null {
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 {
const haystack = `${stdout}\n${stderr}`;
return /ProviderModelNotFoundError|provider model not found/i.test(haystack);
}
function claudeSkillsHome(): string {
return path.join(os.homedir(), ".claude", "skills");
}
@@ -342,10 +372,16 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
: null;
const parsedError = typeof attempt.parsed.errorMessage === "string" ? attempt.parsed.errorMessage.trim() : "";
const stderrLine = firstNonEmptyLine(attempt.proc.stderr);
const fallbackErrorMessage =
parsedError ||
stderrLine ||
`OpenCode exited with code ${attempt.proc.exitCode ?? -1}`;
const providerCredentialKey = resolveProviderCredentialKey(providerFromModel);
const missingProviderCredential =
providerCredentialKey !== null &&
!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 ||
stderrLine ||
`OpenCode exited with code ${attempt.proc.exitCode ?? -1}`;
return {
exitCode: attempt.proc.exitCode,

View File

@@ -26,6 +26,15 @@ function isNonEmpty(value: unknown): value is string {
return typeof value === "string" && value.trim().length > 0;
}
function getEffectiveEnvValue(envOverrides: Record<string, string>, key: string): string {
if (Object.prototype.hasOwnProperty.call(envOverrides, key)) {
const raw = envOverrides[key];
return typeof raw === "string" ? raw : "";
}
const raw = process.env[key];
return typeof raw === "string" ? raw : "";
}
function firstNonEmptyLine(text: string): string {
return (
text
@@ -49,7 +58,7 @@ function summarizeProbeDetail(stdout: string, stderr: string, parsedError: strin
}
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|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|provider\s+model\s+not\s+found|ProviderModelNotFoundError|login\s+required)/i;
export async function testEnvironment(
ctx: AdapterEnvironmentTestContext,
@@ -97,10 +106,10 @@ export async function testEnvironment(
});
}
const configOpenAiKey = env.OPENAI_API_KEY;
const hostOpenAiKey = process.env.OPENAI_API_KEY;
if (isNonEmpty(configOpenAiKey) || isNonEmpty(hostOpenAiKey)) {
const source = isNonEmpty(configOpenAiKey) ? "adapter config env" : "server environment";
const configDefinesOpenAiKey = Object.prototype.hasOwnProperty.call(env, "OPENAI_API_KEY");
const effectiveOpenAiKey = getEffectiveEnvValue(env, "OPENAI_API_KEY");
if (isNonEmpty(effectiveOpenAiKey)) {
const source = configDefinesOpenAiKey ? "adapter config env" : "server environment";
checks.push({
code: "opencode_openai_api_key_present",
level: "info",
@@ -112,7 +121,9 @@ export async function testEnvironment(
code: "opencode_openai_api_key_missing",
level: "warn",
message: "OPENAI_API_KEY is not set. OpenCode runs may fail until authentication is configured.",
hint: "Set OPENAI_API_KEY in adapter env or shell environment.",
hint: configDefinesOpenAiKey
? "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.",
});
}