Skip to content

Commit 672a85b

Browse files
richiejpclaude
andcommitted
feat: cloud-proxy backend + routing + middleware stack
Big-bang squash-friendly commit covering the work since master: phases 1-7 of the cloud-proxy migration, tool-call support, plus the surrounding routing / middleware / PII / billing scaffolding this branch had been carrying. Cloud-proxy backend (backend/go/cloud-proxy/): * New gRPC backend with two modes. * Passthrough: Forward RPC shovels raw HTTP between client and upstream so the wire format is preserved byte-for-byte. * Translate: PredictRich / PredictStreamRich convert internal proto to OpenAI Chat Completions or Anthropic Messages, preserving tool calls + usage tokens through pb.Reply. * API keys resolved from api_key_env or api_key_file (mutually exclusive), never stored in YAML. gRPC interface (pkg/grpc/): * Forward bidi RPC added to Backend proto. * AIModelRich optional extension interface returning *pb.Reply so backends can surface tool_calls and usage tokens. * Fixed forwardClient.CloseSend prematurely closing the gRPC connection — caught by e2e tests. Cleanup now fires on stream end (Recv error/EOF) instead. Core integration: * IsCloudProxyBackendPassthrough hook in chat + Anthropic endpoints; legacy "proxy-*" backend prefix removed (hard cutover — nothing released). * cloudproxy.ForwardViaBackend + cloudproxy.BuildStreamFilter shared by both endpoint families. * PII filter applies to translate mode via the standard streaming pipeline; verified by e2e. Routing + middleware (carried from earlier on the branch): * Score / Rerank / Embedder / VectorStore interfaces in core/backend with Application factory methods. * Router with score classifier, depth-1 invariant, embedding cache, PII config, billing recorder. * Admission middleware, route-model dispatch, usage stamping. * MITM proxy + CA management for intercepting cloud traffic. * Middleware admin page in the React UI. Local-store backend rewrite + tests covering Set / Get / Delete / Find invariants. Llama-cpp Score concurrency guard: conflict_guard tripwire plus FLAG_SCORE/{CHAT,COMPLETION,EMBEDDINGS} validation rule in core/config. Tests: 60+ new unit tests across cloud-proxy backend, cloudproxy core glue, gRPC server + AIModelRich dispatch, config validation, and 6 e2e specs that stand up a real two-process gRPC link with fake upstreams (gaps #1/#2/#3 from review). Docs: cloud-proxy.md, middleware.md, mitm-proxy.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Signed-off-by: Richard Palethorpe <io@richiejp.com>
1 parent 11d5bd0 commit 672a85b

212 files changed

Lines changed: 24548 additions & 777 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.dockerignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
.devcontainer
55
models
66
backends
7+
volumes
78
examples/chatbot-ui/models
89
backend/go/image/stablediffusion-ggml/build/
910
backend/go/*/build

Makefile

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1213,6 +1213,12 @@ build-mock-backend: protogen-go
12131213
clean-mock-backend:
12141214
rm -f tests/e2e/mock-backend/mock-backend
12151215

1216+
build-cloud-proxy-backend: protogen-go
1217+
$(GOCMD) build -o tests/e2e/mock-backend/cloud-proxy ./backend/go/cloud-proxy
1218+
1219+
clean-cloud-proxy-backend:
1220+
rm -f tests/e2e/mock-backend/cloud-proxy
1221+
12161222
########################################################
12171223
### UI E2E Test Server
12181224
########################################################

backend/backend.proto

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,22 @@ service Backend {
3737

3838
rpc Rerank(RerankRequest) returns (RerankResult) {}
3939

40+
// TokenClassify runs a token-classification (NER) model on the
41+
// supplied text and returns each detected entity span. Used by the
42+
// PII redactor's optional NER tier — the regex tier still handles
43+
// formatted hits cheaply, while this catches names, locations, and
44+
// other unformatted PII that regex misses.
45+
rpc TokenClassify(TokenClassifyRequest) returns (TokenClassifyResponse) {}
46+
47+
// Score evaluates the model's joint log-probability of each
48+
// supplied candidate continuation given a shared prompt. The
49+
// prompt's KV cache is computed once and reused across candidates.
50+
// Used for routing-policy multi-label classification, reranking,
51+
// calibrated confidence, and reward-model scoring — any task where
52+
// the consumer wants the model's confidence in a pre-specified
53+
// continuation rather than a generated one.
54+
rpc Score(ScoreRequest) returns (ScoreResponse) {}
55+
4056
rpc GetMetrics(MetricsRequest) returns (MetricsResponse);
4157

4258
rpc VAD(VADRequest) returns (VADResponse) {}
@@ -68,6 +84,23 @@ service Backend {
6884
rpc QuantizationProgress(QuantizationProgressRequest) returns (stream QuantizationProgressUpdate) {}
6985
rpc StopQuantization(QuantizationStopRequest) returns (Result) {}
7086

87+
// Forward proxies a raw HTTP request to an upstream provider. The
88+
// cloud-proxy backend implements this for passthrough-mode model
89+
// configs: the client wire format is preserved end-to-end (no
90+
// translation through internal proto), which means new provider
91+
// fields work the day they ship. Translation-mode proxies use the
92+
// standard Predict/PredictStream RPCs instead. Backends that don't
93+
// support this return UNIMPLEMENTED.
94+
//
95+
// The request is bidirectionally streamed so large bodies can flow
96+
// without buffering. In practice the first ForwardRequest carries
97+
// path, method, headers, and the initial body chunk; subsequent
98+
// messages append body chunks. The first ForwardReply carries the
99+
// upstream status and response headers; subsequent messages stream
100+
// body chunks (SSE frames or chunked transfer). Cancellation of the
101+
// gRPC context closes the upstream connection.
102+
rpc Forward(stream ForwardRequest) returns (stream ForwardReply) {}
103+
71104
}
72105

73106
// Define the empty request
@@ -81,6 +114,76 @@ message MetricsResponse {
81114
int32 prompt_tokens_processed = 5;
82115
}
83116

117+
// TokenClassifyRequest carries the text to classify plus an optional
118+
// score threshold. The transformers backend interprets threshold as
119+
// the minimum confidence to include in the response; 0 = include all.
120+
message TokenClassifyRequest {
121+
string text = 1;
122+
float threshold = 2;
123+
}
124+
125+
// TokenClassifyEntity is one detected entity span. Byte offsets are
126+
// into the original UTF-8 text — start..end is a half-open range that
127+
// addresses the substring corresponding to entity_group.
128+
//
129+
// entity_group follows HuggingFace's aggregated-tag convention (e.g.
130+
// "PER", "LOC", "ORG", or a PII-specific label like "EMAIL" /
131+
// "SSN" depending on the model). The redactor's per-pattern action
132+
// map keys off this string.
133+
message TokenClassifyEntity {
134+
string entity_group = 1;
135+
int32 start = 2;
136+
int32 end = 3;
137+
float score = 4;
138+
string text = 5;
139+
}
140+
141+
message TokenClassifyResponse {
142+
repeated TokenClassifyEntity entities = 1;
143+
}
144+
145+
// ScoreRequest carries one shared prompt and one or more continuations
146+
// to score against it. The backend tokenises the prompt once and reuses
147+
// the resulting KV cache across all candidates in this request.
148+
message ScoreRequest {
149+
string prompt = 1;
150+
repeated string candidates = 2;
151+
// Return per-token logprobs for each candidate when true. Default
152+
// false to keep the wire response small; the joint log_prob field
153+
// covers the common ranking case.
154+
bool include_token_logprobs = 3;
155+
// When true, the response also populates length_normalized_log_prob
156+
// (joint log-prob divided by candidate token count). Useful when
157+
// candidates differ in length and the consumer wants a per-token
158+
// measure comparable across them (PMI-style scoring).
159+
bool length_normalize = 4;
160+
}
161+
162+
// CandidateScore is one row in the ScoreResponse, matching by index
163+
// the candidate in ScoreRequest.candidates.
164+
message CandidateScore {
165+
// Sum of log P(token_i | prompt, candidate_token_<i) across the
166+
// candidate's tokens. The primary ranking signal.
167+
double log_prob = 1;
168+
// log_prob / num_tokens — populated when length_normalize=true on
169+
// the request.
170+
double length_normalized_log_prob = 2;
171+
// Per-token detail — populated when include_token_logprobs=true.
172+
repeated TokenLogProb tokens = 3;
173+
// Number of tokens the backend tokenised this candidate into, after
174+
// any backend-specific normalisation (e.g. leading-space handling).
175+
int32 num_tokens = 4;
176+
}
177+
178+
message TokenLogProb {
179+
string token = 1;
180+
double log_prob = 2;
181+
}
182+
183+
message ScoreResponse {
184+
repeated CandidateScore candidates = 1;
185+
}
186+
84187
message RerankRequest {
85188
string query = 1;
86189
repeated string documents = 2;
@@ -325,6 +428,25 @@ message ModelOptions {
325428
// applied verbatim to the backend's engine constructor (e.g. vLLM AsyncEngineArgs).
326429
// Unknown keys produce an error at LoadModel time.
327430
string EngineArgs = 73;
431+
432+
// Proxy carries the cloud-proxy backend's per-model configuration.
433+
// Empty for non-proxy backends.
434+
ProxyOptions Proxy = 74;
435+
}
436+
437+
// ProxyOptions configures the cloud-proxy backend. UpstreamURL and
438+
// Mode are always meaningful; Provider only matters in translate mode.
439+
// The two api_key_* fields are mutually exclusive and resolved by the
440+
// backend at LoadModel — core forwards the references rather than the
441+
// plaintext key.
442+
message ProxyOptions {
443+
string upstream_url = 1;
444+
string mode = 2;
445+
string provider = 3;
446+
string api_key_env = 4;
447+
string api_key_file = 5;
448+
string upstream_model = 6;
449+
int32 request_timeout_seconds = 7;
328450
}
329451

330452
message Result {
@@ -1002,3 +1124,32 @@ message QuantizationStopRequest {
10021124
string job_id = 1;
10031125
}
10041126

1127+
// ForwardHeader is one HTTP header on the request or response. Headers
1128+
// like Authorization are typically injected by the backend (from the
1129+
// resolved API key) rather than passed through from the client.
1130+
message ForwardHeader {
1131+
string name = 1;
1132+
string value = 2;
1133+
}
1134+
1135+
// ForwardRequest is a streamed HTTP request to the upstream. First
1136+
// message carries path/method/headers; subsequent messages carry
1137+
// body_chunk only. All fields except body_chunk are honoured on the
1138+
// first message and ignored thereafter.
1139+
message ForwardRequest {
1140+
string path = 1; // e.g. "/v1/chat/completions" — appended to the model's upstream_url
1141+
string method = 2; // usually "POST"
1142+
repeated ForwardHeader headers = 3;
1143+
bytes body_chunk = 4;
1144+
}
1145+
1146+
// ForwardReply is a streamed HTTP response from the upstream. First
1147+
// message carries status/headers; subsequent messages carry body_chunk
1148+
// only. SSE responses arrive as a sequence of body_chunk frames; the
1149+
// caller is responsible for any parsing.
1150+
message ForwardReply {
1151+
int32 status = 1;
1152+
repeated ForwardHeader headers = 2;
1153+
bytes body_chunk = 3;
1154+
}
1155+

0 commit comments

Comments
 (0)