diff --git a/MODELS.md b/MODELS.md index 443ca058..e8921a6a 100644 --- a/MODELS.md +++ b/MODELS.md @@ -8,6 +8,7 @@ This agent supports multiple model providers. By default, it uses models from th | ------------ | ------------------------------- | ---------------------------- | -------------------------------------------------- | | OpenCode Zen | `opencode/` | N/A (public for free models) | [OpenCode Zen](https://opencode.ai/docs/zen/) | | Kilo Gateway | `kilo/` | N/A (public for free models) | [Kilo Gateway Documentation](docs/kilo.md) | +| Formal AI | `formal-ai` | `FORMAL_AI_API_KEY` | [Formal AI Documentation](docs/formal-ai.md) | | Anthropic | `anthropic/` | `ANTHROPIC_API_KEY` | [Anthropic Docs](https://docs.anthropic.com/) | | Claude OAuth | `claude-oauth/` | `CLAUDE_CODE_OAUTH_TOKEN` | [Claude OAuth Documentation](docs/claude-oauth.md) | | Groq | `groq/` | `GROQ_API_KEY` | [Groq Documentation](docs/groq.md) | @@ -114,6 +115,10 @@ echo "hello" | agent --model opencode/gpt-5-nano # Big Pickle echo "hello" | agent --model opencode/big-pickle + +# Formal AI local server +echo "hello" | agent --model formal-ai +echo "hello" | agent --model @link-assistant/formal-ai ``` ### Using Paid Models @@ -151,6 +156,22 @@ For complete details about OpenCode Zen subscription and pricing, visit the [Ope --- +## Formal AI Provider + +[Formal AI](https://github.com/link-assistant/formal-ai) provides a local symbolic assistant through OpenAI-compatible API routes. Start the local server, export a client key, then use the built-in selector: + +```bash +formal-ai serve --agent-mode --host 127.0.0.1 --port 8080 +export FORMAL_AI_API_KEY="local-test-token" +agent --model formal-ai --permission-mode plan -p "hi" +``` + +Agent also accepts `formal-ai/formal-ai`, `@link-assistant/formal-ai`, and `formalai/formal-ai`. For remote hosts or non-default ports, set `FORMAL_AI_BASE_URL` to the full OpenAI-compatible base URL ending in `/api/openai/v1`. + +For setup, server checks, and troubleshooting, see the [Formal AI Documentation](docs/formal-ai.md). + +--- + ## Groq Provider [Groq](https://groq.com/) provides ultra-fast inference for open-source large language models. To use Groq models, set your API key: diff --git a/README.md b/README.md index 3cfc3b51..5ebf3a7b 100644 --- a/README.md +++ b/README.md @@ -116,7 +116,7 @@ While maintaining 100% compatibility with OpenCode's JSON interface, Agent offer - **Plain Text Input**: Also accepts plain text messages (auto-converted to JSON format) - **Unrestricted Access**: Full file system and command execution access (no sandbox, no restrictions) - **Tool Support**: 13 tools including websearch, codesearch, batch - all enabled by default -- **Flexible Model Selection**: Supports [OpenCode Zen](https://opencode.ai/docs/zen/), [Claude OAuth](docs/claude-oauth.md), [Groq](docs/groq.md), [OpenRouter](docs/openrouter.md), and more - see [MODELS.md](MODELS.md) +- **Flexible Model Selection**: Supports [OpenCode Zen](https://opencode.ai/docs/zen/), [Formal AI](docs/formal-ai.md), [Claude OAuth](docs/claude-oauth.md), [Groq](docs/groq.md), [OpenRouter](docs/openrouter.md), and more - see [MODELS.md](MODELS.md) - **Public Domain**: Unlicense - use it however you want ## Quick Start @@ -206,14 +206,15 @@ See [js/README.md](js/README.md#mcp-model-context-protocol-support) for full MCP ## Documentation -| Document | Description | -| -------------------------------- | ----------------------------------------- | -| [MODELS.md](MODELS.md) | Available models, providers, and pricing | -| [TOOLS.md](TOOLS.md) | Complete tool documentation | -| [EXAMPLES.md](EXAMPLES.md) | Usage examples for each tool | -| [TESTING.md](TESTING.md) | Testing guide | -| [js/README.md](js/README.md) | JavaScript/Bun implementation (full docs) | -| [rust/README.md](rust/README.md) | Rust implementation | +| Document | Description | +| -------------------------------------- | ----------------------------------------- | +| [MODELS.md](MODELS.md) | Available models, providers, and pricing | +| [docs/formal-ai.md](docs/formal-ai.md) | Formal AI local server setup | +| [TOOLS.md](TOOLS.md) | Complete tool documentation | +| [EXAMPLES.md](EXAMPLES.md) | Usage examples for each tool | +| [TESTING.md](TESTING.md) | Testing guide | +| [js/README.md](js/README.md) | JavaScript/Bun implementation (full docs) | +| [rust/README.md](rust/README.md) | Rust implementation | ## Files diff --git a/docs/case-studies/issue-270/README.md b/docs/case-studies/issue-270/README.md new file mode 100644 index 00000000..2409dd72 --- /dev/null +++ b/docs/case-studies/issue-270/README.md @@ -0,0 +1,162 @@ +# Case Study: Issue #270 - Formal AI setup for Agent CLI + +## Issue Reference + +- **GitHub Issue**: https://github.com/link-assistant/agent/issues/270 +- **Prepared PR**: https://github.com/link-assistant/agent/pull/277 +- **Formal AI upstream repo**: https://github.com/link-assistant/formal-ai +- **Formal AI upstream issue #621**: https://github.com/link-assistant/formal-ai/issues/621 +- **Research date**: 2026-07-03 + +## Collected Data + +This folder contains the raw issue and PR metadata used for the analysis: + +- `issue-270.json` +- `issue-comments.json` +- `pr-277.json` +- `pr-comments.json` +- `pr-review-comments.json` +- `pr-reviews.json` +- `formal-ai-issue-621.json` +- `formal-ai-with-formal-ai-prs.json` +- `formal-ai-alias-prs.json` +- `formal-ai-code-search.json` + +At collection time, issue #270 had no comments, PR #277 had no discussion comments, no review comments, and no reviews. + +## Summary + +Agent could only use Formal AI through a manual OpenAI-compatible provider config. The short selector `formal-ai` failed during short-name resolution because Formal AI is not in the models.dev catalog, and `@link-assistant/formal-ai` did not have an Agent provider entry. Formal AI issue #621 separately tracks adding Agent to the `with-formal-ai` wrapper, but issue #270 requires Agent itself to know the aliases by default. + +The implemented solution adds a built-in OpenAI-compatible Formal AI provider backed by the already bundled `@ai-sdk/openai-compatible` package. Agent now accepts `formal-ai`, `formal-ai/formal-ai`, `@link-assistant/formal-ai`, and `formalai/formal-ai` without a manual config. Documentation now covers install, local server setup, remote base URLs, auth, and common failure modes. + +## Requirements + +| Requirement from issue #270 | Resolution | +| -------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | +| Check current Formal AI docs and options, including `with-formal-ai` | Reviewed Formal AI README, install docs, API routes, wrappers, and integration tests from commit `6f94f490f4a2cf361991932856264086c0e2a2a8`. | +| Verify behavior with actual execution/tests | Added and ran `bun test ./tests/formal-ai-provider.ts`; it fails before the provider change and passes after it. | +| Agent CLI knows `formal-ai` by default | Added built-in `formal-ai` provider and short-model resolution through provider state. | +| Agent CLI knows `@link-assistant/formal-ai` by default | Added `@link-assistant` as a provider alias whose `formal-ai` model resolves to the canonical server model id. | +| Provide setup instructions | Added `docs/formal-ai.md` and linked it from `MODELS.md`, `README.md`, and `js/README.md`. | +| Explain what to do when the local/remote server is unavailable | Added server checks and troubleshooting for connection failures, auth failures, missing agent mode, and old Agent versions. | +| Collect related data in `docs/case-studies/issue-270` | Added raw GitHub metadata plus this analysis. | +| List possible solutions and plans | Captured below. | +| Check existing components/libraries | Reused existing provider custom loaders, synthetic provider injection, and bundled `@ai-sdk/openai-compatible`. | + +## Upstream Facts + +- Formal AI exposes protocol namespaces under `/api/openai/v1`, `/api/anthropic/v1`, `/api/gemini/v1beta`, and `/api/vertex/v1`. +- Formal AI's canonical model id is `formal-ai`. +- Formal AI's API accepts `@link-assistant/formal-ai`, `link-assistant/formal-ai`, `formal-ai-latest`, and `latest` as aliases and returns `formal-ai` in responses. +- Formal AI's OpenAI-compatible base URL for Agent is `http://127.0.0.1:8080/api/openai/v1`. +- Formal AI supports bearer auth with server-side `FORMAL_AI_API_BEARER_TOKEN`; clients send the same value through their API key path, documented here as `FORMAL_AI_API_KEY`. +- When the server has no bearer token configured, any non-empty client API key is enough for clients that require one. +- `formal-ai with` and `with-formal-ai` currently cover Codex, OpenCode, and Gemini. Upstream issue #621 tracks adding Agent as another wrapper target. + +Primary upstream references: + +- Formal AI README at commit `6f94f490f4a2cf361991932856264086c0e2a2a8`: https://github.com/link-assistant/formal-ai/blob/6f94f490f4a2cf361991932856264086c0e2a2a8/README.md +- Model alias contract tests: https://github.com/link-assistant/formal-ai/blob/6f94f490f4a2cf361991932856264086c0e2a2a8/tests/unit/specification/openai_compatibility.rs +- `with-formal-ai` integration tests: https://github.com/link-assistant/formal-ai/blob/6f94f490f4a2cf361991932856264086c0e2a2a8/tests/integration/with_formal_ai.rs + +## Existing Agent Components Checked + +- `Provider.state()` already supports synthetic provider records before environment, auth, custom loader, and config loading. +- `CUSTOM_LOADERS` already supports provider-specific autoload and SDK options. +- `@ai-sdk/openai-compatible` is already bundled in `BUNDLED_PROVIDERS`, so no dynamic package install is needed. +- `resolveShortModelName()` resolves short model names from active providers, so a default-loaded Formal AI provider is enough for `--model formal-ai`. +- `parseModelConfig()` validates explicit `provider/model` selections against active provider metadata, so `@link-assistant/formal-ai` needs a real `@link-assistant` provider entry. + +## Root Cause + +Formal AI was only known to Agent when users supplied a custom provider config. It was not part of models.dev and had no built-in synthetic provider record. As a result: + +- `agent --model formal-ai` failed with `ProviderModelNotFoundError` because no active provider had a `formal-ai` model. +- `agent --model @link-assistant/formal-ai` parsed as provider `@link-assistant` plus model `formal-ai`, but that provider did not exist. +- Users had to copy a manual provider block from Formal AI docs or use environment-only config workarounds. + +## Solution Options Considered + +### Option 1: Documentation-only manual config + +Documenting the existing provider block would be low risk, but it would not satisfy the requirement that Agent knows `formal-ai` and `@link-assistant/formal-ai` by default. + +### Option 2: Wait for `with-formal-ai agent` + +Formal AI issue #621 would make wrapper-based usage easier, but users would still need an extra wrapper and the Agent CLI itself would not know the aliases. This remains a useful upstream follow-up, not a blocker. + +### Option 3: Built-in Formal AI provider aliases + +Add synthetic provider records for `formal-ai`, `formalai`, and `@link-assistant`, all using `@ai-sdk/openai-compatible` and the same local base URL. This satisfies the default alias requirement, keeps config override support, and reuses the existing provider loading architecture. This is the implemented solution. + +## Implementation Notes + +- `FORMAL_AI_BASE_URL` overrides the default base URL and must include `/api/openai/v1`. +- `FORMAL_AI_API_KEY` overrides the default placeholder key. The placeholder is useful because local Formal AI servers without bearer auth accept any non-empty key and the AI SDK requires one. +- `formalai/formal-ai` is included for compatibility with Formal AI's OpenCode wrapper naming. +- `link-assistant/formal-ai` is intentionally not a documented Agent selector because `link-assistant` is already an internal Agent provider namespace. Users should use `@link-assistant/formal-ai` instead. + +## Verification + +The new regression test launches a fresh child process per selector, parses CLI model config, calls `Provider.getModel()`, and asserts that the configured base URL and API key are applied. + +Initial pre-fix failure for `formal-ai`: + +```text +ProviderModelNotFoundError: Model "formal-ai" not found in any provider. +``` + +Post-fix command: + +```bash +cd js +bun test ./tests/formal-ai-provider.ts +``` + +Result: + +```text +4 pass +0 fail +``` + +Live Formal AI smoke test: + +```bash +cd /tmp/formal-ai-issue270 +cargo run --bin formal-ai -- serve --agent-mode --host 127.0.0.1 --port 18080 +``` + +Server checks: + +```bash +curl -s http://127.0.0.1:18080/health +curl -s http://127.0.0.1:18080/api/openai/v1/models +curl -s http://127.0.0.1:18080/api/openai/v1/chat/completions \ + -H 'content-type: application/json' \ + -d '{"model":"@link-assistant/formal-ai","messages":[{"role":"user","content":"Hi"}]}' +``` + +Observed result: `/health` returned `status: ok`, `/api/openai/v1/models` advertised `formal-ai`, and the chat response returned model `formal-ai` with text `Hi, how may I help you?`. + +Agent smoke tests against the live server: + +```bash +FORMAL_AI_BASE_URL=http://127.0.0.1:18080/api/openai/v1 \ +FORMAL_AI_API_KEY=local-test-token \ +bun run src/index.js --model formal-ai --permission-mode plan -p "hi" --compact-json + +FORMAL_AI_BASE_URL=http://127.0.0.1:18080/api/openai/v1 \ +FORMAL_AI_API_KEY=local-test-token \ +LINK_ASSISTANT_AGENT_DEFAULT_COMPACTION_MODELS='(same)' \ +bun run src/index.js --model @link-assistant/formal-ai --permission-mode plan -p "hi" --compact-json +``` + +Both Agent runs exited with code 0 and emitted the assistant text `Hi, how may I help you?`. The scoped alias run logged `providerID: "@link-assistant"`, `modelID: "formal-ai"`, and `resolvedModelID: "formal-ai"`. + +## Follow-Ups + +- Formal AI issue #621 can still add `with-formal-ai agent` support for users who prefer wrapper-managed invocation. +- Formal AI's README Agent section can be simplified after this Agent release because manual provider config is no longer required for the built-in selectors. diff --git a/docs/case-studies/issue-270/formal-ai-alias-prs.json b/docs/case-studies/issue-270/formal-ai-alias-prs.json new file mode 100644 index 00000000..fe51488c --- /dev/null +++ b/docs/case-studies/issue-270/formal-ai-alias-prs.json @@ -0,0 +1 @@ +[] diff --git a/docs/case-studies/issue-270/formal-ai-code-search.json b/docs/case-studies/issue-270/formal-ai-code-search.json new file mode 100644 index 00000000..3c3eb3f9 --- /dev/null +++ b/docs/case-studies/issue-270/formal-ai-code-search.json @@ -0,0 +1 @@ +[{"path":"docs/case-studies/issue-84/logs/run-25960469844-failed.log","repository":{"id":"R_kgDOSbOoUw","isFork":false,"isPrivate":false,"nameWithOwner":"link-assistant/formal-ai","url":"https://github.com/link-assistant/formal-ai"},"url":"https://github.com/link-assistant/formal-ai/blob/6f94f490f4a2cf361991932856264086c0e2a2a8/docs/case-studies/issue-84/logs/run-25960469844-failed.log"},{"path":"docs/case-studies/issue-84/logs/run-25960908607-failed.log","repository":{"id":"R_kgDOSbOoUw","isFork":false,"isPrivate":false,"nameWithOwner":"link-assistant/formal-ai","url":"https://github.com/link-assistant/formal-ai"},"url":"https://github.com/link-assistant/formal-ai/blob/6f94f490f4a2cf361991932856264086c0e2a2a8/docs/case-studies/issue-84/logs/run-25960908607-failed.log"},{"path":"docs/case-studies/issue-390/raw-data/desktop-release-26955193358-key-lines.txt","repository":{"id":"R_kgDOSbOoUw","isFork":false,"isPrivate":false,"nameWithOwner":"link-assistant/formal-ai","url":"https://github.com/link-assistant/formal-ai"},"url":"https://github.com/link-assistant/formal-ai/blob/6f94f490f4a2cf361991932856264086c0e2a2a8/docs/case-studies/issue-390/raw-data/desktop-release-26955193358-key-lines.txt"},{"path":"docs/case-studies/issue-84/logs/run-25960195129-failed.log","repository":{"id":"R_kgDOSbOoUw","isFork":false,"isPrivate":false,"nameWithOwner":"link-assistant/formal-ai","url":"https://github.com/link-assistant/formal-ai"},"url":"https://github.com/link-assistant/formal-ai/blob/6f94f490f4a2cf361991932856264086c0e2a2a8/docs/case-studies/issue-84/logs/run-25960195129-failed.log"},{"path":"experiments/agent_cli_e2e/run_agent_cli.sh","repository":{"id":"R_kgDOSbOoUw","isFork":false,"isPrivate":false,"nameWithOwner":"link-assistant/formal-ai","url":"https://github.com/link-assistant/formal-ai"},"url":"https://github.com/link-assistant/formal-ai/blob/6f94f490f4a2cf361991932856264086c0e2a2a8/experiments/agent_cli_e2e/run_agent_cli.sh"},{"path":"docs/case-studies/issue-84/logs/run-25961456440-failed.log","repository":{"id":"R_kgDOSbOoUw","isFork":false,"isPrivate":false,"nameWithOwner":"link-assistant/formal-ai","url":"https://github.com/link-assistant/formal-ai"},"url":"https://github.com/link-assistant/formal-ai/blob/6f94f490f4a2cf361991932856264086c0e2a2a8/docs/case-studies/issue-84/logs/run-25961456440-failed.log"},{"path":"docs/case-studies/issue-390/raw-data/ci-cd-26954637153-key-lines.txt","repository":{"id":"R_kgDOSbOoUw","isFork":false,"isPrivate":false,"nameWithOwner":"link-assistant/formal-ai","url":"https://github.com/link-assistant/formal-ai"},"url":"https://github.com/link-assistant/formal-ai/blob/6f94f490f4a2cf361991932856264086c0e2a2a8/docs/case-studies/issue-390/raw-data/ci-cd-26954637153-key-lines.txt"},{"path":"docs/case-studies/issue-479/ci-logs/desktop-release-27572474798-resolve.log","repository":{"id":"R_kgDOSbOoUw","isFork":false,"isPrivate":false,"nameWithOwner":"link-assistant/formal-ai","url":"https://github.com/link-assistant/formal-ai"},"url":"https://github.com/link-assistant/formal-ai/blob/6f94f490f4a2cf361991932856264086c0e2a2a8/docs/case-studies/issue-479/ci-logs/desktop-release-27572474798-resolve.log"},{"path":"docs/case-studies/issue-479/ci-logs/ci-cd-pipeline-27563367172.log","repository":{"id":"R_kgDOSbOoUw","isFork":false,"isPrivate":false,"nameWithOwner":"link-assistant/formal-ai","url":"https://github.com/link-assistant/formal-ai"},"url":"https://github.com/link-assistant/formal-ai/blob/6f94f490f4a2cf361991932856264086c0e2a2a8/docs/case-studies/issue-479/ci-logs/ci-cd-pipeline-27563367172.log"},{"path":"docs/case-studies/issue-561/raw-data/ci-cd-pipeline-28118275692.log","repository":{"id":"R_kgDOSbOoUw","isFork":false,"isPrivate":false,"nameWithOwner":"link-assistant/formal-ai","url":"https://github.com/link-assistant/formal-ai"},"url":"https://github.com/link-assistant/formal-ai/blob/6f94f490f4a2cf361991932856264086c0e2a2a8/docs/case-studies/issue-561/raw-data/ci-cd-pipeline-28118275692.log"},{"path":"docs/case-studies/issue-479/ci-logs/desktop-release-27572474798-linux-x64.log","repository":{"id":"R_kgDOSbOoUw","isFork":false,"isPrivate":false,"nameWithOwner":"link-assistant/formal-ai","url":"https://github.com/link-assistant/formal-ai"},"url":"https://github.com/link-assistant/formal-ai/blob/6f94f490f4a2cf361991932856264086c0e2a2a8/docs/case-studies/issue-479/ci-logs/desktop-release-27572474798-linux-x64.log"},{"path":"docs/case-studies/issue-479/ci-logs/desktop-release-27572474798-macos-x64.log","repository":{"id":"R_kgDOSbOoUw","isFork":false,"isPrivate":false,"nameWithOwner":"link-assistant/formal-ai","url":"https://github.com/link-assistant/formal-ai"},"url":"https://github.com/link-assistant/formal-ai/blob/6f94f490f4a2cf361991932856264086c0e2a2a8/docs/case-studies/issue-479/ci-logs/desktop-release-27572474798-macos-x64.log"},{"path":"docs/case-studies/issue-479/ci-logs/desktop-release-27572474798-macos-arm64.log","repository":{"id":"R_kgDOSbOoUw","isFork":false,"isPrivate":false,"nameWithOwner":"link-assistant/formal-ai","url":"https://github.com/link-assistant/formal-ai"},"url":"https://github.com/link-assistant/formal-ai/blob/6f94f490f4a2cf361991932856264086c0e2a2a8/docs/case-studies/issue-479/ci-logs/desktop-release-27572474798-macos-arm64.log"},{"path":"docs/case-studies/issue-479/ci-logs/desktop-release-27600376260-macos-x64.log","repository":{"id":"R_kgDOSbOoUw","isFork":false,"isPrivate":false,"nameWithOwner":"link-assistant/formal-ai","url":"https://github.com/link-assistant/formal-ai"},"url":"https://github.com/link-assistant/formal-ai/blob/6f94f490f4a2cf361991932856264086c0e2a2a8/docs/case-studies/issue-479/ci-logs/desktop-release-27600376260-macos-x64.log"},{"path":"docs/case-studies/issue-479/ci-logs/desktop-release-27600376260-macos-arm64.log","repository":{"id":"R_kgDOSbOoUw","isFork":false,"isPrivate":false,"nameWithOwner":"link-assistant/formal-ai","url":"https://github.com/link-assistant/formal-ai"},"url":"https://github.com/link-assistant/formal-ai/blob/6f94f490f4a2cf361991932856264086c0e2a2a8/docs/case-studies/issue-479/ci-logs/desktop-release-27600376260-macos-arm64.log"},{"path":"docs/case-studies/issue-121/ci-logs/job-76668537953.log","repository":{"id":"R_kgDOSbOoUw","isFork":false,"isPrivate":false,"nameWithOwner":"link-assistant/formal-ai","url":"https://github.com/link-assistant/formal-ai"},"url":"https://github.com/link-assistant/formal-ai/blob/6f94f490f4a2cf361991932856264086c0e2a2a8/docs/case-studies/issue-121/ci-logs/job-76668537953.log"},{"path":"docs/case-studies/issue-410/raw-data/formal-ai/README.md","repository":{"id":"R_kgDOSbOoUw","isFork":false,"isPrivate":false,"nameWithOwner":"link-assistant/formal-ai","url":"https://github.com/link-assistant/formal-ai"},"url":"https://github.com/link-assistant/formal-ai/blob/6f94f490f4a2cf361991932856264086c0e2a2a8/docs/case-studies/issue-410/raw-data/formal-ai/README.md"},{"path":"tests/unit/docs_requirements_issue_607.rs","repository":{"id":"R_kgDOSbOoUw","isFork":false,"isPrivate":false,"nameWithOwner":"link-assistant/formal-ai","url":"https://github.com/link-assistant/formal-ai"},"url":"https://github.com/link-assistant/formal-ai/blob/6f94f490f4a2cf361991932856264086c0e2a2a8/tests/unit/docs_requirements_issue_607.rs"},{"path":"desktop/scripts/vscode-install.test.mjs","repository":{"id":"R_kgDOSbOoUw","isFork":false,"isPrivate":false,"nameWithOwner":"link-assistant/formal-ai","url":"https://github.com/link-assistant/formal-ai"},"url":"https://github.com/link-assistant/formal-ai/blob/6f94f490f4a2cf361991932856264086c0e2a2a8/desktop/scripts/vscode-install.test.mjs"},{"path":"tests/unit/docs_requirements_issue_603.rs","repository":{"id":"R_kgDOSbOoUw","isFork":false,"isPrivate":false,"nameWithOwner":"link-assistant/formal-ai","url":"https://github.com/link-assistant/formal-ai"},"url":"https://github.com/link-assistant/formal-ai/blob/6f94f490f4a2cf361991932856264086c0e2a2a8/tests/unit/docs_requirements_issue_603.rs"},{"path":"README.md","repository":{"id":"R_kgDOSbOoUw","isFork":false,"isPrivate":false,"nameWithOwner":"link-assistant/formal-ai","url":"https://github.com/link-assistant/formal-ai"},"url":"https://github.com/link-assistant/formal-ai/blob/6f94f490f4a2cf361991932856264086c0e2a2a8/README.md"},{"path":"docs/desktop/server-api.md","repository":{"id":"R_kgDOSbOoUw","isFork":false,"isPrivate":false,"nameWithOwner":"link-assistant/formal-ai","url":"https://github.com/link-assistant/formal-ai"},"url":"https://github.com/link-assistant/formal-ai/blob/6f94f490f4a2cf361991932856264086c0e2a2a8/docs/desktop/server-api.md"}] diff --git a/docs/case-studies/issue-270/formal-ai-issue-621.json b/docs/case-studies/issue-270/formal-ai-issue-621.json new file mode 100644 index 00000000..8034f1bc --- /dev/null +++ b/docs/case-studies/issue-270/formal-ai-issue-621.json @@ -0,0 +1 @@ +{"body":"## Summary\n\nOur own `@link-assistant/agent` CLI is **not** a `with-formal-ai` tool, so it can't be connected to Formal AI with a single command like the others:\n\n```\nwith-formal-ai agent hi\n# Error: unsupported tool `agent`; supported tools: codex, opencode, gemini\n```\n\nIt should be added to `data/seed/client-integrations.lino` so `with-formal-ai agent -p hi` works by default, consistent with codex/opencode/gemini.\n\nTested `2026-07-04`, `@link-assistant/agent 0.24.1` (latest via `bun add --global`), server `formal-ai serve --agent-mode` on `127.0.0.1:8080`.\n\n## It's fully configurable via env alone (no config file needed)\n\n`agent` is OpenCode-JSON-compatible and reads an inline config from `LINK_ASSISTANT_AGENT_CONFIG_CONTENT` (or a dir from `LINK_ASSISTANT_AGENT_CONFIG_DIR`). Providing the `formalai` provider that way plus `FORMAL_AI_API_KEY` is enough — verified working:\n\n```\nFORMAL_AI_API_KEY=formal-ai \\\nLINK_ASSISTANT_AGENT_CONFIG_CONTENT='{\"$schema\":\"https://opencode.ai/config.json\",\"provider\":{\"formalai\":{\"name\":\"formal-ai local server\",\"npm\":\"@ai-sdk/openai-compatible\",\"options\":{\"baseURL\":\"http://127.0.0.1:8080/api/openai/v1\",\"apiKey\":\"{env:FORMAL_AI_API_KEY}\"},\"models\":{\"formal-ai\":{\"name\":\"formal-ai\"}}}},\"model\":\"formalai/formal-ai\"}' \\\nagent --model formalai/formal-ai -p hi\n# => assistant text: \"Hi, how may I help you?\" (exit 0)\n```\n\n## Suggested fix\n\nAdd an `agent` entry to the seed reusing the `opencode` OpenAI provider shape:\n- **ephemeral:** inject the provider JSON via `LINK_ASSISTANT_AGENT_CONFIG_CONTENT` (env-only, no temp file needed), set `FORMAL_AI_API_KEY`, and pass `--model formalai/formal-ai` (`-p` for the prompt).\n- **global (`-g`) target:** `~/.config/link-assistant-agent/opencode.json` (same OpenCode provider block as opencode's global config).\n\nSince the CLI needs no on-disk config for the ephemeral case, this mirrors the existing tools cleanly.\n\nMinor upstream note (filed on link-assistant/agent): with a single provider, `agent` logs harmless `ProviderModelNotFoundError` \"skipping unresolvable compaction model in cascade\" for its default compaction list — cosmetic, doesn't affect output.\n\nPart of the \"`with-formal-ai` should work with no extra args/env by default\" effort alongside the gemini-default-failure issue.\n","createdAt":"2026-07-03T20:39:28Z","labels":[],"number":621,"state":"OPEN","title":"with-formal-ai: add support for our own 'agent' CLI (@link-assistant/agent) — it is fully env-configurable","updatedAt":"2026-07-03T20:39:51Z","url":"https://github.com/link-assistant/formal-ai/issues/621"} diff --git a/docs/case-studies/issue-270/formal-ai-with-formal-ai-prs.json b/docs/case-studies/issue-270/formal-ai-with-formal-ai-prs.json new file mode 100644 index 00000000..76bb74b8 --- /dev/null +++ b/docs/case-studies/issue-270/formal-ai-with-formal-ai-prs.json @@ -0,0 +1 @@ +[{"author":{"id":"MDQ6VXNlcjE0MzE5MDQ=","is_bot":false,"login":"konard","type":"User","url":"https://github.com/konard"},"closedAt":"2026-07-03T21:39:12Z","number":623,"title":"Fix with-formal-ai Gemini auth isolation","url":"https://github.com/link-assistant/formal-ai/pull/623"},{"author":{"id":"MDQ6VXNlcjE0MzE5MDQ=","is_bot":false,"login":"konard","type":"User","url":"https://github.com/konard"},"closedAt":"2026-07-03T08:21:44Z","number":615,"title":"Add with-formal-ai wrapper for external CLIs","url":"https://github.com/link-assistant/formal-ai/pull/615"},{"author":{"id":"MDQ6VXNlcjE0MzE5MDQ=","is_bot":false,"login":"konard","type":"User","url":"https://github.com/konard"},"closedAt":"2026-07-02T21:00:09Z","number":609,"title":"Emit Agent CLI shell tool calls for ls requests","url":"https://github.com/link-assistant/formal-ai/pull/609"},{"author":{"id":"MDQ6VXNlcjE0MzE5MDQ=","is_bot":false,"login":"konard","type":"User","url":"https://github.com/konard"},"closedAt":"2026-07-02T22:07:51Z","number":610,"title":"Fix OpenAI chat completion streaming compatibility","url":"https://github.com/link-assistant/formal-ai/pull/610"},{"author":{"id":"MDQ6VXNlcjE0MzE5MDQ=","is_bot":false,"login":"konard","type":"User","url":"https://github.com/konard"},"closedAt":"2026-07-02T20:07:49Z","number":601,"title":"Make our meanings and words more detailed (grammatical number, part of speech, Wikidata grounding)","url":"https://github.com/link-assistant/formal-ai/pull/601"},{"author":{"id":"MDQ6VXNlcjE0MzE5MDQ=","is_bot":false,"login":"konard","type":"User","url":"https://github.com/konard"},"closedAt":"2026-07-02T23:31:00Z","number":611,"title":"Fix formal-ai model id and aliases","url":"https://github.com/link-assistant/formal-ai/pull/611"},{"author":{"id":"MDQ6VXNlcjE0MzE5MDQ=","is_bot":false,"login":"konard","type":"User","url":"https://github.com/konard"},"closedAt":"2026-07-03T00:33:03Z","number":612,"title":"Add multi-protocol API gateway","url":"https://github.com/link-assistant/formal-ai/pull/612"},{"author":{"id":"MDQ6VXNlcjE0MzE5MDQ=","is_bot":false,"login":"konard","type":"User","url":"https://github.com/konard"},"closedAt":"2026-06-30T22:45:33Z","number":597,"title":"Natural-language whole-memory read+write control (Rust + browser, 4 languages)","url":"https://github.com/link-assistant/formal-ai/pull/597"},{"author":{"id":"MDQ6VXNlcjE0MzE5MDQ=","is_bot":false,"login":"konard","type":"User","url":"https://github.com/konard"},"closedAt":"2026-06-30T01:03:07Z","number":590,"title":"Add natural-language conversation and memory recall","url":"https://github.com/link-assistant/formal-ai/pull/590"},{"author":{"id":"MDQ6VXNlcjE0MzE5MDQ=","is_bot":false,"login":"konard","type":"User","url":"https://github.com/konard"},"closedAt":"2026-06-28T17:57:02Z","number":582,"title":"Fix multilingual elided how-to procedural prompts","url":"https://github.com/link-assistant/formal-ai/pull/582"},{"author":{"id":"MDQ6VXNlcjE0MzE5MDQ=","is_bot":false,"login":"konard","type":"User","url":"https://github.com/konard"},"closedAt":"2026-06-29T09:04:17Z","number":587,"title":"Route install how-to prompts to official docs","url":"https://github.com/link-assistant/formal-ai/pull/587"},{"author":{"id":"MDQ6VXNlcjE0MzE5MDQ=","is_bot":false,"login":"konard","type":"User","url":"https://github.com/konard"},"closedAt":"2026-06-18T19:56:13Z","number":532,"title":"E4: AgentProvider seam for desktop agent execution","url":"https://github.com/link-assistant/formal-ai/pull/532"},{"author":{"id":"MDQ6VXNlcjE0MzE5MDQ=","is_bot":false,"login":"konard","type":"User","url":"https://github.com/konard"},"closedAt":"2026-07-03T07:12:18Z","number":613,"title":"Expose reasoning through standard API fields","url":"https://github.com/link-assistant/formal-ai/pull/613"},{"author":{"id":"MDQ6VXNlcjE0MzE5MDQ=","is_bot":false,"login":"konard","type":"User","url":"https://github.com/konard"},"closedAt":"2026-06-18T21:01:00Z","number":533,"title":"E5: Installable Formal-AI agent environment","url":"https://github.com/link-assistant/formal-ai/pull/533"},{"author":{"id":"MDQ6VXNlcjE0MzE5MDQ=","is_bot":false,"login":"konard","type":"User","url":"https://github.com/konard"},"closedAt":"2026-07-03T10:19:25Z","number":617,"title":"Fix previous-user question recall","url":"https://github.com/link-assistant/formal-ai/pull/617"},{"author":{"id":"MDQ6VXNlcjE0MzE5MDQ=","is_bot":false,"login":"konard","type":"User","url":"https://github.com/konard"},"closedAt":"2026-06-25T22:10:52Z","number":564,"title":"Support repository resource summarization (files and folders)","url":"https://github.com/link-assistant/formal-ai/pull/564"},{"author":{"id":"MDQ6VXNlcjE0MzE5MDQ=","is_bot":false,"login":"konard","type":"User","url":"https://github.com/konard"},"closedAt":"2026-06-26T22:34:23Z","number":570,"title":"Fix train meeting word problem reasoning","url":"https://github.com/link-assistant/formal-ai/pull/570"},{"author":{"id":"MDQ6VXNlcjE0MzE5MDQ=","is_bot":false,"login":"konard","type":"User","url":"https://github.com/konard"},"closedAt":"2026-06-19T22:51:25Z","number":537,"title":"Add cold-start agent ls home E2E coverage","url":"https://github.com/link-assistant/formal-ai/pull/537"},{"author":{"id":"MDQ6VXNlcjE0MzE5MDQ=","is_bot":false,"login":"konard","type":"User","url":"https://github.com/konard"},"closedAt":"2026-06-17T21:00:11Z","number":470,"title":"Prepare Telegram Docker image + one-click/one-line Telegram bot and OpenAI server","url":"https://github.com/link-assistant/formal-ai/pull/470"},{"author":{"id":"MDQ6VXNlcjE0MzE5MDQ=","is_bot":false,"login":"konard","type":"User","url":"https://github.com/konard"},"closedAt":"2026-06-26T10:03:38Z","number":569,"title":"Fix Russian PHP Hello World follow-up","url":"https://github.com/link-assistant/formal-ai/pull/569"},{"author":{"id":"MDQ6VXNlcjE0MzE5MDQ=","is_bot":false,"login":"konard","type":"User","url":"https://github.com/konard"},"closedAt":"2026-06-29T21:41:28Z","number":591,"title":"Search unresolved bare terms","url":"https://github.com/link-assistant/formal-ai/pull/591"},{"author":{"id":"MDQ6VXNlcjE0MzE5MDQ=","is_bot":false,"login":"konard","type":"User","url":"https://github.com/konard"},"closedAt":"2026-06-28T15:48:08Z","number":579,"title":"Fix neural inference concept lookup","url":"https://github.com/link-assistant/formal-ai/pull/579"},{"author":{"id":"MDQ6VXNlcjE0MzE5MDQ=","is_bot":false,"login":"konard","type":"User","url":"https://github.com/konard"},"closedAt":"2026-06-26T00:21:10Z","number":566,"title":"Fix Rust self-source metrics write_program prompt","url":"https://github.com/link-assistant/formal-ai/pull/566"},{"author":{"id":"MDQ6VXNlcjE0MzE5MDQ=","is_bot":false,"login":"konard","type":"User","url":"https://github.com/konard"},"closedAt":"2026-07-03T12:42:15Z","number":618,"title":"Solve the whole class of external-entity questions by reasoning (#571)","url":"https://github.com/link-assistant/formal-ai/pull/618"},{"author":{"id":"MDQ6VXNlcjE0MzE5MDQ=","is_bot":false,"login":"konard","type":"User","url":"https://github.com/konard"},"closedAt":"2026-06-21T20:14:49Z","number":553,"title":"Add shared dialog replay conversion","url":"https://github.com/link-assistant/formal-ai/pull/553"},{"author":{"id":"MDQ6VXNlcjE0MzE5MDQ=","is_bot":false,"login":"konard","type":"User","url":"https://github.com/konard"},"closedAt":"2026-06-28T18:09:07Z","number":583,"title":"Fix release badges and restore README badges","url":"https://github.com/link-assistant/formal-ai/pull/583"},{"author":{"id":"MDQ6VXNlcjE0MzE5MDQ=","is_bot":false,"login":"konard","type":"User","url":"https://github.com/konard"},"closedAt":"2026-06-28T21:11:28Z","number":588,"title":"Route topic-interest prompts to web search","url":"https://github.com/link-assistant/formal-ai/pull/588"},{"author":{"id":"MDQ6VXNlcjE0MzE5MDQ=","is_bot":false,"login":"konard","type":"User","url":"https://github.com/konard"},"closedAt":"2026-06-26T11:05:10Z","number":567,"title":"Fix crypto portfolio tracker blueprint routing","url":"https://github.com/link-assistant/formal-ai/pull/567"},{"author":{"id":"MDQ6VXNlcjE0MzE5MDQ=","is_bot":false,"login":"konard","type":"User","url":"https://github.com/konard"},"closedAt":"2026-06-26T10:25:24Z","number":568,"title":"Fix smart travel planner class requests","url":"https://github.com/link-assistant/formal-ai/pull/568"},{"author":{"id":"MDQ6VXNlcjE0MzE5MDQ=","is_bot":false,"login":"konard","type":"User","url":"https://github.com/konard"},"closedAt":"2026-06-25T23:56:31Z","number":565,"title":"Handle research result follow-ups","url":"https://github.com/link-assistant/formal-ai/pull/565"}] diff --git a/docs/case-studies/issue-270/issue-270.json b/docs/case-studies/issue-270/issue-270.json new file mode 100644 index 00000000..69b4981f --- /dev/null +++ b/docs/case-studies/issue-270/issue-270.json @@ -0,0 +1 @@ +{"author":{"id":"MDQ6VXNlcjE0MzE5MDQ=","is_bot":false,"login":"konard","name":"Konstantin Diachenko"},"body":"https://github.com/link-assistant/formal-ai now has with-formal-ai and other options.\n\nDouble check the docs, make sure it is fully verified (actually executed, tested and working).\n\nWe also need to make sure Agent CLI knows about `formal-ai` and `@link-assistant/formal-ai` model aliases by default.\n\nAnd we can also give instructions to user on installation, setup, when used with `formal-ai` model or alias and it is not available (server locally or remotely is not up and running).\n\nWe might need to use workarounds if https://github.com/link-assistant/formal-ai/issues/621 is not yet implemented, also double check if you find any additional issues - report all of them to https://github.com/link-assistant/formal-ai. About missing features or docs and so on, or anything broken.\n\nWe need to collect data related about the issue to this repository, make sure we compile that data to `./docs/case-studies/issue-{id}` folder, and use it to do deep case study analysis (also make sure to search online for additional facts and data), list of each and all requirements from the issue, and propose possible solutions and solution plans for each requirement (we should also check known existing components/libraries, that solve similar problem or can help in solutions).\n\nPlease plan and execute everything in this single pull request, you have unlimited time and context, as context auto-compacts and you can continue indefinitely, until it is each and every requirement fully addressed, and everything is totally done.\n\n","createdAt":"2026-06-14T21:56:51Z","labels":[{"id":"LA_kwDOQYTy3M8AAAACQHojAA","name":"documentation","description":"Improvements or additions to documentation","color":"0075ca"},{"id":"LA_kwDOQYTy3M8AAAACQHojCw","name":"enhancement","description":"New feature or request","color":"a2eeef"}],"number":270,"state":"OPEN","title":"Add instructions on how to setup Formal AI with Agent CLI","updatedAt":"2026-07-03T21:41:32Z","url":"https://github.com/link-assistant/agent/issues/270"} diff --git a/docs/case-studies/issue-270/issue-comments.json b/docs/case-studies/issue-270/issue-comments.json new file mode 100644 index 00000000..0637a088 --- /dev/null +++ b/docs/case-studies/issue-270/issue-comments.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/docs/case-studies/issue-270/pr-277.json b/docs/case-studies/issue-270/pr-277.json new file mode 100644 index 00000000..53e04bd1 --- /dev/null +++ b/docs/case-studies/issue-270/pr-277.json @@ -0,0 +1 @@ +{"author":{"id":"MDQ6VXNlcjE0MzE5MDQ=","is_bot":false,"login":"konard","name":"Konstantin Diachenko"},"baseRefName":"main","body":"## 🤖 AI-Powered Solution Draft\n\nThis pull request is being automatically generated to solve issue #270.\n\n### 📋 Issue Reference\nFixes #270\n\n### 🚧 Status\n**Work in Progress** - The AI assistant is currently analyzing and implementing the solution draft.\n\n### 📝 Implementation Details\n_Details will be added as the solution draft is developed..._\n\n---\n*This PR was created automatically by the AI issue solver*","createdAt":"2026-07-03T21:40:45Z","headRefName":"issue-270-377e5a73eb84","isDraft":true,"number":277,"state":"OPEN","title":"[WIP] Add instructions on how to setup Formal AI with Agent CLI","updatedAt":"2026-07-03T21:40:46Z","url":"https://github.com/link-assistant/agent/pull/277"} diff --git a/docs/case-studies/issue-270/pr-comments.json b/docs/case-studies/issue-270/pr-comments.json new file mode 100644 index 00000000..0637a088 --- /dev/null +++ b/docs/case-studies/issue-270/pr-comments.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/docs/case-studies/issue-270/pr-review-comments.json b/docs/case-studies/issue-270/pr-review-comments.json new file mode 100644 index 00000000..0637a088 --- /dev/null +++ b/docs/case-studies/issue-270/pr-review-comments.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/docs/case-studies/issue-270/pr-reviews.json b/docs/case-studies/issue-270/pr-reviews.json new file mode 100644 index 00000000..0637a088 --- /dev/null +++ b/docs/case-studies/issue-270/pr-reviews.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/docs/formal-ai.md b/docs/formal-ai.md new file mode 100644 index 00000000..4554278a --- /dev/null +++ b/docs/formal-ai.md @@ -0,0 +1,149 @@ +# Formal AI Provider + +[Formal AI](https://github.com/link-assistant/formal-ai) is a local symbolic assistant server with OpenAI-compatible API routes. Agent can use it without a custom provider config through these selectors: + +| Selector | Notes | +| --------------------------- | ----------------------------------------------------------------- | +| `formal-ai` | Short model alias, resolved to `formal-ai/formal-ai` | +| `formal-ai/formal-ai` | Canonical Agent provider/model form | +| `@link-assistant/formal-ai` | Scoped Formal AI alias accepted by Agent | +| `formalai/formal-ai` | Compatibility alias used by Formal AI's OpenCode wrapper examples | + +Agent sends all of these to the Formal AI server as the canonical model id `formal-ai`. + +## Quick Start + +1. Install the Formal AI CLI: + + ```bash + cargo install formal-ai + ``` + + You can also use Formal AI's universal installer: + + ```bash + curl -fsSL https://raw.githubusercontent.com/link-assistant/formal-ai/main/scripts/install.sh | sh -s -- cli + ``` + +2. Start the local server with agent mode enabled: + + ```bash + formal-ai serve --agent-mode --host 127.0.0.1 --port 8080 + ``` + +3. Export a client API key. If the server was started with `FORMAL_AI_API_BEARER_TOKEN`, this value must match it. If no bearer token is configured, any non-empty value is enough for clients that require an API key. + + ```bash + export FORMAL_AI_API_KEY="local-test-token" + ``` + +4. Run Agent with a Formal AI selector: + + ```bash + agent --model formal-ai --permission-mode plan -p "run ls to list files here" + agent --model @link-assistant/formal-ai --permission-mode plan -p "hi" + agent --model formalai/formal-ai --permission-mode plan -p "hi" + ``` + +Use `--permission-mode plan` when you want Agent to allow read-only shell commands such as `ls` after approval. Use `--read-only` when shell execution should be disabled entirely. + +## Server Checks + +Before running Agent, verify the Formal AI server is reachable: + +```bash +curl -s http://127.0.0.1:8080/health +curl -s http://127.0.0.1:8080/api/openai/v1/models +``` + +The OpenAI-compatible base URL that Agent uses by default is: + +```text +http://127.0.0.1:8080/api/openai/v1 +``` + +## Remote Servers and Non-Default Ports + +Set `FORMAL_AI_BASE_URL` when the server is on another host, port, or reverse proxy. Include the OpenAI protocol path: + +```bash +export FORMAL_AI_BASE_URL="http://127.0.0.1:18080/api/openai/v1" +export FORMAL_AI_API_KEY="local-test-token" +agent --model formal-ai -p "hi" +``` + +For persistent config, you can still override the built-in provider: + +```json +{ + "$schema": "https://opencode.ai/config.json", + "provider": { + "formal-ai": { + "name": "formal-ai local server", + "npm": "@ai-sdk/openai-compatible", + "options": { + "baseURL": "http://127.0.0.1:18080/api/openai/v1", + "apiKey": "{env:FORMAL_AI_API_KEY}" + }, + "models": { + "formal-ai": { + "name": "formal-ai" + } + } + } + }, + "model": "formal-ai/formal-ai" +} +``` + +## `formal-ai with` and `with-formal-ai` + +Formal AI also ships wrapper commands for other CLIs: + +```bash +formal-ai with --start-server codex "hi" +formal-ai with opencode run "hi" +formal-ai with gemini -p "hi" + +with-formal-ai -g codex +with-formal-ai -g opencode +with-formal-ai -g gemini +with-formal-ai -g --all +with-formal-ai -g --undo codex +``` + +Agent does not require a wrapper or generated config for the built-in selectors above. The wrappers are still useful for Codex, OpenCode, Gemini, and other clients that need per-tool config files or protocol-specific environment variables. + +## Troubleshooting + +### `ProviderModelNotFoundError` for `formal-ai` + +Use an Agent version that includes the built-in Formal AI provider. Older Agent versions require the manual provider config shown above. + +### Connection refused, `fetch failed`, or server unavailable + +Start the server and check the base URL: + +```bash +formal-ai serve --agent-mode --host 127.0.0.1 --port 8080 +curl -s http://127.0.0.1:8080/health +``` + +If you use a remote host or non-default port, set `FORMAL_AI_BASE_URL` to the full OpenAI-compatible path ending in `/api/openai/v1`. + +### 401 or 403 authentication errors + +If the server was started with `FORMAL_AI_API_BEARER_TOKEN`, export the same value as `FORMAL_AI_API_KEY` before running Agent: + +```bash +FORMAL_AI_API_BEARER_TOKEN="local-test-token" formal-ai serve --agent-mode --host 127.0.0.1 --port 8080 +export FORMAL_AI_API_KEY="local-test-token" +``` + +### No tool calls are produced + +Start the server with `--agent-mode`. Agent mode makes Formal AI emit tool calls for requests such as listing files, while Agent remains responsible for deciding whether tools are allowed. + +### `link-assistant/formal-ai` does not select Formal AI in Agent + +Formal AI's HTTP API accepts `link-assistant/formal-ai`, but Agent already uses the `link-assistant` provider namespace internally. Use `@link-assistant/formal-ai`, `formal-ai`, or `formal-ai/formal-ai` in Agent. diff --git a/js/.changeset/formal-ai-provider.md b/js/.changeset/formal-ai-provider.md new file mode 100644 index 00000000..020c798e --- /dev/null +++ b/js/.changeset/formal-ai-provider.md @@ -0,0 +1,5 @@ +--- +'@link-assistant/agent': patch +--- + +Add built-in Formal AI provider aliases and setup documentation. diff --git a/js/README.md b/js/README.md index ea0ba7ad..17490c03 100644 --- a/js/README.md +++ b/js/README.md @@ -24,7 +24,7 @@ This is an MVP implementation of an OpenCode-compatible CLI agent, focused on ma - ✅ **JSON Input/Output**: Compatible with `opencode run --format json --model opencode/minimax-m2.5-free` - ✅ **Plain Text Input**: Also accepts plain text messages (auto-converted to JSON format) -- ✅ **Flexible Model Selection**: Defaults to free OpenCode Zen MiniMax M2.5 Free, supports [OpenCode Zen](https://opencode.ai/docs/zen/), [Claude OAuth](../docs/claude-oauth.md), [Groq](../docs/groq.md), and [OpenRouter](../docs/openrouter.md) providers +- ✅ **Flexible Model Selection**: Defaults to free OpenCode Zen MiniMax M2.5 Free, supports [OpenCode Zen](https://opencode.ai/docs/zen/), [Formal AI](../docs/formal-ai.md), [Claude OAuth](../docs/claude-oauth.md), [Groq](../docs/groq.md), and [OpenRouter](../docs/openrouter.md) providers - ✅ **No Restrictions**: Fully unrestricted file system and command execution access (no sandbox) - ✅ **Minimal Footprint**: Built with Bun.sh for maximum efficiency - ✅ **Full Tool Support**: 13 tools including websearch, codesearch, batch - all enabled by default @@ -201,6 +201,10 @@ echo "hi" | agent --model opencode/nemotron-3-super-free echo "hi" | agent --model opencode/gpt-5-nano echo "hi" | agent --model opencode/big-pickle +# Formal AI local server +echo "hi" | agent --model formal-ai +echo "hi" | agent --model @link-assistant/formal-ai + # Premium models (OpenCode Zen subscription) echo "hi" | agent --model opencode/sonnet # Claude Sonnet 4.5 echo "hi" | agent --model opencode/haiku # Claude Haiku 4.5 @@ -237,6 +241,7 @@ echo "hi" | agent --model github-copilot/gpt-4o # Uses Copilot ``` See [MODELS.md](../MODELS.md) for complete list of available models and pricing. +See [docs/formal-ai.md](../docs/formal-ai.md) for Formal AI local server setup. See [docs/groq.md](../docs/groq.md) for Groq provider documentation. See [docs/openrouter.md](../docs/openrouter.md) for OpenRouter provider documentation. See [docs/claude-oauth.md](../docs/claude-oauth.md) for Claude OAuth provider documentation. diff --git a/js/src/provider/provider.ts b/js/src/provider/provider.ts index a997ce52..61913e24 100644 --- a/js/src/provider/provider.ts +++ b/js/src/provider/provider.ts @@ -75,7 +75,79 @@ export namespace Provider { type Source = 'env' | 'config' | 'custom' | 'api'; + const FORMAL_AI_DEFAULT_BASE_URL = 'http://127.0.0.1:8080/api/openai/v1'; + const FORMAL_AI_API_KEY_ENV = 'FORMAL_AI_API_KEY'; + const FORMAL_AI_BASE_URL_ENV = 'FORMAL_AI_BASE_URL'; + const FORMAL_AI_DEFAULT_API_KEY = 'formal-ai'; + + function getFormalAiBaseURL(provider?: ModelsDev.Provider) { + return ( + process.env[FORMAL_AI_BASE_URL_ENV] ?? + provider?.api ?? + FORMAL_AI_DEFAULT_BASE_URL + ); + } + + function createFormalAiLoader(input?: ModelsDev.Provider) { + return { + autoload: true, + options: { + apiKey: process.env[FORMAL_AI_API_KEY_ENV] ?? FORMAL_AI_DEFAULT_API_KEY, + baseURL: getFormalAiBaseURL(input), + }, + }; + } + + function createFormalAiModel(name = 'Formal AI'): ModelsDev.Model { + return { + id: 'formal-ai', + name, + release_date: '2026-07-03', + attachment: false, + reasoning: true, + temperature: true, + tool_call: true, + cost: { + input: 0, + output: 0, + cache_read: 0, + cache_write: 0, + }, + limit: { + context: 60000, + output: 8192, + }, + modalities: { + input: ['text'], + output: ['text'], + }, + options: {}, + provider: { + npm: '@ai-sdk/openai-compatible', + }, + }; + } + + function createFormalAiProvider( + id: string, + name: string + ): ModelsDev.Provider { + return { + id, + name, + npm: '@ai-sdk/openai-compatible', + api: getFormalAiBaseURL(), + env: [FORMAL_AI_API_KEY_ENV], + models: { + 'formal-ai': createFormalAiModel('formal-ai'), + }, + }; + } + const CUSTOM_LOADERS: Record = { + 'formal-ai': async (input) => createFormalAiLoader(input), + formalai: async (input) => createFormalAiLoader(input), + '@link-assistant': async (input) => createFormalAiLoader(input), async anthropic(input) { // Check if OAuth credentials are available via the auth plugin const auth = await Auth.get('anthropic'); @@ -791,6 +863,22 @@ export namespace Provider { realIdByKey.set('google/gemini-3-pro', 'gemini-3-pro-preview'); } + // Add Formal AI local OpenAI-compatible provider and selectors. + // Formal AI serves the canonical model ID "formal-ai"; Agent also accepts + // the scoped selector "@link-assistant/formal-ai" as a provider alias. + database['formal-ai'] = createFormalAiProvider( + 'formal-ai', + 'Formal AI Local Server' + ); + database['formalai'] = createFormalAiProvider( + 'formalai', + 'Formal AI Local Server' + ); + database['@link-assistant'] = createFormalAiProvider( + '@link-assistant', + 'Formal AI Local Server (@link-assistant alias)' + ); + // Add link-assistant echo provider for dry-run testing // This synthetic provider echoes back user input without API calls // @see https://github.com/link-assistant/agent/issues/89 @@ -1994,6 +2082,17 @@ export namespace Provider { } } + if (modelID === 'formal-ai') { + const formalAiProvider = s.providers['formal-ai']; + if (formalAiProvider && formalAiProvider.info.models[modelID]) { + log.info(() => ({ + message: 'resolved short model name to formal-ai (canonical)', + modelID, + })); + return { providerID: 'formal-ai', modelID }; + } + } + // Check if model exists in any provider const matchingProviders: string[] = []; for (const [providerID, provider] of Object.entries(s.providers)) { diff --git a/js/tests/formal-ai-provider.ts b/js/tests/formal-ai-provider.ts new file mode 100644 index 00000000..f214c9eb --- /dev/null +++ b/js/tests/formal-ai-provider.ts @@ -0,0 +1,101 @@ +import { describe, expect, test } from 'bun:test'; + +async function resolveFormalAiSelector(selector: string) { + const script = ` + import { parseModelConfig } from './src/cli/model-config.js'; + import { initConfig, resetConfig } from './src/config/config.ts'; + import { Instance } from './src/project/instance.ts'; + import { Provider } from './src/provider/provider.ts'; + + process.argv = ['bun', 'agent', '--model', ${JSON.stringify(selector)}]; + resetConfig(); + initConfig(process.argv); + + await Instance.provide({ + directory: process.cwd(), + fn: async () => { + const parsed = await parseModelConfig( + { model: ${JSON.stringify(selector)}, 'compaction-models': '(same)' }, + () => {}, + () => {}, + { defaultCompactionModels: '(same)' } + ); + const model = await Provider.getModel(parsed.providerID, parsed.modelID); + const state = await Provider.state(); + const provider = state.providers[parsed.providerID]; + + console.log( + 'RESULT ' + + JSON.stringify({ + providerID: parsed.providerID, + modelID: parsed.modelID, + canonicalModelID: model.info.id, + providerName: provider.info.name, + baseURL: provider.options.baseURL, + apiKey: provider.options.apiKey, + }) + ); + }, + }); + + await Instance.disposeAll(); + `; + + const proc = Bun.spawn({ + cmd: ['bun', '--eval', script], + cwd: process.cwd(), + stdout: 'pipe', + stderr: 'pipe', + env: { + ...process.env, + FORMAL_AI_API_KEY: 'local-test-token', + FORMAL_AI_BASE_URL: 'http://127.0.0.1:18080/api/openai/v1', + LINK_ASSISTANT_AGENT_CONFIG_CONTENT: '{}', + LINK_ASSISTANT_AGENT_DEFAULT_COMPACTION_MODELS: '(same)', + }, + }); + + const [stdout, stderr, exitCode] = await Promise.all([ + new Response(proc.stdout).text(), + new Response(proc.stderr).text(), + proc.exited, + ]); + + if (exitCode !== 0) { + throw new Error( + `Formal AI selector ${selector} failed with exit ${exitCode}\nSTDOUT:\n${stdout}\nSTDERR:\n${stderr}` + ); + } + + const resultLine = stdout + .split('\n') + .find((line) => line.startsWith('RESULT ')); + if (!resultLine) { + throw new Error( + `Formal AI selector ${selector} did not print a RESULT line\nSTDOUT:\n${stdout}\nSTDERR:\n${stderr}` + ); + } + + return JSON.parse(resultLine.slice('RESULT '.length)); +} + +describe('Formal AI provider defaults', () => { + test.each([ + ['formal-ai', 'formal-ai'], + ['formal-ai/formal-ai', 'formal-ai'], + ['@link-assistant/formal-ai', '@link-assistant'], + ['formalai/formal-ai', 'formalai'], + ])( + 'resolves %s without a manual provider config', + async (selector, expectedProviderID) => { + const result = await resolveFormalAiSelector(selector); + + expect(result.providerID).toBe(expectedProviderID); + expect(result.modelID).toBe('formal-ai'); + expect(result.canonicalModelID).toBe('formal-ai'); + expect(result.providerName).toContain('Formal AI'); + expect(result.baseURL).toBe('http://127.0.0.1:18080/api/openai/v1'); + expect(result.apiKey).toBe('local-test-token'); + } + ); +});