different-ai/openwork · error
invalid_api_keys
invalid_api_keys
Error message
${error.message} What it means
resolveCredentialColumn delegates credential resolution to resolveProviderCredential, which throws ProviderCredentialError when the supplied apiKey/apiKeys/providerConfig cannot be resolved into a usable credential. This is translated into a 400 with code invalid_api_keys carrying the underlying error.message as the detail.
Source
Thrown at ee/apps/den-api/src/routes/org/llm-providers.ts:365
existingProvider: Pick<LlmProviderRow, "apiKey" | "providerConfig"> | null
apiKey?: string
apiKeys?: Record<string, string>
}) {
try {
return resolveProviderCredential({
envNames: readProviderEnvNames(input.providerConfig),
existing: input.existingProvider
? {
value: input.existingProvider.apiKey,
envNames: readProviderEnvNames(input.existingProvider.providerConfig ?? {}),
}
: null,
apiKey: input.apiKey,
apiKeys: input.apiKeys,
})
} catch (error) {
if (error instanceof ProviderCredentialError) {
throw createFailure(400, "invalid_api_keys", error.message)
}
throw error
}
}
function resolveMemberCredentialSecret(
provider: Pick<LlmProviderRow, "providerConfig">,
input: z.infer<typeof memberCredentialWriteSchema>,
) {
const secret = resolveCredentialColumn({
providerConfig: provider.providerConfig,
existingProvider: null,
apiKey: input.apiKey,
apiKeys: input.apiKeys,
})
if (!secret) {
throw createFailure(400, "invalid_api_keys", "Provide a non-empty credential.")View on GitHub (pinned to 2b7df46e8a)
Solutions
- Read error.message in the 400 response — it states the exact credential problem.
- Provide a non-empty apiKey or a well-formed apiKeys map matching the provider's expected credential shape.
- Trim whitespace/quotes from pasted keys and confirm the provider requires a key at all (some local providers do not).
- Verify secrets are present in the environment/CI before submitting.
Example fix
// before
await createProvider({ source: "models_dev", providerId: "openai", apiKey: process.env.OPENAI_API_KEY ?? "" })
// after
if (!process.env.OPENAI_API_KEY) throw new Error("OPENAI_API_KEY missing");
await createProvider({ source: "models_dev", providerId: "openai", apiKey: process.env.OPENAI_API_KEY }) Defensive patterns
Strategy: validation
Validate before calling
if (!apiKey || typeof apiKey !== 'string' || apiKey.trim() === '') throw new Error('apiKey must be non-empty before calling the API'); Type guard
function hasCredential(input: { apiKey?: string | null; apiKeys?: Record<string, string> | null }): boolean { return Boolean(input.apiKey?.trim()) || Object.values(input.apiKeys ?? {}).some(v => v.trim() !== '') } Try / catch
try { await createProvider(payload) } catch (e) { if (e.code === 'invalid_api_keys') console.error('credential rejected:', e.message); } Prevention
- Read error.message from the 400 response — it names the exact problem
- Verify secret env vars are set in CI before submission
- Trim whitespace/quotes from pasted API keys
- Match credential shape to the provider (single key vs key map)
When it happens
Trigger: Creating or updating an org LLM provider with apiKey/apiKeys that are empty, malformed, or inconsistent with providerConfig (e.g. missing key when providerConfig expects one, or invalid key entries in the multi-key map).
Common situations: Secrets not injected in CI so apiKey is empty string; rotating keys and leaving the map with blank entries; pasting a key with surrounding whitespace/quotes; switching source/models_dev but leaving incompatible credential fields.
Related errors
AI-assisted analysis of different-ai/openwork@2b7df46e8a (2026-09-01).
Data as JSON: /api/errors/4a91ac1b182593ba.
Report an issue: GitHub.