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)
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.
## 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](https://claude.com/claude-code)
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
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 fromdevelopand 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 theassistant_*channel wall)assistant_providergains: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.assistant_modeltable, 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.SPI (additive)
ProviderSettings(kind, key, base URL, API version, region, timeouts, CA trust).AssistantProviderTypegets kinds, per-kind defaults,create(settings),discoverModelsandcheckHealthas default methods.create(apiKey, baseUrl)implementations, including the Anthropic module, keep working unchanged.RegistryModelProviderapplies theassistant_modelcapabilities and output cap per call.assistant_model, thenassistant_model_price, then config. An unpriced local model costs 0.New module
qbit-assistant-provider-openaiUses JDK
HttpClientand Jackson, with no vendor SDK.stopwith tool calls is handled. Arguments that do not parse go back to the model as invalid input instead of failing the call.max_completion_tokensversusmax_tokens, and Azure deployment URLs withapi-versionand theapi-keyheader.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.strictis 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 setsstrictSchemaRejected, soTurnEngineruns its existing one-retry fallback.cancel()aborts the call. Errors map to the existing kinds with sanitized provider messages and request ids. Keys never appear in logs, exceptions ortoString.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)
assistant_modelrows. It never overwrites admin edits, and it disables cloud catalogues of more than 20 models.Tests
154 in total, all passing in
./mvnw verifywith the JaCoCo gates:qbit-assistant: 99. Registry, settings validation, model selection and price precedence, the processes with a fake type, and CA bundles.LiveOpenAiCompatibleTest(taglive, excluded by default) is configured throughASSISTANT_LIVE_OPENAI_BASE_URL,_KIND,_KEY_ENV,_MODELand_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
OpenAiDemoServerwith 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.All 16 files are in
docs/screenshots/p-*.🤖 Generated with Claude Code