{"id":50140328,"url":"https://github.com/kakilangit/arcanum","last_synced_at":"2026-05-24T01:01:25.662Z","repository":{"id":356939569,"uuid":"1232640554","full_name":"kakilangit/arcanum","owner":"kakilangit","description":"Provider-agnostic AI inference library for Elixir","archived":false,"fork":false,"pushed_at":"2026-05-13T12:55:48.000Z","size":143,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-18T07:27:48.575Z","etag":null,"topics":["antrophic","deepseek","elixir","inference","openai","xai","zai"],"latest_commit_sha":null,"homepage":"","language":"Elixir","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/kakilangit.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","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":null,"dco":null,"cla":null}},"created_at":"2026-05-08T06:05:02.000Z","updated_at":"2026-05-13T12:56:59.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/kakilangit/arcanum","commit_stats":null,"previous_names":["kakilangit/arcanum"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/kakilangit/arcanum","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kakilangit%2Farcanum","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kakilangit%2Farcanum/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kakilangit%2Farcanum/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kakilangit%2Farcanum/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/kakilangit","download_url":"https://codeload.github.com/kakilangit/arcanum/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kakilangit%2Farcanum/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33417489,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-23T22:14:44.296Z","status":"ssl_error","status_checked_at":"2026-05-23T22:14:43.778Z","response_time":53,"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":["antrophic","deepseek","elixir","inference","openai","xai","zai"],"created_at":"2026-05-24T01:01:10.860Z","updated_at":"2026-05-24T01:01:25.614Z","avatar_url":"https://github.com/kakilangit.png","language":"Elixir","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Arcanum\n\nProvider-agnostic AI inference library for Elixir.\n\n## Overview\n\nArcanum provides a unified interface for chat completion, streaming, embeddings, tool use, and media generation across multiple AI providers. Model capabilities are declared upfront via profiles — no runtime detection or error-code fallbacks.\n\n## Supported Providers\n\n| Provider | API Format | Features |\n|----------|-----------|----------|\n| OpenAI | OpenAI | Chat, stream, tools, vision, image generation, embeddings |\n| Anthropic | Anthropic | Chat, stream, tools, vision |\n| Ollama | Ollama | Chat, stream, tools, vision, embeddings |\n| Grimoire | Grimoire | Chat, stream, model listing (plugin-based providers) |\n| DeepSeek | OpenAI | Chat, stream, tools |\n| GitHub Copilot | OpenAI | Chat, stream, tools, vision (OAuth device flow) |\n| OpenRouter | OpenAI | Chat, stream, tools |\n| xAI (Grok) | OpenAI | Chat, stream, tools, vision, image generation |\n| ZAI / Zhipu | OpenAI | Chat, stream, tools |\n\n## Installation\n\n```elixir\ndef deps do\n  [\n    {:arcanum, \"~\u003e 0.1.4\"}\n  ]\nend\n```\n\n## Usage\n\nAll inference goes through `Arcanum.Gateway`. Callers never touch adapters directly.\n\n### Provider Map\n\nEvery Gateway function takes a provider map describing the endpoint:\n\n```elixir\nprovider = %{\n  base_url: \"https://api.openai.com\",\n  api_key: \"sk-...\",\n  kind: \"openai\",\n  api_format: :openai,\n  type: :cloud\n}\n```\n\n| Key | Type | Description |\n|-----|------|-------------|\n| `base_url` | `String.t()` | Required. Provider API base URL. |\n| `api_key` | `String.t() \\| nil` | API key. Not needed for local providers or Copilot. |\n| `api_format` | `:openai \\| :anthropic \\| :grimoire \\| :custom` | Determines which adapter handles the request. |\n| `kind` | `String.t()` | Provider ID (e.g. `\"openai\"`, `\"anthropic\"`, `\"ollama\"`, `\"github-copilot\"`). Used for profile resolution and provider-specific behavior. |\n| `type` | `:cloud \\| :local` | Used by `Arcanum.Probe` to skip TCP checks for cloud providers. |\n| `extra_headers` | `[{String.t(), String.t()}] \\| nil` | Additional HTTP headers (injected automatically for Copilot). |\n\n### Chat Completion\n\n```elixir\nalias Arcanum.{Gateway, Intent}\n\nintent = %Intent{\n  model: \"gpt-4o\",\n  messages: [\n    %{role: :system, content: Intent.text(\"You are a helpful assistant.\")},\n    %{role: :user, content: Intent.text(\"What is Elixir?\")}\n  ],\n  temperature: 0.7,\n  max_tokens: 1024\n}\n\n{:ok, response} = Gateway.chat(provider, intent)\ntext = Arcanum.Response.text(response)\n```\n\n### Streaming\n\n```elixir\n{:ok, stream} = Gateway.stream(provider, intent)\n\nEnum.each(stream, fn\n  {:data, %Arcanum.Response{} = response} -\u003e\n    IO.write(Arcanum.Response.text(response) || \"\")\n  :done -\u003e IO.puts(\"\\n--- done ---\")\n  {:error, reason} -\u003e IO.puts(\"Error: #{inspect(reason)}\")\nend)\n```\n\n### Tool Use\n\nPass tools in the intent. Arcanum handles native, XML-text, and JSON-text tool call formats transparently based on the model profile.\n\n```elixir\nintent = %Intent{\n  model: \"gpt-4o\",\n  messages: [%{role: :user, content: Intent.text(\"What is the weather in Berlin?\")}],\n  tools: [\n    %{\n      type: \"function\",\n      function: %{\n        name: \"get_weather\",\n        description: \"Get current weather for a location\",\n        parameters: %{\n          \"type\" =\u003e \"object\",\n          \"properties\" =\u003e %{\n            \"location\" =\u003e %{\"type\" =\u003e \"string\", \"description\" =\u003e \"City name\"}\n          },\n          \"required\" =\u003e [\"location\"]\n        }\n      }\n    }\n  ]\n}\n\n{:ok, %Arcanum.Response{tool_calls: tool_calls}} = Gateway.chat(provider, intent)\n\n# tool_calls is a list of:\n# %{id: \"call_abc\", function: %{name: \"get_weather\", arguments: \"{\\\"location\\\":\\\"Berlin\\\"}\"}}\n```\n\nModels that don't support native tool calls (e.g. some Ollama models) automatically get XML-text or JSON-text extraction based on their profile's `tool_call_format`.\n\n### Vision (Multimodal)\n\n```elixir\nintent = %Intent{\n  model: \"gpt-4o\",\n  messages: [\n    %{role: :user, content: [\n      %{type: :text, text: \"What's in this image?\"},\n      %{type: :image_url, url: \"https://example.com/photo.jpg\"}\n    ]}\n  ]\n}\n\n# Or with base64:\n%{type: :image_base64, media_type: \"image/png\", data: \"iVBOR...\"}\n```\n\n### Embeddings\n\n```elixir\n{:ok, embeddings} = Gateway.embed(provider, \"gpt-4o\", \"Hello world\")\n# embeddings is a list of floats\n```\n\nSupported by OpenAI and Ollama adapters. Returns `{:error, :not_supported}` for adapters that don't override the default.\n\n### Image Generation\n\n```elixir\nalias Arcanum.{Gateway, Intent}\n\nintent = %Intent{\n  model: \"gpt-image-1\",\n  prompt: \"A cat wearing a wizard hat\",\n  size: \"1024x1024\",\n  quality: \"auto\",\n  n: 1,\n  format: \"png\"\n}\n\n{:ok, %Arcanum.Response{content: [%{type: :image} = image | _]}} =\n  Gateway.generate_image(provider, intent)\n\n# image fields:\n#   data: binary()          — decoded image bytes (from b64_json)\n#   url: String.t() | nil   — image URL (if provider returns one)\n#   revised_prompt: String.t() | nil\n#   content_type: \"image/png\"\n```\n\nImage generation parameters (`size`, `quality`, `style`) are profile-driven — only sent when the model's overlay declares support via `supported_sizes`, `supported_qualities`, or `supports_style`.\n\n### List Models\n\n```elixir\n{:ok, models} = Gateway.list_models(provider)\n# [\"gpt-4o\", \"gpt-4o-mini\", \"gpt-4.1\", ...]\n```\n\n### Probe Availability\n\n```elixir\nArcanum.Probe.probe_provider(provider)\n# :online | :offline\n```\n\nCloud providers always return `:online`. Local providers get a TCP connect check (2s timeout).\n\n### GitHub Copilot Authentication\n\n```elixir\nalias Arcanum.Auth.Copilot\n\n# 1. Start device flow\n{:ok, flow} = Copilot.start_device_flow()\n# flow.verification_uri -\u003e \"https://github.com/login/device\"\n# flow.user_code -\u003e \"ABCD-1234\"\n\n# 2. User visits URL and enters code, then:\n{:ok, access_token} = Copilot.poll_for_token(flow)\n\n# 3. Use the token as the provider's api_key\nprovider = %{\n  base_url: Copilot.base_url(),\n  api_key: access_token,\n  kind: \"github-copilot\",\n  api_format: :openai,\n  type: :cloud,\n  extra_headers: Copilot.copilot_headers(access_token)\n}\n```\n\nFor non-blocking flows, use `Copilot.poll_once/1` for single-attempt polling (e.g. from an Oban job).\n\n## Configuration\n\n### Application Config\n\n```elixir\n# Required for GitHub Copilot OAuth\nconfig :arcanum, copilot_client_id: \"your-github-oauth-client-id\"\n\n# Optional: override HTTP client (defaults to Req)\nconfig :arcanum, http_client: MyCustomClient\n```\n\n### Model Profile System\n\nEvery model gets a `ModelProfile` that declares its capabilities upfront. Profiles drive serialization, normalization, and feature gating — the adapter never guesses.\n\n```elixir\n%Arcanum.ModelProfile{\n  supports_system_role:      true,       # can the model accept system messages?\n  supports_tools:            true,       # native tool call support?\n  supports_vision:           false,      # multimodal image input?\n  supports_image_generation: false,      # image generation capability?\n  supports_video_generation: false,      # video generation capability?\n  tool_call_format:          :native,    # :native | :xml_text\n  reasoning_field:           nil,        # atom — where the model puts thinking (e.g. :reasoning_content)\n  thinking_param:            nil,        # map sent to provider to enable thinking (e.g. %{type: \"enabled\"})\n  preserve_reasoning:        false,      # keep thinking content in response?\n  uses_max_completion_tokens: false,     # use max_completion_tokens instead of max_tokens?\n  max_context:               131_072,    # maximum context window\n  max_images_per_message:    4,          # vision: max images per message\n  max_outputs_per_request:   4,          # media generation: max outputs\n  supported_sizes:           [],         # media generation: allowed dimensions\n  supported_formats:         [],         # media generation: allowed formats\n  supported_qualities:       [],         # media generation: allowed quality levels\n  supports_style:            false,      # media generation: accepts style parameter\n  image_response_mode:       nil,        # :native_b64 | :request_b64\n  provider_routing:          nil         # provider-specific routing metadata\n}\n```\n\n### Profile Resolution\n\nProfiles are resolved automatically by `Gateway` via `Arcanum.ModelProfile.Resolver`. Resolution follows a strict priority chain:\n\n```\n1. User overrides     (highest — caller-provided fields)\n2. Overlay            (provider/model-specific, from priv/overlays.json)\n3. Registry           (models.dev cache — single source of truth)\n4. Provider default   (fallback for local providers not in models.dev)\n5. Global default     (lowest — assumes weakest capabilities)\n```\n\n#### Registry (models.dev)\n\nThe `Arcanum.ModelProfile.Registry` GenServer fetches model capabilities from [models.dev](https://models.dev) and caches them in ETS. Refreshes hourly. Falls back gracefully if the fetch fails.\n\nDefault providers fetched: `openai`, `anthropic`, `deepseek`, `openrouter`, `xai`, `zai`, `zhipuai`, `github-copilot`.\n\n```elixir\n# Lookup a cached profile (returns nil if not found)\nArcanum.ModelProfile.Registry.lookup(\"openai\", \"gpt-4o\")\n\n# List all cached provider IDs\nArcanum.ModelProfile.Registry.cached_providers()\n```\n\n#### Overlays (`priv/overlays.json`)\n\nOverlays patch capabilities that models.dev doesn't track (vision, image generation, reasoning params). They are compiled into the Resolver at build time.\n\n```json\n{\n  \"overlays\": {\n    \"openai\": {\n      \"gpt-4o\": { \"supports_vision\": true },\n      \"gpt-image-1\": {\n        \"supports_image_generation\": true,\n        \"supported_sizes\": [\"1024x1024\", \"1024x1536\", \"1536x1024\", \"auto\"],\n        \"supported_formats\": [\"png\", \"webp\", \"jpeg\"],\n        \"max_outputs_per_request\": 4\n      }\n    },\n    \"deepseek\": {\n      \"deepseek-r1\": { \"preserve_reasoning\": true }\n    }\n  },\n  \"provider_defaults\": {\n    \"ollama\": {\n      \"supports_system_role\": true,\n      \"supports_tools\": false,\n      \"tool_call_format\": \"xml_text\",\n      \"max_context\": 32768\n    }\n  }\n}\n```\n\n#### Provider Defaults\n\nFor local providers not in models.dev (Ollama), provider defaults from `priv/overlays.json` are used as the base profile. These assume conservative capabilities.\n\n#### Profile Overrides\n\nCallers can override any profile field at call time via the `:profile_overrides` option. Overrides take the highest priority in the resolution chain.\n\n```elixir\n# Force a model to use XML text tool calls\nGateway.chat(provider, intent, profile_overrides: %{tool_call_format: :xml_text})\n\n# Override context window for a specific call\nGateway.chat(provider, intent, profile_overrides: %{max_context: 65_536})\n\n# Enable vision for a model not in the registry\nGateway.chat(provider, intent, profile_overrides: %{supports_vision: true})\n\n# Multiple overrides\nGateway.chat(provider, intent,\n  profile_overrides: %{\n    supports_tools: false,\n    tool_call_format: :xml_text,\n    max_context: 16_384\n  }\n)\n```\n\nAny field from `ModelProfile` can be overridden. The override map is merged on top of the resolved profile, so you only need to specify the fields you want to change.\n\n### Gateway Options\n\nAll `Gateway.chat/3` and `Gateway.stream/3` calls accept an opts keyword list:\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `:profile_overrides` | `map()` | Override any `ModelProfile` fields for this call. |\n| `:adapter` | `module()` | Override the adapter module (useful for testing). |\n\n## Architecture\n\n```\nGateway (single public entry point)\n  -\u003e Auth resolution (API key, Copilot OAuth headers)\n  -\u003e Profile resolution (Resolver: overrides \u003e overlay \u003e registry \u003e provider default \u003e global default)\n  -\u003e Adapter dispatch (OpenAI, Anthropic, Ollama)\n  -\u003e Response normalization (Normalizer: content fallback, think-tag stripping, tool-call extraction)\n```\n\n### Core Modules\n\n| Module | Purpose |\n|--------|---------|\n| `Arcanum.Gateway` | Single entry point for all inference calls. |\n| `Arcanum.Intent` | Canonical request struct for chat, streaming, and media generation. Content is always `[content_block()]`. |\n| `Arcanum.Response` | Canonical response struct (content, thinking, tool_calls, usage). Also used for image generation results. |\n| `Arcanum.ModelProfile` | Declares model capabilities (tools, vision, reasoning, context, image gen params). |\n| `Arcanum.ModelProfile.Resolver` | Multi-layer profile resolution with override support. |\n| `Arcanum.ModelProfile.Registry` | ETS cache backed by models.dev, refreshed hourly. |\n| `Arcanum.Response.Normalizer` | Profile-driven post-processing (XML/JSON tool extraction, think tags). |\n| `Arcanum.Provider` | Behaviour + macro (`use Arcanum.Provider`) with defoverridable defaults. |\n| `Arcanum.Probe` | TCP availability check for local providers. |\n| `Arcanum.Auth.Copilot` | GitHub Copilot OAuth device code flow (RFC 8628). |\n\n### Shared Infrastructure\n\n| Module | Purpose |\n|--------|---------|\n| `Arcanum.HTTP` | Configurable HTTP client, URL construction, async body draining (10 MB limit). |\n| `Arcanum.Retry` | Generic retry wrapper with exponential backoff (2s base, 30s cap, 3 attempts). |\n| `Arcanum.SSE` | Callback-driven Server-Sent Events stream parsing with configurable done sentinel. |\n\n### Adapters\n\n| Adapter | Behaviour Callbacks |\n|---------|-------------------|\n| `Arcanum.Adapters.OpenAI` | `chat`, `stream`, `list_models`, `embed`, `generate_image` |\n| `Arcanum.Adapters.Anthropic` | `chat`, `stream`, `list_models` |\n| `Arcanum.Adapters.Ollama` | `chat`, `stream`, `list_models`, `embed` |\n| `Arcanum.Adapters.Grimoire` | `chat`, `stream`, `list_models` |\n\n### Error Handling\n\nAll Gateway functions return `{:ok, result}` or `{:error, reason}`. Error shapes:\n\n| Error | Meaning |\n|-------|---------|\n| `{:error, {:api_error, status, body}}` | HTTP error from the provider. |\n| `{:error, {:api_error, :max_retries_exceeded}}` | All retry attempts exhausted. |\n| `{:error, :context_overflow}` | Input exceeded the model's context window. |\n| `{:error, :not_supported}` | Adapter doesn't implement the requested callback. |\n| `{:error, :copilot_auth_required}` | Copilot provider needs OAuth authentication. |\n| `{:error, term()}` | Network or other transient errors. |\n\nTransient HTTP errors (429, 502, 503, 529) are retried automatically up to 3 times with exponential backoff via `Arcanum.Retry`.\n\n## Development\n\n```sh\nmake deps       # fetch dependencies\nmake lint       # format + credo --strict + compile --warnings-as-errors\nmake test       # unit tests\nmake regression # full regression suite (unit + integration + examples)\n```\n\nThe regression script supports flags:\n\n```sh\n./test/regression.sh --skip-cloud     # local providers only\n./test/regression.sh --skip-local     # cloud providers only\n./test/regression.sh --skip-vision    # skip vision tests\n./test/regression.sh --skip-image-gen # skip image generation tests\n```\n\n## Contributing\n\nSee [CONTRIBUTING.md](CONTRIBUTING.md) for how to add new providers and models.\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkakilangit%2Farcanum","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fkakilangit%2Farcanum","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkakilangit%2Farcanum/lists"}