Skip to content

feat: customized channel endpoints#1529

Merged
looplj merged 1 commit into
unstablefrom
dev-tmp
Apr 28, 2026
Merged

feat: customized channel endpoints#1529
looplj merged 1 commit into
unstablefrom
dev-tmp

Conversation

@looplj

@looplj looplj commented Apr 28, 2026

Copy link
Copy Markdown
Owner

@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 a significant feature allowing users to configure custom API endpoints and path overrides for channels. The implementation spans the entire stack, including database schema updates, GraphQL API enhancements, a new frontend dialog for endpoint management, and logic in the backend orchestrator to select appropriate transformers based on request types. Additionally, the linting configuration was updated to support multi-module checks. Feedback focuses on a critical security risk where several gosec analyzers were disabled globally without sufficient justification, and a logic bug in the channel mutation input handling where the wrong field was being referenced during endpoint appending.

Comment thread .golangci.yml
Comment on lines +303 to +304
- linters: [gosec]
text: "(G704: SSRF via taint analysis|G703: Path traversal via taint analysis|G702: Command injection via taint analysis|G115: integer overflow conversion uintptr|G705: XSS via taint analysis)"

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.

security-critical critical

Disabling these gosec taint analysis rules (G702, G703, G704, G705) and the integer overflow check (G115) across the entire project is highly risky. These rules protect against critical vulnerabilities like Command Injection, Path Traversal, SSRF, and XSS. The comment "those reports are not relevant" is not a sufficient justification for disabling such important security checks globally. If there are specific false positives, they should be excluded on a case-by-case basis using //nolint:gosec comments with a clear explanation for each case. Please remove this global exclusion.

Comment on lines +273 to +275
if i.AppendEndpoints != nil {
m.AppendEndpoints(i.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.

critical

There is a bug in the UpdateChannelInput.Mutate method. When AppendEndpoints is used in a mutation, the code incorrectly appends i.Endpoints instead of i.AppendEndpoints. This will lead to incorrect data being persisted.

Suggested change
if i.AppendEndpoints != nil {
m.AppendEndpoints(i.Endpoints)
}
if i.AppendEndpoints != nil {
m.AppendEndpoints(i.AppendEndpoints)
}

@devin-ai-integration devin-ai-integration 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.

Devin Review found 2 potential issues.

View 5 additional findings in Devin Review.

Open in Devin Review

Comment on lines +218 to +233
var SupportedAPIFormats = map[string]struct{}{
"openai/chat_completions": {},
"openai/responses": {},
"openai/responses_compact": {},
"openai/embeddings": {},
"openai/image_generation": {},
"openai/image_edit": {},
"openai/image_variation": {},
"openai/video": {},
"anthropic/messages": {},
"gemini/contents": {},
"gemini/embeddings": {},
"jina/rerank": {},
"jina/embeddings": {},
"ollama/chat": {},
}

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.

🔴 SupportedAPIFormats allows formats that buildNonDefaultEndpointOutbound cannot handle, silently disabling channels

ValidateEndpoints (channel_override.go:218-233) uses SupportedAPIFormats which includes openai/responses_compact, openai/video, and gemini/embeddings. However, buildNonDefaultEndpointOutbound (channel_llm.go:228-289) has no case for these formats and returns an error via the default branch. This means: (1) a user can successfully save an endpoint with e.g. openai/responses_compact because validation passes, (2) on the next enabled-channels cache reload, buildChannelWithOutbounds propagates the error, and (3) reloadEnabledChannels (channel.go:227-228) logs a warning and skips the channel entirely via continue. The channel silently disappears from load balancing with no user-visible error at save time.

Prompt for agents
The SupportedAPIFormats map in channel_override.go (used by ValidateEndpoints) accepts formats that buildNonDefaultEndpointOutbound in channel_llm.go cannot build outbound transformers for. The formats openai/responses_compact, openai/video, and gemini/embeddings pass validation but cause buildChannelWithOutbounds to fail, which silently excludes the channel from the enabled channels list.

Two possible fixes:
1. Remove the unsupported formats (openai/responses_compact, openai/video, gemini/embeddings) from SupportedAPIFormats so they are rejected at validation time.
2. Add handling for these formats in buildNonDefaultEndpointOutbound so they actually work.

Option 1 is safer if these formats are not yet implemented. The key files are internal/server/biz/channel_override.go (SupportedAPIFormats map) and internal/server/biz/channel_llm.go (buildNonDefaultEndpointOutbound switch).
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +1085 to +1138
var defaultEndpointsForChannelType = map[channel.Type][]objects.ChannelEndpoint{
channel.TypeOpenai: {{APIFormat: "openai/chat_completions"}},
channel.TypeOpenaiResponses: {{APIFormat: "openai/responses"}},
channel.TypeCodex: {{APIFormat: "openai/responses"}},
channel.TypeVercel: {{APIFormat: "openai/chat_completions"}},
channel.TypeAnthropic: {{APIFormat: "anthropic/messages"}},
channel.TypeAnthropicAWS: {{APIFormat: "anthropic/messages"}},
channel.TypeAnthropicGcp: {{APIFormat: "anthropic/messages"}},
channel.TypeGeminiOpenai: {{APIFormat: "openai/chat_completions"}},
channel.TypeGemini: {{APIFormat: "gemini/contents"}},
channel.TypeGeminiVertex: {{APIFormat: "gemini/contents"}},
channel.TypeDeepseek: {{APIFormat: "openai/chat_completions"}},
channel.TypeDeepseekAnthropic: {{APIFormat: "anthropic/messages"}},
channel.TypeDeepinfra: {{APIFormat: "openai/chat_completions"}},
channel.TypeFireworks: {{APIFormat: "openai/chat_completions"}},
channel.TypeDoubao: {{APIFormat: "openai/chat_completions"}},
channel.TypeDoubaoAnthropic: {{APIFormat: "anthropic/messages"}},
channel.TypeMoonshot: {{APIFormat: "openai/chat_completions"}},
channel.TypeMoonshotAnthropic: {{APIFormat: "anthropic/messages"}},
channel.TypeZhipu: {{APIFormat: "openai/chat_completions"}},
channel.TypeZai: {{APIFormat: "openai/chat_completions"}},
channel.TypeZhipuAnthropic: {{APIFormat: "anthropic/messages"}},
channel.TypeZaiAnthropic: {{APIFormat: "anthropic/messages"}},
channel.TypeAnthropicFake: {{APIFormat: "anthropic/messages"}},
channel.TypeOpenaiFake: {{APIFormat: "openai/chat_completions"}},
channel.TypeOpenrouter: {{APIFormat: "openai/chat_completions"}},
channel.TypeXiaomi: {{APIFormat: "openai/chat_completions"}},
channel.TypeXai: {{APIFormat: "openai/chat_completions"}},
channel.TypePpio: {{APIFormat: "openai/chat_completions"}},
channel.TypeSiliconflow: {{APIFormat: "openai/chat_completions"}},
channel.TypeVolcengine: {{APIFormat: "openai/chat_completions"}},
channel.TypeLongcat: {{APIFormat: "openai/chat_completions"}},
channel.TypeLongcatAnthropic: {{APIFormat: "anthropic/messages"}},
channel.TypeMinimax: {{APIFormat: "openai/chat_completions"}},
channel.TypeMinimaxAnthropic: {{APIFormat: "anthropic/messages"}},
channel.TypeAihubmix: {{APIFormat: "openai/chat_completions"}},
channel.TypeBurncloud: {{APIFormat: "openai/chat_completions"}},
channel.TypeModelscope: {{APIFormat: "openai/chat_completions"}},
channel.TypeBailian: {{APIFormat: "openai/chat_completions"}},
channel.TypeBailianAnthropic: {{APIFormat: "anthropic/messages"}},
channel.TypeMoonshotCoding: {{APIFormat: "anthropic/messages"}},
channel.TypeJina: {
{APIFormat: "jina/rerank"},
{APIFormat: "jina/embeddings"},
},
channel.TypeGithub: {{APIFormat: "openai/chat_completions"}},
channel.TypeGithubCopilot: {{APIFormat: "openai/chat_completions"}},
channel.TypeClaudecode: {{APIFormat: "anthropic/messages"}},
channel.TypeCerebras: {{APIFormat: "openai/chat_completions"}},
channel.TypeAntigravity: {{APIFormat: "gemini/contents"}},
channel.TypeNanogpt: {{APIFormat: "openai/chat_completions"}},
channel.TypeNanogptResponses: {{APIFormat: "openai/responses"}},
channel.TypeOllama: {{APIFormat: "ollama/chat"}},
}

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.

🟡 Three channel types missing from defaultEndpointsForChannelType map

The defaultEndpointsForChannelType map in channel_llm.go:1085-1138 is missing entries for TypeXiaomiAnthropic, TypeVolcengineAnthropic, and TypeAihubmixAnthropic. These types are valid channel types (defined in internal/ent/channel/channel.go) and use Anthropic transformers. Without entries, ResolveEndpoints() returns nil for these types, so buildChannelWithOutbounds returns early with a nil Outbounds map and the multi-endpoint feature is inoperative. The same three types are also missing from the frontend's CHANNEL_TYPE_TO_DEFAULT_ENDPOINTS in frontend/src/features/channels/data/config_channels.ts:708-761, causing the endpoints dialog to show empty defaults for these channel types.

Prompt for agents
Three channel types are missing from the defaultEndpointsForChannelType map in internal/server/biz/channel_llm.go: TypeXiaomiAnthropic, TypeVolcengineAnthropic, and TypeAihubmixAnthropic. All three should map to []objects.ChannelEndpoint{{APIFormat: "anthropic/messages"}} since they are Anthropic-protocol variants of their respective providers.

The same three types (xiaomi_anthropic, volcengine_anthropic, aihubmix_anthropic) are also missing from CHANNEL_TYPE_TO_DEFAULT_ENDPOINTS in frontend/src/features/channels/data/config_channels.ts. They should each map to [{ apiFormat: ANTHROPIC_MESSAGES }].

Both maps need to be updated together to keep frontend and backend consistent.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@greptile-apps

greptile-apps Bot commented Apr 28, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds per-channel customizable endpoint configuration, allowing administrators to assign one or more API-format/path pairs to a channel, overriding the default inferred from the channel type. The change spans the full stack: a new Ent endpoints JSON field, a SaveChannelEndpoints GraphQL mutation, ResolveEndpoints / buildNonDefaultEndpointOutbound in the biz layer, SelectAPIFormatForRequestType in the orchestrator to pick the right format per request type, and a new frontend dialog with default-locked row rendering.

  • P1 – aisdk/text and aisdk/datastream in the frontend format dropdown are rejected by the backend. Both values appear as selectable options in apiFormatSchema but are absent from SupportedAPIFormats; saving will always fail with an "unsupported api_format" validation error.

Confidence Score: 4/5

Safe to merge after fixing the aisdk/text and aisdk/datastream schema mismatch that makes those dropdown options always error on save

One P1 bug: two API format values in the frontend dropdown (aisdk/text, aisdk/datastream) are not in the backend SupportedAPIFormats, so selecting either and saving will always fail with a validation error. The rest of the implementation is solid — validation, fallback logic, outbound builder dispatch, and cache invalidation all look correct.

frontend/src/features/channels/data/schema.ts (remove unsupported aisdk formats) and internal/server/biz/channel_override.go (consider syncing with frontend list)

Important Files Changed

Filename Overview
frontend/src/features/channels/data/schema.ts Adds channelEndpointSchema / ChannelEndpoint type and extends channelSchema; apiFormatSchema includes aisdk/text and aisdk/datastream which the backend rejects as unsupported (P1 bug)
frontend/src/features/channels/components/channels-endpoints-dialog.tsx New dialog for managing per-channel endpoint overrides; validates duplicates client-side, locks default endpoints, renders clean table-based UI
internal/server/orchestrator/custom_endpoints.go Adds SelectAPIFormatForRequestType that picks the appropriate endpoint API format based on request type with correct fallback to first endpoint
internal/server/biz/channel_llm.go Adds buildChannelWithOutbounds, buildNonDefaultEndpointOutbound, ResolveEndpoints, and DefaultEndpointsForChannelType — well-structured endpoint resolution and outbound builder dispatch
internal/server/biz/channel_override.go Adds SupportedAPIFormats map and ValidateEndpoints; backend format list is out of sync with frontend apiFormatSchema (P2)
llm/transformer/url.go Adds BuildRequestURL helper; NormalizeBaseURL contains a dead-code branch (always-false HasSuffix guard) — both branches return the same value
internal/server/biz/channel.go Adds SaveChannelEndpoints mutation with endpoint validation and async channel reload; clean and consistent with existing update patterns
frontend/src/features/channels/data/config_channels.ts Adds CHANNEL_TYPE_TO_DEFAULT_ENDPOINTS mapping all channel types to their default API format(s); comprehensive coverage across all defined channel types
internal/server/orchestrator/candidates.go Extended to call channel.ResolveEndpoints() + SelectAPIFormatForRequestType for both legacy channel and model-association candidate paths; APIFormat is always populated before returning

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Incoming Request] --> B[CandidateSelector]
    B --> C{Model Association or Legacy Channel?}
    C -->|Association| D[resolveAssociations]
    C -->|Legacy| E[selectChannelCandidates]
    D --> F[filterResolvedCandidatesForRequest]
    F --> G[aggregateChannelModelCandidates]
    G --> H[populateAPIFormat]
    E --> H
    H --> I[Channel.ResolveEndpoints]
    I --> J{Custom Endpoints Configured?}
    J -->|Yes| K[Use ch.Endpoints]
    J -->|No| L[DefaultEndpointsForChannelType]
    K --> M[SelectAPIFormatForRequestType]
    L --> M
    M --> N{Request Type Match?}
    N -->|Match Found| O[Use Matched APIFormat]
    N -->|No Match| P[Fallback: endpoints 0 .APIFormat]
    O --> Q[ChannelModelsCandidate with APIFormat]
    P --> Q
    Q --> R[buildChannelWithOutbounds]
    R --> S{Endpoint = default with no path?}
    S -->|Yes| T[Reuse existing ch.Outbound]
    S -->|No| U[buildNonDefaultEndpointOutbound]
    U --> V[New Outbound Transformer per api_format]
    T --> W[ch.Outbounds map]
    V --> W
Loading

Comments Outside Diff (2)

  1. frontend/src/features/channels/data/schema.ts, line 4-18 (link)

    P1 Frontend API formats not recognized by backend

    apiFormatSchema includes 'aisdk/text' and 'aisdk/datastream', but neither is present in the backend's SupportedAPIFormats map (channel_override.go). Both appear as valid choices in the endpoint-selector dropdown. Saving a channel endpoint with either format will always return an error from ValidateEndpoints: "unsupported api_format \"aisdk/text\"", while the UI gave no indication the option was invalid.

    The backend also supports 'openai/responses_compact' and 'openai/video' but those can be added incrementally; the immediate fix is removing the two unsupported values.

  2. llm/transformer/url.go, line 34-39 (link)

    P2 Dead-code branch in NormalizeBaseURL

    After strings.TrimRight(url, "/"), trimmed can never end with "/", so the if strings.HasSuffix(trimmed, "/") guard is always false. Both branches return the identical expression trimmed + "/" + version. The inner if should simply be removed.

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

Comment on lines +218 to +233
var SupportedAPIFormats = map[string]struct{}{
"openai/chat_completions": {},
"openai/responses": {},
"openai/responses_compact": {},
"openai/embeddings": {},
"openai/image_generation": {},
"openai/image_edit": {},
"openai/image_variation": {},
"openai/video": {},
"anthropic/messages": {},
"gemini/contents": {},
"gemini/embeddings": {},
"jina/rerank": {},
"jina/embeddings": {},
"ollama/chat": {},
}

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.

P2 SupportedAPIFormats and frontend apiFormatSchema are out of sync

SupportedAPIFormats contains gemini/embeddings, openai/responses_compact, and openai/video — none of which appear in the frontend's apiFormatSchema enum. Conversely, the frontend enum includes aisdk/text and aisdk/datastream which are absent from SupportedAPIFormats (see related comment on schema.ts). Consider either adding a shared source-of-truth (e.g. GraphQL enum) that both sides derive from, or at least a test that cross-checks them. Without this, divergence will silently re-occur whenever formats are added on one side.

@looplj looplj merged commit b57114a into unstable Apr 28, 2026
5 checks passed
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