feat: add user agent pass-through settings#1204
feat: add user agent pass-through settings#1204djdembeck wants to merge 7 commits intolooplj:release/v0.9.xfrom
Conversation
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.
There was a problem hiding this comment.
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.
| // 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 | ||
| } |
There was a problem hiding this comment.
There is a critical inconsistency between the query and mutation for the global userAgentPassThrough setting.
- The
updateUserAgentPassThroughmutation correctly updates the dedicateduser_agent_pass_throughkey in the database viasystemService.SetUserAgentPassThrough. - However, the corresponding query in the frontend (
systemChannelSettings { userAgentPassThrough }) reads fromsystemService.ChannelSetting, which is backed by thesystem_channel_settingsdatabase 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).
| React.useEffect(() => { | ||
| if (userAgentPassThrough !== undefined) { | ||
| setUserAgentPassThroughEnabled(userAgentPassThrough); | ||
| } | ||
| }, [userAgentPassThrough]); |
There was a problem hiding this comment.
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.
| React.useEffect(() => { | |
| if (userAgentPassThrough !== undefined) { | |
| setUserAgentPassThroughEnabled(userAgentPassThrough); | |
| } | |
| }, [userAgentPassThrough]); | |
| React.useEffect(() => { | |
| if (userAgentPassThrough !== undefined) { | |
| setUserAgentPassThroughEnabled(userAgentPassThrough ?? false); | |
| } | |
| }, [userAgentPassThrough]); |
| 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 | ||
| } |
There was a problem hiding this comment.
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
}| const [passThroughUserAgent, setPassThroughUserAgent] = useState<boolean | null>(() => { | ||
| if (initialRow?.settings?.passThroughUserAgent !== undefined) { | ||
| return initialRow.settings.passThroughUserAgent; | ||
| } | ||
| return null; | ||
| }); |
There was a problem hiding this comment.
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);
| onValueChange={(value) => { | ||
| const newValue = value === 'inherit' ? null : value === 'enable' ? true : false; | ||
| setPassThroughUserAgent(newValue); | ||
| field.onChange(newValue); | ||
| }} |
There was a problem hiding this comment.
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);
}}
|
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. |
|
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. |
This PR adds user agent pass-through settings for channels.