Ir para o conteúdo

SDK Kotlin

Instale o SDK oficial em Kotlin e use chat, streaming, busca na web, agentes e busca semântica no HiNow com corrotinas.

Atualizado em 09 de ago. de 2026

O SDK oficial em Kotlin para a API do HiNow. É construído sobre Ktor e corrotinas: toda chamada é suspend, e as respostas são data classes tipadas com kotlinx.serialization.

A API fala o protocolo da OpenAI, e o SDK segue o mesmo formato. Se você já integrou com a OpenAI, o desenho das chamadas é o que você conhece.

Instalação

A biblioteca é publicada pelo JitPack, então o repositório precisa ser declarado junto com a dependência. Só a dependência não resolve.

build.gradle.ktskotlin
repositories {
    mavenCentral()
    maven("https://jitpack.io")
}

dependencies {
    implementation("com.github.hinow-ai:sdk-kotlin:v2.0.2")
}

Guarde a chave em HINOW_API_KEY e construa o cliente sem argumentos. Passar apiKey também funciona, mas evite deixar o valor no código.

terminalbash
export HINOW_API_KEY="hi_sua_chave_aqui"

A primeira chamada

Main.ktkotlin
import ai.hinow.ChatCompletionRequest
import ai.hinow.Hinow
import ai.hinow.Message
import kotlinx.coroutines.runBlocking

fun main(): Unit = runBlocking {
    // With no arguments, the client reads the key from HINOW_API_KEY.
    // use { } closes the HTTP client when the block ends.
    Hinow().use { client ->
        val answer = client.chat.completions.create(
            ChatCompletionRequest(
                model = "hinow/himax",
                messages = listOf(
                    Message.system("You answer in English."),
                    Message.user("What is an API? Answer in one paragraph."),
                ),
            )
        )

        println(answer.choices[0].message.text)

        val uso = answer.usage
        println("\ninput: ${uso?.promptTokens} · output: ${uso?.completionTokens}")
    }
}

O prefixo hinow/ faz parte do nome do modelo

Mandar himax em vez de hinow/himax devolve 404 model_not_found, e a mensagem não deixa claro que faltou o prefixo. Vale para todos: hinow/himax, hinow/hinova, hinow/higenesis.

Declare fun main(): Unit

Sem o : Unit, o Kotlin infere o tipo da última expressão do bloco. Se ela não for Unit, a JVM não reconhece a função como ponto de entrada e o programa falha com "método main não encontrado" — um erro que não aparece na compilação.

Mostrando a resposta enquanto ela chega

createStream entrega cada pedaço à sua lambda assim que ele chega e retorna quando o modelo termina. Numa tela de conversa isso muda a percepção de velocidade mais do que trocar de modelo.

Streaming.ktkotlin
import ai.hinow.ChatCompletionRequest
import ai.hinow.Hinow
import ai.hinow.Message
import kotlinx.coroutines.runBlocking

fun main(): Unit = runBlocking {
    Hinow().use { client ->
        client.chat.completions.createStream(
            ChatCompletionRequest(
                model = "hinow/hinova",
                messages = listOf(Message.user("Write three lines about the sea at dawn.")),
            )
        ) { chunk ->
            // Note the delta: each chunk carries the new fragment, not the whole answer.
            print(chunk.choices.firstOrNull()?.delta?.content ?: "")
        }

        println()
    }
}

É delta, não message

Na resposta completa o texto vem em message.text. No streaming, cada pedaço traz apenas o trecho novo, em delta.content — juntar tudo é com você.

Busca na web

Responde na hora, sem job para acompanhar, a US$ 0,005 por chamada. Os nove tipos ficam em SearchType, e um tipo inválido é recusado antes de a requisição sair.

BuscaWeb.ktkotlin
import ai.hinow.Hinow
import ai.hinow.SearchType
import kotlinx.coroutines.runBlocking

fun main(): Unit = runBlocking {
    Hinow().use { client ->
        // Answers on the spot: there is no job to follow.
        val search = client.tools.search(
            query = "best beaches in northeast Brazil",
            type = SearchType.SEARCH,
            country = "br",
            lang = "pt-br",
        )

        search.results.forEach { item ->
            println("${item.position}. ${item.title}")
            println("   ${item.url}")
        }

        println("\n${search.results.size} results · US$ ${search.cost}")
    }
}
TipoO que vem preenchido em cada resultado
SearchType.SEARCH, SCHOLAR, PATENTSposition, title, url, snippet
SearchType.NEWSos acima mais source, date, imageUrl
SearchType.IMAGESlink, imageUrl, thumbnailUrl, width, height
SearchType.VIDEOSchannel, duration, date, thumbnailUrl
SearchType.PLACESaddress, category, phone, website, rating, coordenadas
SearchType.SHOPPINGprice, delivery, rating, source
SearchType.AUTOCOMPLETEsuggestions; results fica vazio

Contatos de um site

Varre um ou mais sites atrás de e-mails, telefones e perfis sociais, e cada achado vem com a página exata onde apareceu. Como a varredura leva tempo, esta roda como job.

ContatosSite.ktkotlin
import ai.hinow.Hinow
import kotlinx.coroutines.runBlocking

fun main(): Unit = runBlocking {
    Hinow().use { client ->
        // The crawl takes a while, so it runs as a job. This method waits for you.
        val job = client.tools.websiteContactsAndWait(
            websites = listOf("https://www.teclia.com"),
            maxDepth = 2,
            maxLinksPerPage = 10,
        )

        val cache = if (job.cached == true) " (from cache)" else ""
        println("status: ${job.status} · US$ ${job.cost}$cache\n")

        // Each item carries the type, the value and the exact page it was found on.
        job.result?.items?.forEach { item ->
            when (item.type) {
                "email", "phone" -> {
                    println(item.type.padEnd(9) + item.value)
                    println("".padEnd(9) + "em ${item.sourceUrl}")
                }
                else -> println((item.platform ?: "").padEnd(9) + (item.url ?: ""))
            }
        }
    }
}

Repetir uma execução idêntica não cobra de novo

O job volta com cached verdadeiro quando o resultado veio do cache. Se quiser controlar o ciclo você mesmo, use websiteContacts e acompanhe com tools.jobs.retrieve(job.jobId) — repare que o campo é job_id, não id.

Valide o que vier antes de usar

A varredura devolve o que encontrou no HTML da página, sem julgar. Endereços truncados ou repetidos aparecem — trate a lista como matéria-prima e valide antes de gravar no seu banco.

Agentes

Assistentes, threads e runs no mesmo formato da API da OpenAI. Você declara as funções, o modelo decide quando chamá-las: o run para, você executa e devolve o resultado.

Agente.ktkotlin
import ai.hinow.*
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.json.*

// Your real function: a when here, just for the example.
fun getOrder(code: String): String = when (code) {
    "A-1001" -> """{"status":"delivered","delivered":"2026-08-02"}"""
    "A-1002" -> """{"status":"in transit","estimated":"2026-08-12"}"""
    else -> """{"error":"order not found"}"""
}

fun main(): Unit = runBlocking {
    Hinow().use { client ->
        // The argument schema, in JSON Schema.
        val schema = buildJsonObject {
            put("type", "object")
            putJsonObject("properties") {
                putJsonObject("code") {
                    put("type", "string")
                    put("description", "Order code, e.g. A-1001")
                }
            }
            putJsonArray("required") { add("code") }
        }

        // 1. The assistant: model, instructions and the functions it may call.
        val assistant = client.beta.assistants.create(
            AssistantRequest(
                model = "hinow/himax",
                name = "Support",
                instructions = "You answer questions about orders. " +
                    "Check the tool before stating any status.",
                tools = listOf(
                    Tool.function(
                        "get_order",
                        "Look up an order by its code.",
                        schema,
                    )
                ),
            )
        )

        // 2. The conversation and the question.
        val thread = client.beta.threads.create()
        client.beta.threads.messages.create(thread.id, "Has order A-1002 arrived?")

        // 3. Run it and wait.
        var run = client.beta.threads.runs.createAndPoll(thread.id, assistant.id)

        // 4. While the model asks for functions, run them and hand the result back.
        while (run.status == "requires_action") {
            val outputs = run.requiredAction?.submitToolOutputs?.toolCalls.orEmpty().map { call ->
                val args = Json.parseToJsonElement(call.function.arguments).jsonObject
                val code = args["code"]?.jsonPrimitive?.content ?: ""
                println("-> the model called ${call.function.name}($code)")

                ToolOutput(call.id, getOrder(code))
            }

            client.beta.threads.runs.submitToolOutputs(thread.id, run.id, outputs)
            run = client.beta.threads.runs.poll(thread.id, run.id)
        }

        // 5. The final answer is the last message on the thread.
        val messages = client.beta.threads.messages.list(thread.id, limit = 1, order = "desc")
        println("\n${messages.data[0].text}")

        client.beta.assistants.delete(assistant.id)
    }
}

requires_action não é erro

É o run devolvendo o controle para você executar uma função. Por isso o poll retorna nesse estado em vez de continuar girando: leia o requiredAction, chame submitToolOutputs e volte a acompanhar.

Documentos e busca semântica

Suba arquivos, junte numa base e busque por significado. A indexação é assíncrona — buscar antes de terminar devolve zero resultado, sem erro, e é para isso que existe o poll.

Conhecimento.ktkotlin
import ai.hinow.Hinow
import kotlinx.coroutines.runBlocking

fun main(): Unit = runBlocking {
    Hinow().use { client ->
        // 1. Upload the document.
        val file = client.files.create("returns-policy.txt")
        println("file: ${file.id} (${file.bytes} bytes)")

        // 2. Create the store and attach the file to it.
        val base = client.vectorStores.create("Support base")
        client.vectorStores.files.create(base.id, file.id)
        println("store: ${base.id}")

        // 3. Indexing is asynchronous. Searching before it finishes returns nothing
        //    at all, with no error, so poll waits for it.
        val state = client.vectorStores.files.poll(base.id, file.id)
        println("indexing: ${state.status}\n")

        // 4. Search by meaning, not by exact word.
        val hits = client.rag.search(
            query = "How many days do I have to return an item?",
            ragId = base.id,
            topK = 3,
        )

        hits.results.forEach { achado ->
            val excerpt = achado.text.replace("\n", " ").take(110)
            println("%.2f  %s".format(achado.score, achado.source))
            println("      $excerpt…")
        }
    }
}

O filtro por base chama-se ragId

A API ignora vector_store_id nesse endpoint: a busca simplesmente varre todos os documentos da conta em vez da base que você queria. É o tipo de detalhe que faz parecer que a busca está devolvendo lixo.

Erros

Cada falha vira uma classe própria, então dá para tratar por tipo em vez de comparar string de mensagem. Todas herdam de HinowException, e as que vieram da API carregam statusCode, errorType e o corpo da resposta.

Erros.ktkotlin
import ai.hinow.*
import kotlinx.coroutines.runBlocking

fun main(): Unit = runBlocking {
    Hinow().use { client ->
        try {
            client.chat.completions.create(
                ChatCompletionRequest(
                    model = "himax", // the hinow/ prefix is missing
                    messages = listOf(Message.user("Olá")),
                )
            )
        } catch (e: AuthenticationException) {
            println("Invalid or expired key. Check HINOW_API_KEY.")
        } catch (e: NotFoundException) {
            println("Not found: ${e.message}")
        } catch (e: InvalidRequestException) {
            println("Invalid request: ${e.message}")
        } catch (e: RateLimitException) {
            println("Rate limited. The SDK already retried; wait a moment.")
        } catch (e: ConnectionException) {
            // Nothing reached the API, so nothing was charged.
            println("Sem answer da API: ${e.message}")
        } catch (e: HinowException) {
            // Safety net: any other error the API reported.
            println("Error ${e.statusCode}: ${e.message}")
        }
    }
}
ClasseQuando acontece
AuthenticationException401 — chave ausente, inválida ou revogada
PermissionException403 — a chave não tem acesso a esse recurso
NotFoundException404 — modelo, arquivo ou assistente inexistente
InvalidRequestException400 ou 422 — falta um campo ou um valor está fora da faixa
RateLimitException429 — limite atingido
InsufficientBalanceException402 — saldo esgotado
ServerException5xx — a falha é do lado da API
ConnectionExceptiona requisição não chegou; nada foi cobrado
TimeoutExceptionum job ou run não terminou no tempo dado
Modelos.ktkotlin
import ai.hinow.Hinow
import kotlinx.coroutines.runBlocking

fun main(): Unit = runBlocking {
    Hinow().use { client ->
        // Account credit, in US dollars.
        val balance = client.getBalance()
        println("balance: US$ %.2f\n".format(balance.balance))

        // One specific model. The id is namespaced: hinow/himax, not himax.
        val model = client.models.retrieve("hinow/himax")
        println("${model.name} (${model.id})")
        println("categories: ${model.category.joinToString(", ")}")
        println("price per million tokens: input US$ ${model.cost?.input} · output US$ ${model.cost?.output}")
    }
}

Tudo que o cliente expõe

PropriedadePara quê
chat.completionsConversa, streaming, chamada de funções, modo JSON
embeddingsVetores para busca semântica
images · audio · videoGeração
modelsCatálogo, preço e recursos de cada modelo
toolsBusca na web e contatos de site
filesUpload de documentos
vectorStoresBases de conhecimento pesquisáveis
ragBusca semântica nos seus documentos
beta.assistants · beta.threadsAgentes executados no servidor
getBalance()Saldo da conta

Configuração

Cliente.ktkotlin
val client = Hinow(
    apiKey = System.getenv("HINOW_API_KEY"),  // ou deixe em branco e use a variável
    baseUrl = "https://api.hinow.ai",         // ou HINOW_BASE_URL
    timeout = 120_000,                        // milissegundos
    maxRetries = 2,                           // repete 429 e 5xx
)

Vindo da versão 1.x

A 1.0.0 devolvia JsonObject cru em toda chamada e não checava o status HTTP. Um 401 ou um 404 voltavam como se tivessem dado certo: o JSON de erro era entregue como se fosse a resposta. Isso foi corrigido — as respostas são data classes tipadas e qualquer coisa fora de 2xx levanta a exceção correspondente.

Outras duas mudanças:

  • chat.completions.create recebia parâmetros soltos e empacotava temperature, maxTokens e topP num objeto parameters, com os números convertidos em texto. A API aceita esse formato e ignora, então essas opções nunca surtiam efeito. Agora recebe um ChatCompletionRequest e manda tudo no nível raiz.
  • Message virou data class com a propriedade text, no lugar de um Map<String, Any>.

Escolhendo entre HiMax, HiNova e HiGenesis

O que cada modelo faz bem, quanto custa e como escrever o prompt para cada um.

Esta página foi útil?