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.
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.
repositories {
mavenCentral()
maven("https://jitpack.io")
}
dependencies {
implementation("com.github.hinow-ai:sdk-kotlin:v2.0.2")
}repositories {
mavenCentral()
maven { url '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.
export HINOW_API_KEY="hi_sua_chave_aqui"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.
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.
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ê.
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.
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}")
}
}| Tipo | O que vem preenchido em cada resultado |
|---|---|
SearchType.SEARCH, SCHOLAR, PATENTS | position, title, url, snippet |
SearchType.NEWS | os acima mais source, date, imageUrl |
SearchType.IMAGES | link, imageUrl, thumbnailUrl, width, height |
SearchType.VIDEOS | channel, duration, date, thumbnailUrl |
SearchType.PLACES | address, category, phone, website, rating, coordenadas |
SearchType.SHOPPING | price, delivery, rating, source |
SearchType.AUTOCOMPLETE | suggestions; results fica vazio |
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.
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.
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.
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.
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.
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.
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.
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}")
}
}
}| Classe | Quando acontece |
|---|---|
AuthenticationException | 401 — chave ausente, inválida ou revogada |
PermissionException | 403 — a chave não tem acesso a esse recurso |
NotFoundException | 404 — modelo, arquivo ou assistente inexistente |
InvalidRequestException | 400 ou 422 — falta um campo ou um valor está fora da faixa |
RateLimitException | 429 — limite atingido |
InsufficientBalanceException | 402 — saldo esgotado |
ServerException | 5xx — a falha é do lado da API |
ConnectionException | a requisição não chegou; nada foi cobrado |
TimeoutException | um job ou run não terminou no tempo dado |
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}")
}
}| Propriedade | Para quê |
|---|---|
chat.completions | Conversa, streaming, chamada de funções, modo JSON |
embeddings | Vetores para busca semântica |
images · audio · video | Geração |
models | Catálogo, preço e recursos de cada modelo |
tools | Busca na web e contatos de site |
files | Upload de documentos |
vectorStores | Bases de conhecimento pesquisáveis |
rag | Busca semântica nos seus documentos |
beta.assistants · beta.threads | Agentes executados no servidor |
getBalance() | Saldo da conta |
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
)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.createrecebia parâmetros soltos e empacotavatemperature,maxTokensetopPnum objetoparameters, com os números convertidos em texto. A API aceita esse formato e ignora, então essas opções nunca surtiam efeito. Agora recebe umChatCompletionRequeste manda tudo no nível raiz.Messagevirou data class com a propriedadetext, no lugar de umMap<String, Any>.
Escolhendo entre HiMax, HiNova e HiGenesis
O que cada modelo faz bem, quanto custa e como escrever o prompt para cada um.

