☰
SuperPlane Selectable LLM Models:一套模型选择列表的架构设计与工程实现
2026/9/28 3:17:20 网站建设 项目流程

【免费下载链接】superplane

Open source factory for one-shot engineering

项目地址:https://gitcode.com/gh_mirrors/su/superplane
点击查看免费下载

本文对应仓库文档 docs/prd/selectable-llm-models.md(状态:Draft,面向产品与工程团队),并结合仓库源码展开实现级解读。

导读

SuperPlane 通过一套**单一的模型选择列表(Selectable LLM Models)**统一支撑所有模型选择器:无论是 "Create with an Agent" 的对话框、SuperPlane 节点(SuperPlane node)的模型字段,还是后续新增的各类 picker,都共享同一个数据源与同一个 RPC。本文从产品规则、领域模型、API 契约、前后端实现与底层 allowlist 体系四个层面,完整还原这套 "One list、One RPC" 架构的设计动机与代码落点,读完你可以:① 理解hosted::provider::model/byok::provider::model三段式 key 的生成与解析规则;② 掌握ListSelectableLLMModelsRPC 的完整调用链与排序语义;③ 看懂 Create with an Agent 切换模型时"保留会话、取消旧 run、重启新 run"的完整流程与 runner 映射。

设计目标:一个列表服务所有 picker

产品目标非常清晰:用户从一份权威列表里挑选模型,SuperPlane 托管的模型(hosted)与用户自带密钥选中的模型(BYOK)并列展示,且选择器明确展示"谁为这次运行买单"(who pays for the run)。文档为此锁定了六条不可动摇的决策(Locked decisions):

  1. 一个列表函数 + 一个 RPC:所有 picklist 都加载ListSelectableLLMModels,调用方自行过滤(filter),绝不允许另起第二个 catalog。
  2. Item 形状固定为 source、provider、model、key、label 五元组:key 是source::provider::model,label 是技术名provider/model;OpenRouter 的 label 直接保留 model id。
  3. Source 只区分"SuperPlane 托管"与"用户自带密钥",不叫 "Integration"。托管行 source 名为SuperPlane,BYOK 行 source 名为Your keys。
  4. source 不同时保留重复项:同一个 model id 可以同时以 hosted 行和 BYOK 行出现两次。
  5. Create with an Agent 是第一个 picker:标题栏展示Using {label};切换模型会重载 agent、保留会话,并按"每次执行 × 模型"记录花费(spend)。
  6. 后续 picker 复用同一列表:SuperPlane 节点字段、draft start、BYOK runner 字段都只对该列表做过滤,不新建 catalog。

这套"单一事实来源(single source of truth)"的设计,从源头避免了各 picker 之间的模型不一致问题。

领域模型:从三层 allowlist 到 picker

文档给出了清晰的层级模型:

Installation hosted allowlists Organization BYOK selected lists Optional factory subset └── ListSelectableLLMModels └── picker (Create with an Agent, SuperPlane node, later fields)

即:安装级托管 allowlist与组织级 BYOK 已选列表构成"父列表",可选的factory 子集在其上再做收窄,最终由ListSelectableLLMModels汇总输出给各 picker。

每个条目的五元组结构

字段含义示例
source{ id: hosted \| byok, name: SuperPlane \| Your keys }{id: "hosted", name: "SuperPlane"}
provider{ id, name },限于 Anthropic / OpenAI / OpenRouter{id: "anthropic", name: "Anthropic"}
model{ id, name }{id: "claude-sonnet-4-6", name: "claude-sonnet-4-6"}
keysource::provider::modelhosted::anthropic::claude-sonnet-4-6
labelprovider/model(OpenRouter 直接用 model id)anthropic/claude-sonnet-4-6

排序规则固定为:先按 label(不区分大小写),再按 source id,最后按 key,保证列表顺序在任何 picker 中都一致。

源码中的数据结构

该模型在 Go 侧由 pkg/models/selectable_llm.go 定义:

type SelectableLLMNamedID struct { ID string Name string } type SelectableLLMModel struct { Source SelectableLLMNamedID Provider SelectableLLMNamedID Model SelectableLLMNamedID Key string Label string }

source 与 provider 的展示名由SelectableLLMSourceName/SelectableLLMProviderName映射:hosted→SuperPlane、byok→Your keys;anthropic→Anthropic、openai→OpenAI、openrouter→OpenRouter(selectable_llm.go)。前端 TypeScript 侧的类型定义与之镜像对应(web_src/src/lib/selectableLLMModels.ts),并声明了常量SELECTABLE_LLM_SOURCE_HOSTED = "hosted"、SELECTABLE_LLM_SOURCE_BYOK = "byok"(selectableLLMModels.ts)。

三层"现成能力":不要重复发明

文档明确列出了项目已存在的构件,新功能只需复用,严禁再造:

  • Installation hosted allowlists:安装级托管模型白名单,已在 gate(拦截)SuperPlane 托管的模型。
  • Organization BYOK selected lists:组织管理员在 "Organization LLM Models" 页面维护"用户已选中的 BYOK 模型列表"。
  • Factory 可进一步对上述列表做子集(subset)。
  • ResolveSelectableLLMModels已按 provider 与 funding source 应用这些 allowlist(即"解析出某个 provider + 某种付费来源下当前可选的所有模型")。
  • Factory 花费记录在workspace_usage_events表;Create with an Agent 的花费属于 factory usage,而不是 canvas 的 Agent Tokens。
  • SuperPlane execute 已经在运行配置上盖章(stamp)hostedProvider、model与credentials.source=hosted。

另外有一条边界必须守住:不要把实时的 BYOK 候选目录(live candidate catalog)混入这张列表。BYOK 列表为空是正常状态,直到组织保存了自己的已选列表为止。

数据库层面的三层结构

三层 allowlist 在数据库里由两张表承载,迁移文件 db/migrations/20260828005803_add-llm-model-allowlists.up.sql 定义:

CREATE TABLE IF NOT EXISTS organization_byok_model_allowlists ( organization_id UUID NOT NULL, provider TEXT NOT NULL, allowed_models JSONB NOT NULL DEFAULT '[]'::jsonb, updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), PRIMARY KEY (organization_id, provider), CONSTRAINT organization_byok_model_allowlists_known_provider CHECK (provider IN ('anthropic', 'openai', 'openrouter')) ); CREATE TABLE IF NOT EXISTS factory_llm_model_allowlists ( factory_id UUID NOT NULL, provider TEXT NOT NULL, funding_source TEXT NOT NULL, allowed_models JSONB NOT NULL DEFAULT '[]'::jsonb, updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), PRIMARY KEY (factory_id, provider, funding_source), CONSTRAINT factory_llm_model_allowlists_known_provider CHECK (provider IN ('anthropic', 'openai', 'openrouter')), CONSTRAINT factory_llm_model_allowlists_funding CHECK (funding_source IN ('hosted', 'byok')) );

对应的 Go 模型分别是OrganizationBYOKModelAllowlist与FactoryLLMModelAllowlist(pkg/models/llm_model_allowlist.go)。安装级托管 allowlist 则存储在hosted_llm_providers表中(db/migrations/20260822125958_add-hosted-llm-billing.up.sql),每个 provider 一行,包含enabled、api_key、base_url、allowed_models等字段;花费表原名llm_usage_events,在迁移 20260901010326_rename-llm-usage-events-to-workspace-usage.up.sql 中更名为workspace_usage_events。

产品规则速查表

主题规则
Catalog一个 RPC。调用方传factory_id并过滤sources
Keyhosted::anthropic::claude-sonnet-4-6或byok::openai::gpt-5
Labelanthropic/claude-sonnet-4-6;OpenRouter 只用 model id
Source nameshosted → SuperPlane;byok → Your keys
Duplicatessource 不同时两行都保留
Factory subset可选;设置后隐藏 factory 不允许的模型
Create with an Agent标题栏Using {label},带波浪下划线
Reload保留 planning session,启动新机器,保留 transcript
Shared canvas不把选择写入 liveplanning-agent节点
Spend旧 execution 保留旧花费,新 execution 记录新花费

API 契约:ListSelectableLLMModelsRPC

ListSelectableLLMModels是组织(Organization)服务上的一个 RPC,proto 定义位于 protos/organizations.proto:

rpc ListSelectableLLMModels(ListSelectableLLMModelsRequest) returns (ListSelectableLLMModelsResponse) { option (google.api.http) = { get: "/api/v1/organizations/{id}/selectable-llm-models" }; // summary: "List selectable LLM models" // description: "Returns SuperPlane-hosted and selected BYOK models a picker may show" }

请求与响应消息(protos/organizations.proto):

message ListSelectableLLMModelsRequest { string id = 1; // 组织 id 或 slug string factory_id = 2; // 可选,用于 factory 子集 } message SelectableLLMNamedID { string id = 1; string name = 2; } message SelectableLLMModel { SelectableLLMNamedID source = 1; SelectableLLMNamedID provider = 2; SelectableLLMNamedID model = 3; string key = 4; string label = 5; } message ListSelectableLLMModelsResponse { repeated SelectableLLMModel models = 1; }

后端实现路径

请求先由OrganizationService.ListSelectableLLMModels接收(pkg/grpc/organization_service.go),转发到 actions 层实现 pkg/grpc/actions/organizations/list_selectable_llm_models.go。该实现的调用链如下:

  1. 通过models.FindOrganizationByIDOrSlug解析并校验组织;
  2. 解析可选参数factory_id(空串视为不传),若传入则用models.FindFactory校验该 factory 确实属于该组织,否则返回 NotFound;
  3. 调用enableAllConnectedBYOKModelsByDefault做 BYOK 默认值补全(见下文);
  4. 调用领域函数models.ListSelectableLLMModels(tx, orgID, factoryID);
  5. 序列化输出。

授权层面,该 RPC 需要组织级读取权限,测试用例 pkg/authorization/interceptor_test.go 的TestListSelectableLLMModelsUsesOrgRead验证了这一点。

领域函数的汇总与排序逻辑

核心函数位于 pkg/models/selectable_llm.go:对三个已知 provider(Anthropic、OpenAI、OpenRouter)各自遍历hosted与byok两种 funding source,用ResolveSelectableLLMModels解析出该 provider+source 下可用的模型 id,再构造条目,最后按label(小写)→ source id → key排序:

func ListSelectableLLMModels(tx *gorm.DB, orgID uuid.UUID, factoryID *uuid.UUID) ([]SelectableLLMModel, error) { out := make([]SelectableLLMModel, 0) for _, provider := range KnownHostedLLMProviders() { for _, source := range []string{UsageFundingSourceHosted, UsageFundingSourceBYOK} { ids, err := ResolveSelectableLLMModels(tx, orgID, factoryID, provider, source) // ... for _, id := range ids { out = append(out, newSelectableLLMModel(source, provider, id)) } } } sort.Slice(out, func(i, j int) bool { if out[i].Label != out[j].Label { return strings.ToLower(out[i].Label) < strings.ToLower(out[j].Label) } if out[i].Source.ID != out[j].Source.ID { return out[i].Source.ID < out[j].Source.ID } return out[i].Key < out[j].Key }) return out, nil }

三个已知 provider 由 pkg/models/hosted_llm_provider.go 的hostedLLMProviders常量列表给出,与数据库 CHECK 约束('anthropic', 'openai', 'openrouter')保持一致。

底层的 allowlist 解析:ResolveSelectableLLMModels

这是整条链路上最关键的函数(pkg/models/llm_model_allowlist.go)。它的语义是"返回某个 picker 和运行门禁(run gate)可用的模型列表",并且实现了文档所说的继承规则:

// ResolveSelectableLLMModels returns the models a picker and run gate may use. // Factory lists that are missing or empty inherit the parent list. func ResolveSelectableLLMModels(tx *gorm.DB, orgID uuid.UUID, factoryID *uuid.UUID, provider, fundingSource string) ([]string, error) { // ...normalize fundingSource... parent, err := parentSelectableLLMModels(tx, orgID, provider, source) if factoryID == nil || *factoryID == uuid.Nil { return parent, nil } subset, err := FindFactoryLLMModelAllowlist(tx, *factoryID, provider, source) if subset == nil || !hasAllowedModel(subset.AllowedModels) { return parent, nil // 工厂未设置或设置为空 → 继承父列表 } return IntersectModelIDs(parent, subset.AllowedModels), nil // 否则取交集 }

其中parentSelectableLLMModels(llm_model_allowlist.go)区分两种来源:hosted来源读取hosted_llm_providers中对应 provider 的AllowedModels(要求该 provider 开启且提供托管模型);byok来源读取organization_byok_model_allowlists。关键规则:工厂子集缺失或为空时继承父列表,只有明确设置了非空子集才做收窄,且子集与父列表取交集(IntersectModelIDs),这防止工厂列出父列表之外的模型。

key 的生成与解析

key 的拼装与解析分别在 pkg/models/selectable_llm.go:

func FormatSelectableLLMModelKey(source, provider, model string) string { return strings.TrimSpace(source) + hostedLLMModelKeySeparator + FormatHostedLLMModelKey(provider, model) } func ParseSelectableLLMModelKey(value string) (SelectableLLMModel, error) { trimmed := strings.TrimSpace(value) parts := strings.SplitN(trimmed, hostedLLMModelKeySeparator, 3) // "::" 分隔,最多切 3 段 if len(parts) != 3 { return SelectableLLMModel{}, ErrSelectableLLMModelIncomplete } // source 段必须归一化为 hosted / byok;provider/model 段做标准归一化 // ... }

注意SplitN(..., 3)的细节:model 段内部如果再出现::(例如带版本号后缀的模型 id),会完整保留在第三段中,不会被误拆——这也是把 key 设计为三段式而非扁平拼接的原因。前端 web_src/src/lib/selectableLLMModels.ts 的parseSelectableLLMModelKey同样用split("::")并parts.slice(2).join("::")还原 model 段,与后端语义保持一致。

校验与查错语义

  • ErrSelectableLLMModelIncomplete:key 段数不足或 source 不合法;
  • ErrSelectableLLMModelNotAllowed:模型不在当前组织/工厂允许范围内。

FindSelectableLLMModel(selectable_llm.go)先解析 key,再用ModelIsSelectable走一遍 allowlist 解析,双向校验后才返回条目;测试 pkg/models/selectable_llm_test.go 演示了同一组织下hosted::anthropic::claude-sonnet-4-6允许、hosted::anthropic::claude-opus-4-6被工厂子集拒绝的行为。

前端实现:一个 hook + 客户端过滤

前端统一通过 React Query hook 加载列表,位于 web_src/src/hooks/useSelectableLLMModels.ts:

export function useSelectableLLMModels( organizationId: string | undefined, options?: { factoryId?: string; sources?: SelectableLLMSourceID[]; enabled?: boolean }, ) { // queryKey: ["organizations", orgId, "selectable-llm-models", factoryId ?? "", sources ?? ""] // 调 organizationsListSelectableLlmModels,服务端只传 factoryId,sources 由客户端过滤 // staleTime: 30 * 1000 }

值得注意的工程细节:queryKey 同时包含factoryId与sources,因此切换工厂或切换来源过滤时会触发正确的新请求/缓存命中;服务端请求本身只传factoryId,sources过滤在客户端完成——这正对应文档"Callers filter"的决策:RPC 返回全集(hosted + byok),由各 picker 按需裁剪。sources过滤逻辑在selectableLLMModelsFromResponse(web_src/src/lib/selectableLLMModels.ts)中实现,过滤后再统一排序,保证无论请求来源如何,最终列表顺序都一致。

Create with an Agent:第一个 picker

Create with an Agent 是文档钦定的"第一个吃螃蟹的产品面"——它第一个加载这份列表、展示它并执行选择。

交互位置与默认值

picker 位于对话框标题栏,在机器状态(machine status)与 End session 按钮的左侧。默认展示值按以下优先级取第一个非空:

  1. 会话中已存储的 key(session stored key);
  2. 当前planning-agent节点的模型;
  3. 实例级 SuperPlane agent 模型。

选中后标题栏显示Using {label}(带波浪下划线,提示该值来自当前列表)。

切换模型时系统做什么

一次切换(pick)不会触发 End session。文档规定的行为是:

  • 在 planning session 上持久化selectable_model_key;
  • 取消当前 canvas run;
  • 在同一 session 上启动新的 canvas run;
  • 新 run 指向一个session 级、未发布的 canvas 版本(session-scoped unpublished canvas version);
  • 保持 live planning canvas 不变;
  • 用之前会话消息的**简短回放(rewind)**作为新 run 的初始 prompt。

"Shared canvas"规则进一步强调:不把选择写入 live 的planning-agent节点——选择是会话级的,不是画布级的。花费规则同样对称:旧 execution 保留旧花费,新 execution 记录新花费;被取消或失败但已消耗的 token 依然留在账本(ledger)上。

源码中的会话状态

planning session 模型上有SelectableModelKey字段(pkg/models/factory_planning_session.go),AttachAgentRun在挂载新 run 时将其持久化(factory_planning_session.go):

func (s *FactoryPlanningSession) AttachAgentRun(tx *gorm.DB, runID uuid.UUID, modelKey string) error { // guardOpen: 仅允许开放中的会话 s.CanvasRunID = &runID s.SelectableModelKey = strings.TrimSpace(modelKey) s.clearWait() s.clearSurvey() // 更新 canvas_run_id / selectable_model_key / wait_state / survey ... }

取消旧 run 通过cancelPlanningSessionRun→canvases.CancelRun完成(pkg/grpc/actions/factories/planning_session.go),序列化响应时selectable_model_key会随会话一并返回给前端(planning_session.go),保证刷新页面后默认值仍能从"会话存储的 key"取到。

把 key 映射到 runner

文档给出了固定的映射表:

key 的 source + provider目标 runner 组件
hosted(任意 provider)runnerSuperPlane
byok+anthropicrunnerClaudeCode
byok+openairunnerCodex
byok+openrouterrunnerOpenRouter

该映射在源码中由SelectableLLMRunnerComponent实现(pkg/models/selectable_llm.go),并有配套测试 pkg/models/selectable_llm_test.go:hosted::openai::gpt-5解析为runnerSuperPlane,byok::anthropic::claude-sonnet-4-6解析为runnerClaudeCode。

配套的两个辅助函数进一步说明 hosted 与 BYOK 运行方式的差异:

  • SelectableLLMRunnerModel(selectable_llm.go):hosted 场景直接回传完整 key(hosted::openai::gpt-5,SuperPlane 内部据此解码 provider/model),BYOK 场景只回传 model id。
  • SelectableLLMRunnerCredentials(selectable_llm.go):hosted 场景返回nil(密钥由 SuperPlane 侧持有,无需注入);BYOK 场景则查找该组织下 ready 状态的 BYOK 集成(FindReadyBYOKIntegration,按 provider 映射 app_name:claude/openai/openrouter),并返回{"source": "integration", "integration": {"name": <installation name>}},由集成凭据代跑。对应测试 selectable_llm_test.go 验证了凭据中携带的是集成的 installation name。

花费记录与credentials.source

"谁为 run 买单"的信息流落在 runner 的 usage 记录逻辑里。文档提到"SuperPlane execute 已盖章hostedProvider、model、credentials.source=hosted",这在 pkg/components/runner/agent_steps.go 的常量中可以看到三种凭据来源:secret(用户密钥)、integration(集成密钥)、hosted(SuperPlane 托管)。

usage 解析函数ParseRunnerLLMUsage(pkg/components/runner/agent_usage.go)从 run 结果中抽取 model、各类 token(含缓存读写与推理 token)与成本(cost_micros),并结合配置中的credentials.source推导出FundingSource(hosted 或 byok);providerForFinishedEvent(agent_usage.go)则把runnerClaudeCode.finished/runnerCodex.finished/runnerOpenRouter.finished分别映射到 anthropic / openai / openrouter,而runnerSuperPlane.finished从配置的hostedProvider字段读取真实 provider。最终花费记入workspace_usage_events——这正是文档强调"Create with an Agent 的花费是 factory usage"的落点。

时序上的硬约束

文档特别强调一个顺序约束:被取消的 run 必须先到达终态的 broker task 并持久化 usage,新 run 才能启动,否则旧 run 的 token 消耗可能丢失。这意味着"取消旧 run"与"启动新 run"之间存在明确的先后依赖,工程上通过 run 状态机与 usage 记录的幂等键(UsageIdempotencyKeyRunner,见 agent_usage.go)共同保障账本完整性。

SuperPlane 节点:第二个消费者

SuperPlane 节点(SuperPlane node)的 "SuperPlane Model" 字段是这张列表的第二个消费者,行为与 Create with an Agent 略有不同:

  • 加载同一份列表,但用sources: [hosted]过滤,只展示托管模型(BYOK 模型不出现在节点字段中);
  • 存储的是三段式 key(而非裸 model id);
  • Execute 阶段解析该 key,运行对应的 SuperPlane 托管模型。

在工厂(factory)的 line runner 模型解析中可以看到这一分支的实现(pkg/grpc/actions/factories/line_runner_models.go):当节点的组件是SuperPlaneRunnerComponent时,调用models.ListSelectableLLMModels并仅保留source.id == hosted的行作为可选模型 id;其他 runner 组件(Claude Code / Codex / OpenRouter)则按组件反推 provider,用ResolveSelectableLLMModels解析 BYOK 或 hosted 允许列表。这与"SuperPlane node 只跑托管模型"的产品语义严格一致。

BYOK 的默认值策略:连接即启用

为了让"空 BYOK 列表"不会让新用户无模型可用,后端在每次列出可选择模型时执行一个温和的默认值补全:enableAllConnectedBYOKModelsByDefault(pkg/grpc/actions/organizations/byok_llm_model_defaults.go)。

// 对每个已知 provider: // 1. 若组织已保存过该 provider 的列表(哪怕为空)→ 尊重组织选择,跳过; // 2. 否则查找 ready 的 BYOK 集成; // 3. 有集成则拉取候选模型并保存为已选列表(CreateOrganizationBYOKModelAllowlistIfAbsent)。

单 provider 的失败(比如集成不可达)只跳过该 provider 的默认补全,不影响整个列表返回,保证"列表始终可读"。这也解释了文档中"Empty BYOK is normal until the organization selected list exists"背后的产品逻辑:列表为空只是表象,一旦连接 provider,默认补全会让候选模型自动进入可选列表。

测试与验证

围绕这套架构仓库内有多层测试可直接作为行为规范:

  • pkg/models/selectable_llm_test.go:key 解析、runner 组件映射、凭据注入、FindSelectableLLMModel的允许/拒绝判定;
  • pkg/grpc/actions/organizations/list_selectable_llm_models_test.go:RPC 层面对正常请求、非法 org id、非法 factory id 的响应,以及"连接了 key 的模型默认启用"的行为;
  • pkg/models/llm_model_allowlist_test.go:ResolveSelectableLLMModels在"无工厂子集 / 有子集 / 子集含父列表外模型"三种场景下的解析结果;
  • pkg/authorization/interceptor_test.go:权限校验(组织级读权限);
  • web_src/src/lib/selectableLLMModels.spec.ts:前端响应序列化与 sources 过滤。

小结

SuperPlane 的 Selectable LLM Models 是一次典型的"单一事实来源"架构实践:产品层用六条锁定决策保证任何 picker 都消费同一份列表;数据层用"安装托管白名单 + 组织 BYOK 已选列表 + 工厂子集"三层 allowlist 构成可继承、可收窄的权威数据源;接口层用一个 RPC 返回 hosted 与 BYOK 合并的全集,过滤交给调用方;运行层通过三段式 key 完成从"用户选择"到"runner 组件 + 凭据来源 + 花费记账"的完整闭环。无论是 Create with an Agent 的会话级切换,还是 SuperPlane 节点的托管模型选择,最终都收敛到同一套模型与同一份账本上——这正是该设计在工程上可维护、在产品上可扩展的根本原因。

【免费下载链接】superplane

Open source factory for one-shot engineering

项目地址:https://gitcode.com/gh_mirrors/su/superplane
点击查看免费下载
上一篇:抖音视频批量下载终极指南:douyin-downloader让你5分钟搞定全部收藏
下一篇:PyVideoTrans视频翻译全流程指南:3步实现多语言视频转换

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询