feat: provider and model registry, OpenAI-compatible adapter, discovery and health (P-a) #22

Merged
KofTwentyTwo merged 3 commits from feature/p1-provider-registry into develop 2026-10-13 22:59:53 +00:00
KofTwentyTwo commented 2026-10-13 22:18:06 +00:00 (Migrated from github.com)

P-a: provider and model registry, OpenAI-compatible adapter, discovery and health

Spec: docs/DESIGN.md, "Owner decisions (2026-10-13), continued", part P, phase P-a. This PR branches from develop and is independent of #19 and #21. It touches some of the same files as #21 (AssistantMetaDataBuilder, AssistantRecordScreens, AssistantStore, schema-postgres.sql, CHANGELOG.md), so whichever lands second gets a rebase.

Data (standard assistant_* tables, assistant.admin, AI exposure NONE, behind the assistant_* channel wall)

  • assistant_provider gains:
    • kind: Anthropic, OpenAI, Azure OpenAI, OpenRouter, Ollama, LM Studio, vLLM, llama.cpp, or generic OpenAI-compatible;
    • location: LOCAL or CLOUD;
    • hipaa_eligible, which records a fact and does not create a BAA;
    • region, api_version, the connect and request timeouts, tls_ca_bundle, and the last discovery status.
  • New assistant_model table, a child list on the provider: model id, display name, capabilities (tools, vision, streaming, reasoning effort, context window, max output), price in and out per million tokens (0 for local models unless set), enabled flag and last test.
  • Model ids grow to 200 characters on turn, message, usage and provider (OpenRouter ids are long).

SPI (additive)

  • ProviderSettings (kind, key, base URL, API version, region, timeouts, CA trust).
  • AssistantProviderType gets kinds, per-kind defaults, create(settings), discoverModels and checkHealth as default methods. create(apiKey, baseUrl) implementations, including the Anthropic module, keep working unchanged.
  • RegistryModelProvider applies the assistant_model capabilities and output cap per call.
  • Prices: assistant_model, then assistant_model_price, then config. An unpriced local model costs 0.

New module qbit-assistant-provider-openai

Uses JDK HttpClient and Jackson, with no vendor SDK.

  • Calls: Chat Completions with SSE streaming, or single JSON when the model cannot stream.
  • Tool calls: fragmented and parallel tool calls are reassembled. Repeated ids and names are tolerated, a missing id is generated, and older Ollama's stop with tool calls is handled. Arguments that do not parse go back to the model as invalid input instead of failing the call.
  • Usage: cached tokens are counted as cache reads.
  • Dialects: max_completion_tokens versus max_tokens, and Azure deployment URLs with api-version and the api-key header.
  • Keys and trust: local kinds can run without a key, or with a bearer key from an allow-listed environment variable (for an auth proxy). A per-provider CA bundle (withTrustedCaBundle(name, pem) on the config) is trusted on top of the JVM roots. Hostname verification stays on and there is no trust-all option.
  • Strict tools: strict is sent only for OpenAI and Azure, only for opted-in tools, and only when the schemas are recursively strict. A 400 that mentions strict, schema or grammar sets strictSchemaRejected, so TurnEngine runs its existing one-retry fallback.
  • Timeouts, cancel and errors: there is a connect timeout, a request timeout and an idle-read guard, and cancel() aborts the call. Errors map to the existing kinds with sanitized provider messages and request ids. Keys never appear in logs, exceptions or toString.
  • Discovery and health: discovery calls GET /v1/models (Azure: /openai/models) and reads the context length when the server reports one. Health is the same call with a short timeout.

Record actions (streamed ETL with Review and Result lines, never in the nav)

  • Discover models on providers adds the missing assistant_model rows. It never overwrites admin edits, and it disables cloud catalogues of more than 20 models.
  • Test connection on models makes a one-shot call with that model and records the usage as a TEST row.
  • Test connection on providers now runs the health check first, then the completion.

Tests

154 in total, all passing in ./mvnw verify with the JaCoCo gates:

  • qbit-assistant: 99. Registry, settings validation, model selection and price precedence, the processes with a fake type, and CA bundles.
  • Anthropic: 14, unchanged.
  • OpenAI: 41.
    • Request shape per dialect.
    • Streaming, tools and usage.
    • Every error mapping, and timeouts and cancel.
    • Discovery and health.
    • HTTPS against a test CA: untrusted fails with a hint, the bundle succeeds, and a hostname mismatch still fails.
    • A golden turn with a real tool round trip through the adapter and a mock server.
  • LiveOpenAiCompatibleTest (tag live, excluded by default) is configured through ASSISTANT_LIVE_OPENAI_BASE_URL, _KIND, _KEY_ENV, _MODEL and _CA_FILE. It skips when these are unset or the host is unreachable, which covers the local Ollama hosts until their TLS is finished.

Screenshots

These were taken from the test-only OpenAiDemoServer with Next UI RC.18. Its OpenAI-compatible host is a mock, but Discover models, Test connection and one turn really run. At 390 and 1440 px, light and dark, no page or inner horizontal scroll was found.

light 1440 dark 390
Home, providers card
Local provider
Its Models child list
A model

All 16 files are in docs/screenshots/p-*.

🤖 Generated with Claude Code

## P-a: provider and model registry, OpenAI-compatible adapter, discovery and health Spec: `docs/DESIGN.md`, "Owner decisions (2026-10-13), continued", part P, phase P-a. This PR branches from `develop` and is independent of #19 and #21. It touches some of the same files as #21 (`AssistantMetaDataBuilder`, `AssistantRecordScreens`, `AssistantStore`, `schema-postgres.sql`, `CHANGELOG.md`), so whichever lands second gets a rebase. ### Data (standard `assistant_*` tables, `assistant.admin`, AI exposure NONE, behind the `assistant_*` channel wall) - `assistant_provider` gains: - `kind`: Anthropic, OpenAI, Azure OpenAI, OpenRouter, Ollama, LM Studio, vLLM, llama.cpp, or generic OpenAI-compatible; - `location`: LOCAL or CLOUD; - `hipaa_eligible`, which records a fact and does not create a BAA; - `region`, `api_version`, the connect and request timeouts, `tls_ca_bundle`, and the last discovery status. - New **`assistant_model`** table, a child list on the provider: model id, display name, capabilities (tools, vision, streaming, reasoning effort, context window, max output), price in and out per million tokens (0 for local models unless set), enabled flag and last test. - Model ids grow to 200 characters on turn, message, usage and provider (OpenRouter ids are long). ### SPI (additive) - `ProviderSettings` (kind, key, base URL, API version, region, timeouts, CA trust). - `AssistantProviderType` gets kinds, per-kind defaults, `create(settings)`, `discoverModels` and `checkHealth` as default methods. `create(apiKey, baseUrl)` implementations, including the Anthropic module, keep working unchanged. - `RegistryModelProvider` applies the `assistant_model` capabilities and output cap per call. - Prices: `assistant_model`, then `assistant_model_price`, then config. An unpriced local model costs 0. ### New module `qbit-assistant-provider-openai` Uses JDK `HttpClient` and Jackson, with no vendor SDK. - **Calls:** Chat Completions with SSE streaming, or single JSON when the model cannot stream. - **Tool calls:** fragmented and parallel tool calls are reassembled. Repeated ids and names are tolerated, a missing id is generated, and older Ollama's `stop` with tool calls is handled. Arguments that do not parse go back to the model as invalid input instead of failing the call. - **Usage:** cached tokens are counted as cache reads. - **Dialects:** `max_completion_tokens` versus `max_tokens`, and Azure deployment URLs with `api-version` and the `api-key` header. - **Keys and trust:** local kinds can run without a key, or with a bearer key from an allow-listed environment variable (for an auth proxy). A per-provider CA bundle (`withTrustedCaBundle(name, pem)` on the config) is trusted on top of the JVM roots. Hostname verification stays on and there is no trust-all option. - **Strict tools:** `strict` is sent only for OpenAI and Azure, only for opted-in tools, and only when the schemas are recursively strict. A 400 that mentions strict, schema or grammar sets `strictSchemaRejected`, so `TurnEngine` runs its existing one-retry fallback. - **Timeouts, cancel and errors:** there is a connect timeout, a request timeout and an idle-read guard, and `cancel()` aborts the call. Errors map to the existing kinds with sanitized provider messages and request ids. Keys never appear in logs, exceptions or `toString`. - **Discovery and health:** discovery calls `GET /v1/models` (Azure: `/openai/models`) and reads the context length when the server reports one. Health is the same call with a short timeout. ### Record actions (streamed ETL with Review and Result lines, never in the nav) - **Discover models** on providers adds the missing `assistant_model` rows. It never overwrites admin edits, and it disables cloud catalogues of more than 20 models. - **Test connection** on models makes a one-shot call with that model and records the usage as a TEST row. - **Test connection** on providers now runs the health check first, then the completion. ### Tests 154 in total, all passing in `./mvnw verify` with the JaCoCo gates: - `qbit-assistant`: 99. Registry, settings validation, model selection and price precedence, the processes with a fake type, and CA bundles. - Anthropic: 14, unchanged. - OpenAI: 41. - Request shape per dialect. - Streaming, tools and usage. - Every error mapping, and timeouts and cancel. - Discovery and health. - HTTPS against a test CA: untrusted fails with a hint, the bundle succeeds, and a hostname mismatch still fails. - A golden turn with a real tool round trip through the adapter and a mock server. - `LiveOpenAiCompatibleTest` (tag `live`, excluded by default) is configured through `ASSISTANT_LIVE_OPENAI_BASE_URL`, `_KIND`, `_KEY_ENV`, `_MODEL` and `_CA_FILE`. It skips when these are unset or the host is unreachable, which covers the local Ollama hosts until their TLS is finished. ### Screenshots These were taken from the test-only `OpenAiDemoServer` with Next UI RC.18. Its OpenAI-compatible host is a mock, but Discover models, Test connection and one turn really run. At 390 and 1440 px, light and dark, no page or inner horizontal scroll was found. | | light 1440 | dark 390 | |---|---|---| | Home, providers card | ![](https://github.com/QRun-IO/qbit-assistant/blob/feature/p1-provider-registry/docs/screenshots/p-home-providers-light-1440.png?raw=1) | ![](https://github.com/QRun-IO/qbit-assistant/blob/feature/p1-provider-registry/docs/screenshots/p-home-providers-dark-390.png?raw=1) | | Local provider | ![](https://github.com/QRun-IO/qbit-assistant/blob/feature/p1-provider-registry/docs/screenshots/p-provider-local-light-1440.png?raw=1) | ![](https://github.com/QRun-IO/qbit-assistant/blob/feature/p1-provider-registry/docs/screenshots/p-provider-local-dark-390.png?raw=1) | | Its Models child list | ![](https://github.com/QRun-IO/qbit-assistant/blob/feature/p1-provider-registry/docs/screenshots/p-provider-models-light-1440.png?raw=1) | ![](https://github.com/QRun-IO/qbit-assistant/blob/feature/p1-provider-registry/docs/screenshots/p-provider-models-dark-390.png?raw=1) | | A model | ![](https://github.com/QRun-IO/qbit-assistant/blob/feature/p1-provider-registry/docs/screenshots/p-model-light-1440.png?raw=1) | ![](https://github.com/QRun-IO/qbit-assistant/blob/feature/p1-provider-registry/docs/screenshots/p-model-dark-390.png?raw=1) | All 16 files are in `docs/screenshots/p-*`. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign in to join this conversation.