feat(operators): self-hosted OpenAI-compatible model provider, off by default - #1436
Conversation
… default Adds an optional second model provider so the open-source stack can run the AI operators on open weights through any OpenAI-compatible Chat Completions server (Ollama, LM Studio, vLLM, NVIDIA NIM), with no proprietary inference service. Bedrock remains the platform path. - OPENAI_COMPAT_ENABLED gates it (env, then the SSM feature flag) and is off by default. When off, the `openai-compat` registry row and its `openai_compat` rate-card key do not exist, so nothing can resolve to the model and the meter refuses it. - OPERATOR_PROFILE_ECONOMY/_BALANCED/_QUALITY point a tier at any registered model for one deployment. An unknown name, or a tier on the self-hosted model while it is off, fails the boot. - Converse stays the canonical transcript. openai_compat.py translates at the edge (tool calls, tool results with error status, reasoning dropped, cached-token usage), so the tool loop, the operators and the meter are unchanged. Transport is httpx; no new dependency. - Self-hosted rates are configured per deployment and default to 0, since the rate card is a cost passthrough. - BedrockModel is renamed OperatorModel now that the registry is not Bedrock-only.
|
Change classification: normal — platform or application code changed. A normal change carries a documented automated review on this pull request before merge, in place of a second human approver. The |
|
@claude please review this PR |
|
Claude finished @jfrench9's task in 3m 7s —— View job Review: self-hosted OpenAI-compatible provider
Solid piece of work — the off-by-default gating is airtight (env var read only when One finding worth addressing before this ships as a real Bedrock-free path:
Minor, non-blocking:
Not verified in this review: I was unable to run |
…o-AWS claim to dev
|
Review addressed in 088a359:
|
Summary
Adds an optional second model provider, so the open-source stack can run the AI operators on open weights through any OpenAI-compatible Chat Completions server (Ollama, LM Studio, vLLM, NVIDIA NIM), with no proprietary inference service. Bedrock stays the platform path. The new provider is off by default: hosted deployments leave it off, and a deployment turns it on only when it wants its own model.
Changes
Registry and gating (
config/operators.py,config/env.py,config/billing/ai.py)OPENAI_COMPAT_ENABLEDturns the provider on. It is read from the env var first, then thefeatures/SSM flag, and defaults to off.openai-compatregistry row and itsopenai_compatrate-card key are never created. Nothing can resolve to the model, and the meter refuses it.OPENAI_COMPAT_BASE_URL,OPENAI_COMPAT_MODELandOPENAI_COMPAT_API_KEYare read throughget_secret_value: the env var first, then the deployment secret in staging/prod. When off, none of the three is read at all.OPENAI_COMPAT_MODELcollides with a registered short name, a profile name or a Bedrock wire id, because resolution would then run and bill it as a different model;OPERATOR_PROFILE_*value is unknown, or names the self-hosted model while the provider is off.ModelSpecgains aproviderfield (bedrock|openai_compat).OPERATOR_PROFILE_ECONOMY/_BALANCED/_QUALITYpoint a tier at any registered model for one deployment. This is deployment-scoped and never set by customers. With all three unset, the platform mapping is unchanged.OPENAI_COMPAT_CREDITS_PER_1K_INPUT/_OUTPUTand default to 0: the rate card is a cost passthrough, and a self-hosted GPU has no per-token vendor cost. Cache reads and writes bill at the input rate, since no caching discount is assumed. Malformed, infinite or negative rates raise.BedrockModelis renamedOperatorModel, because the registry is no longer Bedrock-only. The enum is internal and not part of the API or the SDKs.Client (
operations/operators/ai_client.py, newoperations/operators/openai_compat.py)AIClient.create_messagedispatches onspec.provider. The Bedrock path is unchanged.openai_compat.pytranslates at the edge:toolUseblocks becometool_calls, and eachtoolResultbecomes atoolmessage, with error status written as anError:prefix because Chat Completions has no error flag;tool_use, even when the server saysstop;<think>blocks are stripped;cached_tokensis reported as cache reads.httpx, which is already a dependency, with one client per call so no connection pool outlives its event loop.AIProviderError, the same operator-surfaced failure Bedrock refusals already use.Docs
.env.examplehas a commented self-hosted block.config/README.mdhas a new paragraph describing the provider.Worth a close look: the import-time validation in
build_model_registry/build_profile_models, and the user-turn ordering in_user_messages. Tool messages must come directly after the assistant turn that called them; the loop's wrap-up nudge follows as trailing text.Breaking Changes
None. There is no change to the REST, GraphQL or operations surface, so neither SDK tier is touched.
Testing
tests/operations/test_openai_compat.py(29 tests): message translation, request building, response parsing, HTTP errors againsthttpx.MockTransport, provider dispatch, and a fullrun_tool_looprun on the self-hosted path in which a failed query is fed back as an error, the model corrects it, and the answer lands.tests/config/test_operators.py: off-by-default gating, registry validation and tier overrides.tests/config/billing/test_ai.py: rate validation.tests/config,tests/operations/test_ai_client.pyandtests/operations/operatorspass.just test-codepasses (ruff, format, basedpyright, cf-lint).-n 2, finished with failures only in eight database-backed modules this change does not touch. Their errors were connection exhaustion (too many clients already,out of shared memory) on the local Postgres, which chore(dev): pin local Postgres connection and memory settings #1435 addresses. All eight pass when rerun serially.just test-allat default parallelism hits the same local Postgres limit and was not green locally. CI is the gate.AIProviderError.Certification