Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions .downstream/gpt6-routing-and-prompts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# GPT-6 模型、Codex 路由与提示词

本轮以官方 v0.2.7 和下游生产提交 `300bd6dc5c40191d935cc08db910942f996bb26f` 为基线。宿主目标版本为 `0.2.7-codexrip.2`,七个内置插件为 `0.2.8`;新版插件要求宿主至少为该下游版本。插件固定版本意图继续保留,旧提示词插件不能解释新规则时明确报告不可用。

## 模型同步与容量

账号模型管理的“同步最新支持模型”补齐内置列表;“同步上游支持的模型”才请求账号目录并保存快照。两者保留人工模型和映射。

同步返回的 `model_list_source` 单独标明型号名单来自实时上游还是配置回退,与能力补充来源分开;只有实时目录可用来确认账号当时公布了哪些型号。

| 接入方式 | GPT-6 Sol / Luna 上下文 | 输出与推理 |
| --- | --- | --- |
| 官方 API | 1,050,000 | 最大输出 128,000;none、low、medium、high、xhigh、max |
| Codex 打包参考 | 默认 272,000,扩展最大 872,000 | 默认 medium;Sol 支持客户端 Ultra 委派,Luna 最高 Max |
| 账号原始目录 | 按实际声明及既有覆盖优先级 | 实时能力优先于旧快照和默认值 |

API Key 使用完整 Responses。OAuth 目录保留实际 Lite 与上下文字段,回退值标记为“Codex 参考”。Ultra 不作为原生 API reasoning effort 发送。Cindy 库存不因 OpenAI 发布新模型而扩大。

两款型号有独立价卡及严格大于 272,000 输入的长请求倍率,保留人工定价。未知 GPT-6 名称不套 Astra 身份或价格。固定来源与指令摘要见 [官方提取记录](../backend/internal/pkg/openai/gpt6_codex_reference.json)。

## Cookie 路由状态与指纹

账号的 Cookie 候选、模型验证结果和原生 turn-state 各自独立。采集取得完整成功响应和允许的 Cookie 后生成候选;业务出口完整完成且原始响应模型匹配,才能取得该模型的限时资格。292/312 仅是观测长度,不代表模型质量。

- 恢复 Cookie 只使用 `__cflb` 和 `__oailb`。基础设施 Cookie 使用官方允许集合,登录 Cookie 不进入共享传输状态。宿主处理 Cookie;普通插件头修改继续禁止 Cookie。
- 同一 Cookie 值不延长首次采集时间,删除有持久标记;资格绑定主体、profile、路由、Cookie 版本及实际连接。本地上限为 120 秒,更早的上游到期时间优先。
- 每账号与型号使用固定候选和当前槽;后台清除到期 Cookie 原值,保留摘要、删除标记和诊断账本。同值且未缩短期限的响应不会反复写入新状态。清除任务在采集开关关闭时仍执行,整个插件停用期间不承诺后台清除。
- 未知轮换出口通过实际连接租约复用,连接丢失后不能自动在新连接上继续发送旧资格。客户端 WS 在只有 HTTP 资格时使用已有 HTTP bridge,页面明确显示 WS 入站、HTTP 出站;原生 WS 验证只证明该次连接。
- 已登记且有请求需求的账号在到期前 20 秒续获。每周期最多一次采集与一次复验,连续两个失败周期后持久停止;闲置账号不持续探测。`fail_closed` 保留。
- 旧 292 不转换为 Cookie 验证成功,旧停止状态保留。原先默认的 Astra/5.6-Sol 名单只在旧配置版本迁移时追加 6-Sol/6-Luna;自定义名单保留,之后显式修改不自动补回。
- 原生 turn-state 在同一轮固定首次值,下一轮或换账号清除。

账号详情右栏的“Codex 出站指纹”读取配置和已经记录的出站事实,刷新不会发模型请求。页面展示 profile 来源、UA、版本、身份摘要及一致性、传输、Cookie 名称和模型验证结果。新建账号采用官方 Windows CLI 0.156.0 的应用层参照;已有配置保留,可显式选择新版参照,设备种子和手动 UA 覆盖不被重置。

本机官方 CLI 通过隔离合成认证、本地可信证书和受控 HTTP/WS 端点取得参照。生产 Go TLS 的实际值与参照分开展示;未捕获生产 ClientHello 时不宣称 JA3/JA4 相同。原始 Cookie、STATE、Bearer 和代理密码不进入这些页面或通用诊断。

管理员接口位于 `/api/v1/admin/accounts/:id/codex-fingerprint`,profile 选择使用其 `/profile` 子路径和账号版本 CAS。`/codex-routing/validate` 是独立、不登记自动续期的限额验证操作,固定支持三款 GPT-6,一次 UUID 绑定一个账号和最多七个出站阶段;重复操作不能重新花费相同阶段。

## 提示词规则与账号选择

系统提示词页面的“应用规则”复用原有模板及不可变版本。规则稳定排序,可按请求模型或最终上游模型精确匹配;空模型集合表示全部。

| 位置 | 含义 |
| --- | --- |
| control_prepend / control_append | 控制指令区前后 |
| conversation_head / conversation_tail | 本次最终发送的消息数组头尾 |

发送方式与位置分开:`native_control` 使用目标原生控制字段,仅用于控制区;`system` 和 `developer` 要求最终发包保留对应显式角色。Codex OAuth 不接受严格 system 的组合会明确返回不支持,原生模式标明实际使用 `instructions`。续接数组位置不包含 `previous_response_id` 指向的隐藏历史。

账号详情和账号批量操作提供 `inherit / off / custom`:继承默认规则、关闭、或用指定规则替换默认集合。默认集合留空、指定账号选择 custom,即可只对指定账号开启。账号仅存规则引用,不复制正文;shadow 使用自身绑定。

预览选择账号、模型、协议、HTTP/WS和样例请求,显示网关基础模板、客户指令、站点规则及最终结构。它不访问上游。实际发送前执行同一载体校验,覆盖 Responses、Chat、转换链路和 WS passthrough;重试及换账号精确撤回本站插入,不按相同正文猜测客户内容。规则变化隔离缓存,保留续接标识和已有回显设置。

迁移 `253_prompt_rule_policies.sql` 为旧活动版本建立默认规则,保留原总开关、账号范围与 256 KiB 合法编译上限。配置和账号绑定均使用版本比较,冲突需要重新读取后处理;被账号引用的规则不能直接删除。

## 界面与验证边界

公共插件 context 的同值刷新不再重建 iframe。主题按 URL/变量差异更新,新 CSS 及字体准备完成再替换旧外观;加载失败保留最后成功版本。内容测量按帧合并,使用自然尺寸而非随滚动变化的 viewport 坐标。

离线真实 Chromium 验证了左侧导航、分类栏和右侧账号详情:两次正常 15 秒刷新、未保存输入与滚动保留、主题失败恢复、真实字体加载、菜单伸缩及跨 lg 布局。跨 lg 后表格切换为文档流时,其原容器滚动值会归零,这是原有响应式行为,不属于本轮刷新验收结果。

本地测试只覆盖实际修改风险,正常 PR 必需检查继续执行。真实验证须另用本次固定账本:一个账号,Astra/Sol/Luna 各最多采集与复验一次,另最多一次 WS;每次 25 秒、128 KiB 读取上限,失败不追加请求。真实结果、生产镜像及部署指针由工作区接手手册和本次交付证据维护;本文件不把离线成功写成线上模型质量证明。
2 changes: 1 addition & 1 deletion .github/scripts/test_first_party_plugins.sh
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ go build -trimpath -o "$plugin_build_dir/package-plugin" ./cmd/package-plugin
-bundle-source ../plugins/bundle.source.json \
-binary-dir "$plugin_build_dir/binaries" \
-platform linux-amd64 \
-tested-host-version 0.2.7 \
-tested-host-version 0.2.7-codexrip.2 \
-output "$plugin_build_dir/bundle" \
-signing-key-file "$plugin_build_dir/publisher.key" \
-key-id codexrip-plugins-test
Expand Down
9 changes: 8 additions & 1 deletion .github/workflows/downstream-verify.yml
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,8 @@ jobs:
src/components/admin/account/__tests__/AccountTestModal.spec.ts \
src/components/admin/account/__tests__/AccountTestModal.displayContract.spec.ts \
src/components/admin/account/__tests__/AccountTestModal.policyPlan.spec.ts \
src/components/admin/account/__tests__/CodexFingerprintPanel.spec.ts \
src/components/keys/__tests__/UseKeyModal.spec.ts \
src/views/admin/__tests__/SettingsView.spec.ts \
src/components/common/__tests__/VersionBadge.downstream.spec.ts \
src/api/__tests__/downstreamStatus.spec.ts \
Expand All @@ -169,6 +171,8 @@ jobs:
src/components/plugins/__tests__/pluginViewDispatch.spec.ts \
src/components/plugins/__tests__/resourceAvailability.spec.ts \
src/components/plugins/__tests__/PluginFrame.precondition.spec.ts \
src/components/plugins/__tests__/theme.spec.ts \
src/components/plugins/__tests__/presentationStability.spec.ts \
src/components/plugins/__tests__/resourceClient.spec.ts \
src/components/plugins/__tests__/localResources.spec.ts \
src/features/image-studio/history.legacy.spec.ts \
Expand Down Expand Up @@ -205,6 +209,7 @@ jobs:
src/composables/__tests__/useModelWhitelist.spec.ts \
src/components/account/__tests__/BulkEditAccountModal.spec.ts \
../plugins/prompt-skills/ui/src/__tests__/App.spec.ts \
../plugins/prompt-skills/ui/src/__tests__/PromptRules.spec.ts \
../plugins/prompt-skills/ui/src/__tests__/locales.spec.ts \
src/views/admin/__tests__/AccountsView.console.spec.ts \
src/views/admin/__tests__/AccountsView.priorityColumn.spec.ts \
Expand Down Expand Up @@ -249,7 +254,9 @@ jobs:
- name: Resolve candidate compatibility version
id: version
shell: bash
run: printf 'value=%s-candidate\n' "$(tr -d '\r\n' < backend/cmd/server/VERSION)" >> "$GITHUB_OUTPUT"
# A codexrip prerelease that sorts above every numbered codexrip.N of this
# base, so bundled plugins requiring the newest downstream host verify.
run: printf 'value=%s-codexrip.candidate\n' "$(tr -d '\r\n' < backend/cmd/server/VERSION)" >> "$GITHUB_OUTPUT"
- uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
- name: Build candidate
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
Expand Down
2 changes: 1 addition & 1 deletion backend/cmd/server/wire_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

67 changes: 67 additions & 0 deletions backend/internal/handler/admin/account_codex_routing.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package admin

import (
"strconv"

"github.com/Wei-Shaw/sub2api/internal/pkg/response"
"github.com/Wei-Shaw/sub2api/internal/service"
"github.com/gin-gonic/gin"
)

func (h *AccountHandler) CodexFingerprint(c *gin.Context) {
c.Header("Cache-Control", "no-store")
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil || id <= 0 {
response.BadRequest(c, "Invalid account identifier")
return
}
if h.codexTicketGateway == nil {
response.Error(c, 503, "Codex runtime unavailable")
return
}
view, err := h.codexTicketGateway.CodexFingerprint(c.Request.Context(), id)
if err != nil {
response.Error(c, 503, "Codex fingerprint unavailable")
return
}
response.Success(c, view)
}

func (h *AccountHandler) ValidateCodexRouting(c *gin.Context) {
c.Header("Cache-Control", "no-store")
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
var request service.CodexRoutingValidationRequest
if err != nil || id <= 0 || c.ShouldBindJSON(&request) != nil {
response.BadRequest(c, "Invalid Codex validation request")
return
}
if h.codexTicketGateway == nil {
response.Error(c, 503, "Codex runtime unavailable")
return
}
result, err := h.codexTicketGateway.ValidateCodexRouting(c.Request.Context(), id, request)
if err != nil {
response.Error(c, 409, "Codex validation unavailable or budget already spent")
return
}
response.Success(c, result)
}

func (h *AccountHandler) SelectCodexProfile(c *gin.Context) {
c.Header("Cache-Control", "no-store")
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
var choice service.CodexProfileSelection
if err != nil || id <= 0 || c.ShouldBindJSON(&choice) != nil {
response.BadRequest(c, "Invalid profile selection")
return
}
if h.codexTicketGateway == nil {
response.Error(c, 503, "Codex runtime unavailable")
return
}
if err := h.codexTicketGateway.SelectCodexProfile(c.Request.Context(), id, choice); err != nil {
response.Error(c, 409, "Account identity changed or profile unavailable")
return
}
response.Success(c, gin.H{"applied": true})
}
4 changes: 4 additions & 0 deletions backend/internal/handler/admin/system_prompt_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,10 @@ func remoteSkillVersionDetailResponse(detail service.RemoteSkillBundleVersionDet

func writeBusinessSystemPromptError(c *gin.Context, err error) {
switch {
case errors.Is(err, service.ErrPromptDeliveryUnsupported):
response.ErrorWithDetails(c, http.StatusUnprocessableEntity, "The selected prompt delivery or position is unsupported by this destination", "prompt_delivery_unsupported", nil)
case errors.Is(err, service.ErrPromptRuleReferenced):
response.ErrorWithDetails(c, http.StatusConflict, "An account still references this rule", "prompt_rule_referenced", nil)
case errors.Is(err, service.ErrBusinessSystemPromptRevisionConflict):
response.ErrorWithDetails(c, http.StatusConflict, "system_prompt_revision_conflict", "system_prompt_revision_conflict", nil)
case errors.Is(err, service.ErrBusinessSystemPromptUnavailable):
Expand Down
15 changes: 11 additions & 4 deletions backend/internal/handler/admin/system_prompt_handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,10 +148,13 @@ func (*serviceTestRemoteSkillSource) Build(context.Context, service.RemoteSkillP
func TestWriteBusinessSystemPromptErrorUsesStableProtocolCodes(t *testing.T) {
gin.SetMode(gin.TestMode)
for name, testCase := range map[string]struct {
err error
wantStatus int
wantReason string
err error
wantStatus int
wantReason string
wantMessage string
}{
"unsupported rule delivery": {err: service.ErrPromptDeliveryUnsupported, wantStatus: http.StatusUnprocessableEntity, wantReason: "prompt_delivery_unsupported", wantMessage: "The selected prompt delivery or position is unsupported by this destination"},
"referenced rule": {err: service.ErrPromptRuleReferenced, wantStatus: http.StatusConflict, wantReason: "prompt_rule_referenced", wantMessage: "An account still references this rule"},
"revision conflict": {
err: service.ErrBusinessSystemPromptRevisionConflict, wantStatus: http.StatusConflict,
wantReason: "system_prompt_revision_conflict",
Expand Down Expand Up @@ -184,7 +187,11 @@ func TestWriteBusinessSystemPromptErrorUsesStableProtocolCodes(t *testing.T) {
writeBusinessSystemPromptError(ctx, testCase.err)
require.Equal(t, testCase.wantStatus, recorder.Code)
require.Equal(t, testCase.wantReason, gjson.Get(recorder.Body.String(), "reason").String())
require.Equal(t, testCase.wantReason, gjson.Get(recorder.Body.String(), "message").String())
message := testCase.wantMessage
if message == "" {
message = testCase.wantReason
}
require.Equal(t, message, gjson.Get(recorder.Body.String(), "message").String())
})
}
}
Expand Down
95 changes: 95 additions & 0 deletions backend/internal/handler/admin/system_prompt_rules_handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package admin

import (
"strconv"

"github.com/Wei-Shaw/sub2api/internal/pkg/response"
"github.com/Wei-Shaw/sub2api/internal/server/middleware"
"github.com/Wei-Shaw/sub2api/internal/service"
extensionv1 "github.com/Wei-Shaw/sub2api/pkg/extensionapi/v1"
"github.com/gin-gonic/gin"
)

func (h *SystemPromptHandler) Rules(c *gin.Context) {
state, err := h.service.PromptRulesState()
if err != nil {
writeBusinessSystemPromptError(c, err)
return
}
response.Success(c, state)
}

func (h *SystemPromptHandler) UpdateRules(c *gin.Context) {
var request struct {
ExpectedRevision int64 `json:"expected_revision" binding:"required"`
Policy extensionv1.PromptRulePolicy `json:"policy" binding:"required"`
}
if err := c.ShouldBindJSON(&request); err != nil {
response.BadRequest(c, "Invalid prompt rule policy")
return
}
actor, ok := h.actorID(c)
if !ok {
return
}
state, err := h.service.UpdatePromptRules(c.Request.Context(), request.Policy, request.ExpectedRevision, actor)
if err != nil {
writeBusinessSystemPromptError(c, err)
return
}
middleware.SetAuditExtra(c, map[string]any{"revision": state.Revision, "rule_count": len(state.Policy.Rules), "result": "rules_updated"})
response.Success(c, state)
}

func (h *SystemPromptHandler) AccountBindings(c *gin.Context) {
var request struct {
AccountIDs []int64 `json:"account_ids" binding:"required"`
}
if err := c.ShouldBindJSON(&request); err != nil {
response.BadRequest(c, "Invalid account selection")
return
}
views, err := h.service.PromptAccountBindings(c.Request.Context(), request.AccountIDs)
if err != nil {
writeBusinessSystemPromptError(c, err)
return
}
response.Success(c, views)
}

func (h *SystemPromptHandler) UpdateAccountBindings(c *gin.Context) {
var request struct {
ExpectedRevision int64 `json:"expected_revision" binding:"required"`
Updates []service.PromptBindingUpdate `json:"updates" binding:"required"`
}
if err := c.ShouldBindJSON(&request); err != nil {
response.BadRequest(c, "Invalid account prompt binding")
return
}
results, err := h.service.UpdatePromptAccountBindings(c.Request.Context(), request.Updates, request.ExpectedRevision)
if err != nil {
writeBusinessSystemPromptError(c, err)
return
}
middleware.SetAuditExtra(c, map[string]any{"account_count": len(results), "result": "bindings_updated"})
response.Success(c, results)
}

func (h *SystemPromptHandler) PreviewRules(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("account_id"), 10, 64)
if err != nil || id < 1 {
response.BadRequest(c, "Invalid account")
return
}
var request service.PromptRulesPreviewRequest
if err := c.ShouldBindJSON(&request); err != nil {
response.BadRequest(c, "Invalid prompt preview")
return
}
result, err := h.service.PreviewPromptRules(c.Request.Context(), id, request)
if err != nil {
writeBusinessSystemPromptError(c, err)
return
}
response.Success(c, result)
}
5 changes: 4 additions & 1 deletion backend/internal/handler/setting_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,10 @@ func (h *SettingHandler) GetPublicPluginThemeAsset(c *gin.Context) {
}
c.Header("Cache-Control", "no-store")
c.Header("X-Content-Type-Options", "nosniff")
c.Header("Cross-Origin-Resource-Policy", "same-origin")
// Only manifest-declared public appearance assets reach this handler.
// Opaque sandboxed plugin frames need CORS for stylesheets and font loads.
c.Header("Cross-Origin-Resource-Policy", "cross-origin")
c.Header("Access-Control-Allow-Origin", "*")
c.Data(http.StatusOK, contentType, data)
}

Expand Down
Loading
Loading