Skip to content

LocalAiConnection.kt

src/jvmMain/kotlin/afield/ai/models/LocalAiConnection.kt · 51 lines

package afield.ai.models

import afield.document.workspace.LocalAiConnectionRequest
import afield.document.workspace.LocalAiConnectionStatus
import com.fasterxml.jackson.databind.ObjectMapper
import java.net.URI
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
import java.time.Duration

The connection check behind the local-model panel on Field's welcome page.

Field asks the endpoint for its OpenAI-style model list at /models, from the server JVM, so no code is sent and no inference runs. The answer says whether the endpoint replied, which model ids it lists, and whether the one chosen in the panel is among them; a non-200 reply, or a body that is not a model list, fails with that as the reason rather than as a missing model. A saved key is reused only for the endpoint it was saved against, and a cloud credential is never offered to a local one.

internal fun checkLocalAiConnection(request: LocalAiConnectionRequest): LocalAiConnectionStatus

Checks discovery from the Field JVM, without sending code or running inference.

internal fun checkLocalAiConnection(request: LocalAiConnectionRequest): LocalAiConnectionStatus {
    val base = normalizeLocalAiBaseUrl(request.baseUrl)
    // Reuse a saved key only for the endpoint it belongs to. Never use cloud credentials here.
    val key = request.apiKey.trim().takeIf { it.isNotBlank() }
        ?: if (base == configuredAiBaseUrl("local")) readLocalAiCredential()?.first else null
    val builder = HttpRequest.newBuilder(URI.create("$base/models"))
        .timeout(Duration.ofSeconds(8)).header("Accept", "application/json").GET()
    key?.let { builder.header("Authorization", "Bearer $it") }
    val response = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5))
        .followRedirects(HttpClient.Redirect.NEVER).build()
        .send(builder.build(), HttpResponse.BodyHandlers.ofString())
    require(response.statusCode() == 200) {
        "Model discovery returned HTTP ${response.statusCode()}. Check the API base URL and optional API key."
    }
    val data = ObjectMapper().readTree(response.body()).path("data")
    require(data.isArray) { "The endpoint did not return an OpenAI-style model list at /models." }
    val models = data.mapNotNull { it.path("id").asText().takeIf(String::isNotBlank) }.distinct()
    val selected = request.model.trim()
    val available = selected.isBlank() || selected in models
    return LocalAiConnectionStatus(
        connected = true, models = models, modelAvailable = available,
        message = when {
            models.isEmpty() -> "Connected, but no models are listed. Load a model on the inference server."
            !available -> "Connected, but '$selected' is not listed. Choose one of the model IDs below."
            else -> "Connected from Field. Model discovery succeeded; AI4 also needs Responses API and tool support."
        }
    )
}