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

  1. Read error.message in the 400 response — it states the exact credential problem.
  2. Provide a non-empty apiKey or a well-formed apiKeys map matching the provider's expected credential shape.
  3. Trim whitespace/quotes from pasted keys and confirm the provider requires a key at all (some local providers do not).
  4. 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

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.