Skip to content

Resolve hierarchical runtime profiles - #62

Merged
adityathebe merged 3 commits into
mainfrom
feat/hierarchical-runtime-profiles-dklw887kn9hc
Aug 12, 2026
Merged

Resolve hierarchical runtime profiles#62
adityathebe merged 3 commits into
mainfrom
feat/hierarchical-runtime-profiles-dklw887kn9hc

Conversation

@moshloop

@moshloop moshloop commented Aug 11, 2026

Copy link
Copy Markdown
Member

Summary by CodeRabbit

  • New Features

    • Added layered runtime configuration across global, context, surface, and user settings.
    • Added resolved model availability, runtime restrictions, quotas, token limits, and timeouts.
    • Preserved system prompts and configuration details throughout chat execution.
    • Improved availability messages with actionable restriction and quota information.
  • Bug Fixes

    • Requests now consistently enforce resolved runtime limits and provide clearer profile-loading errors.
    • Exhausted quotas are reported without attempting provider configuration.
    • Resolved runtime details are now retained during authorized chat execution.

@github-actions

github-actions Bot commented Aug 11, 2026

Copy link
Copy Markdown

Gavel summary

Source Pass Fail Skip Duration

Totals: 0 passed · 0 failed · 0 skipped · -

View full results

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@adityathebe, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 53 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 2f91a3ab-c9a9-4ced-a186-eb5991b6f063

📥 Commits

Reviewing files that changed from the base of the PR and between 07c2c4d and 404080c.

📒 Files selected for processing (11)
  • pkg/aichat/approval_execution.go
  • pkg/aichat/approval_execution_test.go
  • pkg/aichat/execution_authority_ginkgo_test.go
  • pkg/aichat/messages.go
  • pkg/aichat/provider_config.go
  • pkg/aichat/runtime_profile_ginkgo_test.go
  • pkg/aichat/runtime_settings.go
  • pkg/aichat/service.go
  • pkg/aichat/service_ginkgo_test.go
  • pkg/api/spec_layers.go
  • pkg/api/spec_layers_ginkgo_test.go

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 8769f594-4ee4-4e3e-852f-785baa65d67b

📥 Commits

Reviewing files that changed from the base of the PR and between 39c0427 and 07c2c4d.

📒 Files selected for processing (1)
  • pkg/aichat/approval_execution.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • pkg/aichat/approval_execution.go

Walkthrough

The change replaces request-scoped runtime settings with resolved runtime profiles. It adds layered specification resolution, model restrictions, quota enforcement, profile-aware chat handling, and propagation of resolution data into authoritative execution records.

Changes

Runtime profile integration

Layer / File(s) Summary
Layered specification resolution
pkg/api/spec_layers.go, pkg/api/spec_layers_ginkgo_test.go
Adds scoped specification layers, resolved limits, quotas, model intersection, validation, timeout parsing, provenance, and resolution tests.
Runtime profile service flow
pkg/aichat/service.go, pkg/aichat/messages.go, pkg/aichat/runtime_settings.go, pkg/aichat/provider_config.go, pkg/aichat/*_ginkgo_test.go, pkg/cli/serve_chat.go
Replaces runtime settings with runtime profiles. Request layers resolve into api.ResolvedSpec. Resolved limits enforce quotas and input-token limits. Model and runtime catalogs receive restriction annotations.
Resolved profile execution and persistence
pkg/aichat/execution.go, pkg/aichat/execution_database_authority.go, pkg/aichat/approval_execution.go, pkg/aichat/execution_authority_ginkgo_test.go
Authoritative execution receives the resolved profile. Persisted specifications include profile constraints and resolution traces. Approval resumption loads profile provider configuration.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 7.41% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly and concisely describes the main change: resolving hierarchical runtime profiles.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/hierarchical-runtime-profiles-dklw887kn9hc
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch feat/hierarchical-runtime-profiles-dklw887kn9hc

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai 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.

Actionable comments posted: 5

🧹 Nitpick comments (4)
pkg/aichat/service.go (1)

139-143: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Document why handleTools loads a profile it does not use.

The handler discards the profile and only propagates the load error. The intent appears to be a consistent failure mode across chat endpoints when profile resolution is broken. Add a short comment so the call is not removed later as dead work.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/aichat/service.go` around lines 139 - 143, In Service.handleTools, add a
short comment immediately before the runtimeProfile call explaining that the
profile is intentionally loaded only to preserve consistent error handling when
profile resolution fails across chat endpoints.
pkg/api/spec_layers_ginkgo_test.go (1)

69-99: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider adding specs for the rejection paths.

The three specs cover the success paths. validateSpecLayer, parseOptionalDuration, and the empty-catalog guard in ResolveSpecLayers carry most of the new failure behavior and none of it is exercised. Add cases for an invalid scope, an empty model selector, a quota declared on a surface or user layer, a duplicate quota name within one layer, and a malformed Timeout string.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/api/spec_layers_ginkgo_test.go` around lines 69 - 99, Add
rejection-focused specs for ResolveSpecLayers and its validation helpers,
covering invalid scope, empty model selector, quotas on surface or user layers,
duplicate quota names within one layer, and malformed Budget.Timeout values.
Assert each case returns an error and preserve the existing success-path
coverage.
pkg/aichat/provider_config.go (1)

61-79: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider collapsing the two restriction-layer finders.

modelRestrictionLayer and runtimeRestrictionLayer differ only in the predicate applied to layer.Constraints.Models. A single helper that walks resolved.Trace in reverse and takes a func([]string) bool would remove the duplicated traversal.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/aichat/provider_config.go` around lines 61 - 79, Collapse
modelRestrictionLayer and runtimeRestrictionLayer into one reverse-traversal
helper that accepts a predicate over layer.Constraints.Models. Preserve the
existing non-empty-model constraint check and have each caller supply its
respective model or runtime predicate, returning the same restricting
*api.SpecLayer or nil behavior.
pkg/cli/serve_chat.go (1)

40-56: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Resolve the static layer once, outside the per-request closure.

The captain serve layer is constant for the process. The closure rebuilds and re-resolves it on every chat, models, runtimes, and tools request, and each call sorts, merges, and clones. Resolving once in newCaptainChatService also moves a configuration error to startup, where it fails the command instead of returning HTTP 500 per request.

♻️ Proposed refactor
+	resolved, err := api.ResolveSpecLayers(api.SpecLayer{
+		Name: "captain serve", Scope: api.SpecLayerGlobal,
+		Spec: api.Spec{
+			Model: api.Model{Name: "sol", Mode: registry.ModeAgent},
+			Setup: &shell.Setup{Cwd: cwd},
+		},
+	})
+	if err != nil {
+		return nil, nil, err
+	}
+	profile := aichat.RuntimeProfile{
+		System: "You are Captain's coding-agent launcher assistant. Use Captain and Clicky tools when useful, " +
+			"prefer read-only inspection unless the user explicitly asks for edits, and keep follow-up guidance concise.",
+		Resolved: resolved,
+	}
 	chat := aichat.NewService(aichat.ServiceOptions{
 		Profile: aichat.RuntimeProfileProviderFunc(func(context.Context) (aichat.RuntimeProfile, error) {
-			resolved, err := api.ResolveSpecLayers(api.SpecLayer{
-				Name: "captain serve", Scope: api.SpecLayerGlobal,
-				Spec: api.Spec{
-					Model: api.Model{Name: "sol", Mode: registry.ModeAgent},
-					Setup: &shell.Setup{Cwd: cwd},
-				},
-			})
-			if err != nil {
-				return aichat.RuntimeProfile{}, err
-			}
-			return aichat.RuntimeProfile{
-				System: "You are Captain's coding-agent launcher assistant. Use Captain and Clicky tools when useful, " +
-					"prefer read-only inspection unless the user explicitly asks for edits, and keep follow-up guidance concise.",
-				Resolved: resolved,
-			}, nil
+			return profile, nil
 		}),

This shares one Resolved value across requests. requestSpec copies Trace before appending the user layer, so no request mutates the shared profile.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/cli/serve_chat.go` around lines 40 - 56, In newCaptainChatService,
construct and resolve the constant “captain serve” SpecLayer once before
creating the RuntimeProfileProviderFunc, returning the startup error immediately
if resolution fails. Have the per-request closure reuse the shared Resolved
value, preserving requestSpec’s Trace copy before adding any user-specific layer
and avoiding mutation of the shared profile.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pkg/aichat/approval_execution.go`:
- Around line 19-21: The approval resume flow around ResolveToolApproval and
ExecuteStream must revalidate the admitted runtime profile before execution.
Preserve the persisted api.ResolvedSpec from renderedSpecMap and apply it when
resuming, or rerun the complete profile resolution, model-constraint, and quota
checks before using the current ProviderConfig; do not execute solely from the
decoded api.Spec.

In `@pkg/aichat/provider_config.go`:
- Around line 81-93: Update runtimeAllowed to avoid treating a provider-wide
selector prefix as sufficient for every mode sharing that provider. Match
selectors against the runtime mode’s DefaultModel or require the selector to
resolve to the specific backend, ensuring annotateProfileRuntimes only enables
modes with at least one allowed model while preserving validateResolvedModels
enforcement.

In `@pkg/api/spec_layers_ginkgo_test.go`:
- Around line 37-38: Strengthen the immutability checks in the resolution test
by initializing context.Spec.Model.Name and global.Spec.Model.Effort with
non-empty values before calling ResolveSpecLayers, then assert each input layer
still matches its original value afterward instead of asserting emptiness.

In `@pkg/api/spec_layers.go`:
- Around line 177-192: Update intersectModels to trim selector names from both
current and restrictive before comparing or returning them. Apply the same
normalization when current is empty, build allowed from trimmed restrictive
values, and compare trimmed current values so padded selectors intersect
correctly without producing whitespace-padded results.
- Around line 294-299: Update cloneSpecLayer to deep-copy mutable values inside
layer.Spec.CLIArgs, including interface-contained maps and slices such as
[]string, rather than relying on merge.Apply’s shallow map copy. Preserve scalar
values while cloning each nested mutable value so later CLI argument mutations
cannot affect the resolved layer.

---

Nitpick comments:
In `@pkg/aichat/provider_config.go`:
- Around line 61-79: Collapse modelRestrictionLayer and runtimeRestrictionLayer
into one reverse-traversal helper that accepts a predicate over
layer.Constraints.Models. Preserve the existing non-empty-model constraint check
and have each caller supply its respective model or runtime predicate, returning
the same restricting *api.SpecLayer or nil behavior.

In `@pkg/aichat/service.go`:
- Around line 139-143: In Service.handleTools, add a short comment immediately
before the runtimeProfile call explaining that the profile is intentionally
loaded only to preserve consistent error handling when profile resolution fails
across chat endpoints.

In `@pkg/api/spec_layers_ginkgo_test.go`:
- Around line 69-99: Add rejection-focused specs for ResolveSpecLayers and its
validation helpers, covering invalid scope, empty model selector, quotas on
surface or user layers, duplicate quota names within one layer, and malformed
Budget.Timeout values. Assert each case returns an error and preserve the
existing success-path coverage.

In `@pkg/cli/serve_chat.go`:
- Around line 40-56: In newCaptainChatService, construct and resolve the
constant “captain serve” SpecLayer once before creating the
RuntimeProfileProviderFunc, returning the startup error immediately if
resolution fails. Have the per-request closure reuse the shared Resolved value,
preserving requestSpec’s Trace copy before adding any user-specific layer and
avoiding mutation of the shared profile.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 31e2a8d9-811f-4313-a8d7-e9f4e864a644

📥 Commits

Reviewing files that changed from the base of the PR and between 5db1179 and 39c0427.

📒 Files selected for processing (14)
  • pkg/aichat/aimock_lifecycle_integration_test.go
  • pkg/aichat/approval_execution.go
  • pkg/aichat/execution.go
  • pkg/aichat/execution_authority_ginkgo_test.go
  • pkg/aichat/execution_database_authority.go
  • pkg/aichat/messages.go
  • pkg/aichat/provider_config.go
  • pkg/aichat/runtime_profile_ginkgo_test.go
  • pkg/aichat/runtime_settings.go
  • pkg/aichat/service.go
  • pkg/aichat/service_ginkgo_test.go
  • pkg/api/spec_layers.go
  • pkg/api/spec_layers_ginkgo_test.go
  • pkg/cli/serve_chat.go

Comment thread pkg/aichat/approval_execution.go
Comment thread pkg/aichat/provider_config.go
Comment thread pkg/api/spec_layers_ginkgo_test.go
Comment thread pkg/api/spec_layers.go
Comment thread pkg/api/spec_layers.go
@github-actions

github-actions Bot commented Aug 11, 2026

Copy link
Copy Markdown

Gavel summary

Source Pass Fail Skip Duration
ai 87 0 0 17ms
aichat 119 0 2 11.6s
anthropicmock 1 0 0 1ms
api 84 0 0 44ms
attachments 5 0 0 7ms
bash 4 0 0 199.065µs
callertools 10 0 0 76ms
captain 6 0 0 2ms
claude 22 0 0 13ms
claudeagent 13 0 0 210ms
cli 209 0 0 6.1s
cmux 4 0 0 231.704µs
collections 12 0 0 225.295µs
credentials 5 0 0 36ms
database 15 0 0 3.4s
genkit 25 0 0 66ms
gitagent 66 0 0 8.2s
github.com/flanksource/captain/migrations 11 0 0 3.8s
github.com/flanksource/captain/pkg/ai 232 0 0 370ms
github.com/flanksource/captain/pkg/ai/agent 23 0 0 -
github.com/flanksource/captain/pkg/ai/agent/commit 52 0 0 4.0s
github.com/flanksource/captain/pkg/ai/agent/setup 16 0 0 80ms
github.com/flanksource/captain/pkg/ai/agent/verify 21 0 0 380ms
github.com/flanksource/captain/pkg/ai/agent/worktree 6 0 0 -
github.com/flanksource/captain/pkg/ai/assistanttags 15 0 0 -
github.com/flanksource/captain/pkg/ai/fixture 42 0 0 170ms
github.com/flanksource/captain/pkg/ai/fixture/kubeproxy 2 0 0 30ms
github.com/flanksource/captain/pkg/ai/fixture/mcpproxy 6 0 0 -
github.com/flanksource/captain/pkg/ai/history 54 0 0 -
github.com/flanksource/captain/pkg/ai/internal/gen-model-registry 12 0 0 -
github.com/flanksource/captain/pkg/ai/middleware 25 0 0 -
github.com/flanksource/captain/pkg/ai/pricing 9 0 0 10ms
github.com/flanksource/captain/pkg/ai/prompt 16 0 0 10ms
github.com/flanksource/captain/pkg/ai/provider 144 0 0 20ms
github.com/flanksource/captain/pkg/ai/provider/claudeagent 44 0 0 1.4s
github.com/flanksource/captain/pkg/ai/provider/cmux 119 0 0 830ms
github.com/flanksource/captain/pkg/ai/provider/genkit 36 0 0 10ms
github.com/flanksource/captain/pkg/ai/provider/jsonrpc 6 0 0 50ms
github.com/flanksource/captain/pkg/aichat 9 0 0 100ms
github.com/flanksource/captain/pkg/aimock 50 0 7 380ms
github.com/flanksource/captain/pkg/aimock/anthropicmock 14 0 0 10ms
github.com/flanksource/captain/pkg/aimock/openaimock 18 0 0 60ms
github.com/flanksource/captain/pkg/api 136 0 0 340ms
github.com/flanksource/captain/pkg/api/registry 96 0 0 -
github.com/flanksource/captain/pkg/bash 348 0 0 10ms
github.com/flanksource/captain/pkg/captainconfig 25 0 0 -
github.com/flanksource/captain/pkg/claude 138 0 0 -
github.com/flanksource/captain/pkg/claude/tools 18 0 0 -
github.com/flanksource/captain/pkg/cli 532 0 0 40.5s
github.com/flanksource/captain/pkg/cmux 1 0 0 -
github.com/flanksource/captain/pkg/codexconfig 10 0 0 -
github.com/flanksource/captain/pkg/container 72 0 1 -
github.com/flanksource/captain/pkg/database 101 0 0 12.7s
github.com/flanksource/captain/pkg/dod 11 0 0 1m0s
github.com/flanksource/captain/pkg/gitagent 31 0 0 380ms
github.com/flanksource/captain/pkg/gitagent/proxy 12 0 0 -
github.com/flanksource/captain/pkg/monitor 54 0 0 1.9s
github.com/flanksource/captain/pkg/sandbox 1 0 0 -
github.com/flanksource/captain/pkg/sandbox/adapter 26 0 0 -
github.com/flanksource/captain/pkg/sandbox/presets 13 0 0 -
github.com/flanksource/captain/pkg/session 70 0 0 -
history 69 0 0 23ms
migrations 6 0 0 3.6s
openaimock 2 0 0 139.287µs
provider 10 0 0 14ms
registry 38 0 0 4ms
session 28 0 0 16ms
tools 38 0 0 4ms

Totals: 3555 passed · 0 failed · 10 skipped · 2m41s

View full results

Claude-Session-Id: 019fef59-46c9-7b83-bc02-6bd4246d90de
@adityathebe
adityathebe force-pushed the feat/hierarchical-runtime-profiles-dklw887kn9hc branch from 39c0427 to 07c2c4d Compare August 12, 2026 04:59
@adityathebe adityathebe self-assigned this Aug 12, 2026
@adityathebe
adityathebe force-pushed the feat/hierarchical-runtime-profiles-dklw887kn9hc branch from 3866b41 to 07c2c4d Compare August 12, 2026 05:29
Comment thread pkg/api/spec_layers.go Outdated
Comment on lines +21 to +35
type RuntimeLimits struct {
MaxInputTokens int `json:"maxInputTokens,omitempty" yaml:"maxInputTokens,omitempty"`
Budget Budget `json:"budget,omitempty" yaml:"budget,omitempty"`
}

// RuntimeQuota is one independently enforced usage allowance.
type RuntimeQuota struct {
Name string `json:"name" yaml:"name"`
Scope SpecLayerScope `json:"scope" yaml:"scope"`
Layer string `json:"layer" yaml:"layer"`
TokenLimit int `json:"tokenLimit,omitempty" yaml:"tokenLimit,omitempty"`
TokensUsed int `json:"tokensUsed,omitempty" yaml:"tokensUsed,omitempty"`
CostLimitUSD float64 `json:"costLimitUsd,omitempty" yaml:"costLimitUsd,omitempty"`
CostUsedUSD float64 `json:"costUsedUsd,omitempty" yaml:"costUsedUsd,omitempty"`
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

RuntimeLimits means per run while RuntimeQuota means per arbitrary period (monthly for eg). The name feels overloaded.

Perhaps, RunLimits vs (UsageQuota or PeriodQuota)?

@adityathebe

Copy link
Copy Markdown
Member

I don't see a way to set quota ?

@adityathebe adityathebe left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Three findings from a code review of this PR — the first two are functional bugs verified by executing the functions on this branch; the third is an error-classification issue. Details inline.


Generated by Claude Code

return nil
}

func runtimeAllowed(models []string, backend api.Backend) bool {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

runtimeAllowed cannot match bare model-name selectors, so a name-style constraint disables every runtime family.

AllowsModel matches selectors by model name, so a profile constraint like Models: ["gpt-5.6-sol"] is valid and /api/chat/models correctly shows that model as available. But runtimeAllowed only recognizes a selector as covering a backend via an explicit provider prefix (e.g. openai/) or via (api.Model{Name: selector}).Expand() — and Expand() returns an empty Backend for any bare name (no : or ,). Verified on this branch: runtimeAllowed([]string{"gpt-5.6-sol"}, api.BackendOpenAI) == false.

As a result annotateProfileRuntimes marks all runtime modes Disabled — including the backend whose model the profile explicitly allows — while /api/chat/models says the model is available: a self-contradictory catalog.

Suggestion: derive backend coverage from the same selector-matching semantics as AllowsModel/modelSelectorMatches in pkg/api/spec_layers.go (e.g. resolve the selector against the model registry and take the resolved model's backend) instead of re-implementing matching here.


Generated by Claude Code

Comment thread pkg/api/spec_layers.go
}
out := make([]string, 0, len(current))
for _, model := range current {
if allowed[model] {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

intersectModels trims only one side, so a whitespace-only difference empties the catalog and hard-fails profile resolution.

The restrictive layer's selectors are inserted trimmed (allowed[strings.TrimSpace(model)] = true), but current — copied verbatim from the first constraining layer via append([]string(nil), restrictive...) — is looked up untrimmed. A global layer with Models: [" gpt-5.4 "] intersected with a context layer's Models: ["gpt-5.4"] therefore produces an empty catalog. Verified on this branch: ResolveSpecLayers fails with spec layer "b" leaves the effective model catalog empty for semantically identical selectors — and with a request-time provider (as wired in serve_chat.go) that rejects every chat request.

validateSpecLayer trims only for its own checks and never normalizes the stored list. Suggestion: normalize selectors once when they enter the resolved constraints (and/or look up with allowed[strings.TrimSpace(model)] here) so both sides of the intersection use the same canonical form.


Generated by Claude Code

Comment thread pkg/aichat/service.go
}
}
spec, err := requestSpec(chat, settings, attachments)
resolved, err := requestSpec(chat, profile, attachments)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Server-side profile failures from requestSpec surface as HTTP 400.

Every error returned by requestSpec is mapped to http.StatusBadRequest, but some of them are server configuration defects rather than client mistakes: a RuntimeProfileProvider that returns Resolved without a Trace yields "chat runtime profile must include its resolution trace", and internal ResolveSpecLayers failures are wrapped as resolve chat runtime profile: .... Both would be reported as client errors, so monitoring/alerting on 5xx never sees them.

Suggestion: distinguish profile/resolution failures (500) from request-validation failures (400) — e.g. sentinel/typed errors from requestSpec, or resolve the profile separately from per-request validation so each path can map to the right status.


Generated by Claude Code

Bare model selectors could disable every runtime, and whitespace-only selector differences could empty an otherwise valid catalog. Server-owned profile resolution failures were also reported as client errors.

Match runtime constraints against concrete registry models, normalize catalog intersections, validate profiles before layering request fields, and clarify per-run limit and accumulated quota names.
@adityathebe
adityathebe merged commit d4b78a5 into main Aug 12, 2026
15 of 16 checks passed
@adityathebe
adityathebe deleted the feat/hierarchical-runtime-profiles-dklw887kn9hc branch August 12, 2026 10:26
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