Skip to content

feat(coordinator): route Gemma 4 only to dedicated providers; 429 shed#434

Merged
Gajesh2007 merged 3 commits into
masterfrom
feat/gemma4-dedicated-routing
Jun 21, 2026
Merged

feat(coordinator): route Gemma 4 only to dedicated providers; 429 shed#434
Gajesh2007 merged 3 commits into
masterfrom
feat/gemma4-dedicated-routing

Conversation

@Gajesh2007

@Gajesh2007 Gajesh2007 commented Jun 21, 2026

Copy link
Copy Markdown
Member

What & why

Gemma 4 destabilizes when it shares a Mac with other models/work (memory contention, OOM/jetsam, ragged-batch NaN, Metal resource-count leaks). This restricts a dedicated model family (default gemma-4) to providers whose entire advertised catalog is that family — i.e. machines dedicated to Gemma 4. Those dedicated boxes are the only routing candidates and the only providers counted when deciding whether to accept or shed a request to OpenRouter.

Coordinator-only — no provider/Swift or protocol changes.

Routing behavior: before → after

Before — a Gemma 4 request could land on any box serving it (including mixed boxes where it contends), and "no provider" shed as 503:

flowchart TD
    Req["Gemma 4 request"] --> Gate{"Provider serves gemma-4?"}
    Gate -->|"Dedicated box ✓<br/>(gemma-4 only)"| Cand["Candidate pool"]
    Gate -->|"Mixed box ✓<br/>(gemma-4 + qwen/gpt-oss)"| Cand
    Cand --> Cap{"Capacity?"}
    Cap -->|"Yes"| Route["Route to best score —<br/>⚠️ often a MIXED box →<br/>Gemma 4 contends → OOM / NaN / leak"]
    Cap -->|"All busy"| R429["429 rate_limit"]
    Cap -->|"None eligible"| R503["❌ 503 model_unavailable<br/>(OpenRouter may derank us)"]
Loading

After — mixed boxes are filtered out at the single shared gate (routing and capacity preflight), so only dedicated boxes are eligible; "no dedicated box" sheds as 429:

flowchart TD
    Req["Gemma 4 request"] --> Gate{"Provider serves gemma-4?"}
    Gate -->|"Dedicated box ✓"| Ded{"Dedicated to gemma-4?<br/>(advertised catalog == gemma-4 only)"}
    Gate -->|"Mixed box ✓"| Ded
    Ded -->|"No — mixed"| Drop["🚫 Excluded from routing<br/>AND capacity count<br/>(self-route to own box exempt)"]
    Ded -->|"Yes — dedicated"| Cand["Candidate pool =<br/>dedicated boxes only"]
    Cand --> Cap{"Capacity?"}
    Cap -->|"Yes"| Route["✅ Route to a DEDICATED box →<br/>Gemma 4 isolated, no contention"]
    Cap -->|"No dedicated box free"| R429["✅ 429 + Retry-After →<br/>clean OpenRouter failover"]
Loading

How

One shared predicate, registry.providerExcludedByDedicatedRuleLocked(p, model) (model is a dedicated family AND the provider isn't dedicated to it), applied at every public provider-selection site so routing, shedding, and pre-warming can never drift:

  • providerPassesRoutingGatesLockedEx — the documented single gate shared by the dispatch hot path (snapshotProviderLockedEx) and the OpenRouter capacity preflight (quickCapacityCheck).
  • coldSpillProviderEligibleLocked, warmPoolCandidateLocked, modelLoadCandidatePendingLocked — so the autoscaler/swap planner never pre-warms a dedicated model onto a mixed box.
  • providerHasWarmModelLocked — so a warm mixed box isn't counted as warm capacity for a dedicated model (which would suppress warming a real dedicated box).

A caller self-routing to its own machine is exempt (!selfRouteOwner). Eligibility is catalog-based, not load-based, so an idle dedicated box stays eligible (cold-loads on demand).

429, not 503

When the fleet serves the model but no dedicated box is available (none exist, or all busy), the preflight no_eligible_provider shed now returns 429 rate_limit_exceeded + Retry-After instead of 503 — for OpenRouter this is a clean transient failover rather than an "unhealthy endpoint" signal. Applied in both handleChatCompletions and the generic /v1/completions + /v1/messages path. A genuinely-absent model still 503s (guarded by IsDedicatedModel(model) && HasProviderForModel(model, serials...)).

Config & rollout

  • EIGENINFERENCE_DEDICATED_MODELS — comma-separated, case-insensitive substring patterns. Unset → default gemma-4 (ON); set to none/empty → disabled.
  • The registry field defaults nil (OFF), so unit tests and the e2e testbed (both build via registry.New) are unaffected — the rule only activates via cmd/coordinator/main.go at startup.

Important

Default is ON. Once deployed, if the fleet has no Gemma-4-only boxes, all Gemma 4 traffic sheds to OpenRouter (429). Ensure dedicated machines exist (operationally: only Gemma 4 downloaded on them) or set EIGENINFERENCE_DEDICATED_MODELS=none. Coordinator deploy is human-only.

Testing

  • New: 15 registry tests (parse, predicate, routing-selects-only-dedicated, never-falls-back-to-mixed, preflight shed counts only dedicated boxes, self-route exempt, default-off, warm-pool/swap/warm-detection gating) + a real-HTTP test asserting 429 + Retry-After for both /v1/chat/completions and /v1/completions, with a 503 control for a truly-absent model. Tests fail without the change; baselines confirm the dedicated rule is the differentiator.
  • go build ./..., linux cross-compile, and the full coordinator suite pass; gofmt clean.

Known follow-ups (non-blocking, cosmetic)

  • ModelCapacitySnapshot / /v1/models / the OpenRouter feed are display-only and left ungated, so they over-report Gemma 4 capacity by counting mixed boxes. No routing/admission impact.

🤖 Generated with Claude Code

Restrict a dedicated model family (default: gemma-4, via
EIGENINFERENCE_DEDICATED_MODELS) to providers whose entire advertised
catalog is that family. Enforced via one shared predicate
(providerExcludedByDedicatedRuleLocked) at every public selection site:
the routing gate (providerPassesRoutingGatesLockedEx, covering both the
dispatch hot path and the OpenRouter capacity preflight), cold-spill,
the warm-pool target picker, and the model-swap planner/warm-detection —
so routing, shedding, and pre-warming can never drift. Self-route to
one's own machine is exempt.

When the fleet serves the model but no dedicated box is available, shed
to OpenRouter as a transient 429 + Retry-After instead of 503.
@vercel

vercel Bot commented Jun 21, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
d-inference Ready Ready Preview Jun 21, 2026 1:14am
d-inference-console-ui-dev Ready Ready Preview Jun 21, 2026 1:14am
d-inference-landing Ready Ready Preview Jun 21, 2026 1:14am

Request Review

@github-actions

github-actions Bot commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

This PR introduces dedicated-box routing isolation for specific model families (e.g. Gemma-4); it is operationally significant but has no material security impact on the trust model — one minor trust-boundary interaction deserves a brief note.


Trust Boundaries Touched

  • TB-002 (Coordinator → Provider WebSocket): routing gate logic in registry.go is modified
  • TB-006 (Admin → Coordinator): new EIGENINFERENCE_DEDICATED_MODELS env var alters routing behavior at startup

Threat Assessment

Threat File(s) Verdict Notes
T-034 (Provider runs modified code while advertising trusted identity) registry/registry.go:1829–1852 ℹ️ Neutral allowPrivate flag reuses the existing selfRouteOwner exemption pattern. The new providerExcludedByDedicatedRuleLocked call is inserted before trust/attestation gates, not instead of them — the full APNs code-identity and SE challenge chain is still required. No regression to the CodeAttested gate.
T-038 (Slow-header / HTTP server config) cmd/coordinator/main.go ℹ️ Neutral New block inserted between existing registry configuration lines. No change to http.Server fields (ReadHeaderTimeout, MaxHeaderBytes, etc.).
T-032 / T-041 (Solana mnemonic / profile signing identity exfiltration) cmd/coordinator/main.go ℹ️ Neutral New env-var block does not touch KMS-injected secrets, their loading order, or logging of sensitive material.

New Attack Surface — EIGENINFERENCE_DEDICATED_MODELS

Not covered by an existing threat, but low severity:

  • cmd/coordinator/main.go:170-180 — The new env var is logged at startup: logger.Info("dedicated-model routing ENABLED", "patterns", strings.Join(dedicatedModels, ",")). The patterns are model-family substrings (e.g. "gemma-4"), not secrets, so this is fine.
  • The "none" sentinel uses strings.EqualFold + strings.TrimSpace which is correct; no injection risk from the comma-separated parse path (registry.ParseDedicatedModels), though that function is in a file not shown in the diff — reviewers should confirm ParseDedicatedModels does not panic or allocate unboundedly on a pathological env var value (e.g. a very long string or degenerate pattern count). This is not a critical gap but worth a glance given it runs at coordinator startup.
  • A misconfigured or empty EIGENINFERENCE_DEDICATED_MODELS causes all dedicated-family requests to shed to OpenRouter as 429s. This is a denial-of-service primitive via environment misconfiguration, not an external attacker surface, but ops should treat this env var as deployment-critical.

One Behavioral Note on allowPrivate Reuse (registry.go:1829)

The comment says allowPrivate marks the owner self-route context, exempt like selfRouteOwner. Reusing a privacy-semantics flag as a dedicated-routing-exemption flag is a minor abstraction concern — if a future caller sets allowPrivate=true for a different reason, it would silently bypass dedicated isolation. Not a current vulnerability, but worth a rename or a dedicated boolean parameter when this is next touched.


Open Findings Resolved

None — this PR does not address any SEC-* findings.


Bottom line: merge is safe from a security standpoint. Confirm ParseDedicatedModels input handling and document the operational consequence of an empty/none value for on-call runbooks.


🔐 Threat model: docs/threat-model.yaml · Updates on each push to this PR

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a5fc81f361

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread coordinator/api/consumer.go
Comment thread coordinator/registry/dedicated_models.go
…ore-shed)

Dedicated-family models (e.g. Gemma 4) bypass the queue-before-shed
window when their dedicated boxes are at capacity: holding an OpenRouter
request up to 120s would blow its TTFT SLA, so shed immediately with a
429 + Retry-After for a clean failover. Applied in both
handleChatCompletions and handleGenericInference. Non-dedicated models
keep queue-before-shed unchanged.

Addresses Codex PR review (saturated-dedicated queue-before-shed). The
structurally-unroutable 429-vs-503 nit is intentionally left at 429 per
product decision (clean OpenRouter failover over an honest 503).
@Gajesh2007

Copy link
Copy Markdown
Member Author

Thanks @chatgpt-codex-connector — addressed both findings in f28e89a9:

① Shed saturated dedicated models before queueing — ✅ fixed. Dedicated-family models now bypass queue-before-shed when their dedicated boxes are saturated (candidateCount==0 && capacityRejections>0) and fast-429 with Retry-After instead of sitting in the 120s queue — holding an OpenRouter request that long would blow its TTFT SLA. Applied in both handleChatCompletions and handleGenericInference; non-dedicated models keep queue-before-shed unchanged. New test: TestDedicatedSaturatedBoxFast429 (saturated dedicated box + queue-before-shed left ON → immediate 429).

② Apply routing gates before treating a provider as available — intentionally left at 429 (product decision). For a dedicated model, we prefer a transient 429 + Retry-After (clean OpenRouter failover) over an "honest" 503 even in the narrow case where the only advertiser is structurally unroutable (private-only / sub-trust / unverified). Routing itself is unaffected — the gate still excludes those providers; this only concerns the shed status code, and 429 is the desired behavior for OpenRouter compatibility.

Net effect: every "can't serve a dedicated model right now" case (no dedicated box, or dedicated boxes saturated) now sheds as 429 + Retry-After; only a model genuinely absent from the fleet returns 503.

@chatgpt-codex-connector

Copy link
Copy Markdown

Summary

  • No code changes were needed, so I did not commit or open a new PR.
  • I confirmed the current branch already includes the saturated dedicated-box fast-429 path for dedicated models, with Retry-After and machine_busy rejection metadata. coordinator/api/consumer.goL2105-L2137
  • I confirmed there is a regression test covering the queue-before-shed bypass for a saturated dedicated Gemma 4 box. coordinator/api/dedicated_models_test.goL121-L166
  • I also confirmed the intentionally retained HasProviderForModel behavior only checks online/trusted catalog advertisers, matching the stated product decision to keep this class of dedicated-model shed as transient 429 instead of 503. coordinator/registry/dedicated_models.goL127-L141

Testing

  • ⚠️ go test ./coordinator/api -run TestDedicatedSaturatedBoxFast429 -count=1 — blocked by environment/toolchain setup: mise could not install/resolve required tools because remote downloads failed with tunnel/network errors after trusting mise.toml.
  • git status --short — working tree is clean.

View task →

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f28e89a94b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread coordinator/registry/scheduler.go
providerCanRouteBuildLocked (used by alias resolution, constrained
resolution, and RoutableProviderIDsForBuild) now applies
providerExcludedByDedicatedRuleLocked, so alias routability matches
dispatch routability. Fixes staged-rollout failover: an alias whose
Desired build is advertised only by a mixed box now correctly resolves
to a Previous build on a dedicated box instead of resolving Desired and
429ing at dispatch. Owner self-route (allowPrivate) stays exempt.

Addresses Codex PR review (alias routability gate).
@Gajesh2007

Copy link
Copy Markdown
Member Author

@chatgpt-codex-connector finding ③ (alias routability gate) — ✅ fixed in ef17e01f.

providerCanRouteBuildLocked — the single source of truth used by alias resolution (ResolveModel / ResolveModelConstrainedanyProviderCanRouteBuildLocked / anyEligibleProviderCanRouteLocked) and RoutableProviderIDsForBuild — now applies providerExcludedByDedicatedRuleLocked, so alias routability matches actual dispatch routability. During a staged rollout, an alias whose Desired build is advertised only by a mixed box now correctly fails over to a Previous build on a dedicated box instead of resolving to Desired and 429ing at dispatch. Owner self-route (allowPrivate) stays exempt, mirroring the selfRouteOwner exemption in the routing gate. New test: TestDedicatedAliasFailsOverToPreviousOnDedicatedBox (rule-off baseline resolves Desired; rule-on fails over to the dedicated Previous).

All three review threads are now addressed — ① fast-429 when saturated (f28e89a9), ② intentionally left at 429 per product decision, ③ alias gate (ef17e01f). Resolving the threads.

@chatgpt-codex-connector

Copy link
Copy Markdown

Summary

  • No code changes were needed, so I did not create a commit or open a new PR.
  • Verified the PR is already at ef17e01fde2b139caffd4de6460ccfd3cd5a3406, whose latest commit is fix(coordinator): apply dedicated gate to alias routability checks.
  • Confirmed providerCanRouteBuildLocked now applies the dedicated-provider exclusion before alias routability succeeds, preserving owner self-route via allowPrivate. coordinator/registry/registry.goL1813-L1825
  • Confirmed the regression test covers the staged-rollout alias case: with the rule off the alias resolves to Desired, and with the dedicated rule on it falls back to Previous on a dedicated provider. coordinator/registry/dedicated_models_test.goL337-L364

Testing / Checks

  • find .. -name AGENTS.md -print && git status --short && git rev-parse HEAD
  • cat AGENTS.md && git status --short && git show --stat --oneline HEAD
  • ⚠️ mise trust && go test ./coordinator/registry -run TestDedicatedAliasFailsOverToPreviousOnDedicatedBox -count=1 — blocked by environment/network toolchain setup; mise could not download Go/tool metadata due tunnel/network failures.
  • command -v go; /usr/local/go/bin/go version || true; nl -ba coordinator/registry/registry.go | sed -n '1805,1830p'; nl -ba coordinator/registry/dedicated_models_test.go | sed -n '336,385p'; git status --short

View task →

@Gajesh2007
Gajesh2007 merged commit 4421c6f into master Jun 21, 2026
6 checks passed

@ethenotethan ethenotethan left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated Code Review — Layr-Labs/d-inference#

Verdict: REQUEST_CHANGES

Security — ✅ No issues found

Performance — 4 finding(s) (2 blocking)

  • 🟡 [MEDIUM] coordinator/api/consumer.go:2104-2141 — Duplicate rejection recording and response writing logic between chat completions and generic inference handlers
    • Suggestion: Extract the dedicated model 429 response logic into a shared helper function to eliminate code duplication and reduce maintenance burden
  • 🟡 [MEDIUM] coordinator/api/consumer.go:4931-4968 — Identical rejection recording and response writing logic duplicated from chat completions handler
    • Suggestion: Create a shared helper function for dedicated model 429 responses that both handlers can call, reducing code duplication by ~40 lines
  • 🔵 [INFO] coordinator/registry/dedicated_models.go:122-141 — O(n) linear scan through all providers for each HasProviderForModel call
    • Suggestion: Consider adding an index by model ID to avoid scanning all providers, or cache results if this method is called frequently during request processing
  • 🔵 [INFO] coordinator/registry/dedicated_models.go:123-127 — Map allocation for allowedSerials set on every call without pre-sizing
    • Suggestion: Pre-allocate the map with make(map[string]struct{}, len(allowedSerials)) to avoid potential rehashing during insertion

Type_diligence — ✅ No issues found

Additive_complexity — 5 finding(s) (2 blocking)

  • 🟡 [MEDIUM] coordinator/api/consumer.go:2104-2141 — Identical 38-line dedicated model shed logic duplicated between handleChatCompletions and handleGenericInference
    • Suggestion: Extract the dedicated model 429 shed logic into a shared helper method to eliminate duplication and ensure consistent behavior
  • 🟡 [MEDIUM] coordinator/api/consumer.go:4931-4968 — Second instance of identical dedicated model shed logic - exact copy of lines 2104-2141
    • Suggestion: Use the same extracted helper method for both endpoints to maintain DRY principle
  • 🔵 [INFO] coordinator/registry/dedicated_models.go:17-28 — ParseDedicatedModels reimplements basic CSV parsing with manual string splitting and trimming
    • Suggestion: Consider using encoding/csv package or at minimum extract this as a generic utility function if the simple splitting is intentional
  • 🔵 [INFO] coordinator/registry/dedicated_models.go:122-142 — HasProviderForModel method has complex logic but only used for distinguishing 429 vs 503 error codes
    • Suggestion: Consider inlining this logic at the call site or simplifying the interface since it's a single-purpose helper
  • 🔵 [INFO] coordinator/registry/dedicated_models_test.go:1-400 — 400-line test file with 15+ test functions for a relatively simple substring matching feature
    • Suggestion: Consider consolidating related test cases into table-driven tests to reduce test code volume while maintaining coverage

9 finding(s) total, 4 blocking. Verdict: REQUEST_CHANGES.

🤖 Automated review by Centaur · DAR-186

@ethenotethan ethenotethan left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated Code Review — Layr-Labs/d-inference#

Verdict: REQUEST_CHANGES

Security — ✅ No issues found

Performance — 4 finding(s) (1 blocking)

  • 🔵 [INFO] coordinator/api/consumer.go:2105-2141 — Duplicate rejection recording and response writing logic
    • Suggestion: Extract common rejection handling into a shared function to avoid code duplication between handleChatCompletions and handleGenericInference
  • 🔵 [INFO] coordinator/api/consumer.go:4932-4968 — Duplicate rejection recording and response writing logic
    • Suggestion: Extract common rejection handling into a shared function to avoid code duplication between handleChatCompletions and handleGenericInference
  • 🟡 [MEDIUM] coordinator/registry/dedicated_models.go:122-141 — HasProviderForModel iterates all providers with nested locking
    • Suggestion: Consider caching provider-model mappings or using a more efficient lookup structure to avoid O(n) iteration with nested locks on hot path
  • 🔵 [INFO] coordinator/registry/dedicated_models.go:17-27 — ParseDedicatedModels creates slice without pre-allocation
    • Suggestion: Pre-allocate slice with capacity len(strings.Split(csv, ',')) to avoid repeated allocations during append operations

Type_diligence — ✅ No issues found

Additive_complexity — 4 finding(s) (2 blocking)

  • 🟡 [MEDIUM] coordinator/api/consumer.go:2105-2141 — Identical 37-line rejection handling block duplicated between chat completions and generic inference
    • Suggestion: Extract the dedicated model rejection logic into a shared helper method like handleDedicatedModelRejection(w, r, model, publicModel, retryAfter, refundReservation)
  • 🟡 [MEDIUM] coordinator/api/consumer.go:4932-4968 — Second instance of identical 37-line rejection handling block
    • Suggestion: Use the same extracted helper method as suggested for the first instance
  • 🔵 [INFO] coordinator/registry/dedicated_models.go:73-85 — providerDedicatedToPatternLocked could be simplified by combining the loop logic
    • Suggestion: Consider inlining this into the single caller or simplifying the advertised counter logic
  • 🔵 [INFO] coordinator/registry/dedicated_models.go:122-142 — HasProviderForModel in registry layer duplicates routing logic that belongs in scheduler
    • Suggestion: Move this method to scheduler.go or make it use existing scheduler methods to avoid duplicating provider filtering logic

8 finding(s) total, 3 blocking. Verdict: REQUEST_CHANGES.

🤖 Automated review by Centaur · DAR-186

Comment on lines +2105 to +2141
} else if s.registry.IsDedicatedModel(model) && s.registry.HasProviderForModel(model, allowedProviderSerials...) {
// Dedicated-box model (e.g. Gemma 4): the fleet DOES serve this
// model, but no provider DEDICATED to it can take the request right
// now — either none are dedicated, or the dedicated ones are busy/
// cooling. That is transient capacity pressure, not an absent model,
// so shed to OpenRouter as a 429 + Retry-After (clean failover)
// rather than a 503 (which can get the endpoint marked unhealthy /
// deranked). Mirrors the capacity_429 path above.
retryAfter := s.estimateRetryAfter(model)
w.Header().Set("Retry-After", strconv.Itoa(retryAfter))
refundReservation()
s.ddIncr("routing.decisions", []string{"model:" + model, "model_type:" + s.registry.ModelType(model), "outcome:dedicated_capacity_429"})
s.recordRejection(rejectionInfo{
r: r,
stage: "preflight_capacity",
reasonCode: "machine_busy",
httpStatus: http.StatusTooManyRequests,
keyID: keyIDFromContext(r.Context()),
consumerKeyHash: store.HashKey(consumerKeyFromContext(r.Context())),
requestedModel: publicModel,
resolvedModel: model,
stream: stream,
estimatedPromptTokens: estimatedPromptTokens,
requestedMaxTokens: requestedMaxTokens,
requiresVision: requiresVision,
hasTools: hasTools,
retryAfterMs: retryAfter * 1000,
params: rejectionSamplingParams(parsed),
servabilityComputed: true,
candidateCount: candidateCount,
capacityRejections: capacityRejections,
modelTooLargeRejections: modelTooLarge,
bestTTFTMs: ttftMsForRejection(bestTTFT, hasTTFT),
})
writeJSON(w, http.StatusTooManyRequests, errorResponse("rate_limit_exceeded",
fmt.Sprintf("no provider dedicated to model %q is available right now — retry after %ds", publicModel, retryAfter),
withCode("rate_limit_exceeded")))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 [INFO] ⚡ Duplicate rejection recording and response writing logic

💡 Suggestion: Extract common rejection handling into a shared function to avoid code duplication between handleChatCompletions and handleGenericInference

📊 Score: 2×3 = 6 · Category: repeated_work

Comment on lines +4932 to 4968
} else if s.registry.IsDedicatedModel(model) && s.registry.HasProviderForModel(model, allowedProviderSerials...) {
// Dedicated-box model served by the fleet but no dedicated box is
// available right now: transient capacity, so shed with 429 +
// Retry-After (not 503). Mirrors the chat-completions preflight so
// /v1/completions and /v1/messages classify the shed identically.
retryAfter := s.estimateRetryAfter(model)
w.Header().Set("Retry-After", strconv.Itoa(retryAfter))
refundReservation()
s.ddIncr("routing.decisions", []string{"model:" + model, "model_type:" + s.registry.ModelType(model), "outcome:dedicated_capacity_429"})
s.recordRejection(rejectionInfo{
r: r,
stage: "preflight_capacity",
reasonCode: "machine_busy",
httpStatus: http.StatusTooManyRequests,
keyID: keyIDFromContext(r.Context()),
consumerKeyHash: store.HashKey(consumerKeyFromContext(r.Context())),
requestedModel: publicModel,
resolvedModel: model,
stream: stream,
estimatedPromptTokens: estimatedPromptTokens,
requestedMaxTokens: requestedMaxTokens,
requiresVision: requiresVision,
hasTools: hasTools,
retryAfterMs: retryAfter * 1000,
params: rejectionSamplingParams(parsed),
servabilityComputed: true,
candidateCount: candidateCount,
capacityRejections: capacityRejections,
modelTooLargeRejections: modelTooLarge,
bestTTFTMs: ttftMsForRejection(bestTTFT, hasTTFT),
})
writeJSON(w, http.StatusTooManyRequests, errorResponse("rate_limit_exceeded",
fmt.Sprintf("no provider dedicated to model %q is available right now — retry after %ds", publicModel, retryAfter),
withCode("rate_limit_exceeded")))
return
} else {
// Queueing cannot help — fail fast with a retryable 503 instead of

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 [INFO] ⚡ Duplicate rejection recording and response writing logic

💡 Suggestion: Extract common rejection handling into a shared function to avoid code duplication between handleChatCompletions and handleGenericInference

📊 Score: 2×3 = 6 · Category: repeated_work

Comment on lines +122 to +141
func (r *Registry) HasProviderForModel(model string, allowedSerials ...string) bool {
allowedSet := make(map[string]struct{}, len(allowedSerials))
for _, s := range allowedSerials {
allowedSet[s] = struct{}{}
}
r.mu.RLock()
defer r.mu.RUnlock()
for _, p := range r.providers {
if len(allowedSet) > 0 && !providerMatchesAllowedSerial(p, allowedSet) {
continue
}
p.mu.Lock()
eligible := p.Status != StatusOffline && p.Status != StatusUntrusted &&
r.providerServesCatalogModelLocked(p, model)
p.mu.Unlock()
if eligible {
return true
}
}
return false

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 [MEDIUM] ⚡ HasProviderForModel iterates all providers with nested locking

💡 Suggestion: Consider caching provider-model mappings or using a more efficient lookup structure to avoid O(n) iteration with nested locks on hot path

📊 Score: 3×4 = 12 · Category: blocking_io

Comment on lines +17 to +27
// or all-blank input yields a nil slice (feature disabled).
func ParseDedicatedModels(csv string) []string {
var out []string
for _, p := range strings.Split(csv, ",") {
p = strings.ToLower(strings.TrimSpace(p))
if p == "" {
continue
}
out = append(out, p)
}
return out

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 [INFO] ⚡ ParseDedicatedModels creates slice without pre-allocation

💡 Suggestion: Pre-allocate slice with capacity len(strings.Split(csv, ',')) to avoid repeated allocations during append operations

📊 Score: 2×2 = 4 · Category: unbounded_allocations

Comment on lines +2105 to +2141
} else if s.registry.IsDedicatedModel(model) && s.registry.HasProviderForModel(model, allowedProviderSerials...) {
// Dedicated-box model (e.g. Gemma 4): the fleet DOES serve this
// model, but no provider DEDICATED to it can take the request right
// now — either none are dedicated, or the dedicated ones are busy/
// cooling. That is transient capacity pressure, not an absent model,
// so shed to OpenRouter as a 429 + Retry-After (clean failover)
// rather than a 503 (which can get the endpoint marked unhealthy /
// deranked). Mirrors the capacity_429 path above.
retryAfter := s.estimateRetryAfter(model)
w.Header().Set("Retry-After", strconv.Itoa(retryAfter))
refundReservation()
s.ddIncr("routing.decisions", []string{"model:" + model, "model_type:" + s.registry.ModelType(model), "outcome:dedicated_capacity_429"})
s.recordRejection(rejectionInfo{
r: r,
stage: "preflight_capacity",
reasonCode: "machine_busy",
httpStatus: http.StatusTooManyRequests,
keyID: keyIDFromContext(r.Context()),
consumerKeyHash: store.HashKey(consumerKeyFromContext(r.Context())),
requestedModel: publicModel,
resolvedModel: model,
stream: stream,
estimatedPromptTokens: estimatedPromptTokens,
requestedMaxTokens: requestedMaxTokens,
requiresVision: requiresVision,
hasTools: hasTools,
retryAfterMs: retryAfter * 1000,
params: rejectionSamplingParams(parsed),
servabilityComputed: true,
candidateCount: candidateCount,
capacityRejections: capacityRejections,
modelTooLargeRejections: modelTooLarge,
bestTTFTMs: ttftMsForRejection(bestTTFT, hasTTFT),
})
writeJSON(w, http.StatusTooManyRequests, errorResponse("rate_limit_exceeded",
fmt.Sprintf("no provider dedicated to model %q is available right now — retry after %ds", publicModel, retryAfter),
withCode("rate_limit_exceeded")))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 [MEDIUM] 🧩 Identical 37-line rejection handling block duplicated between chat completions and generic inference

💡 Suggestion: Extract the dedicated model rejection logic into a shared helper method like handleDedicatedModelRejection(w, r, model, publicModel, retryAfter, refundReservation)

📊 Score: 3×4 = 12 · Category: duplicate logic

Comment on lines +4932 to 4968
} else if s.registry.IsDedicatedModel(model) && s.registry.HasProviderForModel(model, allowedProviderSerials...) {
// Dedicated-box model served by the fleet but no dedicated box is
// available right now: transient capacity, so shed with 429 +
// Retry-After (not 503). Mirrors the chat-completions preflight so
// /v1/completions and /v1/messages classify the shed identically.
retryAfter := s.estimateRetryAfter(model)
w.Header().Set("Retry-After", strconv.Itoa(retryAfter))
refundReservation()
s.ddIncr("routing.decisions", []string{"model:" + model, "model_type:" + s.registry.ModelType(model), "outcome:dedicated_capacity_429"})
s.recordRejection(rejectionInfo{
r: r,
stage: "preflight_capacity",
reasonCode: "machine_busy",
httpStatus: http.StatusTooManyRequests,
keyID: keyIDFromContext(r.Context()),
consumerKeyHash: store.HashKey(consumerKeyFromContext(r.Context())),
requestedModel: publicModel,
resolvedModel: model,
stream: stream,
estimatedPromptTokens: estimatedPromptTokens,
requestedMaxTokens: requestedMaxTokens,
requiresVision: requiresVision,
hasTools: hasTools,
retryAfterMs: retryAfter * 1000,
params: rejectionSamplingParams(parsed),
servabilityComputed: true,
candidateCount: candidateCount,
capacityRejections: capacityRejections,
modelTooLargeRejections: modelTooLarge,
bestTTFTMs: ttftMsForRejection(bestTTFT, hasTTFT),
})
writeJSON(w, http.StatusTooManyRequests, errorResponse("rate_limit_exceeded",
fmt.Sprintf("no provider dedicated to model %q is available right now — retry after %ds", publicModel, retryAfter),
withCode("rate_limit_exceeded")))
return
} else {
// Queueing cannot help — fail fast with a retryable 503 instead of

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 [MEDIUM] 🧩 Second instance of identical 37-line rejection handling block

💡 Suggestion: Use the same extracted helper method as suggested for the first instance

📊 Score: 3×4 = 12 · Category: duplicate logic

Comment on lines +73 to +85
// Caller holds r.mu AND p.mu (mirrors providerServesCatalogModelLocked).
func (r *Registry) providerDedicatedToPatternLocked(p *Provider, pattern string) bool {
advertised := 0
for _, m := range p.Models {
if !r.modelAllowedByCatalogLocked(m) {
continue
}
advertised++
if !strings.Contains(strings.ToLower(m.ID), pattern) {
return false
}
}
return advertised > 0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 [INFO] 🧩 providerDedicatedToPatternLocked could be simplified by combining the loop logic

💡 Suggestion: Consider inlining this into the single caller or simplifying the advertised counter logic

📊 Score: 2×3 = 6 · Category: over-abstraction

Comment on lines +122 to +142
func (r *Registry) HasProviderForModel(model string, allowedSerials ...string) bool {
allowedSet := make(map[string]struct{}, len(allowedSerials))
for _, s := range allowedSerials {
allowedSet[s] = struct{}{}
}
r.mu.RLock()
defer r.mu.RUnlock()
for _, p := range r.providers {
if len(allowedSet) > 0 && !providerMatchesAllowedSerial(p, allowedSet) {
continue
}
p.mu.Lock()
eligible := p.Status != StatusOffline && p.Status != StatusUntrusted &&
r.providerServesCatalogModelLocked(p, model)
p.mu.Unlock()
if eligible {
return true
}
}
return false
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 [INFO] 🧩 HasProviderForModel in registry layer duplicates routing logic that belongs in scheduler

💡 Suggestion: Move this method to scheduler.go or make it use existing scheduler methods to avoid duplicating provider filtering logic

📊 Score: 2×2 = 4 · Category: misplaced responsibility

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants