Conversation
New workspace member crates/larql-cloud/. Pairs with larql-server's
existing OpenAI-compatible /answerer/ surface (routes/openai/) by
providing the inverse: a uniform Rust trait for /calling/ external
LLM services from larql.
The single trait CloudClient exposes infer / embed / chat over two
implementations:
OpenAiCompatible
Drives every OpenAI-API-shaped backend behind a single impl:
- openai() — OpenAI proper, OPENAI_API_KEY
- exoscale() — Exoscale.ch AI gateway, EXOSCALE_API_KEY
(configurable base URL via EXOSCALE_AI_BASE)
- together() — Together AI, TOGETHER_API_KEY
- local() — vLLM / llama.cpp / ollama / pgesq's own
larql-server proxy. Optional bearer.
All four route to /v1/chat/completions and /v1/embeddings with
the OpenAI request/response shape. infer() synthesises top-K
predictions from chat output (no logprobs assumed).
BedrockClient
Two auth modes via BedrockAuth:
- BearerToken — AWS_BEARER_TOKEN_BEDROCK (the 2024 short-lived
API-key flow). Plain HTTPS Bearer header.
- SigV4 — AWS_ACCESS_KEY_ID / SECRET_ACCESS_KEY (+ optional
SESSION_TOKEN), AWS_REGION. Hand-rolled SigV4
using hmac + sha2 + hex; 60 LOC, validated
against the AWS canonical signing-key example.
Speaks the Anthropic Messages API for chat/infer (the dominant
Bedrock model family). Embeddings go through amazon.titan-embed-*.
Non-anthropic models reject chat with Unsupported; non-titan
models reject embed.
Provider matrix (see lib.rs docstring) covers OpenAI, Exoscale,
Together, Fireworks (constructor parity to be added),
vLLM/llama.cpp/ollama, and Bedrock with both auth modes.
Tests: 11 passing.
- openai: chat round trip + embed round trip + infer-from-chat
synthesis + transport error propagation, all against an in-process
hyper mock that asserts the request URI per call.
- bedrock: auth env precedence (bearer > sigv4), missing-creds
error, provider_id reflects auth mode, embed/chat reject
wrong-family models, SigV4 derived signing key matches the
AWS canonical test vector.
Workspace registration: Cargo.toml gains crates/larql-cloud as a
non-default member behind features openai (default) and bedrock
(default, gates the SigV4 hmac/sha2/hex deps).
Build: cargo check -p larql-cloud clean. cargo test -p larql-cloud
11/11 green.
Single-binary HTTP server that serves a CloudClient as an
OpenAI-compatible JSON API. Drops in front of any provider:
larql-cloud-proxy --provider openai --model gpt-4o-mini --port 8080
larql-cloud-proxy --provider bedrock --model anthropic.claude-3-haiku-20240307-v1:0 --region us-east-1 --port 8081
larql-cloud-proxy --provider exoscale --model meta-llama-3.1-8b-instruct --port 8082
larql-cloud-proxy --provider local --base-url http://ollama:11434 --model qwen2:1.5b --port 8083
Endpoints (axum 0.8):
GET /v1/health — liveness + version
GET /v1/stats — model + mode='cloud-proxy' + provider id;
pg_infer's RemoteBackend reads this on
infer_create_model_remote registration
POST /v1/infer — {prompt, top, temperature, max_tokens}
→ {predictions, latency_ms, usage}
POST /v1/embeddings — OpenAI-shaped {input, model} →
{object, model, data: [{embedding}], usage}
POST /v1/chat/completions — OpenAI-shaped passthrough; for Bedrock
this is the Anthropic Messages API
hidden behind the OpenAI shape
GET /v1/walk, /v1/describe, /v1/relations
— 501 with 'vindex-only endpoint not
available on cloud-proxy' so pg_infer
short-circuits cleanly
Auth picked up from env per provider:
OpenAI — OPENAI_API_KEY
Exoscale — EXOSCALE_API_KEY (+ EXOSCALE_AI_BASE override)
Together — TOGETHER_API_KEY
Bedrock — AWS_BEARER_TOKEN_BEDROCK (preferred) or
AWS_ACCESS_KEY_ID/SECRET_ACCESS_KEY (SigV4)
Local — optional LARQL_PROXY_API_KEY
Graceful shutdown on SIGINT.
Tests: 12 total (11 unit + 1 end-to-end integration).
- proxy_end_to_end_against_local_provider spawns a hyper mock OpenAI
server, launches the proxy binary, and exercises every endpoint
including the vindex-only-501 path. Asserts the mock saw exactly
the requests pg_infer would issue (1x infer→chat, 1x embed,
1x chat).
- Env-touching bedrock tests wrapped in a process-global Mutex so
they're safe under the default --test-threads parallel runner.
Build: cargo check -p larql-cloud --bin larql-cloud-proxy clean.
cargo test -p larql-cloud: 12/12 green at default concurrency.
is_anthropic() previously matched only foundation model ids
("anthropic.claude-...") and 501'd cross-region inference profile
ids like "us.anthropic.claude-haiku-4-5-20251001-v1:0" with
'provider does not support chat (only Anthropic Messages API is
wired)'. Cross-region inference profiles are the AWS-recommended
way to invoke Claude on Bedrock now (region prefix selects the
inference profile; model is routed by Bedrock to whichever region
has capacity), and `us.`/`eu.`/`apac.`/`us-gov.` are all
valid prefixes.
Match by either leading `anthropic.` (foundation models) or the
`.anthropic.` substring (any inference profile shape). No
non-Anthropic Bedrock model id contains that token, so the
substring match is sufficient.
Verified end-to-end against live Bedrock with
AWS_BEARER_TOKEN_BEDROCK against
`us.anthropic.claude-haiku-4-5-20251001-v1:0` in us-east-1:
/v1/health -> 200 ok
/v1/stats -> 200 ok (provider=bedrock-bearer)
/v1/chat/completions -> 200 ok, OpenAI-shaped response
/v1/embeddings (Titan) -> 200 ok, 1024-dim vectors
/v1/walk -> 501 (vindex-only, expected)
/v1/embeddings (Anthropic) -> 501 'embed (model is not
amazon.titan-embed-*)' (expected)
Tests
Added is_anthropic_matches_cross_region_inference_profiles to
guard against regression on the four prefix shapes.
cargo test -p larql-cloud --lib bedrock: 8 passed.
Known gaps left for future work (out of scope here):
- TitanEmbedResponse drops inputTextTokenCount, so embedding
usage comes back null. Wire the field if downstream needs it.
- Titan dimensions are hardcoded to 1024; should be configurable.
- Streaming chat (invoke-model-with-response-stream + EventStream
decoding) is not wired through cloud-proxy.
- SigV4 path is untested against live Bedrock; only bearer-token
auth verified.
The crate was written against a June 2026 upstream and carried its own
`[lints.clippy]` table (`unwrap_used`/`panic`/`panic_in_result_fn` =
deny). Every other crate in the workspace now uses `[lints] workspace =
true`, and the workspace table denies neither -- `larql-vindex` alone
has ~5100 `.unwrap()` calls. All 14 violations this produced were in
`#[cfg(test)]` blocks, i.e. exactly the sites the rest of the repo
permits. Adopting the workspace table is the convention, not a
loosening; a crate-local policy stricter than the workspace is drift.
Also, to pass CI's `clippy --all-targets -- -D warnings`:
* `#[allow(clippy::too_many_arguments)]` on `sigv4::sign` (9 args --
SigV4 needs them all; the repo's own idiom, ~30 sites)
* de-indent one doc list continuation (`doc_overindented_list_items`,
a lint that postdates the crate)
* `cargo fmt` under the pinned 1.98.0
Verified with the pinned toolchain rather than the ambient nix one --
nix's clippy-driver 1.94 shadows rustup's and silently reports
`unknown lint: clippy::chunks_exact_to_as_chunks` (E0602) for the
workspace's own allow, which looks like a real finding and is not.
clippy -D warnings: clean. cargo test -p larql-cloud: 13/13.
Every other crate in the workspace has its own per-crate workflow, so without this one larql-cloud is the only crate CI never builds or tests. Modelled on larql-core.yml, which is the closest match: portable code, no model weights, no platform backend. Linux + Windows on pull requests and macOS additionally on main, for the reason larql-core.yml documents -- this crate has no `cfg(target_os = ...)` surface, no unix/windows split that Linux and Windows do not already cover, and no Accelerate linkage, so a macOS PR leg would compile the same portable code a third time while macOS queue waits set the repo's PR wall clock. Every command in the workflow was run locally on the pinned 1.98.0 before committing, across the whole feature matrix, and one of them failed: clippy -p larql-cloud --no-default-features --all-targets -- -D warnings `BedrockClient::send` takes `service` for the SigV4 signing scope, and that argument is only read by the `cfg(feature = "bedrock")` arm of the auth match. With the feature off, it is an unused variable -- a latent warning in the crate as merged, which would have made this workflow red on its first run. Fixed with a cfg'd `allow(unused_variables)` rather than by cfg-ing the parameter out of the signature: `service` is part of the method's contract on every feature combination, and removing it under `--no-default-features` would give the two builds different call sites. The feature matrix is worth testing rather than assuming, since `bedrock` is the combination that pulls extra dependencies (hmac/sha2/hex for SigV4) and `openai` is dependency-free; a no-default build is what proves the gating is real. Clippy runs on both default and no-default because the proxy binary is what operators actually run, and a lint-clean library with a broken binary is not a useful green. Verified: fmt, 5 check variants, 2 clippy variants, 4 test variants all pass; workflow YAML parses.
|
Heads-up on the two red checks (
This PR's So any PR opened against |
|
Follow-up on the two red checks: I've opened #481, which bumps On that branch |
Adds
larql-cloud: outbound LLM clients, so larql-server can act as a caller of OpenAI-compatible services and AWS Bedrock, not only as an answerer of that API.Purely additive — one new crate plus its CI workflow. No existing code is touched.
What it is
larql-server already speaks the OpenAI API as an answerer (
crates/larql-server/src/routes/openai/). This is the mirror: one trait,CloudClient, exposing the three operations a database front-end needs to delegate to a cloud model.Providers:
OPENAI_API_KEYbearergpt-4o-miniEXOSCALE_API_KEYbearermeta-llama-3.1-8bTOGETHER_API_KEYbearermeta-llama/Llama-3-…FIREWORKS_API_KEYbeareraccounts/fireworks/…qwen2:1.5banthropic.claude-…, incl. cross-region inference profilesAlso
larql-cloud-proxy: a binary exposing/v1/infer,/v1/embeddings,/v1/chat/completionsand a/v1/statsshaped so an existing client's registration probe succeeds, with/v1/walkand/v1/describeanswering501(there is no vindex behind a cloud API). That lets a client point at either a real vindex or a hosted model without knowing which.Why upstream rather than in my fork
The crate exists because I needed it for pg_infer, but nothing in it is pg_infer-specific — it is a general outbound client for this workspace, and the proxy is the piece that makes "vindex or hosted model, same wire protocol" work for any client. Carrying it in a fork indefinitely means the workspace has two divergent answers to "how does larql call out to a model".
Happy to drop the proxy binary and land only the library if you'd rather keep the binary surface smaller.
Feature gating
default = ["openai", "bedrock"].openaiis dependency-free;bedrockgates the SigV4 dependencies (hmac,sha2,hex). Both build and test independently — CI checks all four combinations, since a no-default build is what proves the gating is real.CI
Every crate here has its own workflow, so this adds
larql-cloud.yml, modelled onlarql-core.yml(the closest match: portable, no weights, no platform backend). Linux + Windows on PRs, macOS additionally on main, following the rationalelarql-core.ymldocuments about macOS queue waits.Every command in the workflow was run locally on the pinned 1.98.0 before committing, and one failed:
BedrockClient::sendtakesservicefor the SigV4 signing scope, read only by thecfg(feature = "bedrock")arm of the auth match — so with the feature off it is an unused variable. A latent warning that would have made the workflow red on its first run. Fixed with a cfg'dallow(unused_variables)rather than cfg-ing the parameter out of the signature, so both builds keep the same call sites.Verification
Pinned toolchain (1.98.0), full feature matrix:
fmt, 5checkvariants, 2clippy -D warningsvariants, 4testvariants — all pass. 13 tests including a proxy integration test that binds loopback only (no network, no credentials, hermetic).