Skip to content

opt: custom endpoint api format#1545

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

opt: custom endpoint api format#1545
looplj merged 1 commit into
unstablefrom
dev-tmp

Conversation

@looplj

@looplj looplj commented Apr 30, 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 refactors the channel endpoint management system, transitioning from static frontend configurations to a backend-driven model that distinguishes between dynamically resolved default endpoints and user-provided overrides. The changes include updates to the GraphQL schema, frontend UI components, and backend outbound transformer selection logic to support multiple API formats per channel. Feedback indicates that the ollama/chat format is missing from the validation map and identifies a logic contradiction where the implementation currently blocks overrides of default endpoints despite the architectural intent to allow them.

Comment on lines +25 to +26
llm.APIFormatJinaEmbedding.String(): {},
}

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 ollama/chat API format is missing from the SupportedAPIFormats map. This will cause validation to fail when configuring endpoints for Ollama channels, even though it is defined as a default format in channel_llm.go.

Suggested change
llm.APIFormatJinaEmbedding.String(): {},
}
llm.APIFormatJinaEmbedding.String(): {},
llm.APIFormatOllamaChat.String(): {},
}

Comment on lines +674 to +681
defaults := DefaultEndpointsForChannelType(ch.Type)
for _, userEP := range input.Endpoints {
for _, defaultEP := range defaults {
if userEP.APIFormat == defaultEP.APIFormat {
return nil, fmt.Errorf("endpoint api_format %q conflicts with default endpoint for channel type %s", userEP.APIFormat, ch.Type)
}
}
}

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

There is a logic inconsistency regarding endpoint overrides. The comments in SaveChannelEndpointsInput (line 351) and ResolveEndpoints (line 1250 in channel_llm.go) state that user-configured endpoints are intended to override defaults. However, this check explicitly forbids any overlap between user endpoints and defaults, preventing users from customizing the path of a built-in endpoint (e.g., changing the path for openai/chat_completions). If overrides are intended, this validation should be removed.

Comment on lines +63 to +66
if (defaultApiFormats.has(newApiFormat)) {
setError(t('channels.endpoints.defaultConflictError', 'Cannot override a default endpoint'));
return;
}

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

This check prevents users from overriding default endpoints, which contradicts the stated goal of supporting endpoint overrides. If the backend logic in mergeEndpoints is intended to be used, this restriction should be removed from the UI.

@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 6 additional findings in Devin Review.

Open in Devin Review

Comment on lines +209 to 217
for _, ep := range userEndpoints {
if ep.APIFormat == "" {
continue
}

out, err := svc.buildNonDefaultEndpointOutbound(c, ch, accountIdentity, ep)
if err != nil {
return nil, fmt.Errorf("failed to build outbound for api_format %q on channel %s: %w", ep.APIFormat, c.Name, err)
}

outbounds[ep.APIFormat] = out

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.

🔴 Missing ollama/chat case in buildNonDefaultEndpointOutbound breaks existing Ollama channels on upgrade

The buildNonDefaultEndpointOutbound switch statement has no case for ollama/chat (or seedance/video). When buildChannelWithOutbounds iterates over user-configured endpoints (c.Endpoints), any ollama/chat entry hits the default error branch at channel_llm.go:301, causing the entire channel build to fail.

This is a real upgrade regression: in the old code, ollama/chat was listed in SupportedAPIFormats (channel_override.go:233), so users could save it as a user endpoint. Furthermore, the old dialog initialized state from defaults (channel.endpoints.length > 0 ? channel.endpoints : defaultEndpoints), so any Ollama user who opened the endpoints dialog and clicked Save would have persisted ollama/chat as a user endpoint. After this PR, the enabled channels cache refresh calls buildChannelWithOutbounds which will error on these pre-existing user endpoints, silently dropping the channel from the active pool.

Fix suggestion In the user-endpoints loop of `buildChannelWithOutbounds`, skip user endpoints whose `APIFormat` already exists in `defaultEndpoints` (they're already mapped to `ch.Outbound`), or add a data migration to strip default-format entries from the `endpoints` column.
Prompt for agents
In buildChannelWithOutbounds (channel_llm.go around line 198-227), the user-endpoint loop unconditionally calls buildNonDefaultEndpointOutbound for every user endpoint. However, buildNonDefaultEndpointOutbound does not handle ollama/chat (or seedance/video), so pre-existing Ollama channels whose endpoints column contains ollama/chat will fail to load.

The simplest fix is to skip user endpoints whose APIFormat is already covered by the default endpoints. In the user-endpoint loop, before calling buildNonDefaultEndpointOutbound, check if outbounds[ep.APIFormat] already exists (it would have been set by the default-endpoint loop above). If it does AND the user endpoint has no custom Path, skip it. If it has a custom Path, build the override normally but also add ollama/chat and seedance/video cases to buildNonDefaultEndpointOutbound.

Alternatively, add a data migration that removes default-format entries from the user endpoints column for all channels.
Open in Devin Review

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

Comment on lines +142 to +148
<label className='text-sm font-medium'>{t('channels.endpoints.defaultEndpoints', 'Default endpoints')}</label>
<span className='text-muted-foreground text-xs'>{defaultEndpoints.length > 0 && `${defaultEndpoints.length} resolved`}</span>
</div>
{endpoints.length === 0 ? (
<div className="rounded-lg border border-dashed p-4">
<p className="text-sm text-muted-foreground text-center">{t('channels.endpoints.emptyHint')}</p>
{defaultEndpoints.length === 0 ? (
<div className='rounded-lg border border-dashed p-4'>
<p className='text-muted-foreground text-center text-sm'>
{t('channels.endpoints.noDefaultEndpoints', 'No default endpoints resolved for this channel type.')}

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.

🔴 New i18n keys used in code but missing from locale files (en.json and zh.json)

Per the mandatory rule in .agent/rules/frontend-i18n.md: "Any new i18n key used in code must be added to both frontend/src/locales/en.json and frontend/src/locales/zh.json."

The dialog component introduces at least 8 new translation keys (channels.endpoints.defaultEndpoints, channels.endpoints.noDefaultEndpoints, channels.endpoints.primaryBadge, channels.endpoints.readOnly, channels.endpoints.noOverridesHint, channels.endpoints.unsupportedBadge, channels.endpoints.invalidApiFormat, channels.endpoints.defaultConflictError) but the PR diff contains no changes to either locale file. While the code uses inline fallback strings (second argument to t()), the Chinese locale will show English fallbacks instead of proper translations.

Prompt for agents
Add all new i18n keys used in channels-endpoints-dialog.tsx to both frontend/src/locales/en.json and frontend/src/locales/zh.json under the channels.endpoints namespace. The keys to add are: defaultEndpoints, noDefaultEndpoints, primaryBadge, readOnly, noOverridesHint, unsupportedBadge, invalidApiFormat, and defaultConflictError. Use the inline fallback strings as the English values and provide appropriate Chinese translations in zh.json.
Open in Devin Review

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

@greptile-apps

greptile-apps Bot commented Apr 30, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR refactors the channel endpoint model to separate default endpoints (built-in, read-only capability surfaces derived from channel type) from user-configured custom endpoints (additive overrides). Default endpoints are now resolved dynamically server-side and exposed via a new defaultEndpoints GraphQL field, while user endpoints are strictly additive — they cannot shadow a default format. The UI is updated to display both sections clearly and enforce the new constraint at save time. Multiple channel types get expanded default endpoint sets (e.g., OpenAI-compatible channels now include embeddings, image, and video defaults; Gemini channels include both gemini/contents and gemini/embeddings).

  • Behavioral break for existing channels: Any channel that previously had user-configured endpoints (which fully replaced defaults under the old ResolveEndpoints logic) will now have its defaults merged back in. Users who intentionally limited a channel to fewer formats may find additional endpoints unexpectedly active after the upgrade.
  • Server-side validation gap: SaveChannelEndpoints now performs a Get then a separate UpdateOne, introducing a narrow window where the channel type could change between the two calls, making the default-conflict check stale. This race is unlikely in practice but the check is no longer atomic.

Confidence Score: 5/5

Safe to merge; all findings are P2 (design/style/theoretical) with no runtime correctness bugs.

The refactor is well-structured with good test coverage (new test files for Gemini, NanoGPT, OpenAI-compatible channels, and a comprehensive mapping test). No P0/P1 bugs were found. The P2 comments cover: a theoretical non-atomic DB check, an implicit behavioral contract on the primary outbound shared across all default formats, a global linter suppression, and a test that exercises a code path now blocked by the API. None of these block merge.

.golangci.yml (global linter suppressions) and internal/server/biz/channel_llm.go (implicit primary-outbound-sharing contract for default endpoints).

Important Files Changed

Filename Overview
internal/server/biz/channel_llm.go Core change: buildChannelWithOutbounds now maps all default endpoints to ch.Outbound (primary transformer) and only builds new transformers for user-configured endpoints. All default endpoint API formats share the primary outbound, which works because transformers route internally by request type.
internal/server/biz/channel_endpoint.go New file with SupportedAPIFormats map (moved from channel_override.go) and ValidateEndpoints. Uses llm.APIFormat* constants instead of string literals. ollama/chat intentionally removed since Ollama is now default-only.
internal/server/biz/channel.go SaveChannelEndpoints now performs a Get then conflict-check then UpdateOne, adding a theoretical race window where the channel type might change between Get and UpdateOne making the default-conflict check stale.
frontend/src/features/channels/components/channels-endpoints-dialog.tsx UI split into two sections: read-only default endpoints and editable custom endpoints. Client-side validation correctly guards against adding formats that match defaults or unsupported formats, mirroring server-side rules.
internal/server/orchestrator/outbound.go Extracts selectOutboundForCandidate helper and applies it consistently across TransformRequest, NextChannel, and PrepareForRetry. Adds api_format logging on retry/next-channel paths.
.golangci.yml Globally disables exhaustive, containedctx, and wsl_v5 linters. exhaustive catches non-exhaustive enum switch statements; disabling it globally removes that safety net for the entire codebase.
internal/server/biz/channel_endpoint_mapping_test.go New test file; comprehensive coverage of DefaultEndpointsForChannelType, ValidateEndpoints, ResolveEndpoints merge behavior, and SupportedAPIFormats completeness.
internal/server/biz/video.go Video task loading now uses the Outbounds map to select the correct video outbound (SeedanceVideo for Doubao, OpenAIVideo otherwise), replacing the direct Outbound type-assertion. Clean refactor.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Channel Request] --> B{Route by API Format}
    B --> C[BuildOutboundByAPIFormat]
    C --> D{Outbounds map lookup}
    D -->|Found| E[Return matched outbound]
    D -->|Not found| F[Return ch.Outbound primary]

    G[buildChannelWithOutbounds] --> H[DefaultEndpointsForChannelType]
    H --> I[Map ALL defaults to ch.Outbound primary]
    G --> J[User c.Endpoints]
    J --> K[buildNonDefaultEndpointOutbound]
    K --> L[New format-specific transformer]

    I --> M[outbounds map]
    L --> M
    M --> C

    N[SaveChannelEndpoints] --> O[ValidateEndpoints format/path]
    O --> P[Get channel from DB]
    P --> Q{Conflict with defaults?}
    Q -->|Yes| R[Return error]
    Q -->|No| S[UpdateOne endpoints]

    T[ResolveEndpoints] --> U[mergeEndpoints]
    U --> V[defaults + user extras]
Loading

Comments Outside Diff (3)

  1. internal/server/biz/channel.go, line 830-844 (link)

    P2 Non-atomic default-conflict check

    The Get and UpdateOne are two separate transactions. Between them, another concurrent request could change the channel's type (e.g., via an updateChannel mutation), so the conflict check runs against the old type while the UpdateOne persists against the new one. In practice this window is tiny and channel type changes are rare, but the check is no longer truly atomic. Wrapping these operations in a single Ent transaction would close the gap.

  2. .golangci.yml, line 9-10 (link)

    P2 exhaustive disabled globally instead of per-site

    exhaustive catches non-exhaustive switch statements over enum-like types. Disabling it project-wide removes that safety net from the entire codebase. The existing outbound.go nolint comment pattern shows per-instance suppression is already in use. Consider suppressing only the specific sites that triggered the linter rather than turning them off globally.

  3. internal/server/biz/channel_endpoint_mapping_test.go, line 1092-1113 (link)

    P2 TestResolveEndpoints_MergesDefaultsAndUserOverrides exercises a path the API now actively rejects

    The test constructs a channel whose user endpoints include openai/chat_completions — a format that is also a default endpoint for TypeOpenai. SaveChannelEndpoints now returns an error for exactly this configuration, so this endpoint combination cannot be created through the API. It would be clearer to either add a comment explaining it's testing backward-compatibility for pre-existing DB rows, or replace it with a test that only uses non-conflicting user endpoint formats.

Reviews (1): Last reviewed commit: "opt: custom endpoint api format" | Re-trigger Greptile

Comment on lines 1196 to +1225
}

func mergeEndpoints(defaultEndpoints, userEndpoints []objects.ChannelEndpoint) []objects.ChannelEndpoint {
if len(defaultEndpoints) == 0 && len(userEndpoints) == 0 {
return nil
}

merged := make([]objects.ChannelEndpoint, 0, len(defaultEndpoints)+len(userEndpoints))

overrides := make(map[string]objects.ChannelEndpoint, len(userEndpoints))

for _, ep := range userEndpoints {
if ep.APIFormat == "" {
continue
}

overrides[ep.APIFormat] = ep
}

for _, ep := range defaultEndpoints {
if ep.APIFormat == "" {
continue
}

if override, ok := overrides[ep.APIFormat]; ok {
merged = append(merged, override)

delete(overrides, ep.APIFormat)

continue

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 All default endpoints share the primary outbound — implicit contract

Every API format in defaultEndpoints is unconditionally mapped to ch.Outbound (same pointer for every format). This works today because each channel type's primary transformer inspects the incoming request type to decide which provider endpoint to call—not the key it was stored under. However, this is a silent implicit contract. If a future default endpoint is added for a channel type whose primary transformer does not handle that format, the outbound will silently dispatch incorrect requests without any compile- or runtime error. A comment on this invariant, or an assertion in a test, would make the assumption explicit.

@looplj looplj merged commit a62dc22 into unstable Apr 30, 2026
9 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