Skip to content

feat: customized channel endpoints with base url#1635

Merged
looplj merged 1 commit into
unstablefrom
dev-tmp
May 10, 2026
Merged

feat: customized channel endpoints with base url#1635
looplj merged 1 commit into
unstablefrom
dev-tmp

Conversation

@looplj

@looplj looplj commented May 10, 2026

Copy link
Copy Markdown
Owner

No description provided.

@gemini-code-assist gemini-code-assist Bot 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.

Code Review

This pull request introduces the ability to override the base URL for specific channel endpoints and adds an API Format column to the request logs. Key updates include frontend dialog enhancements for managing endpoint base URLs, schema modifications to support the new field, and backend logic to apply these overrides during LLM calls. Review feedback pointed out a bug in the dialog's state management where background refreshes could cause data loss and recommended using the Badge component in the request logs for better UI consistency.

Comment on lines +93 to +101
useEffect(() => {
if (open) {
setEndpoints(channel.endpoints ?? []);
setNewApiFormat('');
setNewPath('');
setNewBaseURL('');
setError(null);
}
}, [open, channel.endpoints]);

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 useEffect hook that resets the dialog state includes channel.endpoints in its dependency array. Since the channels are refetched every 5 seconds in the background (as seen in useQueryChannels), this dependency will trigger a state reset whenever a background refresh occurs, causing any unsaved changes the user has made in the dialog to be lost.

You should change the dependency to channel.id so that the state only resets when the dialog is opened or when the active channel changes, but not on background data refreshes.

Suggested change
useEffect(() => {
if (open) {
setEndpoints(channel.endpoints ?? []);
setNewApiFormat('');
setNewPath('');
setNewBaseURL('');
setError(null);
}
}, [open, channel.endpoints]);
useEffect(() => {
if (open) {
setEndpoints(channel.endpoints ?? []);
setNewApiFormat('');
setNewPath('');
setNewBaseURL('');
setError(null);
}
}, [open, channel.id]);

Comment on lines +103 to +113
cell: ({ row }) => {
const format = row.original.format;
if (!format) {
return <div className='text-muted-foreground text-xs'>-</div>;
}
return (
<span className='inline-flex items-center rounded-md border border-zinc-200 bg-zinc-50 px-2 py-0.5 text-xs font-medium text-zinc-700 dark:border-zinc-700 dark:bg-zinc-800/50 dark:text-zinc-300'>
{format}
</span>
);
},

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

For better UI consistency and maintainability, consider using the Badge component to display the API format, similar to how it is displayed in the channel endpoints dialog. The current implementation uses hardcoded zinc colors which might not align with the project's theme variables or other parts of the UI.

Suggested change
cell: ({ row }) => {
const format = row.original.format;
if (!format) {
return <div className='text-muted-foreground text-xs'>-</div>;
}
return (
<span className='inline-flex items-center rounded-md border border-zinc-200 bg-zinc-50 px-2 py-0.5 text-xs font-medium text-zinc-700 dark:border-zinc-700 dark:bg-zinc-800/50 dark:text-zinc-300'>
{format}
</span>
);
},
cell: ({ row }) => {
const format = row.original.format;
if (!format) {
return <div className='text-muted-foreground text-xs'>-</div>;
}
return (
<Badge variant='secondary' className='font-mono text-[10px]'>
{format}
</Badge>
);
},

@greptile-apps

greptile-apps Bot commented May 10, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR extends channel endpoints with a per-endpoint baseURL override, allowing each API format (e.g. openai/chat_completions, anthropic/messages) to route to a different upstream host instead of always inheriting the channel-level base URL.

  • Backend: Adds BaseURL string to objects.ChannelEndpoint, threads it through the GraphQL schema and generated code, and uses it in buildNonDefaultEndpointOutbound to override the channel-level URL per endpoint.
  • Frontend: Extends channelEndpointSchema with z.url().optional().or(z.literal('')) validation, adds a baseURL input to the endpoints dialog, and refactors the endpoint table into a shared EndpointTable component with an optional hideBaseURL column.

Confidence Score: 4/5

Safe to merge for normal users who go through the UI, but a direct API call with a malformed baseURL bypasses all validation and can corrupt a channel's endpoint data.

The frontend correctly validates baseURL with z.url() and the happy path works end-to-end. However, ValidateEndpoints was not updated to check the new BaseURL field. Because the frontend also uses z.url() to parse server responses, a malformed URL stored in the database would cause every subsequent channel-list fetch to throw a Zod parse error for that channel, silently breaking its display in the UI.

The gap is in internal/server/biz/channel_endpoint.go (not changed by this PR) — ValidateEndpoints needs a url.ParseRequestURI check for ep.BaseURL, mirroring the path validation already there. The trigger is the new BaseURL field added in internal/objects/channel.go.

Important Files Changed

Filename Overview
internal/objects/channel.go Adds BaseURL field to ChannelEndpoint; no server-side URL format validation is added for this new field, leaving a path where invalid URLs can be persisted and break the channel-list parse on next fetch.
internal/server/biz/channel_llm.go Adds buildNonDefaultEndpointOutbound which falls back to channel-level BaseURL when the endpoint's BaseURL is empty — logic is correct. Also retains a local isOAuthJSON that diverges subtly from the version in objects/channel.go (pre-existing).
frontend/src/features/channels/data/schema.ts Adds baseURL: z.url('Invalid URL').optional().or(z.literal('')) to channelEndpointSchema, correctly accepting undefined, empty string (server zero value), or a valid URL.
frontend/src/features/channels/components/channels-endpoints-dialog.tsx Adds baseURL input field and newBaseURL state; EndpointTable component correctly shows/hides the baseURL column; useEffect reset on dialog open is appropriate.
internal/server/gql/axonhub.graphql Adds nullable baseURL: String to ChannelEndpoint type and ChannelEndpointInput, consistent with the Go struct change.
frontend/src/features/channels/data/channels.ts Queries updated to include baseURL in endpoint fragment; SaveChannelEndpointsInput type extended with optional baseURL.
internal/ent/migrate/schema.go Auto-generated schema migration; endpoints column is a JSON blob so BaseURL is stored inside the JSON without a schema change — no migration needed.

Sequence Diagram

sequenceDiagram
    participant UI as Frontend Dialog
    participant GQL as GraphQL Mutation
    participant Biz as ChannelService.SaveChannelEndpoints
    participant Val as ValidateEndpoints
    participant DB as Database
    participant Route as buildNonDefaultEndpointOutbound

    UI->>UI: z.url().optional().or(z.literal('')) validates baseURL
    UI->>GQL: "saveChannelEndpoints({endpoints: [{apiFormat, path, baseURL}]})"
    GQL->>Biz: SaveChannelEndpoints(input)
    Biz->>Val: ValidateEndpoints(endpoints)
    Note over Val: Checks api_format + path<br/>does NOT check baseURL
    Val-->>Biz: ok
    Biz->>DB: SetEndpoints(endpoints).Save()
    DB-->>Biz: channel
    Note over Route: At request time...
    Route->>Route: "baseURL = channel.BaseURL"
    Route->>Route: "if ep.BaseURL != empty then baseURL = ep.BaseURL"
    Route->>Route: build outbound transformer with baseURL
Loading

Reviews (3): Last reviewed commit: "feat: customized channel endpoints with ..." | Re-trigger Greptile

Comment thread frontend/src/features/channels/data/schema.ts Outdated
@looplj looplj merged commit 7d20d40 into unstable May 10, 2026
4 checks passed
Comment on lines 16 to 20
type ChannelEndpoint struct {
APIFormat string `json:"api_format"`
Path string `json:"path,omitempty"`
BaseURL string `json:"base_url,omitempty"`
}

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.

P1 Missing server-side URL validation for BaseURL

The existing ValidateEndpoints function in channel_endpoint.go validates api_format (non-empty, supported, unique) and path (must start with /, must not be a full URL), but it was not updated to validate the new BaseURL field. If a caller bypasses the frontend and submits an invalid string (e.g. "not-a-url") via the GraphQL mutation, it is persisted to the database without error. When the channel is subsequently fetched, the frontend's channelEndpointSchema — which uses z.url('Invalid URL').optional().or(z.literal('')) — will reject the malformed string, causing channelSchema.parse() to throw and breaking the channel list entirely for that channel. Adding a url.ParseRequestURI check on ep.BaseURL in ValidateEndpoints (similar to how path guards against accidental full URLs) would prevent invalid data from ever reaching the database.

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