Skip to content

Bug: AI Translation fails with any cloud provider except Gemini — availability check reads only the Gemini API key #459

Description

@foegra

Summary

On the Standard / Standard Full build, AI Translation fails with every cloud provider except Gemini, while AI Proofreading works perfectly with the same provider, endpoint and model. Translation via the local GGUF (llama.cpp) model also works on the Offline flavor.

Provider used for testing: OpenAI-compatible custom endpoint (verified separately as functional — the exact same provider/model pair succeeds at proofreading).

Root cause (from v4.1.7 source, masterv4.1.7)

In app/src/standard/java/helium314/keyboard/latin/utils/ProofreadHelper.kt, translateAsync() gates all built-in-AI fallback branches on:

// ProofreadHelper.kt, line ~386
val hasAiConfigured = !service.getApiKey().isNullOrBlank()

but ProofreadService.getApiKey() reads only the Gemini key:

// ProofreadService.kt, line ~79
fun getApiKey(): String? = securePrefs.getString(KEY_API_KEY, null)?.takeIf { it.isNotBlank() }

// ProofreadService.kt, line ~645
private const val KEY_API_KEY = "gemini_api_key"

So when the configured provider is AIProvider.OPENAI (custom OpenAI-compatible endpoint) or AIProvider.GROQ, getApiKey() returns null even though the provider is fully configured, hasAiConfigured is false, and every fallback-to-AI branch short-circuits before any network request is made:

// ProofreadHelper.kt, lines ~414 / ~436 / ~449 / ~462
if (isOfflineOnly || !hasAiConfigured) {
    ...
    Result.failure(Exception("Translation plugin not available"))
} else {
    ... service.translate(text)   // never reached for OPENAI/GROQ
}

The failure surfaces to the user as the misleading toast "Offline model not downloaded" (R.string.translation_model_not_downloaded).

Why the other paths are unaffected

  • Proofreading (proofreadAsync()): no such gate; the OPENAI/GROQ path resolves the correct per-provider token via getHuggingFaceToken() (stored under huggingface_token) inside huggingFaceTranslate() — which is why proofreading works.
  • GGUF translation (Offline flavor): translateAsync() checks !service.getModelPath().isNullOrBlank() (the local model file) instead — which is why GGUF translation works.

The gate appears to be a leftover from when Gemini was the only cloud provider.

Steps to reproduce

  1. Settings → AI Integration → set provider to OpenAI-compatible (custom endpoint; e.g. a LiteLLM/LM Studio/llama.cpp server) and complete both proofread and translation model selection.
  2. Verify AI Proofread works with the endpoint.
  3. Set Translation mode to Cloud/Local AI, enter text in any field, tap the Translate toolbar key.
  4. → Toast: "Offline model not downloaded" / result failure — despite a working, fully configured cloud provider and no offline models involved.
  5. Repeat with provider Gemini + valid Gemini key → translation works.

Expected behavior

The AI-fallback availability check in translateAsync() should test the key/token of the active provider (e.g. getHuggingFaceToken() for OPENAI, getGroqToken() for GROQ), the same way the proofreading path does — not hardcode the Gemini key.

Suggested one-line direction:

val hasAiConfigured = when (service.getProvider()) {
    AIProvider.GEMINI  -> !service.getApiKey().isNullOrBlank()
    AIProvider.GROQ    -> !service.getGroqToken().isNullOrBlank()
    AIProvider.OPENAI  -> !service.getHuggingFaceToken().isNullOrBlank()
}

Environment

  • App version: v4.1.7 (standardfull APK)
  • Device: Google Pixel 10a, Android 16
  • Provider: OpenAI-compatible custom endpoint (OpenAI /v1/chat/completions format)
  • Verified affected: master HEAD (≈ v4.1.7)

Workaround (for affected users)

Paste any dummy value (e.g. x) into the Gemini API key field while keeping the provider on OpenAI-compatible — this flips hasAiConfigured to true and translation then correctly falls through to the active OpenAI-compatible provider. Gemini itself is never contacted since translate() dispatches on the selected provider.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions