Skip to content

feat: add user agent pass-through settings#1204

Closed
djdembeck wants to merge 7 commits intolooplj:release/v0.9.xfrom
djdembeck:feature/add-user-agent-pass-through-settings
Closed

feat: add user agent pass-through settings#1204
djdembeck wants to merge 7 commits intolooplj:release/v0.9.xfrom
djdembeck:feature/add-user-agent-pass-through-settings

Conversation

@djdembeck
Copy link
Copy Markdown
Contributor

This PR adds user agent pass-through settings for channels.

Add centralized resolver package to fix processing stalls by ensuring
correct model-family → endpoint routing before outbound transform.

Changes:
- New llm/resolver/ package with ResolveEndpoint() function
- Resolver maps models to responses/messages/chat_completions endpoints
- Copilot uses resolver for routing with fail-fast on unsupported endpoints
- NanoGPT uses resolver for routing with fail-fast on unsupported endpoints
- Remove redundant DONE check in TransformStreamChunk
- Remove unused helper functions
- Add integration tests for end-to-end validation

Fixes: processing stalls and missing token usage on model-family mismatch
Scope: Copilot + NanoGPT only (Claude Code, Anthropic, Codex deferred)
- Add userAgentPassThrough field to channel configuration
- Add system-level setting for global User-Agent pass-through
- Add override logic in orchestrator middleware
- Update HTTP client to handle pass-through header
- Add frontend toggle in General Settings
- Add channel-level override option
- Rename userAgentPassthrough to passThroughUserAgent in schema.ts for GraphQL consistency
- Add passThroughUserAgent to all channel GraphQL mutations (CREATE, UPDATE, BULK, etc.)
- Update form field name and state references in channels-action-dialog.tsx
- Fix JSX structure to use RadioGroup instead of incorrectly mixed Select component
…disabled

When PassThroughUserAgent is explicitly set to false, the User-Agent
header must be overridden to prevent client headers from leaking to
upstream AI providers. Removed the empty check that allowed client
User-Agents to pass through when the header was already present.

This resolves a medium-severity bug where browsers and SDKs would
have their User-Agent headers forwarded even when the system/channel
setting explicitly disabled pass-through.
Remove llm/resolver package and update transformers to check model
capabilities directly. Improve User-Agent pass-through logic to handle
channel-specific and global settings. Clean up unused integration tests
and dependencies.
@djdembeck djdembeck marked this pull request as draft March 28, 2026 01:08
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a User-Agent pass-through feature, allowing the system to forward client User-Agent headers to upstream AI providers via a global system setting or per-channel overrides. The changes encompass frontend UI components, GraphQL schema updates, backend business logic, and request orchestration middleware. Review feedback identifies a critical data inconsistency where the global setting is saved to one database key but queried from another, a potential runtime error in the frontend due to unhandled null values, and a regression in the dashboard's time-window filtering logic which shifted from calendar-based to relative offsets. Additionally, suggestions were provided to simplify state initialization and improve code readability within the channel configuration dialog.

Comment on lines +192 to +200
// UpdateUserAgentPassThrough is the resolver for the updateUserAgentPassThrough field.
func (r *mutationResolver) UpdateUserAgentPassThrough(ctx context.Context, input bool) (bool, error) {
err := r.systemService.SetUserAgentPassThrough(ctx, input)
if err != nil {
return false, fmt.Errorf("failed to update user agent pass through: %w", err)
}

return true, nil
}
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.

critical

There is a critical inconsistency between the query and mutation for the global userAgentPassThrough setting.

  • The updateUserAgentPassThrough mutation correctly updates the dedicated user_agent_pass_through key in the database via systemService.SetUserAgentPassThrough.
  • However, the corresponding query in the frontend (systemChannelSettings { userAgentPassThrough }) reads from systemService.ChannelSetting, which is backed by the system_channel_settings database key.

This means that when the user toggles the setting in the UI, the change is saved to one place, but the UI reads from another, so the change will not be reflected upon page reload. The setting will appear to be unsaved.

To fix this, the systemChannelSettings resolver should be updated to correctly source this global setting. A possible fix would be to modify the SystemChannelSettings resolver in queryResolver to populate the PassThroughUserAgent field from systemService.UserAgentPassThrough(ctx).

Comment on lines +46 to +50
React.useEffect(() => {
if (userAgentPassThrough !== undefined) {
setUserAgentPassThroughEnabled(userAgentPassThrough);
}
}, [userAgentPassThrough]);
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.

high

There's a potential runtime error here. The useUserAgentPassThrough hook can return null, but the setUserAgentPassThroughEnabled state setter expects a boolean. If userAgentPassThrough is null, this will cause an error.

You should handle the null case, for example by coalescing it to false.

Suggested change
React.useEffect(() => {
if (userAgentPassThrough !== undefined) {
setUserAgentPassThroughEnabled(userAgentPassThrough);
}
}, [userAgentPassThrough]);
React.useEffect(() => {
if (userAgentPassThrough !== undefined) {
setUserAgentPassThroughEnabled(userAgentPassThrough ?? false);
}
}, [userAgentPassThrough]);

Comment on lines +942 to +958
func (r *queryResolver) parseTimeWindow(ctx context.Context, timeWindow *string) (time.Time, bool) {
if timeWindow == nil || *timeWindow == "" || *timeWindow == "allTime" {
return time.Time{}, false
}

now := time.Now()

switch *timeWindow {
case "day":
return now.AddDate(0, 0, -1), true
case "week":
return now.AddDate(0, 0, -7), true
case "month":
return now.AddDate(0, -1, 0), true
default:
return time.Time{}, 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.

high

The implementation of parseTimeWindow has been changed to use relative time offsets (e.g., now.AddDate(0, 0, -1) for 'day'), which differs from the previous implementation that used calendar-based periods (e.g., start of today, start of this week). This is a significant behavioral change and likely a regression, as it alters the meaning of the time window filters.

The previous logic, which used r.systemService.TimeLocation(ctx) and xtime.GetCalendarPeriods(loc), was more accurate for dashboard time windows as it respected the user's configured timezone and aligned with calendar boundaries. Please consider reverting to a logic similar to the previous implementation.

You will also need to add the following import: "github.com/looplj/axonhub/internal/pkg/xtime".

func (r *queryResolver) parseTimeWindow(ctx context.Context, timeWindow *string) (time.Time, bool) {
	if timeWindow == nil || *timeWindow == "" || *timeWindow == "allTime" {
		return time.Time{}, false
	}

	loc := r.systemService.TimeLocation(ctx)
	period := xtime.GetCalendarPeriods(loc)

	var since time.Time

	switch *timeWindow {
	case "day":
		since = period.Today.Start
	case "week":
		since = period.ThisWeek.Start
	case "month":
		since = period.ThisMonth.Start
	default:
		return time.Time{}, false
	}

	return since, true
}

Comment on lines +281 to +286
const [passThroughUserAgent, setPassThroughUserAgent] = useState<boolean | null>(() => {
if (initialRow?.settings?.passThroughUserAgent !== undefined) {
return initialRow.settings.passThroughUserAgent;
}
return null;
});
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

The initialization logic for the passThroughUserAgent state can be simplified for better readability and conciseness by using the nullish coalescing operator (??).

  const [passThroughUserAgent, setPassThroughUserAgent] = useState<boolean | null>(() => initialRow?.settings?.passThroughUserAgent ?? null);

Comment on lines +2238 to +2242
onValueChange={(value) => {
const newValue = value === 'inherit' ? null : value === 'enable' ? true : false;
setPassThroughUserAgent(newValue);
field.onChange(newValue);
}}
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

The logic inside onValueChange can be made more readable by using a switch statement instead of nested ternary operators. This improves maintainability, especially if more options are added in the future.

                              onValueChange={(value) => {
                                let newValue: boolean | null;
                                switch (value) {
                                  case 'enable':
                                    newValue = true;
                                    break;
                                  case 'disable':
                                    newValue = false;
                                    break;
                                  default: // 'inherit'
                                    newValue = null;
                                    break;
                                }
                                setPassThroughUserAgent(newValue);
                                field.onChange(newValue);
                              }}

@djdembeck
Copy link
Copy Markdown
Contributor Author

There was a couple of Git related issues that contaminated this branch, so I need to go over and re-review everything before opening it up for review.

@djdembeck
Copy link
Copy Markdown
Contributor Author

This branch became contaminated with unrelated changes due to Git issues. Superseded by a clean branch with the same user agent pass-through implementation. Closing in favor of the new PR.

@djdembeck djdembeck closed this Mar 31, 2026
@djdembeck djdembeck deleted the feature/add-user-agent-pass-through-settings branch April 3, 2026 03:58
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.

1 participant