diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f9b6f9c..fe69708f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 `~/.amplifier/openai-chatgpt-oauth.json`, refreshing them itself. Requires "Sign in with device code" enabled in the account's ChatGPT Security settings. Like `github-copilot`, `auth set openai-chatgpt` is refused — there is no static key to store. +- **Chat Completions provider.** `provider.module: "chat-completions"` is now a valid + host-config value, backed by `amplifier-module-provider-chat-completions`. It integrates + any server speaking the OpenAI Chat Completions wire format + (`/v1/chat/completions`) — llama.cpp, vLLM, LM Studio, LocalAI, SGLang, TGI, and other + OpenAI-compatible endpoints — distinct from `openai`, which uses the OpenAI Responses + API. Its credential is an endpoint, not a key: `CHAT_COMPLETIONS_BASE_URL` (required) + selects the server, and `CHAT_COMPLETIONS_API_KEY` (optional) is sent only when set, + since local servers commonly need none. Both are environment-only; the persisted + credentials file is not consulted for this provider. Default model is `default`. ## [0.12.0] — 2026-07-29 diff --git a/README.md b/README.md index 1b3663c1..e6f82a79 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ Public integrations run opencode, paperclip, and NanoClaw on it: see [who has in `amplifier-agent` ships with: -- Six providers behind one interface: Anthropic, OpenAI, Azure OpenAI, Ollama, GitHub Copilot, and ChatGPT (a Plus/Pro/Team subscription via OAuth device-code, no API key), with credentials read from the environment or a cached OAuth session +- Seven providers behind one interface: Anthropic, OpenAI, Azure OpenAI, Ollama, GitHub Copilot, ChatGPT (a Plus/Pro/Team subscription via OAuth device-code, no API key), and Chat Completions (any OpenAI Chat Completions-compatible endpoint, e.g. llama.cpp, vLLM, LM Studio), with credentials read from the environment or a cached OAuth session - Role-based model routing, so a sub-agent gets a model matched to its job rather than the frontier model for everything, re-matched when you switch providers - Context management that keeps long sessions running, compacting history before it overruns the window - Tools for filesystem, bash, web, search, todo, and MCP @@ -83,7 +83,7 @@ amplifier-agent auth status # diagnose env-vs-file precedence per p amplifier-agent models list # enumerate available models from providers ``` -Full precedence rules, GitHub Copilot's environment-only caveat, and the host config file schema are in [`docs/CONFIGURATION.md`](docs/CONFIGURATION.md). +Full precedence rules, GitHub Copilot's environment-only caveat, the `chat-completions` provider's required `CHAT_COMPLETIONS_BASE_URL`, and the host config file schema are in [`docs/CONFIGURATION.md`](docs/CONFIGURATION.md). ## Use it from your code diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index c9722535..f6468e32 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -11,11 +11,14 @@ Provider is auto-detected from environment variables in this precedence: 3. `AZURE_OPENAI_API_KEY` + `AZURE_OPENAI_ENDPOINT` 4. `OLLAMA_HOST` (defaults to `http://localhost:11434`) -`github-copilot` and `openai-chatgpt` are excluded from this auto-detect chain -- neither resolves -from a single API-key environment variable. `github-copilot` reads its own token chain from the -environment (see below). `openai-chatgpt` has no credential env var at all: it authenticates via -OAuth device-code, caching tokens to `~/.amplifier/openai-chatgpt-oauth.json`. Both must be -selected explicitly with `provider.module` in a host config file. +`github-copilot`, `openai-chatgpt`, and `chat-completions` are excluded from this auto-detect chain +-- none of them resolves from a single API-key environment variable. `github-copilot` reads its own +token chain from the environment (see below). `openai-chatgpt` has no credential env var at all: it +authenticates via OAuth device-code, caching tokens to `~/.amplifier/openai-chatgpt-oauth.json`. +`chat-completions` needs an endpoint rather than a key (`CHAT_COMPLETIONS_BASE_URL`, plus optional +`CHAT_COMPLETIONS_API_KEY`) and has no implicit default endpoint to fall back to. All three must be +selected explicitly with `provider.module` in a host config file, rather than silently winning a +"first match" race. Override by passing `--config ` at a host config file that names a provider explicitly. @@ -73,6 +76,13 @@ This matters for hosts that spawn `amplifier-agent` as a subprocess: once you ha > > An existing `gh` or VS Code login may already authenticate it through the SDK's cached OAuth, so try it before exporting anything. This is a temporary limitation: the real fix is in the provider module, whose token resolver needs to read the agent-delivered credential from its config before falling back to the environment. `auth set` support returns once that lands. +> **`chat-completions` is environment-only too, but for a different reason.** Its "credential" is the target `base_url`, not an API key, so it is read purely from the environment: `CHAT_COMPLETIONS_BASE_URL` (required — the server to talk to) and `CHAT_COMPLETIONS_API_KEY` (optional; local servers like llama.cpp, vLLM, LM Studio, and LocalAI commonly need none). The persisted credentials file is not consulted for this provider. +> +> ```bash +> export CHAT_COMPLETIONS_BASE_URL=http://localhost:8000/v1 +> # export CHAT_COMPLETIONS_API_KEY=... # only if your server requires one +> ``` + The file format is a versioned JSON envelope: ```jsonc diff --git a/docs/INTEGRATION.md b/docs/INTEGRATION.md index 15e1a181..c9ec3ddc 100644 --- a/docs/INTEGRATION.md +++ b/docs/INTEGRATION.md @@ -8,7 +8,7 @@ The engine runs **one turn per invocation** and exits. Continuity across turns c `amplifier-agent` is a standalone binary. You do not need the Amplifier CLI, bundles, or any other repository in the `microsoft/amplifier*` family, and none of them is a substitute for it here. -Use it when your software needs to run an agent: a loop with tools, file access, sub-agents, and/or multi-turn state. It also works for plain LLM calls, where you get routing across six providers behind one interface. +Use it when your software needs to run an agent: a loop with tools, file access, sub-agents, and/or multi-turn state. It also works for plain LLM calls, where you get routing across seven providers behind one interface. Then pick a surface below, install the engine ([INSTALL.md](INSTALL.md)), and finish with the [checklist](#checklist-for-a-new-integration). diff --git a/docs/LAYERS_AND_RELEASES.md b/docs/LAYERS_AND_RELEASES.md index accc224f..1e39ed2f 100644 --- a/docs/LAYERS_AND_RELEASES.md +++ b/docs/LAYERS_AND_RELEASES.md @@ -115,7 +115,7 @@ The engine ships with `bundle.md` baked into the wheel. It declares which module **Pre-wired modules:** -- **Providers:** `provider-anthropic`, `provider-openai`, `provider-azure-openai`, `provider-ollama`, `provider-github-copilot`, `provider-openai-chatgpt` +- **Providers:** `provider-anthropic`, `provider-openai`, `provider-azure-openai`, `provider-ollama`, `provider-github-copilot`, `provider-openai-chatgpt`, `provider-chat-completions` - **Orchestrator:** `loop-streaming` (with `extended_thinking: true`) - **Context:** `context-simple` (300K tokens, auto-compact at 80%) - **Tools:** `tool-filesystem`, `tool-bash`, `tool-web`, `tool-search`, `tool-todo`, `tool-apply-patch`, `tool-delegate`, `tool-mcp`, `tool-skills`, `tool-mode`, `tool-recipes` diff --git a/docs/architecture/architecture.dot b/docs/architecture/architecture.dot index 4e59a9ed..9375d3b3 100644 --- a/docs/architecture/architecture.dot +++ b/docs/architecture/architecture.dot @@ -84,7 +84,7 @@ digraph amplifier_agent { node [fillcolor="#d9d9d9"]; kernel [label="amplifier-foundation\nbundle / session kernel"]; - providers [label="LLM providers\nanthropic · openai · azure\nollama · copilot · chatgpt"]; + providers [label="LLM providers\nanthropic · openai · azure\nollama · copilot · chatgpt\nchat-completions"]; mcp [label="MCP servers"]; } diff --git a/docs/spec/bundle-and-cache.md b/docs/spec/bundle-and-cache.md index fb9908e3..53787a53 100644 --- a/docs/spec/bundle-and-cache.md +++ b/docs/spec/bundle-and-cache.md @@ -30,7 +30,8 @@ default_provider: anthropic REQUIRED, engine-level, top-level key providers: install-only stubs, no config and no credentials provider-anthropic, provider-openai, provider-azure-openai, - provider-ollama, provider-github-copilot, provider-openai-chatgpt + provider-ollama, provider-github-copilot, provider-openai-chatgpt, + provider-chat-completions session.orchestrator: loop-streaming extended_thinking: true session.context: context-simple max_tokens 300000, auto_compact diff --git a/docs/spec/host-config.md b/docs/spec/host-config.md index 6765718b..89d2940b 100644 --- a/docs/spec/host-config.md +++ b/docs/spec/host-config.md @@ -81,7 +81,8 @@ approval.patterns must be a list of strings `provider` selects the provider module and carries its config. ``` -provider.module one of: anthropic, openai, azure-openai, ollama, github-copilot, openai-chatgpt +provider.module one of: anthropic, openai, azure-openai, ollama, github-copilot, + openai-chatgpt, chat-completions provider.config free-form; belongs to the provider module ``` diff --git a/docs/spec/providers-and-models.md b/docs/spec/providers-and-models.md index 926767fc..11d3cec4 100644 --- a/docs/spec/providers-and-models.md +++ b/docs/spec/providers-and-models.md @@ -9,21 +9,22 @@ routes a wire `model` field to a provider (see `http-face.md`). ## Supported providers -Six providers are supported, and only six. The provider name is the value used in configuration, +Seven providers are supported, and only seven. The provider name is the value used in configuration, in `auth` subcommands, and in `models list --provider`. ``` -anthropic provider-anthropic -openai provider-openai -azure-openai provider-azure-openai -ollama provider-ollama -github-copilot provider-github-copilot -openai-chatgpt provider-openai-chatgpt +anthropic provider-anthropic +openai provider-openai +azure-openai provider-azure-openai +ollama provider-ollama +github-copilot provider-github-copilot +openai-chatgpt provider-openai-chatgpt +chat-completions provider-chat-completions ``` Each module is installed from `git+https://github.com/microsoft/amplifier-module-@main`. -All six are declared by the shipped bundle as install-only, so preparing the bundle makes every -provider importable before any session exists. +All seven are declared by the shipped bundle (`bundle.md`'s top-level `providers:` stub list) as +install-only, so preparing the bundle makes every provider importable before any session exists. The agent holds no static table of default models, credential field shapes, or display names. Those come from the provider module at runtime, so they cannot drift from provider truth. @@ -44,12 +45,13 @@ configuration. Primary variables, in the order consulted: ``` -anthropic ANTHROPIC_API_KEY -openai OPENAI_API_KEY -azure-openai AZURE_OPENAI_API_KEY, then AZURE_OPENAI_KEY -ollama OLLAMA_HOST, then OLLAMA_BASE_URL -github-copilot GITHUB_TOKEN -openai-chatgpt (none -- OAuth device-code) +anthropic ANTHROPIC_API_KEY +openai OPENAI_API_KEY +azure-openai AZURE_OPENAI_API_KEY, then AZURE_OPENAI_KEY +ollama OLLAMA_HOST, then OLLAMA_BASE_URL +github-copilot GITHUB_TOKEN +openai-chatgpt (none -- OAuth device-code) +chat-completions CHAT_COMPLETIONS_BASE_URL, plus optional CHAT_COMPLETIONS_API_KEY ``` `AZURE_OPENAI_KEY` is the only deprecated alias. Consulting it emits a one-time warning on stderr. @@ -68,13 +70,25 @@ openai-chatgpt has no environment variable at all. It resolves from a cached OAu flow (`login_on_mount`) and refreshed automatically thereafter. Its resolution reports source `"file"` when a token is cached and `"none"` otherwise -- never `"env"`. +chat-completions has its own dedicated resolution branch, distinct from the generic +env-then-file chain above: its primary variable's value lands in `fields["base_url"]`, not +`fields["api_key"]`, because the thing it needs to know is *which server to talk to*, not a +secret. `CHAT_COMPLETIONS_API_KEY` is consulted only when set (local servers such as llama.cpp, +vLLM, LM Studio, and LocalAI commonly need none) and lands in `fields["api_key"]` alongside it. +Unlike every other provider, the persisted credentials file is never consulted for +chat-completions -- with no `CHAT_COMPLETIONS_BASE_URL` in the environment it resolves +unconditionally to `source == "none"`, with no file fallback and no usable default to fall back +to (unlike ollama's built-in localhost). + A resolution reports the provider, whether it resolved, the source (`env`, `file`, `default`, or `none`), the variable consulted, and the resolved fields. Ollama backed only by the built-in default host reports unresolved on purpose, so auto-enrollment does not enlist a local daemon that may not be running. -Requesting credentials for a key-based provider that resolves to `none` is an error. Ollama and -unrecognized provider names never raise. +Requesting credentials for a key-based provider that resolves to `none` is an error. Ollama, +chat-completions, and unrecognized provider names never raise -- chat-completions is not one of +the key-based providers this rule applies to, so a missing `CHAT_COMPLETIONS_BASE_URL` is left for +the provider module itself to reject at call time, not raised here. ## The credentials file @@ -115,6 +129,11 @@ to `~/.amplifier/openai-chatgpt-oauth.json`, refreshed by the provider module it are enumerated in the same `_CONFIG_CREDENTIAL_UNSUPPORTED` gate; this is temporary and specific to these two providers. +`auth set chat-completions` is accepted (unlike github-copilot, it is not refused), but as noted +above the chat-completions resolution branch never reads the credentials file -- only +`CHAT_COMPLETIONS_BASE_URL` / `CHAT_COMPLETIONS_API_KEY` in the environment are consulted. Set +those instead of relying on `auth set` for this provider. + `auth clear` without `--force` exits 2. ## Provider selection at boot @@ -125,7 +144,7 @@ these two providers. 3. no further fallback: a bundle declaring neither is a hard error at boot ``` -`provider.module` is closed to the six supported names. Any other value fails validation with +`provider.module` is closed to the seven supported names. Any other value fails validation with error code `config_invalid_provider_module`. `"auto"` is not a valid value. There is no `--provider` flag and no environment-based provider auto-detection. See Non-goals. diff --git a/skills/amplifier-agent/SKILL.md b/skills/amplifier-agent/SKILL.md index 3a092a0a..719d7bb3 100644 --- a/skills/amplifier-agent/SKILL.md +++ b/skills/amplifier-agent/SKILL.md @@ -22,7 +22,7 @@ metadata: `amplifier-agent` is an agent engine that other software runs on. Give it a prompt and it runs the full loop, with tools, sub-agents, skills, and MCP, then returns a result. Anything that can spawn a subprocess can use it; Python hosts can embed the engine library in-process instead. -Reach for it when the project needs an *agent* (a tool loop, file access, sub-agents, multi-turn state) rather than a single completion. You can also use it for plain LLM calls, with routing across six providers behind one interface. +Reach for it when the project needs an *agent* (a tool loop, file access, sub-agents, multi-turn state) rather than a single completion. You can also use it for plain LLM calls, with routing across seven providers behind one interface. **The engine runs one turn per invocation and exits.** Continuity across turns comes from a session id, not from a long-lived process. Every surface below is a different way of delivering a prompt to that same engine. @@ -54,7 +54,7 @@ The installer needs `uv` and `curl` and will not bootstrap them silently; it tel Install as **the same user that runs the host process**; a host spawning a subprocess inherits that user's `PATH`. `amplifier-agent doctor` is the check that the install actually works, so run it before writing any integration code. -Credentials are read from the environment, first match wins: `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `AZURE_OPENAI_API_KEY` plus `AZURE_OPENAI_ENDPOINT`, `OLLAMA_HOST`. GitHub Copilot is environment-only (`COPILOT_AGENT_TOKEN`, `COPILOT_GITHUB_TOKEN`, `GH_TOKEN`, `GITHUB_TOKEN`). ChatGPT (`openai-chatgpt`) has no credential env var at all: it authenticates via OAuth device-code, caching tokens to `~/.amplifier/openai-chatgpt-oauth.json`. Or store a static key with `amplifier-agent auth set anthropic sk-ant-...` (not supported for github-copilot or openai-chatgpt). +Credentials are read from the environment, first match wins: `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `AZURE_OPENAI_API_KEY` plus `AZURE_OPENAI_ENDPOINT`, `OLLAMA_HOST`. GitHub Copilot is environment-only (`COPILOT_AGENT_TOKEN`, `COPILOT_GITHUB_TOKEN`, `GH_TOKEN`, `GITHUB_TOKEN`). ChatGPT (`openai-chatgpt`) has no credential env var at all: it authenticates via OAuth device-code, caching tokens to `~/.amplifier/openai-chatgpt-oauth.json`. The `chat-completions` provider is environment-only too, for any OpenAI Chat Completions-compatible endpoint (llama.cpp, vLLM, LM Studio, LocalAI, and similar): `CHAT_COMPLETIONS_BASE_URL` (required) plus optional `CHAT_COMPLETIONS_API_KEY`. Or store a static key with `amplifier-agent auth set anthropic sk-ant-...` (not supported for github-copilot or openai-chatgpt). ## Pick a surface @@ -191,7 +191,7 @@ A per-instance config file looks like this: | `config_unreadable`, `config_malformed_json` | The `--config` file could not be opened, or is not a JSON object | Check the path the host wrote, and that it serialized an object | | `config_unknown_key` | Unrecognized **top-level** config key | The top level is closed: `approval`, `provider`, `providers`, `mcp`, `skills`, `debug`, `allowProtocolSkew` | | `config_invalid_type` | A known key has the wrong type, or an unknown sub-key in a closed inner shape | `skills.*` and `debug.*` are closed and raise this rather than `config_unknown_key`, which is reserved for the top level and `providers.` entries | -| `config_invalid_provider_module` | `provider.module` is not a known provider | One of `anthropic`, `openai`, `azure-openai`, `ollama`, `github-copilot`, `openai-chatgpt`. `"auto"` is not valid | +| `config_invalid_provider_module` | `provider.module` is not a known provider | One of `anthropic`, `openai`, `azure-openai`, `ollama`, `github-copilot`, `openai-chatgpt`, `chat-completions`. `"auto"` is not valid | | `protocol_version_mismatch` | Wrapper and engine protocol versions differ | Update the lagging side. `allowProtocolSkew` is an unblock, not a fix | | `lifecycle_unsupported` | `submit()` called twice on one handle | New handle per turn, same `sessionId` with `resume` | | `env_injection_rejected` | The wrapper refused the environment you asked it to inject | Check the key against the wrapper's allowlist and blocked-key list | diff --git a/src/amplifier_agent_cli/provider_sources.py b/src/amplifier_agent_cli/provider_sources.py index d4d20d0f..64553b72 100644 --- a/src/amplifier_agent_cli/provider_sources.py +++ b/src/amplifier_agent_cli/provider_sources.py @@ -112,6 +112,7 @@ def _emit_legacy_env_var_notice(legacy_var: str, preferred_var: str) -> None: "ollama", "github-copilot", "openai-chatgpt", + "chat-completions", ) @@ -147,6 +148,10 @@ def _emit_legacy_env_var_notice(legacy_var: str, preferred_var: str) -> None: "module": "provider-openai-chatgpt", "source": "git+https://github.com/microsoft/amplifier-module-provider-openai-chatgpt@main", }, + "chat-completions": { + "module": "provider-chat-completions", + "source": "git+https://github.com/microsoft/amplifier-module-provider-chat-completions@main", + }, } @@ -422,6 +427,41 @@ def resolve_credential_detailed(provider_name: str) -> CredentialResolution: fields={}, ) + if provider_name == "chat-completions": + # Endpoint-agnostic OpenAI Chat Completions provider. Its required + # "credential" is ``base_url`` (the server to talk to), NOT an api key -- + # local servers (llama.cpp, vLLM, LM Studio, LocalAI) commonly need no + # key at all. This needs a dedicated branch rather than a + # PROVIDER_CREDENTIAL_VARS entry because the generic branch below routes + # the primary env var's value into ``fields["api_key"]``; here the value + # must land in ``fields["base_url"]`` instead. Without base_url the + # provider cannot serve a request, so absent base_url is honestly + # ``resolved=False, source="none"`` (there is no usable localhost default + # to fall back to, unlike ollama). + base_url = os.environ.get("CHAT_COMPLETIONS_BASE_URL", "") + if not base_url: + return CredentialResolution( + provider=provider_name, + resolved=False, + source="none", + env_var="CHAT_COMPLETIONS_BASE_URL", + fields={}, + ) + cc_fields: dict[str, str] = {"base_url": base_url} + # api_key is optional: only inject when explicitly set, so a host-config + # value (or the module's own "not-needed" default) is not clobbered by an + # empty env var during protected-key re-assertion in build_provider_entry. + cc_api_key = os.environ.get("CHAT_COMPLETIONS_API_KEY", "") + if cc_api_key: + cc_fields["api_key"] = cc_api_key + return CredentialResolution( + provider=provider_name, + resolved=True, + source="env", + env_var="CHAT_COMPLETIONS_BASE_URL", + fields=cc_fields, + ) + env_vars = PROVIDER_CREDENTIAL_VARS.get(provider_name) if not env_vars: return CredentialResolution(provider=provider_name, resolved=False, source="none", env_var=None, fields={}) diff --git a/src/amplifier_agent_lib/bundle/bundle.md b/src/amplifier_agent_lib/bundle/bundle.md index b42db36a..cca2655a 100644 --- a/src/amplifier_agent_lib/bundle/bundle.md +++ b/src/amplifier_agent_lib/bundle/bundle.md @@ -70,6 +70,8 @@ providers: source: git+https://github.com/microsoft/amplifier-module-provider-github-copilot@main - module: provider-openai-chatgpt source: git+https://github.com/microsoft/amplifier-module-provider-openai-chatgpt@main + - module: provider-chat-completions + source: git+https://github.com/microsoft/amplifier-module-provider-chat-completions@main session: raw: true diff --git a/src/amplifier_agent_lib/config/loader.py b/src/amplifier_agent_lib/config/loader.py index 0db46887..79bd477b 100644 --- a/src/amplifier_agent_lib/config/loader.py +++ b/src/amplifier_agent_lib/config/loader.py @@ -30,7 +30,7 @@ _VALID_TOP_LEVEL_KEYS = frozenset({"mcp", "approval", "provider", "providers", "allowProtocolSkew", "skills", "debug"}) _VALID_PROVIDER_MODULES = frozenset( - {"anthropic", "openai", "azure-openai", "ollama", "github-copilot", "openai-chatgpt"} + {"anthropic", "openai", "azure-openai", "ollama", "github-copilot", "openai-chatgpt", "chat-completions"} ) # G3: explicit set of host-supplied approval modes. ``CliApprovalSystem`` accepts # exactly these three strings; any other value must be rejected at parse time