add submodel.ai#1763
Conversation
WalkthroughAdds a new Submodel channel/API: constants (channel & API type, base URL), ChannelType→APIType mapping, relay adaptor registration, a new submodel adaptor + model list, default model ratios, and frontend channel option/behavior for type 52. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor Client
participant Relay as Relay Server
participant Adaptor as Submodel Adaptor
participant HTTP as channel.DoApiRequest
participant SubAPI as Submodel API
participant OAI as OpenAI Handlers
Client->>Relay: OpenAI-compatible request
Relay->>Adaptor: GetRequestURL(info)
Adaptor-->>Relay: https://llm.submodel.ai/...
Relay->>Adaptor: SetupRequestHeader(..., ApiKey)
Adaptor-->>Relay: Headers w/ Authorization Bearer
Relay->>Adaptor: DoRequest(body)
Adaptor->>HTTP: Perform HTTP request
HTTP->>SubAPI: POST
SubAPI-->>HTTP: HTTP response
HTTP-->>Adaptor: *http.Response
Adaptor->>Adaptor: DoResponse(resp)
alt streaming
Adaptor->>OAI: oaiStreamHandler(resp)
OAI-->>Client: SSE stream
else non-stream
Adaptor->>OAI: openaiHandler(resp)
OAI-->>Client: JSON response
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested reviewers
Poem
📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
✨ Finishing Touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (8)
web/src/constants/channel.constants.js (1)
138-142: Casing consistency for the new labelConsider "Submodel" (or provider-preferred branding) to keep naming consistent across UI and backend (backend uses "submodel"). Purely cosmetic.
- label: 'SubModel', + label: 'Submodel',relay/channel/submodel/constants.go (2)
3-14: Avoid FE/BE drift on model listsThis hardcoded list duplicates data also used in the web app (EditTagModal.js). Prefer one source of truth (e.g., expose via API or import a shared manifest) to prevent mismatch.
16-16: Use const for channel nameChannelName is immutable; declare it as const.
-var ChannelName = "submodel" +const ChannelName = "submodel"setting/ratio_setting/model_ratio.go (1)
227-238: defaultModelRatio is aligned with ModelList
Optional: future-proof Thinking variants by adding a wildcard key (e.g."Qwen/Qwen3-235B-A22B-Thinking-*": 0.6) indefaultModelRatioand normalizing names inGetModelRatioso any new date-suffix reuses the same ratio.web/src/pages/Channel/EditTagModal.js (2)
101-103: Consistency/readability: use multi-line array as in other casesMatch cases 2/5 formatting for easier diffs/maintenance.
- localModels = ['NousResearch/Hermes-4-405B-FP8', 'Qwen/Qwen3-235B-A22B-Thinking-2507', 'Qwen/Qwen3-Coder-480B-A35B-Instruct-FP8', 'Qwen/Qwen3-235B-A22B-Instruct-2507', 'zai-org/GLM-4.5-FP8', 'openai/gpt-oss-120b', 'deepseek-ai/DeepSeek-R1-0528', 'deepseek-ai/DeepSeek-R1', 'deepseek-ai/DeepSeek-V3-0324', 'deepseek-ai/DeepSeek-V3.1']; + localModels = [ + 'NousResearch/Hermes-4-405B-FP8', + 'Qwen/Qwen3-235B-A22B-Thinking-2507', + 'Qwen/Qwen3-Coder-480B-A35B-Instruct-FP8', + 'Qwen/Qwen3-235B-A22B-Instruct-2507', + 'zai-org/GLM-4.5-FP8', + 'openai/gpt-oss-120b', + 'deepseek-ai/DeepSeek-R1-0528', + 'deepseek-ai/DeepSeek-R1', + 'deepseek-ai/DeepSeek-V3-0324', + 'deepseek-ai/DeepSeek-V3.1', + ];
101-103: Remove unreachable ‘type’ branch or add a ‘type’ field
In EditTagModal.js, the switch inside handleInputChange(…, …) forname === 'type'(lines 101–103) never runs—no<Form>input binds tofield='type'. Either remove this dead code or introduce a Form.Input/Select withfield='type'.relay/channel/submodel/adaptor.go (2)
51-57: Unimplemented endpoints: consider minimal pass-through for embeddings/responsesIf submodel is OpenAI-compatible, you can mirror ConvertOpenAIRequest: validate and pass through Embeddings and Responses to reduce 501s. Otherwise, leave unimplemented.
func (a *Adaptor) ConvertRerankRequest(c *gin.Context, relayMode int, request dto.RerankRequest) (any, error) { return nil, errors.New("not implemented") } func (a *Adaptor) ConvertEmbeddingRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.EmbeddingRequest) (any, error) { - return nil, errors.New("not implemented") + if request.Model == "" { + return nil, errors.New("model is empty") + } + return request, nil } func (a *Adaptor) ConvertOpenAIResponsesRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.OpenAIResponsesRequest) (any, error) { - return nil, errors.New("not implemented") + if request.Model == "" { + return nil, errors.New("model is empty") + } + return request, nil }Also applies to: 59-61
38-42: Add X-API-Key fallback header
Consider also sendingX-API-Key: <key>alongsideAuthorization: Bearer <key>—it’s harmless if unused and covers providers expecting that scheme. For example:func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *relaycommon.RelayInfo) error { channel.SetupApiRequestHeader(info, c, req) req.Set("Authorization", "Bearer "+info.ApiKey) + req.Set("X-API-Key", info.ApiKey) // fallback for providers expecting this header return nil }Confirm with submodel.ai documentation for their required auth header.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (10)
common/api_type.go(1 hunks)constant/api_type.go(1 hunks)constant/channel.go(2 hunks)relay/channel/submodel/adaptor.go(1 hunks)relay/channel/submodel/constants.go(1 hunks)relay/relay_adaptor.go(2 hunks)setting/ratio_setting/model_ratio.go(1 hunks)web/src/constants/channel.constants.js(1 hunks)web/src/helpers/render.js(1 hunks)web/src/pages/Channel/EditTagModal.js(1 hunks)
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-08-05T17:14:17.246Z
Learnt from: neotf
PR: QuantumNous/new-api#1511
File: setting/ratio_setting/model_ratio.go:118-123
Timestamp: 2025-08-05T17:14:17.246Z
Learning: Claude models handle "-thinking" variants differently from Gemini models. For Claude models, only the base model (without "-thinking") gets an entry in defaultModelRatio map. The "-thinking" variants rely on the Claude relay handler stripping the suffix using strings.TrimSuffix(textRequest.Model, "-thinking") before looking up the ratio, so they automatically use the base model's ratio.
Applied to files:
setting/ratio_setting/model_ratio.go
📚 Learning: 2025-08-27T02:15:25.448Z
Learnt from: AAEE86
PR: QuantumNous/new-api#1658
File: web/src/components/table/channels/modals/EditChannelModal.jsx:555-569
Timestamp: 2025-08-27T02:15:25.448Z
Learning: In EditChannelModal.jsx, the applyModelMapping function transforms the models list by replacing original model names (mapping values) with display names (mapping keys). The database stores this transformed list containing mapped keys. On channel load, data.models contains these mapped display names, making the initialization filter if (data.models.includes(key)) correct.
Applied to files:
web/src/pages/Channel/EditTagModal.js
📚 Learning: 2025-08-27T02:15:25.448Z
Learnt from: AAEE86
PR: QuantumNous/new-api#1658
File: web/src/components/table/channels/modals/EditChannelModal.jsx:555-569
Timestamp: 2025-08-27T02:15:25.448Z
Learning: In EditChannelModal.jsx, the database stores mapped keys (display names) in the models field after applying model mapping transformations. When loading a channel, data.models contains the mapped keys, not the original model names. The filtering logic if (data.models.includes(key)) in the initialization is correct.
Applied to files:
web/src/pages/Channel/EditTagModal.js
🧬 Code graph analysis (3)
common/api_type.go (2)
constant/channel.go (1)
ChannelTypeSubmodel(52-52)constant/api_type.go (1)
APITypeSubmodel(34-34)
relay/relay_adaptor.go (3)
constant/api_type.go (1)
APITypeSubmodel(34-34)relay/channel/submodel/adaptor.go (1)
Adaptor(16-17)relay/channel/adapter.go (1)
Adaptor(13-29)
relay/channel/submodel/adaptor.go (11)
relay/common/relay_info.go (1)
RelayInfo(62-112)dto/claude.go (1)
ClaudeRequest(183-199)dto/audio.go (1)
AudioRequest(3-9)relay/common/relay_utils.go (1)
GetFullRequestURL(13-25)relay/channel/api_request.go (2)
SetupApiRequestHeader(23-35)DoApiRequest(37-58)dto/openai_request.go (2)
GeneralOpenAIRequest(21-65)OpenAIResponsesRequest(607-629)dto/rerank.go (1)
RerankRequest(3-11)dto/embedding.go (1)
EmbeddingRequest(14-25)types/error.go (1)
NewAPIError(75-81)relay/channel/openai/relay-openai.go (2)
OaiStreamHandler(109-183)OpenaiHandler(185-241)relay/channel/submodel/constants.go (2)
ModelList(3-14)ChannelName(16-16)
🔇 Additional comments (10)
web/src/helpers/render.js (1)
401-402: Explicit no-icon handling is fineAdding case 52 to return null mirrors 21 and 44. No issues.
relay/relay_adaptor.go (2)
37-37: Import looks correctPackage path and placement are consistent with other channel imports.
100-101: Adaptor registration addedAPITypeSubmodel → submodel.Adaptor wiring is correct.
common/api_type.go (1)
68-70: Channel→API mapping addedChannelTypeSubmodel correctly maps to APITypeSubmodel.
constant/channel.go (2)
52-52: Enum extension OKNew ChannelTypeSubmodel = 52 fits the sequence and sentinel usage.
110-110: Verify provider base URLConfirm https://llm.submodel.ai is the correct base (path/versioning handled by adaptor, e.g., /v1). If a suffix is required by the provider, add it here or in the adaptor builder.
constant/api_type.go (1)
34-35: API type added in the right placeInserted before APITypeDummy; no existing values shift except Dummy. Looks good.
relay/channel/submodel/adaptor.go (3)
34-36: URL construction LGTMUsing GetFullRequestURL keeps Cloudflare Gateway compatibility and honors per-channel base URLs.
67-74: Response handling LGTMDelegating to OpenAI stream/non-stream handlers is appropriate if upstream is OpenAI-compatible.
76-82: Model/channel metadata LGTMExposes backend ModelList and ChannelName as expected.
Summary by CodeRabbit
New Features
UI