SDK Swift
Instale o SDK oficial em Swift e use chat, streaming, busca na web, agentes e busca semântica no HiNow com async/await.
Atualizado em 09 de ago. de 2026
O SDK oficial em Swift para a API do HiNow. Usa async/await e Codable do começo ao fim, sem dependência externa nenhuma, e roda tanto no iOS e macOS quanto no Linux.
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.
Pelo Swift Package Manager. No Xcode: File → Add Package Dependencies e cole a URL do repositório.
dependencies: [
.package(url: "https://github.com/hinow-ai/sdk-swift.git", from: "2.0.0")
]
// e no alvo:
.target(name: "MeuApp", dependencies: [
.product(name: "HinowAI", package: "sdk-swift")
])File → Add Package Dependencies…
https://github.com/hinow-ai/sdk-swift.git
Dependency Rule: Up to Next Major Version — 2.0.0O repositório é sdk-swift, o módulo é HinowAI
O pacote se chama pelo repositório, mas o que você importa no código é import HinowAI.
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 — e num app de iOS, nunca embarque a chave no binário: chame a API a partir do seu servidor.
export HINOW_API_KEY="hi_sua_chave_aqui"import Foundation
import HinowAI
// With no arguments, the client reads the key from HINOW_API_KEY.
let client = try Hinow()
let answer = try await client.chat.completions.create(
ChatCompletionRequest(
model: "hinow/himax",
messages: [
.system("You answer in English."),
.user("What is an API? Answer in one paragraph."),
]
)
)
print(answer.choices[0].message.text)
if let uso = answer.usage {
print("\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.
createStream entrega cada pedaço ao seu closure 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 Foundation
import HinowAI
let client = try Hinow()
try await client.chat.completions.createStream(
ChatCompletionRequest(
model: "hinow/hinova",
messages: [.user("Write three lines about the sea at dawn.")]
)
) { chunk in
// Note the delta: each chunk carries the new fragment, not the whole answer.
print(chunk.choices.first?.delta.content ?? "", terminator: "")
}
print()É 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 são casos de SearchType, então o compilador não deixa você errar o nome.
import Foundation
import HinowAI
let client = try Hinow()
// Answers on the spot: there is no job to follow.
let search = try await client.tools.search(
query: "best beaches in northeast Brazil",
type: .search,
country: "br",
lang: "pt-br"
)
for item in search.results ?? [] {
print("\(item.position ?? 0). \(item.title ?? "")")
print(" \(item.url ?? "")")
}
print("\n\((search.results ?? []).count) results · US$ \(search.cost ?? 0)")| Tipo | O que vem preenchido em cada resultado |
|---|---|
.search, .scholar, .patents | position, title, url, snippet |
.news | os acima mais source, date, imageURL |
.images | link, imageURL, thumbnailURL, width, height |
.videos | channel, duration, date, thumbnailURL |
.places | address, category, phone, website, rating, coordenadas |
.shopping | price, delivery, rating, source |
.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 Foundation
import HinowAI
let client = try Hinow()
// The crawl takes a while, so it runs as a job. This method waits for you.
let job = try await client.tools.websiteContactsAndWait(
websites: ["https://www.teclia.com"],
maxDepth: 2,
maxLinksPerPage: 10
)
let cache = job.cached == true ? " (from cache)" : ""
print("status: \(job.status) · US$ \(job.cost ?? 0)\(cache)\n")
// Each item carries the type, the value and the exact page it was found on.
for item in job.result?.items ?? [] {
switch item.type {
case "email", "phone":
print("\(item.type.padding(toLength: 9, withPad: " ", startingAt: 0))\(item.value)")
print("\(String(repeating: " ", count: 9))em \(item.sourceURL ?? "")")
default:
print("\((item.platform ?? "").padding(toLength: 9, withPad: " ", startingAt: 0))\(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.
O schema da função é JSON arbitrário, então não cabe numa struct fixa — para isso existe o JSONValue, que aceita literais Swift e serializa no formato que a API espera.
import Foundation
import HinowAI
// Your real function: a switch here, just for the example.
func getOrder(_ code: String) -> String {
switch code {
case "A-1001": return #"{"status":"delivered","delivered":"2026-08-02"}"#
case "A-1002": return #"{"status":"in transit","estimated":"2026-08-12"}"#
default: return #"{"error":"order not found"}"#
}
}
let client = try Hinow()
// The argument schema, in JSON Schema. JSONValue takes Swift literals.
let schema: JSONValue = [
"type": "object",
"properties": [
"code": ["type": "string", "description": "Order code, e.g. A-1001"]
],
"required": ["code"],
]
// 1. The assistant: model, instructions and the functions it may call.
let assistant = try await client.beta.assistants.create(
AssistantRequest(
model: "hinow/himax",
name: "Support",
instructions: "You answer questions about orders. "
+ "Check the tool before stating any status.",
tools: [
.function(
name: "get_order",
description: "Look up an order by its code.",
parameters: schema)
]
)
)
// 2. The conversation and the question.
let thread = try await client.beta.threads.create()
try await client.beta.threads.messages.create(
threadID: thread.id, content: "Has order A-1002 arrived?")
// 3. Run it and wait.
var run = try await client.beta.threads.runs.createAndPoll(
threadID: thread.id, assistantID: assistant.id)
// 4. While the model asks for functions, run them and hand the result back.
while run.status == "requires_action" {
var outputs: [ToolOutput] = []
for call in run.requiredAction?.submitToolOutputs?.toolCalls ?? [] {
let args = try JSONDecoder().decode(
JSONValue.self, from: Data(call.function.arguments.utf8))
let code = args["code"]?.stringValue ?? ""
print("-> the model called \(call.function.name)(\(code))")
outputs.append(ToolOutput(toolCallID: call.id, output: getOrder(code)))
}
try await client.beta.threads.runs.submitToolOutputs(
threadID: thread.id, runID: run.id, outputs: outputs)
run = try await client.beta.threads.runs.poll(threadID: thread.id, runID: run.id)
}
// 5. The final answer is the last message on the thread.
let messages = try await client.beta.threads.messages.list(
threadID: thread.id, limit: 1, order: "desc")
print("\n\(messages.data[0].text)")
try await 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 Foundation
import HinowAI
let client = try Hinow()
// 1. Upload the document.
let file = try await client.files.create(path: "returns-policy.txt")
print("file: \(file.id) (\(file.bytes ?? 0) bytes)")
// 2. Create the store and attach the file to it.
let base = try await client.vectorStores.create(name: "Support base")
_ = try await client.vectorStores.files.create(vectorStoreID: base.id, fileID: file.id)
print("store: \(base.id)")
// 3. Indexing is asynchronous. Searching before it finishes returns nothing
// at all, with no error, so poll waits for it.
let state = try await client.vectorStores.files.poll(
vectorStoreID: base.id, fileID: file.id)
print("indexing: \(state.status)\n")
// 4. Search by meaning, not by exact word.
let hits = try await client.rag.search(
query: "How many days do I have to return an item?",
ragID: base.id,
topK: 3
)
for achado in hits.results {
let excerpt = achado.text.replacingOccurrences(of: "\n", with: " ").prefix(110)
print(String(format: "%.2f %@", achado.score, achado.source ?? ""))
print(" \(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.
HinowError é um enum, então o catch cobre cada tipo de falha. statusCode devolve o código HTTP quando a falha veio da API.
import Foundation
import HinowAI
let client = try Hinow()
do {
let answer = try await client.chat.completions.create(
ChatCompletionRequest(
model: "himax", // the hinow/ prefix is missing
messages: [.user("Olá")]
)
)
print(answer.choices[0].message.text)
} catch HinowError.authentication {
print("Invalid or expired key. Check HINOW_API_KEY.")
} catch HinowError.notFound(_, let message) {
print("Not found: \(message)")
} catch HinowError.invalidRequest(_, let message) {
print("Invalid request: \(message)")
} catch HinowError.rateLimit {
print("Rate limited. The SDK already retried; wait a moment.")
} catch HinowError.connection(let message) {
// Nothing reached the API, so nothing was charged.
print("Sem answer da API: \(message)")
} catch let error as HinowError {
// Safety net: any other error the API reported.
print("Error \(error.statusCode ?? 0): \(error.localizedDescription)")
}| Caso | Quando acontece |
|---|---|
.authentication | 401 — chave ausente, inválida ou revogada |
.permission | 403 — a chave não tem acesso a esse recurso |
.notFound | 404 — modelo, arquivo ou assistente inexistente |
.invalidRequest | 400 ou 422 — falta um campo ou um valor está fora da faixa |
.rateLimit | 429 — limite atingido |
.insufficientBalance | 402 — saldo esgotado |
.server | 5xx — a falha é do lado da API |
.connection | a requisição não chegou; nada foi cobrado |
.timedOut | um job ou run não terminou no tempo dado |
import Foundation
import HinowAI
let client = try Hinow()
// Account credit, in US dollars.
let balance = try await client.getBalance()
print(String(format: "balance: US$ %.2f\n", balance.balance))
// One specific model. The id is namespaced: hinow/himax, not himax.
let model = try await client.models.retrieve("hinow/himax")
print("\(model.name ?? "") (\(model.id))")
print("categories: \((model.category ?? []).joined(separator: ", "))")
if let cost = model.cost {
print("price per million tokens: input US$ \(cost.input ?? 0) · output US$ \(cost.output ?? 0)")
}| 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 |
let client = try Hinow(
apiKey: ProcessInfo.processInfo.environment["HINOW_API_KEY"], // ou use a variável
baseURL: "https://api.hinow.ai", // ou HINOW_BASE_URL
timeout: 120, // segundos
maxRetries: 2 // repete 429 e 5xx
)A 1.0.0 devolvia [String: Any] em toda chamada, então ler uma resposta era uma sequência de casts e desempacotamentos. Agora as respostas são structs Codable.
Outras três mudanças:
chat.completions.createempacotavatemperature,maxTokensetopPnum objetoparameters, com os números convertidos em texto. A API aceita esse formato e ignora, então pedirmaxTokens: 10devolvia a resposta inteira. Agora tudo vai no nível raiz.HinowErrorganhou um caso por tipo de falha, no lugar de umapiErrorcom um dicionário.- O
Package.swiftdeclarava um alvo de teste apontando para um diretório que não existe, entãoswift buildfalhava antes de compilar qualquer coisa.
Escolhendo entre HiMax, HiNova e HiGenesis
O que cada modelo faz bem, quanto custa e como escrever o prompt para cada um.

