Skip to content

feat(ai-provider)!: provider catalog, protocol registry, plugin text protocols and OpenAI Decisions - #176

Merged
ackness merged 5 commits into
mainfrom
feat/provider-module
Oct 9, 2026
Merged

ackness merged 5 commits into
mainfrom
feat/provider-module

Conversation

@ackness

@ackness ackness commented Oct 9, 2026

Copy link
Copy Markdown
Owner

Breaking: finishReason in ctx.gateway results is now the same word for every provider (stop, length, tool_calls, content_filter, error, other). A plugin that compared it with a provider's own word (end_turn, max_tokens, tool_use) must compare with the unified value. No stored data is affected.

Summary / 摘要

Adding a provider or a protocol meant editing a protocol list copied by hand in seven places, and each protocol's rules were if (protocol === ...) branches in three files. A player who wanted a provider outside the eight built-in ones had to look up its endpoint and pick a protocol, and a local service without an API key was shown as not configured. This PR reworks the provider module so both are one entry, and brings its formats in line with the AI SDK and the official provider SDKs.

Five commits, each of which passes the gates on its own:

  1. feat(ai-provider)! one finish-reason vocabulary. Adapters returned each provider's own word and three consumers translated it separately. On a call that is not streamed, Anthropic's max_tokens was read as a normal end, so a cut answer was kept as complete. The gateway now returns the unified reason and keeps the provider's word in rawFinishReason (the AI SDK's unified / raw).
  2. fix(ai-provider) retry rules and failure kinds. 408 and 409 are retried, x-should-retry and retry-after-ms are read, and a wait longer than 60 seconds or than the call has left returns at once so a backup model can be used. A connection test sends one request, tests an embedding model with a vector call, and reports errorKind (unreachable, auth, quota, rate_limited, …) so the settings page can say what to do.
  3. feat(ai-provider) protocol table, provider catalog, local services, model lists. Protocol IDs, names and output kind come from one table in @covel/shared; a ProtocolDefinition holds a protocol's reasoning fields, provider options, optional parameters and model list. Built-in providers grow from 8 to 22 (nine cloud providers; Ollama, LM Studio, llama.cpp, vLLM, LiteLLM), the add-provider dialog picks one by name, an llm.toml slot of a built-in provider needs only provider and model, a model on a loopback address is ready without a key, and POST /api/ai/models reads an endpoint's model IDs.
  4. feat(plugins) a plugin can register a text protocol. covel.registerWires({ text: [{ id, generateText, streamText }] }); a model whose protocol is <pluginId>/<wireId> uses it. GET /api/ai/protocols lists them for the settings page.
  5. feat(ai-provider) OpenAI Decisions. openai-decisions-v1 answers gateway.evaluate() through POST /v1/decisions (public beta, gpt-6-luna). The public question and answer types do not change.

Type of change / 变更类型

  • New feature / 新功能 (feat)
  • Bug fix / Bug 修复 (fix)
  • Refactor / 重构 (refactor)
  • Documentation / 文档 (docs)
  • Tests / 测试 (test)
  • Tooling, CI, release / 工具链、CI、发布 (chore / ci)
  • Performance / 性能 (perf)
  • Breaking change / 破坏性变更 (BREAKING CHANGE)

Breaking changes / 破坏性变更

  • finishReason of ctx.gateway.generateText / generateObject and of the gateway's results is the unified value; the provider's word moved to rawFinishReason on gateway results. The bundled history-compaction plugin was the only one that compared a provider's word and is updated.
  • A long Retry-After is no longer waited for. A call whose provider asks for more than 60 seconds, or more than the call's remaining budget, ends as rate limited (and may use a backup model) instead of waiting until the budget runs out.
  • Server environment keys have more trusted origins. A key named for one of the nine new cloud providers (for example GROQ_API_KEY) attaches to that provider's official origin, under the same rule as the existing built-in providers.

No session, snapshot, queued job or world has to be recreated.

Verification / 验证方式

  • pnpm check — on each of the five commits, in a clean worktree
  • pnpm test — on each of the five commits, in a clean worktree
  • pnpm test:pg — no store change
  • pnpm e2e:smoke / pnpm e2e — not run
  • pnpm validate:plugin / pnpm validate:world — covered by pnpm check; no manifest change
  • pnpm e2e:verify — not run
  • Manual check / 手动验证: classifyProviderFailure against real endpoints (a closed port, a host that does not exist, a wrong key) returned unreachable with the host and port, unreachable, and auth.

Not verified / 未验证:

  • No real model was called. The request bodies of the four text protocols are covered by the existing request-body tests, which pass unchanged; pnpm e2e:replay could not run because the repository has no recording for lantern-barrow.
  • OpenAI Decisions follows the API reference and mocked responses; no call was made with a real key.
  • The nine new cloud endpoints were probed for existence (401 without a key), not for a completed chat.
  • The model list was not read from a live local service, and the settings page changes (provider picker, model list, failure hints, protocol list) were not looked at in a browser. Playwright was not run, so an e2e spec that counts the protocol options could need an update.
  • A connection test of an image or speech model still goes through the text path and reports a failure; that is unchanged.

Related issue / context / 关联

  • The comparison of 2026-10-06 (86 recorded requests sent through Covel's adapters and through the AI SDK: equal bodies, same rejection rate) is why this keeps the adapters and adopts the AI SDK's conventions instead of replacing the module.
  • References: @ai-sdk/provider 4.0.26 and ai 7.0.136 sources for finish reasons, retries and the decision model; OpenAI Decisions guide and API reference.
  • Known and unchanged: a loopback baseUrl is accepted on every deployment tier, so on demo / commercial a request-scoped preset can point the server at its own localhost. Telling an operator's loopback endpoint from a request's needs its own change.

Docs sync / 文档同步

  • docs/reference/ updated: slots.md (protocols, providers, adding one, finish reasons, retries, failure kinds, connection test), api.md (/api/ai/models, /api/ai/protocols, ping errorKind), plugin-extensions.md (text wires), evaluation.md (OpenAI Decisions), media-store.md; docs/architecture/security.md (trusted origins, keyless loopback, text wires)
  • Guides and both READMEs — n/a
  • docs/CHANGELOG.md has entries under [Unreleased]
  • AGENTS.md — n/a (no package, root script or convention changed)

Adapters returned each provider's own word (`end_turn`, `max_tokens`,
`tool_use`) and three consumers translated it separately. On a call that is
not streamed the runtime read Anthropic's `max_tokens` as a normal end, so a
cut answer was kept as complete.

The gateway now returns `stop`, `length`, `tool_calls`, `content_filter`,
`error` or `other` and keeps the provider's word in `rawFinishReason`, the
structure the AI SDK uses (`unified` / `raw`). One table in `@covel/shared`
(`unifyFinishReason`) serves the gateway, the runtime and world generation.

BREAKING CHANGE: `ctx.gateway` results carry the unified finish reason. A
plugin that compared it with a provider's word must compare with the unified
value.
…failures

Transport retries: 408 and 409 are sent again like 429 and 5xx,
`x-should-retry` decides when a provider sets it, and `retry-after-ms` is read
before `Retry-After`. A wait of more than 60 seconds, or more than the call
has left, is not waited for: the answer returns as rate limited and the
gateway can use a backup model. Before, the call waited until its budget ran
out and ended with an error that allows no backup.

A connection test sends one request (`transportRetry: false`), tests an
embedding model with a vector call, and reports `errorKind` beside the
provider's words. `classifyProviderFailure` reads the kind from the status,
the provider's error code and the cause chain of a failed connection, so a
refused connection names its host and port instead of `fetch failed`.
…services, model lists

Protocol IDs were copied by hand in seven places (the plugin SDK's copy had
lost Gemini) and each protocol's rules were `if (protocol === ...)` branches
in three files. The IDs, their names and their output kind now come from one
table in `@covel/shared`, and a `ProtocolDefinition` holds a protocol's
reasoning fields, provider options, optional parameters, media-wire rule and
model list.

The built-in providers grow from 8 to 22: nine more cloud providers and five
services on this machine (Ollama, LM Studio, llama.cpp, vLLM, LiteLLM). The
add-provider dialog picks one by name, an `llm.toml` slot of a built-in
provider needs only `provider` and `model`, and a model on a loopback address
is ready without an API key. `POST /api/ai/models` reads an endpoint's model
IDs for the settings page.
`covel.registerWires({ text: [{ id, generateText, streamText }] })` adds a
request and response format beside the built-in protocols. A model whose
`protocol` is `<pluginId>/<wireId>` uses it for every text call; `llm.toml`,
the request validators and the settings page accept the ID, and
`GET /api/ai/protocols` lists the registered ones. Object calls go through
`generateText` with the JSON Schema as an instruction.

The plugin SDK gains the wire's types and the `providerOptions` field of
`ctx.gateway.generateText`, which the runtime already passed on.
`openai-decisions-v1` answers `gateway.evaluate()` through OpenAI's
`POST /v1/decisions` (public beta, model `gpt-6-luna`). The public question
and answer types stay as they are; the adapter converts at the wire:

- `state` goes as `input` text, a JSON value serialized
- the question map becomes a list with each name, and the answer list is
  keyed back by name
- a boolean is a `predicate` whose true/false rubrics join `instructions`
- choice options and score levels are lists; a choice needs 2-255 options
- probability lists become the maps the other protocols return, under the
  same checks

A question the model declines fails the call as a refusal, since the contract
has an answer for every question. Usage carries cache reads and writes, and
per-question confidence goes to `providerMetadata.openai`.

The `openai` provider suggests this protocol for an evaluation model. Field
names follow the API reference; no call was made with a real key.
@ackness ackness added the breaking-change Incompatible contract or data change requiring migration label Oct 9, 2026
@ackness
ackness merged commit c02ba4e into main Oct 9, 2026
6 checks passed
@ackness
ackness deleted the feat/provider-module branch October 9, 2026 05:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking-change Incompatible contract or data change requiring migration

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant