Add developer-level model association rules#1672
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces 'Developer Rules', allowing administrators to configure reusable channel association rules at the model developer level. Models now inherit these rules by default, merging them with model-specific associations based on priority, with an option to explicitly disable inheritance. The implementation includes backend logic for inheritance and merging, frontend UI updates for managing these rules, and comprehensive tests. Feedback is provided regarding the potential for side effects due to shallow cloning of association conditions and performance concerns related to using JSON marshalling for cache signatures on the request hot path.
Greptile SummaryThis PR introduces developer-level model association rules: a new layer of routing configuration that is configured once per model developer and automatically inherited by all models from that developer, with per-model opt-out support.
Confidence Score: 3/5The developer-association save path drops The backend-inheritance logic, deep-copy, and FNV signature are well-written and covered by tests. However, the developer-mode save in frontend/src/features/models/components/models-association-dialog.tsx — specifically the Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[Incoming Request: model ID] --> B[EffectiveModelAssociations]
B --> C{DisableDeveloperInheritance?}
C -- Yes --> D[Model-level rules only]
C -- No --> E[developerAssociationsForDeveloper]
E --> F[inheritDeveloperAssociationsForModel\nfill ModelID from request model]
F --> G[mergeInheritedModelAssociations\npriority sort: model rules first at equal priority]
D --> G
G --> H[resolveAssociations\nFNV cache lookup / fill]
H --> I{Cache hit?}
I -- Yes --> J[Return cached candidates]
I -- No --> K[Match channels / tags]
K --> L[Store in associationCache]
L --> J
J --> M[filterResolvedCandidatesForRequest\napply When conditions]
M --> N[ChannelModelsCandidate list]
Reviews (4): Last reviewed commit: "fix: satisfy errcheck for association si..." | Re-trigger Greptile |
|
已处理本轮检视意见:
验证:
|
|
感谢 pr ,代码有点冲突了。 是否可以看下截图,大概是怎么个交互。 |
6e302e4 to
f4ac948
Compare
|
@looplj 已经解决冲突并补充了交互截图,整体交互如下。 开发者级模型渠道规则说明本次功能的目标是减少同一开发者下多个模型的重复渠道配置。原来每个模型都要单独配置自己的渠道规则;现在可以先在开发者层配置一组通用规则,下面的模型默认继承这些规则,同时仍然保留模型自己的单独配置能力。 主界面入口进入“模型”页面后,模型会按开发者分组展示。每个开发者分组的操作区新增了“开发者规则”入口,旁边的数字表示这个开发者当前配置了几条规则。 在截图中, 开发者规则配置开发者规则窗口用于配置同一开发者下所有模型共享的渠道选择规则。 这里的关键点是:开发者规则只选择“渠道”或“渠道标签”,不选择具体模型 ID。 例如配置一条“渠道标签”规则后,系统会在实际路由时为每个继承该规则的模型自动填入自己的模型 ID。也就是说,同一条开发者规则可以同时服务该开发者下的多个模型,而不会把某一个具体模型固定写死。 右侧“预览”区域会展示当前规则能匹配到的渠道,便于确认规则是否能命中预期渠道。 默认继承关系同一个
合并规则时遵循两个原则:
因此,开发者规则适合放通用渠道策略,模型规则适合放少数模型的特殊覆盖策略。 模型级配置窗口从某个模型行的操作菜单进入“管理关联”,会打开模型自己的关联配置窗口。 截图中的模型自己没有单独配置规则,但因为存在对应开发者规则,右侧预览仍然能看到继承来的渠道。这个预览展示的是模型实际会使用的有效规则,而不是只展示模型自身保存的规则。 不继承开发者配置模型关联窗口新增了“不继承开发者配置”开关。打开后,这个模型只使用自己单独配置的规则,开发者规则不会再应用到它。 截图中打开“不继承开发者配置”后,因为该模型本身没有配置独立规则,右侧预览变为空。 实际验证结果:
生效范围开发者规则继承后的有效规则已经统一应用到主要模型关联路径:
这保证了界面展示、预览结果和实际请求路由使用的是同一套规则语义。 兼容性该功能对老版本配置保持兼容:
推荐用法建议把稳定、通用的渠道策略放到开发者规则里,例如某个开发者统一走一组带标签的渠道。 当单个模型需要特殊处理时,再进入模型自己的“管理关联”窗口追加模型规则。如果这个模型完全不应该继承开发者规则,则打开“不继承开发者配置”。 |
| await updateModelSettings.mutateAsync({ | ||
| fallbackToChannelsOnModelNotFound: settings!.fallbackToChannelsOnModelNotFound, | ||
| queryAllChannelModels: settings!.queryAllChannelModels, | ||
| defaultModelAPIIncludeAll: settings!.defaultModelAPIIncludeAll, | ||
| autoReasoningEffort: settings!.autoReasoningEffort, | ||
| developerSettings: nextDeveloperSettings.sort((a, b) => a.developer.localeCompare(b.developer)), | ||
| }); |
There was a problem hiding this comment.
modelBlacklistRegex is missing from the developer-association save call. Because the GraphQL input field is nullable (String, not String!) and the backing Go struct uses a plain string (not *string), an absent field is unmarshalled as "", which overwrites any blacklist regex the user has set. Every time a developer rule is saved from this dialog, the blacklist is silently cleared.
| await updateModelSettings.mutateAsync({ | |
| fallbackToChannelsOnModelNotFound: settings!.fallbackToChannelsOnModelNotFound, | |
| queryAllChannelModels: settings!.queryAllChannelModels, | |
| defaultModelAPIIncludeAll: settings!.defaultModelAPIIncludeAll, | |
| autoReasoningEffort: settings!.autoReasoningEffort, | |
| developerSettings: nextDeveloperSettings.sort((a, b) => a.developer.localeCompare(b.developer)), | |
| }); | |
| await updateModelSettings.mutateAsync({ | |
| fallbackToChannelsOnModelNotFound: settings!.fallbackToChannelsOnModelNotFound, | |
| queryAllChannelModels: settings!.queryAllChannelModels, | |
| defaultModelAPIIncludeAll: settings!.defaultModelAPIIncludeAll, | |
| autoReasoningEffort: settings!.autoReasoningEffort, | |
| modelBlacklistRegex: settings!.modelBlacklistRegex, | |
| developerSettings: nextDeveloperSettings.sort((a, b) => a.developer.localeCompare(b.developer)), | |
| }); |




Closes #1671
Summary
Validation
PATH=/usr/local/go/bin:$PATH make generatePATH=/usr/local/go/bin:$PATH go test ./...cd frontend && ./node_modules/.bin/tsc --noEmit --pretty falsegit diff --check