{"id":47766873,"url":"https://github.com/quailyquaily/uniai","last_synced_at":"2026-04-03T07:30:22.554Z","repository":{"id":336167833,"uuid":"1148451245","full_name":"quailyquaily/uniai","owner":"quailyquaily","description":"a small Go client that unifies multiple AI features across multiple providers.","archived":false,"fork":false,"pushed_at":"2026-04-01T00:07:38.000Z","size":236,"stargazers_count":3,"open_issues_count":1,"forks_count":1,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-04-01T02:40:47.254Z","etag":null,"topics":["ai-wrapper","oneai"],"latest_commit_sha":null,"homepage":"https://quaily.com/opensource","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/quailyquaily.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-02-03T01:25:20.000Z","updated_at":"2026-04-01T00:07:37.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/quailyquaily/uniai","commit_stats":null,"previous_names":["quailyquaily/uniai"],"tags_count":18,"template":false,"template_full_name":null,"purl":"pkg:github/quailyquaily/uniai","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/quailyquaily%2Funiai","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/quailyquaily%2Funiai/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/quailyquaily%2Funiai/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/quailyquaily%2Funiai/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/quailyquaily","download_url":"https://codeload.github.com/quailyquaily/uniai/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/quailyquaily%2Funiai/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31342111,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-03T06:57:30.245Z","status":"ssl_error","status_checked_at":"2026-04-03T06:57:29.849Z","response_time":107,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ai-wrapper","oneai"],"created_at":"2026-04-03T07:30:13.317Z","updated_at":"2026-04-03T07:30:22.545Z","avatar_url":"https://github.com/quailyquaily.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# uniai\n\n`uniai` is a small Go client that unifies chat, embeddings, image generation, audio transcription, reranking, and classification across multiple providers. It wraps provider-specific clients and normalizes request/response types.\n\n## Features\n\n- Chat routing with OpenAI-compatible providers (OpenAI, DeepSeek, xAI, Groq), Azure OpenAI, Anthropic, AWS Bedrock, and Cloudflare Workers AI.\n- Multimodal chat input via `Message.Parts` (`text`, `image_url`, `image_base64`) with provider-aware validation.\n- Streaming support via callback — same `Chat()` signature, opt-in with `WithOnStream`.\n- Embedding, image, audio, rerank, and classify helpers with provider-specific options.\n- Optional OpenAI-compatible adapter to reuse the official `github.com/openai/openai-go/v3` request types.\n- Tool calling with emulation, to support models which do not natively support tool calling (see [`docs/tool_emulation.md`](docs/tool_emulation.md)).\n\n## Install\n\nThis package is intended to live in a Go module that provides `go.mod`.\n\n```bash\ngo get github.com/quailyquaily/uniai\n```\n\n## Chat\n\n```go\npackage main\n\nimport (\n    \"context\"\n    \"log\"\n\n    \"github.com/quailyquaily/uniai\"\n)\n\nfunc main() {\n    client := uniai.New(uniai.Config{\n        Provider:     \"openai\",\n        OpenAIAPIKey: \"...\",\n        OpenAIModel:  \"gpt-5.2\",\n    })\n\n    resp, err := client.Chat(context.Background(),\n        uniai.WithModel(\"gpt-5.2\"),\n        uniai.WithMessages(\n\t        uniai.System(\"You are a helpful assistant.\"),\n\t        uniai.User(\"Say hello.\"),\n        ),\n        uniai.WithTemperature(0.7),\n    )\n    if err != nil {\n        log.Fatal(err)\n    }\n\n    log.Println(resp.Text)\n}\n```\n\n### Provider selection\n\n`Chat` chooses the provider in this order:\n\n1. `uniai.WithProvider(...)`\n2. `Config.Provider`\n3. default: `\"openai\"`\n\nSupported provider names:\n\n- `openai` (default)\n- `openai_resp` (native OpenAI Responses API)\n- `deepseek` (OpenAI-compatible)\n- `xai` (OpenAI-compatible)\n- `groq` (OpenAI-compatible)\n- `gemini` (native Gemini API)\n- `azure`\n- `anthropic`\n- `bedrock`\n- `cloudflare`\n\nFor custom OpenAI-compatible endpoints, use provider `openai` with `Config.OpenAIAPIBase`.\n\n### `openai` vs `openai_resp`\n\nUse `openai` when you want Chat Completions behavior or compatibility with OpenAI-like providers.\n\nUse `openai_resp` when you want native OpenAI Responses API behavior.\n\nPractical differences:\n\n- `openai` uses `/v1/chat/completions`\n- `openai_resp` uses `/v1/responses`\n- `openai` is the safer choice for OpenAI-compatible endpoints such as DeepSeek, xAI, Groq, or custom compatible bases\n- `openai_resp` is the right choice for current OpenAI-only features such as `previous_response_id` and `WithReasoningDetails()`\n- `openai_resp` is stricter about unsupported Chat Completions-only options such as `stop`, `presence_penalty`, and `frequency_penalty`\n\nImportant GPT-5.4 edge case:\n\n- `openai` can fail on `gpt-5.4` when function tools are combined with reasoning effort, returning a 400 like:\n  `Function tools with reasoning_effort are not supported for gpt-5.4 in /v1/chat/completions. Please use /v1/responses instead.`\n- `openai_resp` exists specifically to handle that native Responses path.\n\nThere is a runnable repro/demo for this in [`cmd/openairesptest`](cmd/openairesptest).\n\n### Reasoning\n\nReasoning-related chat interfaces:\n\n- `uniai.WithReasoningEffort(...)`\n- `uniai.WithReasoningBudgetTokens(...)`\n- `uniai.WithReasoningDetails()`\n- `resp.Reasoning`\n\nAvailable effort constants:\n\n- `uniai.ReasoningEffortNone`\n- `uniai.ReasoningEffortMinimal`\n- `uniai.ReasoningEffortLow`\n- `uniai.ReasoningEffortMedium`\n- `uniai.ReasoningEffortHigh`\n- `uniai.ReasoningEffortMax`\n- `uniai.ReasoningEffortXHigh`\n\nBehavior notes:\n\n- If you do not call any reasoning interface, `uniai` does not send reasoning-related request fields.\n- `WithReasoningEffort(...)` controls reasoning level when the selected provider/model supports effort-style controls.\n- `WithReasoningBudgetTokens(...)` controls reasoning token budget when the selected provider/model supports budget-style controls.\n- `WithReasoningDetails()` opts in to retrieving provider reasoning details into `resp.Reasoning`.\n\nProvider guidance:\n\n- OpenAI Chat Completions (`openai`): use `WithReasoningEffort(...)`. `WithReasoningDetails()` is not supported on this path.\n- OpenAI Responses (`openai_resp`): use `WithReasoningEffort(...)`. `WithReasoningDetails()` is supported.\n- Gemini 3.x: use `WithReasoningEffort(...)`.\n- Gemini 2.5: use `WithReasoningBudgetTokens(...)`.\n- Anthropic Claude 4.6: use `WithReasoningEffort(...)`.\n- Anthropic manual-thinking models: use `WithReasoningBudgetTokens(...)`.\n\nExample:\n\n```go\nresp, err := client.Chat(ctx,\n\tuniai.WithProvider(\"gemini\"),\n\tuniai.WithModel(\"gemini-2.5-pro\"),\n\tuniai.WithMessages(uniai.User(\"Solve this step by step.\")),\n\tuniai.WithReasoningBudgetTokens(4096),\n\tuniai.WithReasoningDetails(),\n)\nif err != nil {\n\tlog.Fatal(err)\n}\nlog.Println(resp.Text)\nif resp.Reasoning != nil {\n\tlog.Printf(\"reasoning summary: %+v\", resp.Reasoning.Summary)\n}\n```\n\n### Multimodal chat input (V1)\n\n`uniai` supports structured chat content with `Message.Parts`.\n\nSupported part types:\n\n- `text`\n- `image_url`\n- `image_base64`\n\nRole constraints:\n\n- `user` can use `text`, `image_url`, and `image_base64`.\n- `system` / `assistant` / `tool` are text-only.\n\nExample:\n\n```go\nresp, err := client.Chat(ctx,\n    uniai.WithProvider(\"openai\"),\n    uniai.WithModel(\"gpt-5.2\"),\n    uniai.WithMessages(\n        uniai.UserParts(\n            uniai.TextPart(\"Describe this image.\"),\n            uniai.ImageURLPart(\"https://example.com/cat.png\"),\n        ),\n    ),\n)\nif err != nil {\n    log.Fatal(err)\n}\nlog.Println(resp.Text)\n```\n\nWith base64 image input:\n\n```go\nresp, err := client.Chat(ctx,\n    uniai.WithProvider(\"openai\"),\n    uniai.WithModel(\"gpt-5.2\"),\n    uniai.WithMessages(\n        uniai.UserParts(\n            uniai.TextPart(\"What do you see?\"),\n            uniai.ImageBase64Part(\"image/png\", base64PNG),\n        ),\n    ),\n)\n```\n\nBehavior notes:\n\n- `Parts` takes precedence over legacy `Content`.\n- If `Parts` is empty and `Content` is set, `Content` is treated as one `text` part.\n- `Result.Text` remains the compatibility field; `Result.Parts` is also populated (currently text parts in V1).\n- Cloudflare native `messages` models such as `@cf/moonshotai/kimi-k2.5` support `image_url` and `image_base64`; the current `gpt-oss` responses-style path remains text-only.\n\nProvider support details and examples: [`docs/multimodal_chat.md`](docs/multimodal_chat.md).\n\n### Tool calling\n\n```go\nresp, err := client.Chat(ctx,\n    uniai.WithModel(\"gpt-5.2\"),\n    uniai.WithMessages(uniai.User(\"What's the weather in Tokyo?\")),\n    uniai.WithTools([]uniai.Tool{\n    uniai.FunctionTool(\"get_weather\", \"Get current weather\", []byte(`{\n            \"type\": \"object\",\n            \"properties\": { \"city\": { \"type\": \"string\" } },\n            \"required\": [\"city\"]\n        }`)),\n    }),\n    uniai.WithToolChoice(uniai.ToolChoiceAuto()),\n)\n```\n\nFor multi-turn tool execution, always preserve `resp.ToolCalls` exactly as returned by `Chat`:\n\n```go\nmessages := []uniai.Message{\n\tuniai.User(\"Use the echo tool to repeat: hello\"),\n}\n\ntools := []uniai.Tool{\n\tuniai.FunctionTool(\"echo\", \"Echo text back\", []byte(`{\n\t\t\"type\":\"object\",\n\t\t\"properties\":{\"text\":{\"type\":\"string\"}},\n\t\t\"required\":[\"text\"]\n\t}`)),\n}\n\nfor {\n\tresp, err := client.Chat(ctx,\n\t\tuniai.WithModel(\"gemini-2.5-pro\"),\n\t\tuniai.WithReplaceMessages(messages...),\n\t\tuniai.WithTools(tools),\n\t\tuniai.WithToolChoice(uniai.ToolChoiceAuto()),\n\t)\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\n\t// No tool call means final answer.\n\tif len(resp.ToolCalls) == 0 {\n\t\tlog.Println(resp.Text)\n\t\tbreak\n\t}\n\n\t// IMPORTANT: append assistant tool calls exactly as returned.\n\tmessages = append(messages, uniai.Message{\n\t\tRole:      uniai.RoleAssistant,\n\t\tContent:   resp.Text,\n\t\tToolCalls: resp.ToolCalls,\n\t})\n\n\tfor _, tc := range resp.ToolCalls {\n\t\tvar in struct {\n\t\t\tText string `json:\"text\"`\n\t\t}\n\t\tresult := map[string]any{\"error\": \"invalid arguments\"}\n\t\tif err := json.Unmarshal([]byte(tc.Function.Arguments), \u0026in); err == nil {\n\t\t\tresult = map[string]any{\"text\": in.Text}\n\t\t}\n\t\tb, _ := json.Marshal(result)\n\n\t\t// IMPORTANT: use tc.ID as-is when sending tool results.\n\t\tmessages = append(messages, uniai.ToolResult(tc.ID, string(b)))\n\t}\n}\n```\n\nNotes:\n- Do not rebuild tool calls manually (for example, `id: \"call_1\"`). Rebuilding can lose provider-specific metadata.\n- For Gemini native tool calling, missing tool-call metadata in follow-up rounds will cause request errors.\n\n### Tool calling emulation\n\nSome models may not support native tool calling. You can enable tools emulation with:\n\n```go\nresp, err := client.Chat(ctx,\n    uniai.WithModel(\"your-model\"),\n    uniai.WithMessages(uniai.User(\"What's the weather in Tokyo?\")),\n    uniai.WithTools([]uniai.Tool{\n        uniai.FunctionTool(\"get_weather\", \"Get current weather\", []byte(`{\n            \"type\": \"object\",\n            \"properties\": { \"city\": { \"type\": \"string\" } },\n            \"required\": [\"city\"]\n        }`)),\n        uniai.FunctionTool(\"get_direction\", \"Get a route from 2 addresses\", []byte(`{\n            \"type\": \"object\",\n            \"properties\": { \"address_from\": { \"type\": \"string\" }, \"address_to\": { \"type\": \"string\" } },\n            \"required\": [\"address_from\", \"address_to\"]\n        }`)),\n    }),\n    uniai.WithToolChoice(uniai.ToolChoiceAuto()),\n    uniai.WithToolsEmulationMode(uniai.ToolsEmulationForce),\n)\n```\n\nSee [`docs/tool_emulation.md`](docs/tool_emulation.md) for other emulation options and detailed behaviors.\n\n### Streaming\n\nPass `WithOnStream` to receive tokens incrementally. The `Chat()` signature stays the same — it still returns the complete `Result` after the stream ends.\n\n```go\nresp, err := client.Chat(ctx,\n    uniai.WithModel(\"gpt-5.2\"),\n    uniai.WithMessages(uniai.User(\"Tell me a story.\")),\n    uniai.WithOnStream(func(ev uniai.StreamEvent) error {\n        if ev.Done {\n            // stream finished; ev.Usage contains token counts\n            return nil\n        }\n        if ev.Delta != \"\" {\n            fmt.Print(ev.Delta) // incremental text\n        }\n        if ev.ToolCallDelta != nil {\n            // incremental tool call (index, id, name, args chunk)\n        }\n        return nil // return non-nil error to cancel the stream\n    }),\n)\n// resp.Text contains the full accumulated text\n```\n\n`StreamEvent` fields:\n\n| Field | Description |\n|---|---|\n| `Delta` | Incremental text content |\n| `ToolCallDelta` | Incremental tool call update (`Index`, `ID`, `Name`, `ArgsChunk`) |\n| `Usage` | Token usage, populated on the final event |\n| `Done` | `true` for the last event |\n\nCheck out the [stream demo](cmd/stream/README.md) for a runnable terminal example.\n\nSupported providers: OpenAI (`openai`, `openai_resp`), OpenAI-compatible (`deepseek`, `xai`, `groq`), Azure, Anthropic, Bedrock. Cloudflare ignores streaming and falls back to blocking.\n\nWhen combined with tool emulation (`WithToolsEmulationMode`), the internal decision request is always non-streaming; only the final text response streams.\n\n## Embeddings\n\n```go\nemb, err := client.Embedding(ctx,\n    uniai.Embedding(\"text-embedding-3-small\", \"hello\"),\n)\n```\n\n## Images\n\n```go\nimg, err := client.Image(ctx,\n    uniai.Image(\"gpt-image-1\", \"a minimal line-art cat\"),\n    uniai.WithCount(1),\n)\n```\n\n## Audio (ASR)\n\n```go\nresp, err := client.Audio(ctx,\n    uniai.Audio(\"@cf/openai/whisper-large-v3-turbo\", base64Audio),\n)\n```\n\n## Rerank\n\n```go\nresp, err := client.Rerank(ctx,\n    uniai.Rerank(\"jina-reranker\", \"what is uniai?\",\n    uniai.RerankInput{Text: \"...\"},\n    uniai.RerankInput{Text: \"...\"},\n    ),\n    uniai.WithTopN(5),\n    uniai.WithReturnDocuments(true),\n)\n```\n\n## Classify\n\n```go\nresp, err := client.Classify(ctx,\n    uniai.Classify(\"jina-classifier\", []string{\"billing\", \"support\"},\n    uniai.ClassifyInput{Text: \"I need a refund\"},\n    ),\n)\n```\n\n## OpenAI-compatible adapter\n\nIf you already use the official OpenAI Go SDK (`github.com/openai/openai-go/v3`), you can reuse its request types:\n\n```go\nimport (\n    \"context\"\n\n    \"github.com/quailyquaily/uniai\"\n    openai \"github.com/openai/openai-go/v3\"\n    uniaiopenai \"github.com/quailyquaily/uniai/chat/openai\"\n)\n\nfunc example(ctx context.Context) error {\n    base := uniai.New(uniai.Config{OpenAIAPIKey: \"...\", OpenAIModel: \"gpt-5.2\"})\n    client := uniaiopenai.New(base)\n\n    _, err := client.CreateChatCompletion(ctx, openai.ChatCompletionNewParams{\n        Model: openai.ChatModel(\"gpt-5.2\"),\n        Messages: []openai.ChatCompletionMessageParamUnion{\n            openai.UserMessage(\"hello\"),\n        },\n    })\n    return err\n}\n```\n\n## Configuration\n\nAll configuration is provided via `uniai.Config`. Only the fields required for the providers you use need to be set.\n\n- Chat defaults: `Provider`, `Debug`, `ChatHeaders` (`ChatHeaders` apply to chat provider HTTP requests only)\n- OpenAI/OpenAI-compatible: `OpenAIAPIKey`, `OpenAIAPIBase`, `OpenAIModel`\n- Azure OpenAI: `AzureOpenAIAPIKey`, `AzureOpenAIEndpoint`, `AzureOpenAIModel`\n- Anthropic: `AnthropicAPIKey`, `AnthropicModel`\n- AWS Bedrock: `AwsKey`, `AwsSecret`, `AwsRegion`, `AwsBedrockModelArn`\n- Cloudflare Workers AI: `CloudflareAccountID`, `CloudflareAPIToken`, `CloudflareAPIBase`\n- Embeddings/Rerank/Classify (Jina): `JinaAPIKey`, `JinaAPIBase`\n- Gemini: `GeminiAPIKey`, `GeminiAPIBase`\n\nExample:\n\n```go\nclient := uniai.New(uniai.Config{\n    Provider:     \"openai\",\n    OpenAIAPIKey: \"...\",\n    OpenAIModel:  \"gpt-5.2\",\n    ChatHeaders: map[string]string{\n        \"X-Request-ID\": \"req-123\",\n    },\n    Debug:        true,\n})\n```\n\n## Debug logging\n\n### Global debug\n\nSet `Config.Debug` to `true` to enable request/response logging for all calls:\n\n```go\nclient := uniai.New(uniai.Config{\n    Provider:     \"openai\",\n    OpenAIAPIKey: \"...\",\n    OpenAIModel:  \"gpt-5.2\",\n    Debug:        true,\n})\n```\n\nIf you want to capture request/response payloads without logging, use `WithDebugFn`:\n\n`WithDebugFn` overrides `Config.Debug`: when set, logs are suppressed and all debug output is sent to the callback.\nOn request failures, providers also forward error payloads (raw API error body when available) through the same `*.response` label. When provider SDKs expose raw HTTP response text, an extra `*.response.raw_text` callback is emitted.\n\n\n```go\nresp, err := client.Chat(ctx,\n    uniai.WithModel(\"gpt-5.2\"),\n    uniai.WithMessages(uniai.User(\"hello\")),\n    uniai.WithDebugFn(func(label, payload string) {\n        // handle debug payloads (request/response)\n        // - label: \"{provider}.{function}.{request|response}\"\n        // \t - e.g. \"openai.chat.request\", \"anthropic.chat.response\"\n        // - payload: the content of the request/response\n        // store them, send to external logger, etc.\n    }),\n)\n```\n\n## Testcase\n\nRun tests from the module root that contains `go.mod`.\n\n```bash\n# all tests\nGOCACHE=/tmp/go-build go test ./...\n\n# only integration tests (chat + other features)\nGOCACHE=/tmp/go-build go test ./... -run TestChatEchoJSON\nGOCACHE=/tmp/go-build go test ./... -run TestOtherFeatures\n```\n\nIntegration tests are enabled by env vars. Common ones:\n\n- Chat: `TEST_OPENAI_API_KEY`, `TEST_OPENAI_MODEL`, `TEST_OPENAI_API_BASE`, `TEST_GROQ_API_KEY`, `TEST_GROQ_MODEL`\n- Cloudflare chat/audio: `TEST_CLOUDFLARE_ACCOUNT_ID`, `TEST_CLOUDFLARE_API_TOKEN`, `TEST_CLOUDFLARE_TEXT_MODEL`, `TEST_CLOUDFLARE_AUDIO_MODEL`, `TEST_CLOUDFLARE_AUDIO_FILEPATH`, `TEST_CLOUDFLARE_API_BASE`\n- Embedding/image/rerank/classify: see `env.example.sh`\n\n## Development\n\nRun from the module root that contains `go.mod`:\n\n```bash\ngo test ./...\ngo vet ./...\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fquailyquaily%2Funiai","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fquailyquaily%2Funiai","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fquailyquaily%2Funiai/lists"}