Ir para o conteúdo

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.

Instalação

Pelo Swift Package Manager. No Xcode: File → Add Package Dependencies e cole a URL do repositório.

Package.swiftswift
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")
])

O 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.

terminalbash
export HINOW_API_KEY="hi_sua_chave_aqui"

A primeira chamada

main.swiftswift
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.

Mostrando a resposta enquanto ela chega

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.

Streaming.swiftswift
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ê.

Busca na web

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.

BuscaWeb.swiftswift
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)")
TipoO que vem preenchido em cada resultado
.search, .scholar, .patentsposition, title, url, snippet
.newsos acima mais source, date, imageURL
.imageslink, imageURL, thumbnailURL, width, height
.videoschannel, duration, date, thumbnailURL
.placesaddress, category, phone, website, rating, coordenadas
.shoppingprice, delivery, rating, source
.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.swiftswift
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.

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.

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.

Agente.swiftswift
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.

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.swiftswift
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.

Erros

HinowError é um enum, então o catch cobre cada tipo de falha. statusCode devolve o código HTTP quando a falha veio da API.

Erros.swiftswift
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)")
}
CasoQuando acontece
.authentication401 — chave ausente, inválida ou revogada
.permission403 — a chave não tem acesso a esse recurso
.notFound404 — modelo, arquivo ou assistente inexistente
.invalidRequest400 ou 422 — falta um campo ou um valor está fora da faixa
.rateLimit429 — limite atingido
.insufficientBalance402 — saldo esgotado
.server5xx — a falha é do lado da API
.connectiona requisição não chegou; nada foi cobrado
.timedOutum job ou run não terminou no tempo dado
Modelos.swiftswift
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)")
}

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.swiftswift
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
)

Vindo da versão 1.x

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.create empacotava temperature, maxTokens e topP num objeto parameters, com os números convertidos em texto. A API aceita esse formato e ignora, então pedir maxTokens: 10 devolvia a resposta inteira. Agora tudo vai no nível raiz.
  • HinowError ganhou um caso por tipo de falha, no lugar de um apiError com um dicionário.
  • O Package.swift declarava um alvo de teste apontando para um diretório que não existe, então swift build falhava 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.

Esta página foi útil?