Fix/dr9 policy before model mapping - #22
Merged
Merged
Conversation
Whitelist entries like "gpt-4o-mini" were being blocked for kids keys because ModelMappedHelper ran first and renamed the model to the upstream name (e.g. llama-3.1-8b-instant), which is not on the whitelist. Moving the Airbotix policy check above ModelMappedHelper means the decision always uses the client-requested model name, so the whitelist match works regardless of how the channel remaps the model downstream. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ponses/gemini handlers Same ordering bug as fixed in compatible_handler.go — applyAirbotixPolicy* was called after ModelMappedHelper, which rewrites request.Model to the upstream channel name before the whitelist check runs. Fix: move each applyAirbotixPolicy* call above ModelMappedHelper so the whitelist always evaluates the client-requested model name. Note for gemini_handler.go: GeminiChatRequest carries no Model field (Gemini puts the model in the URL path). Using info.OriginModelName, which is set by middleware before any mapping, gives the correct pre-mapping name. Closes DR-9. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Same ordering bug as the chat handlers — checkAirbotixModelWhitelist was called with info.UpstreamModelName (post-mapping) instead of the original client-requested model name. Fix moves the check above ModelMappedHelper and switches to info.OriginModelName, consistent with GeminiHelper. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All comments in the four relay handler files are now English-only, consistent with the rest of the codebase. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
docs/wiki/ — GitHub Wiki source for team reference: Home, Sprint-1-Progress, Architecture-Decisions, Bug-Log, Dev-Setup .claude/commands/ — Claude Code slash commands for dev workflow: /dr-status sprint progress report /dr-test e2e policy verification (DR-9 test suite) /dr-pr PR creation checklist .gitignore — expose .claude/commands/ for team sharing; ignore seed outputs .dockerignore — exclude web/*/node_modules (cuts build context ~1.5GB→40MB) AIRBOTIX.md — update Sprint 1 status + known-bug log Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
relayGeminiRouter (/v1beta) was missing middleware.AirbotixPolicy(), so ContextKeyPolicyDecision was never set in the gin context for Gemini requests. applyAirbotixPolicyToGemini reads that key and returns nil (allow-all) when it is absent — meaning kids_mode enforcement was silently skipped for all /v1beta routes. Fix: insert AirbotixPolicy() between TokenAuth and ModelRequestRateLimit in relayGeminiRouter, matching the chain already present in relayV1Router. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes DR-9:
kids_modewhitelist was checked afterModelMappedHelperrewroterequest.Modelto the upstream channel name, causing valid whitelisted models (e.g.gpt-4o-mini) to be incorrectly rejected when the channel remapped them (e.g. tollama-3.1-8b-instanton Groq).Root Cause
applyAirbotixPolicy*was called afterhelper.ModelMappedHelper, butModelMappedHelperoverwritesrequest.Modelwith the upstream name. The whitelist check then compared against the wrong name and always failed.Fix
Moved all policy checks before
ModelMappedHelperacross all 5 relay handlers:relay/compatible_handler.goTextHelperModelMappedHelperrelay/claude_handler.goClaudeHelperModelMappedHelperrelay/responses_handler.goResponsesHelperModelMappedHelperrelay/gemini_handler.goGeminiHelperinfo.OriginModelName(URL path model), policy before mappingrelay/gemini_handler.goGeminiEmbeddingHandlerGemini note:
GeminiChatRequestcarries noModelfield (model is in the URL path). Fixed by readinginfo.OriginModelName(set by middleware before any mapping) instead ofinfo.UpstreamModelName(set byModelMappedHelper).Also Included
docs/wiki/(Architecture Decisions ADR-001–005, Bug Log, Sprint Progress, Dev Setup).claude/commands/(dr-status, dr-test, dr-pr).dockerignore: excludeweb/*/node_modulesAIRBOTIX.md: Sprint 1 status updatedTesting
Tested on dev stack after rebuilding container with the DR-9 fix.
Setup:
http://localhost:3000(Docker dev compose)llama-3.1-8b-instant+gpt-4o-mini{"gpt-4o-mini": "llama-3.1-8b-instant"}← key to reproducing the bugROOT_KEY: user withkids_mode=false(no policy)KIDS_KEY: user withkids_mode=true,policy_profile=kid-safeSuite 1 of 2 — Handler Coverage (32 tests)
Tests each of the 5 relay handlers independently.
The ★ regression tests are the ones that directly verify the DR-9 fix.
§1 — Policy Block: kids + llama (not whitelisted) → must return 400
llama-3.1-8b-instantis in the Groq channel but NOT on the kids whitelist.Policy must reject it with 400 before any upstream call is made, across all 5 handlers.
POST /v1/chat/completions· kids key · model=llama-3.1-8b-instantPOST /v1/responses· kids key · model=llama-3.1-8b-instantPOST /v1/messages· kids key · model=llama-3.1-8b-instantPOST /v1beta/models/llama-3.1-8b-instant:generateContent· kids keyPOST /v1beta/models/llama-3.1-8b-instant:embedContent· kids keyBody on all 5:
{"error":{"message":"model_not_eligible_for_kids_mode: llama-3.1-8b-instant","type":"new_api_error","code":"channel:model_mapped_error"}}§2 ★ Regression: kids + gpt-4o-mini (whitelisted) that maps to llama → must return 200
This is the core DR-9 regression test.
gpt-4o-miniis on the kids whitelist. But the Groq channel maps it tollama-3.1-8b-instantupstream.ModelMappedHelper→ sawllama→ returned 400 ❌ModelMappedHelper→ seesgpt-4o-mini→ allows → 200 ✅POST /v1/chat/completions· kids key · model=gpt-4o-miniPOST /v1/responses· kids key · model=gpt-4o-miniPOST /v1/messages· kids key · model=gpt-4o-miniPOST /v1beta/models/gpt-4o-mini:generateContent· kids keyPOST /v1beta/models/gpt-4o-mini:embedContent· kids keymodel_not_eligible_for_kids_mode§3 — Passthrough: root key + llama → must return 200 (no policy applied)
Root user has
kids_mode=false. Policy middleware runs but issues a passthrough decision.Non-whitelisted model must succeed.
POST /v1/chat/completions· root key · model=llama-3.1-8b-instantPOST /v1/responses· root key · model=llama-3.1-8b-instantPOST /v1/messages· root key · model=llama-3.1-8b-instantPOST /v1beta generateContent· root key · model=llama-3.1-8b-instant§4 — Whitelist Boundary: whitelisted models without dev channels → 503 from routing, NOT 400 from policy
These models ARE on the whitelist. No Groq channel configured for them in dev → routing fails with 503.
The key assertion: if policy were incorrectly blocking them, we'd see 400. We must NOT see 400.
gpt-4oclaude-3-5-haikuclaude-3-5-sonnet§5 — Error Body Validation: 400 must carry the right fields
Policy-blocked responses must have a structured error body that clients can parse.
/v1/chat/completions400 body containsmodel_not_eligible_for_kids_mode/v1/chat/completions400 body contains blocked model namellama-3.1-8b-instant/v1/responses400 body containsmodel_not_eligible_for_kids_mode/v1/messages400 body containsmodel_not_eligible_for_kids_mode§6 — Tenant Isolation: same endpoint, different key → different result
Guards against policy state leaking between tenants in the same process.
/v1/chat/completions· root · llama/v1/chat/completions· kids · llama (same endpoint as 6a)/v1/chat/completions· root · gpt-4o-mini/v1/chat/completions· kids · gpt-4o-mini (same endpoint as 6c)§7 — Edge Cases
modelfieldmodel: ""(empty string)messagesfieldgpt-4o-mini-2024-07-18(HasPrefix match → whitelisted)claude-3-opus(not whitelisted, no channel)Suite 1 total: 32/32 PASS
Suite 2 of 2 — Behavioral / Property Tests (56 tests)
A second independent suite testing DR-9 from the angle of system properties rather than per-handler.
§1 — Streaming: policy must enforce synchronously even with
stream: trueA blocked model must return a JSON 400, never start an SSE stream.
An allowed model must return 200 with proper SSE
data:lines.stream:true→ 400 (policy blocks before stream opens)model_not_eligible_for_kids_mode(not SSE data)stream:true→ 200 (allowed, stream begins)data:markersstream:true→ 200 (passthrough)data:markers6/6 PASS
§2 — Complete Whitelist: all 8 EligibleModels must not be policy-blocked
Every model in
internal/kids/kids.go EligibleModelstested individually.Assertion: none return 400 from policy (may return 503 from routing — expected).
gpt-4o-minigpt-4ogpt-image-2gpt-image-1claude-3-5-haikuclaude-3-5-sonnetflux-schnellflux-1.1-pro8/8 PASS
§3 — HasPrefix Matching: versioned variants inherit whitelist, boundary models excluded
IsModelEligibleusesstrings.HasPrefix—claude-3-5-haiku-20241022inherits fromclaude-3-5-haiku.Should be allowed (prefix matches whitelist entry):
gpt-4o-mini-2024-07-18gpt-4o-minigpt-4o-mini-audio-previewgpt-4o-minigpt-4o-2024-11-20gpt-4oclaude-3-5-haiku-20241022claude-3-5-haikuclaude-3-5-sonnet-20241022claude-3-5-sonnetflux-1.1-pro-ultraflux-1.1-proBoundary (similar-looking but NOT a prefix match — must NOT be whitelisted):
gpt-4-turbogpt-4not in whitelist (onlygpt-4o)No available channelgpt-3.5-turboclaude-3-haikuclaude-3-haiku≠ prefix ofclaude-3-5-haikuclaude-3-opus10/10 PASS
§4 — Error Response Schema: 400 body must have correct JSON structure
"type":"new_api_error""code":"channel:model_mapped_error"llama-3.1-8b-instantin messagemodel_not_eligible_for_kids_modein message/v1/messages(Claude shape) 400 body also hasmodel_not_eligible_for_kids_mode6/6 PASS
§5 — Response Quality: allowed requests return real LLM output
"choices"(OpenAI completion field)"model"field"error"key/v1/messages(Claude) 200 body contains"content"array5/5 PASS
§6 — Policy Stability: identical input must give identical output (no state accumulation)
Same request sent 3 consecutive times. All results must match.
9/9 PASS
§7 — Interleave Isolation: alternating root/kids requests, no state leakage
Policy decision must depend only on the key in the current request.
8/8 PASS
§8 — Auth Boundary: unauthenticated requests rejected before policy runs
Authorizationheader → not 200model_not_eligible_for_kids_mode(policy never ran)4/4 PASS
Final Totals
All 88 tests pass. Safe to merge. ✅