From 5c360113b2fad2a79c598bb6207f1b52c8bb2f66 Mon Sep 17 00:00:00 2001 From: Codex Date: Sun, 4 Oct 2026 12:11:23 +0800 Subject: [PATCH 1/4] chore(reasoning): delete the dated reasoning fidelity diagnostic Owner decision of 2026-10-04, like the live recovery diagnostic deleted with #235. The diagnostic was the fork side of one sealed study (run reasoning-fidelity-20260907): four test files of internal/service under the reasoning_fidelity build tag (the live test, the control test, the response parser with its fixtures, and the support file with the frozen gateway helper). No CI step compiled them since #235 removed the only command that built with that tag. Nothing outside the four files used what they declared, every production function they called has other callers, and no file carries the build tag now. The continuation-repair live test keeps its refusal to start while SUB2API_REASONING_FIDELITY_LIVE is set; it belongs to a sealed study of its own and is not changed. .downstream/upstream-review-v0.2.13.md says what was deleted and when, and states that the vet run and the two offline tests that covered only those files went with them. The history documents of earlier versions and the pinned upstream-risk.json still list the four paths. Checked: go build ./..., go vet -tags unit ./internal/service, test_upstream_automation_contract.py (16 tests), git diff --check, and a search of the tree for the build tag and the deleted names. Co-Authored-By: Claude Opus 5.5 --- .downstream/upstream-review-v0.2.13.md | 7 +- .../reasoning_fidelity_control_test.go | 246 ----- .../service/reasoning_fidelity_live_test.go | 830 ---------------- .../service/reasoning_fidelity_parse_test.go | 923 ------------------ .../reasoning_fidelity_support_test.go | 125 --- 5 files changed, 4 insertions(+), 2127 deletions(-) delete mode 100644 backend/internal/service/reasoning_fidelity_control_test.go delete mode 100644 backend/internal/service/reasoning_fidelity_live_test.go delete mode 100644 backend/internal/service/reasoning_fidelity_parse_test.go delete mode 100644 backend/internal/service/reasoning_fidelity_support_test.go diff --git a/.downstream/upstream-review-v0.2.13.md b/.downstream/upstream-review-v0.2.13.md index f80b9a6c560e..adebbf00109c 100644 --- a/.downstream/upstream-review-v0.2.13.md +++ b/.downstream/upstream-review-v0.2.13.md @@ -45,7 +45,8 @@ Every official commit is adopted with its official behaviour. Where upstream and - The account create, edit and bulk edit dialogs no longer show the two options, and nothing reads, writes or validates them. Deleted: `OpenAIReasoningPolicyFields.vue`, `utils/openaiReasoningPolicy.ts`, their specs and locale keys; the two `extra` key constants with their getters; the validation (`OPENAI_REASONING_POLICY_INVALID`) and the preservation of omitted values in the admin service; the handler checks, the import merge, the bulk-edit preflight and the scheduler cache projection. Migration 267 removes the stored keys (see Migrations). A stored per-account value is not carried over: an account that had recovery switched off follows the global switch after the upgrade. - The rejected-cipher memory runs on one long-lived Redis client of its own, built with the gateway cache from the shared options: every read and write ends at the deadline of the operation's 100 ms budget, a refused dial fails the operation at once, and application cleanup closes the client before the shared one. - The stop reason `policy_unavailable` is gone on both sides: nothing produces it since recovery became a direct call, the continuation diagnostic no longer accepts it, and the Ops error detail has no text for it. The text for `disabled` there names the switch, the account type and the request path instead of a per-account scope. - - The dated live recovery diagnostic is deleted (owner decision of 2026-10-04). It was the fork side of one sealed study of Chat reasoning replay and signature recovery (run `reasoning-replay-recovery-20260907`): a broker outside this repository ran one pinned binary once and passed only when the Chat replay cases completed, and those cases no longer exist. Deleted here: the live test, its offline control test and its support file, and the second command of `.github/scripts/test_reasoning_recovery.sh`. The broker scripts and their command entry outside this repository are deleted with it; the study's ledger on the server is not touched. The frozen gateway helper it shared with the reasoning fidelity harness (`ReasoningFidelityGatewayForTest`) now loads the recovery switch from the same frozen settings snapshot: a missing row keeps the default, a stored off stays off, an invalid row fails the bootstrap. The fidelity harness itself is unchanged. The deleted command was the only CI step that compiled files with the `reasoning_fidelity` build tag, so CI no longer compiles that harness. + - The dated live recovery diagnostic is deleted (owner decision of 2026-10-04). It was the fork side of one sealed study of Chat reasoning replay and signature recovery (run `reasoning-replay-recovery-20260907`): a broker outside this repository ran one pinned binary once and passed only when the Chat replay cases completed, and those cases no longer exist. Deleted here: the live test, its offline control test and its support file, and the second command of `.github/scripts/test_reasoning_recovery.sh`. The broker scripts and their command entry outside this repository are deleted with it; the study's ledger on the server is not touched. + - The reasoning fidelity diagnostic of the same date is deleted as well (owner decision of 2026-10-04), after `v0.2.13-codexrip.1`, whose tree still holds it. It was the fork side of a second sealed study (run `reasoning-fidelity-20260907`), driven by a broker outside this repository: four test files of `service` under the `reasoning_fidelity` build tag, namely the live test, its two offline tests (the control test, and the response parser with its fixtures) and the support file with the frozen gateway helper the recovery diagnostic had shared. For this release that helper was made to load the recovery switch from its frozen settings snapshot, and once the second command of the gate was gone no CI step compiled the four files. Nothing outside them used what they declared, and no file carries the build tag now. The broker scripts outside this repository are deleted with it; the study's run directory and ledger on the server are not touched. The continuation-repair live test still refuses to start while `SUB2API_REASONING_FIDELITY_LIVE` is set; it belongs to a sealed study of its own and is unchanged. - Console theme: the page joins the selectors of the 系统提示词 rules through its own static `data-ui="reasoning-recovery"` hook (`pages/settings-ops-public.css`, and the narrow-screen heading rule of `pages/user.css`); it has no rule of its own. - **Console theme for the new upstream UI.** Behaviour is upstream's; only styling was added under `frontend/src/styles/console`: the eleventh platform segment of the account dialog takes the `sky` identity glyph (`controls.css` 7.1a); the priority control sits as the value of the stacked priority line with label-toned steppers (`pages/accounts.css` 5, reached through the component's own static `data-testid` attributes); the mode toggle of `RechargeBonusTierEditor` carries the static `data-ui="seg-box"` hook. No `:has()`, no runtime-bound hook. - One existing rule also reaches the new UI without a change: the `RechargeBonusTierEditor` card has the same header and `p-4` body as the payment provider card beside it, so the positional body rule of `pages/settings-ops-public.css` 2.2 gives it the settings card padding. The comment of that rule now names both cards. @@ -140,8 +141,8 @@ Every official commit is adopted with its official behaviour. Where upstream and - **Frontend.** `vue-tsc --noEmit` passes. ESLint passes on the 48 changed frontend files. Vitest (`--no-cache`) passes for 13 files with 279 tests: the eleven specs the follow-ups changed (`BulkEditAccountModal`, `CreateAccountModal`, `EditAccountModal`, `AccountActionMenu.spark_shadow`, `AccountBulkActionsBar`, `AppSidebar`, `AppSidebar.simple-mode`, `useModelContextCapacities`, `AccountsView.selectionScope`, `ReasoningRecoveryView`, `groupsProfitControl`), locale key completeness, and `CodexRuntime.native`, which mounts the continuation diagnostics. These ran against a local dependency tree that holds axios 1.18.1 and dompurify 3.3.1, while the lockfile pins 1.20.0 and 3.4.14. - **推理恢复 on both sides.** The page's client and its spec use `GET` and `PUT` on `/admin/reasoning-recovery` under the `/api/v1` base with `{"enabled": }` and read the answer through the envelope the API client unwraps. The handler test pins the path `/api/v1/admin/reasoning-recovery`, the envelope `{"code":0,"message":"success","data":{"enabled": …}}` and the 400 for any other body; the route test pins administrator authentication without step-up, and the page uses none. - **Workflow references.** Every spec and script `.github/workflows/downstream-verify.yml` names exists. Its `Reasoning recovery contracts` step runs the renamed gate script, and its vitest list names `ReasoningRecoveryView.spec.ts` in place of the two deleted reasoning specs. The name filters of its Go steps select what they selected before, less the deleted tests (the retired settings test of the refusal runtime and the three scheduler cache projection tests of the per-account options); the two renamed Alpha Search tests still match. The critical list of the root `Makefile` no longer names the Cindy spec that #206 deleted. `test_upstream_automation_contract.py` (16 tests), `test_upstream_risk.py` (4) and `test_release_preflight.py` (12) pass. -- `.downstream/upstream-risk.json` is unchanged. It is computed from the two upstream tags and the downstream commit of the integration (`291d25e7f`: 157 upstream paths, 65 overlapping, 31 critical, 1420 downstream paths), so it also lists files the follow-ups delete. The gate that recomputes it (`.github/workflows/upstream-risk-gate.yml`) runs for pull requests from `sync/upstream-*` branches and on a manual dispatch for a pull request number. By reading, it recomputes from the tags and the downstream commit the manifest records, not from the head of the pull request, so a run dispatched for the follow-ups reproduces this manifest, deleted files included, and then requires the `upstream-reviewed` label because the class is `review_required`. -- **Changed after the runs above.** Five changes followed the runs above: the comment headers of 266 and 267 now cite the previous release (`v0.2.11-codexrip.9`) and its line numbers, with no statement changed, so the migration tests applied the same SQL; `TestOpenAIReasoningRecoveryChatToolRoundKeepsNoReasoningState` lost its last assertion, a search of the gin context for a key only the deleted replay set; the zh label of the recovery line in the Ops continuation diagnostics is 推理恢复, the name the reason text of the same panel already used; the live recovery diagnostic and the second command of the gate were deleted; and `ReasoningFidelityGatewayForTest` loads the recovery switch from its frozen snapshot. The zh texts of the 推理恢复 page are the ones the owner approved in the preview of 2026-10-03. Run on the final tree: `go build ./...`; `go vet` on `service` with the `unit` tag and with the `reasoning_fidelity` tag; `.github/scripts/test_reasoning_recovery.sh`, which passes with its one remaining command; `TestReasoningFidelityControl` and `TestReasoningFidelityFixtures` with the `reasoning_fidelity` tag; `test_upstream_automation_contract.py`; `vue-tsc --noEmit`; ESLint on the two changed frontend files; and vitest (`--no-cache`) for six files with 20 tests (`ReasoningRecoveryView`, locale key completeness, locale message compile, locale key collision, `CodexRuntime.native`, `OpsErrorDetailModal`). +- `.downstream/upstream-risk.json` is unchanged. It is computed from the two upstream tags and the downstream commit of the integration (`291d25e7f`: 157 upstream paths, 65 overlapping, 31 critical, 1420 downstream paths), so it also lists files the follow-ups delete and the four files of the reasoning fidelity diagnostic, deleted after the release. The gate that recomputes it (`.github/workflows/upstream-risk-gate.yml`) runs for pull requests from `sync/upstream-*` branches and on a manual dispatch for a pull request number. By reading, it recomputes from the tags and the downstream commit the manifest records, not from the head of the pull request, so a run dispatched for the follow-ups reproduces this manifest, deleted files included, and then requires the `upstream-reviewed` label because the class is `review_required`. +- **Changed after the runs above.** Five changes followed the runs above: the comment headers of 266 and 267 now cite the previous release (`v0.2.11-codexrip.9`) and its line numbers, with no statement changed, so the migration tests applied the same SQL; `TestOpenAIReasoningRecoveryChatToolRoundKeepsNoReasoningState` lost its last assertion, a search of the gin context for a key only the deleted replay set; the zh label of the recovery line in the Ops continuation diagnostics is 推理恢复, the name the reason text of the same panel already used; the live recovery diagnostic and the second command of the gate were deleted; and the frozen gateway helper of the reasoning fidelity diagnostic was made to load the recovery switch from its frozen snapshot. The zh texts of the 推理恢复 page are the ones the owner approved in the preview of 2026-10-03. Run on the final tree: `go build ./...`; `go vet` on `service` with the `unit` tag; `.github/scripts/test_reasoning_recovery.sh`, which passes with its one remaining command; `test_upstream_automation_contract.py`; `vue-tsc --noEmit`; ESLint on the two changed frontend files; and vitest (`--no-cache`) for six files with 20 tests (`ReasoningRecoveryView`, locale key completeness, locale message compile, locale key collision, `CodexRuntime.native`, `OpsErrorDetailModal`). The fifth change was covered on that tree by `go vet` on `service` with the diagnostic's build tag and by the diagnostic's two offline tests; the diagnostic was deleted after the release (see Integration decisions), and that change and both checks went with it. - **Checked for the integration before it merged as #232 and not repeated here:** the build for `GOOS=linux GOARCH=amd64`; `go vet` plain, with the `e2e` tag and on the packages the follow-ups do not touch; `go generate ./ent`; the TypeSafe, System One, recharge bonus, security fix and platform list tests; the vitest specs of the new upstream UI. The previews the owner saw on 2026-10-03 (the new upstream UI under the console theme, the profit control section, 推理恢复, the TypeSafe dialogs) were captured from the separate branches against a mock API; the captures are not part of the repository. - **Not run:** full suites, race tests, any other integration test, the production migration path against a database in the state of `v0.2.11-codexrip.9` (read only, see Migrations), a browser preview of the assembled tree, an owner preview of two zh texts of the Ops continuation diagnostics written after 2026-10-03 (the label 推理恢复 and the reason shown for `disabled`), real model or TypeSafe calls, deploy. Source integration does not claim a successful deployment. diff --git a/backend/internal/service/reasoning_fidelity_control_test.go b/backend/internal/service/reasoning_fidelity_control_test.go deleted file mode 100644 index 4c63b82e4b9b..000000000000 --- a/backend/internal/service/reasoning_fidelity_control_test.go +++ /dev/null @@ -1,246 +0,0 @@ -//go:build reasoning_fidelity - -package service_test - -import ( - "bufio" - "bytes" - "context" - "encoding/json" - "io" - "net/http" - "strings" - "testing" - "time" - - "github.com/Wei-Shaw/sub2api/internal/config" - "github.com/Wei-Shaw/sub2api/internal/pkg/tlsfingerprint" - "github.com/Wei-Shaw/sub2api/internal/service" -) - -type fidelityOfflineUpstream struct { - calls int - last *http.Request - body []byte -} - -func (f *fidelityOfflineUpstream) Do(req *http.Request, _ string, _ int64, _ int) (*http.Response, error) { - f.calls++ - f.last = req - f.body, _ = io.ReadAll(req.Body) - response := `{"id":"resp_offline","status":"completed","model":"gpt-5.6-sol","output":[{"type":"message","role":"assistant","phase":"final_answer","content":[{"type":"output_text","text":"{\"count\":25,\"first\":\"ABCDFE\",\"last\":\"CBEADF\"}"}]}],"usage":{"input_tokens":1,"output_tokens":2}}` - contentType := "application/json" - var request struct { - Stream bool `json:"stream"` - } - _ = json.Unmarshal(f.body, &request) - if request.Stream { - contentType = "text/event-stream" - response = "data: {\"type\":\"response.completed\",\"response\":" + response + "}\n\ndata: [DONE]\n\n" - } - return &http.Response{StatusCode: http.StatusOK, Header: http.Header{"Content-Type": {contentType}}, Body: io.NopCloser(strings.NewReader(response))}, nil -} -func (f *fidelityOfflineUpstream) DoWithTLS(req *http.Request, p string, id int64, c int, _ *tlsfingerprint.Profile) (*http.Response, error) { - return f.Do(req, p, id, c) -} - -func fidelityOfflineBootstrap() fidelityBootstrap { - return fidelityBootstrap{SchemaVersion: 1, Phase: "before", RunID: "offline", ConfigMode: "production_env", Source: fidelitySource{ - Account: service.Account{ID: 15522, Name: "白嫖666", Platform: service.PlatformOpenAI, Type: service.AccountTypeAPIKey, Concurrency: 1, Status: service.StatusActive, Schedulable: true, - Credentials: map[string]any{"api_key": "offline-fake", "base_url": "https://upstream.invalid/v1"}, Extra: map[string]any{"openai_responses_supported": true, "openai_responses_mode": "responses"}, GroupIDs: []int64{4}}, - Group: service.Group{ID: 4, Platform: service.PlatformOpenAI, Status: service.StatusActive}, FastPolicy: service.DefaultOpenAIFastPolicySettings(), Settings: map[string]string{}, ChannelModels: map[string]string{"gpt-5.6-sol": "gpt-5.6-sol", "gpt-6-astra": "gpt-6-astra"}, Fingerprint: strings.Repeat("a", 64)}} -} - -func fidelityOfflineBudget(t *testing.T, grant fidelityGrant) (*fidelityBudgetUpstream, *fidelityOfflineUpstream, *bytes.Buffer) { - t.Helper() - var input, output bytes.Buffer - if err := json.NewEncoder(&input).Encode(grant); err != nil { - t.Fatal(err) - } - h := &fidelityHarness{boot: fidelityOfflineBootstrap(), input: bufio.NewReader(&input), output: json.NewEncoder(&output), ctx: context.Background()} - h.sourceAccountSHA = fidelityHashJSON(&h.boot.Source.Account) - fake := &fidelityOfflineUpstream{} - u := &fidelityBudgetUpstream{h: h, inner: fake, used: map[int]bool{}, activeSlot: 1, endpoint: "https://upstream.invalid/v1/responses", authorizationSHA: fidelityHash([]byte("Bearer offline-fake"))} - h.upstream = u - return u, fake, &output -} - -func fidelityOfflineRequest(t *testing.T) *http.Request { - t.Helper() - r, err := http.NewRequest(http.MethodPost, "https://upstream.invalid/v1/responses", strings.NewReader(`{"model":"gpt-5.6-sol","input":[]}`)) - if err != nil { - t.Fatal(err) - } - r.Header.Set("Authorization", "Bearer offline-fake") - return r -} - -func TestReasoningFidelityControl(t *testing.T) { - t.Run("budget_grant_is_single_send_including_tls_retry", func(t *testing.T) { - u, fake, out := fidelityOfflineBudget(t, fidelityGrant{Type: "send_granted", Slot: 1, Fingerprint: strings.Repeat("a", 64), Attempt: 1}) - resp, err := u.Do(fidelityOfflineRequest(t), "", 15522, 1) - if err != nil { - t.Fatal(err) - } - _, _ = io.Copy(io.Discard, resp.Body) - _ = resp.Body.Close() - if fake.calls != 1 || u.attempts != 1 || fake.last.GetBody != nil || !service.HTTPUpstreamRedirectsDisabled(fake.last.Context()) { - t.Fatal("actual transport was not guarded") - } - _, err = u.DoWithTLS(fidelityOfflineRequest(t), "", 15522, 1, &tlsfingerprint.Profile{}) - if err == nil || fake.calls != 1 || u.blocked != 1 || strings.Count(out.String(), "before_send") != 1 { - t.Fatal("same slot retry escaped") - } - if strings.Contains(out.String(), "offline-fake") || strings.Contains(out.String(), "upstream.invalid") { - t.Fatal("broker output leaked source") - } - }) - t.Run("deny_has_zero_calls", func(t *testing.T) { - u, fake, _ := fidelityOfflineBudget(t, fidelityGrant{Type: "send_denied", Slot: 1}) - _, err := u.Do(fidelityOfflineRequest(t), "", 15522, 1) - if err == nil || fake.calls != 0 || u.attempts != 0 { - t.Fatal("denied call was sent") - } - }) - t.Run("different_source_has_zero_calls", func(t *testing.T) { - u, fake, out := fidelityOfflineBudget(t, fidelityGrant{}) - u.h.boot.Source.Account.Credentials["api_key"] = "changed" - _, err := u.Do(fidelityOfflineRequest(t), "", 15522, 1) - if err == nil || fake.calls != 0 || out.Len() != 0 { - t.Fatal("changed source passed verification") - } - }) - t.Run("bad_grant_and_budget_fail_closed", func(t *testing.T) { - for _, grant := range []fidelityGrant{{Type: "send_granted", Slot: 1, Fingerprint: strings.Repeat("b", 64), Attempt: 1}, {Type: "send_granted", Slot: 1, Fingerprint: strings.Repeat("a", 64), Attempt: 2}, {Type: "send_granted", Slot: 1, Fingerprint: strings.Repeat("a", 64), Attempt: 1, ElapsedMS: fidelityMaxDuration.Milliseconds()}} { - u, fake, _ := fidelityOfflineBudget(t, grant) - _, err := u.Do(fidelityOfflineRequest(t), "", 15522, 1) - if err == nil || fake.calls != 0 { - t.Fatal("invalid grant sent request") - } - } - u, fake, _ := fidelityOfflineBudget(t, fidelityGrant{}) - u.attempts = 24 - _, err := u.Do(fidelityOfflineRequest(t), "", 15522, 1) - if err == nil || fake.calls != 0 { - t.Fatal("global budget exceeded") - } - }) - t.Run("cancelled_context_does_not_consume", func(t *testing.T) { - u, fake, out := fidelityOfflineBudget(t, fidelityGrant{}) - ctx, cancel := context.WithCancel(context.Background()) - cancel() - _, err := u.Do(fidelityOfflineRequest(t).WithContext(ctx), "", 15522, 1) - if err == nil || fake.calls != 0 || out.Len() != 0 { - t.Fatal("cancelled request consumed grant") - } - }) - t.Run("detached_forward_context_regains_slot_deadline", func(t *testing.T) { - u, fake, _ := fidelityOfflineBudget(t, fidelityGrant{Type: "send_granted", Slot: 1, Fingerprint: strings.Repeat("a", 64), Attempt: 1}) - ctx, cancel := context.WithTimeout(context.Background(), time.Second) - defer cancel() - u.activeContext = ctx - resp, err := u.Do(fidelityOfflineRequest(t).WithContext(context.WithoutCancel(ctx)), "", 15522, 1) - if err != nil { - t.Fatal(err) - } - if _, ok := fake.last.Context().Deadline(); !ok { - t.Fatal("detached Forward lost experiment deadline") - } - _ = resp.Body.Close() - if fake.last.Context().Err() == nil { - t.Fatal("upstream context not released on close") - } - }) - t.Run("bootstrap_exact_account_group_and_ledger", func(t *testing.T) { - b := fidelityOfflineBootstrap() - if err := fidelityValidateBootstrap(b); err != nil { - t.Fatal(err) - } - b.Source.Account.Name = "another" - if fidelityValidateBootstrap(b) == nil { - t.Fatal("different account accepted") - } - b = fidelityOfflineBootstrap() - b.Ledger = fidelityLedger{Attempts: 2, ConsumedSlots: []int{1, 1}} - if fidelityValidateBootstrap(b) == nil { - t.Fatal("duplicate ledger slot accepted") - } - }) - t.Run("tool_replay_preserves_raw_order_phase_unknowns", func(t *testing.T) { - body := fidelityRequestBody("gpt-5.6-sol", "xhigh", fidelityOrdersPrompt, true) - items := []json.RawMessage{json.RawMessage(`{"type":"reasoning","encrypted_content":"opaque","summary":[],"custom":true}`), json.RawMessage(`{"type":"message","role":"assistant","phase":"commentary","content":[{"type":"output_text","text":"loading"}]}`), json.RawMessage(`{"type":"function_call","call_id":"real_call","name":"load_orders","arguments":"{}"}`)} - next, ok := fidelityToolContinuation(body, fidelityResponse{Status: "completed", Output: items}) - if !ok { - t.Fatal("complete tool history rejected") - } - var v struct { - Input []json.RawMessage `json:"input"` - ToolChoice string `json:"tool_choice"` - } - if json.Unmarshal(next, &v) != nil || len(v.Input) != 5 || v.ToolChoice != "none" { - t.Fatal("incorrect replay shape") - } - for i, item := range items { - if !fidelityJSONEqual(item, v.Input[i+1]) { - t.Fatal("raw output item lost") - } - } - if bytes.Count(next, []byte("Select at most")) != 1 { - t.Fatal("initial constraints were duplicated") - } - if _, ok := fidelityToolContinuation(body, fidelityResponse{Status: "incomplete", Output: items}); ok { - t.Fatal("incomplete response replayed") - } - }) - t.Run("real_forward_is_reached_offline", func(t *testing.T) { - b := fidelityOfflineBootstrap() - fake := &fidelityOfflineUpstream{} - cfg := &config.Config{} - gateway, err := service.ReasoningFidelityGatewayForTest(cfg, fake, b.Source.SystemPrompts, b.Source.Settings) - if err != nil { - t.Fatal(err) - } - h := &fidelityHarness{boot: b, gateway: gateway} - body := fidelityRequestBody("gpt-5.6-sol", "xhigh", fidelitySinglePrompt, false) - ctx, cancel := context.WithTimeout(context.Background(), time.Second) - defer cancel() - c, r := h.newContext(ctx, body, "xhigh") - _, err = gateway.Forward(c.Request.Context(), c, &h.boot.Source.Account, body) - if err != nil || fake.calls != 1 || r.Code != 200 { - t.Fatal("headless Forward did not complete") - } - }) - t.Run("group_policy_applied_once_before_both_arms", func(t *testing.T) { - b := fidelityOfflineBootstrap() - b.Source.Group.MaxReasoningEffort = "high" - fake := &fidelityOfflineUpstream{} - gateway, err := service.ReasoningFidelityGatewayForTest(&config.Config{}, fake, b.Source.SystemPrompts, b.Source.Settings) - if err != nil { - t.Fatal(err) - } - h := &fidelityHarness{boot: b, gateway: gateway} - body, err := fidelityPrepareIngressBody(fidelityRequestBody("gpt-5.6-sol", "xhigh", fidelitySinglePrompt, false), b.Source) - if err != nil { - t.Fatal(err) - } - c, _ := h.newContext(context.Background(), body, "xhigh") - _, err = gateway.Forward(c.Request.Context(), c, &h.boot.Source.Account, body) - if err != nil { - t.Fatal("gateway policy test failed") - } - gatewayBody := append([]byte(nil), fake.body...) - directContext, _ := h.newContext(context.Background(), body, "xhigh") - req, err := service.ReasoningFidelityDirectRequestForTest(directContext.Request.Context(), gateway, directContext, &h.boot.Source.Account, body) - if err != nil { - t.Fatal("direct policy test failed") - } - directBody, _ := io.ReadAll(req.Body) - _ = req.Body.Close() - var a, before map[string]json.RawMessage - _ = json.Unmarshal(gatewayBody, &a) - _ = json.Unmarshal(directBody, &before) - if !fidelityJSONEqual(a["reasoning"], before["reasoning"]) || !bytes.Contains(before["reasoning"], []byte(`"high"`)) { - t.Fatal("arms received different effective effort") - } - }) -} diff --git a/backend/internal/service/reasoning_fidelity_live_test.go b/backend/internal/service/reasoning_fidelity_live_test.go deleted file mode 100644 index 72a69f805fb1..000000000000 --- a/backend/internal/service/reasoning_fidelity_live_test.go +++ /dev/null @@ -1,830 +0,0 @@ -//go:build reasoning_fidelity - -package service_test - -import ( - "bufio" - "bytes" - "context" - "crypto/rand" - "crypto/sha256" - "encoding/hex" - "encoding/json" - "errors" - "io" - "log" - "log/slog" - "net/http" - "net/http/httptest" - "os" - "reflect" - "regexp" - "strings" - "sync" - "testing" - "time" - - "github.com/Wei-Shaw/sub2api/internal/config" - "github.com/Wei-Shaw/sub2api/internal/pkg/logger" - "github.com/Wei-Shaw/sub2api/internal/pkg/tlsfingerprint" - "github.com/Wei-Shaw/sub2api/internal/repository" - "github.com/Wei-Shaw/sub2api/internal/service" - "github.com/gin-gonic/gin" -) - -const ( - fidelityMaxAttempts = 24 - fidelityMaxBody = 8 << 20 - fidelityMaxDuration = 45 * time.Minute - fidelityRequestTimeout = 180 * time.Second -) - -// The runner is the sole persistence owner. It rereads the fixed production -// source before each grant, then durably consumes a slot before acknowledging -// it. The Go process only receives secrets over stdin/inherited environment. -type fidelityBootstrap struct { - SchemaVersion int `json:"schema_version"` - Phase string `json:"phase"` - RunID string `json:"run_id"` - ConfigMode string `json:"config_mode"` - Source fidelitySource `json:"source"` - Ledger fidelityLedger `json:"ledger"` -} - -type fidelitySource struct { - Account service.Account `json:"account"` - Group service.Group `json:"group"` - FastPolicy *service.OpenAIFastPolicySettings `json:"fast_policy"` - SystemPrompts *service.SystemPromptConfig `json:"system_prompts"` - Settings map[string]string `json:"settings"` - ChannelModels map[string]string `json:"channel_models"` - Fingerprint string `json:"fingerprint"` - UserID int64 `json:"user_id"` -} - -type fidelityLedger struct { - Attempts int `json:"attempts"` - ConsumedSlots []int `json:"consumed_slots"` - ElapsedMS int64 `json:"elapsed_ms"` -} - -type fidelityGrant struct { - Type string `json:"type"` - Slot int `json:"slot"` - Fingerprint string `json:"fingerprint"` - Attempt int `json:"attempt"` - ElapsedMS int64 `json:"elapsed_ms"` -} - -type fidelityResult struct { - Type string `json:"type"` - Slot int `json:"slot"` - Phase string `json:"phase"` - Model string `json:"model"` - Effort string `json:"effort"` - Arm string `json:"arm"` - Fixture string `json:"fixture"` - Status string `json:"status"` - ErrorClass string `json:"error_class,omitempty"` - HTTPStatus int `json:"http_status"` - Attempted bool `json:"attempted"` - Attempts int `json:"attempts"` - BlockedRetries int `json:"blocked_retries"` - Correct *bool `json:"correct"` - DurationMS int64 `json:"duration_ms"` - FixtureSHA string `json:"fixture_sha256"` - AnswerSHA string `json:"answer_sha256,omitempty"` - ResponseIDSHA string `json:"response_id_sha256,omitempty"` - RequestIDSHA string `json:"request_id_sha256,omitempty"` - SentBodySHA string `json:"sent_body_sha256,omitempty"` - SentInputSHA string `json:"sent_input_sha256,omitempty"` - ResponseOutputSHA string `json:"response_output_sha256,omitempty"` - SentModelSHA string `json:"sent_model_sha256,omitempty"` - SentEffortSHA string `json:"sent_effort_sha256,omitempty"` - InputPreserved bool `json:"input_preserved"` - ReasoningConfigPreserved bool `json:"reasoning_config_preserved"` - ModelMatchesRequest bool `json:"model_matches_request"` - ResponseModelMatchesSent bool `json:"response_model_matches_sent"` - EncryptedInputBefore int `json:"encrypted_input_before"` - EncryptedInputSent int `json:"encrypted_input_sent"` - PhaseItemsBefore int `json:"phase_items_before"` - PhaseItemsSent int `json:"phase_items_sent"` - EncryptedReasoningComplete bool `json:"encrypted_reasoning_complete"` - Candidate518 bool `json:"candidate_518n_minus_2"` - RawOutputPreserved bool `json:"raw_output_preserved"` - Usage *fidelityUsage `json:"usage"` -} - -type fidelitySlot struct { - ID int - Model, Effort, Arm, Fixture string -} - -type fidelityExchange struct { - Response fidelityResponse - Result fidelityResult - RequestBody []byte - SentBody []byte -} - -type fidelityHarness struct { - boot fidelityBootstrap - input *bufio.Reader - output *json.Encoder - sourceAccountSHA string - started time.Time - ctx context.Context - upstream *fidelityBudgetUpstream - gateway *service.OpenAIGatewayService - stopped string - results []fidelityResult -} - -// Only this test may use the real HTTP repository. Opt-in is mandatory and -// normal unit/CI runs never start a network request or read a live snapshot. -func TestReasoningFidelityLive(t *testing.T) { - if os.Getenv("SUB2API_REASONING_FIDELITY_LIVE") != "1" { - t.Skip("explicit controlled runner opt-in required") - } - out := os.Stdout - devnull, err := os.OpenFile(os.DevNull, os.O_WRONLY, 0) - if err != nil { - t.Fatal("diagnostic_output_initialization_failed") - } - // Suppress every production logging surface before reading any source. - // Deliberately keep stdout redirected until process exit: testing's PASS - // trailer must not leak into the JSON-line broker protocol. - os.Stdout, os.Stderr = devnull, devnull - gin.SetMode(gin.ReleaseMode) - gin.DefaultWriter, gin.DefaultErrorWriter = io.Discard, io.Discard - _ = logger.Init(logger.InitOptions{Level: "fatal", Output: logger.OutputOptions{ToStdout: true}}) - logger.SetSink(nil) - log.SetOutput(io.Discard) - slog.SetDefault(slog.New(slog.NewTextHandler(io.Discard, nil))) - encoder := json.NewEncoder(out) - defer func() { - if recover() != nil { - _ = encoder.Encode(map[string]any{"type": "summary", "status": "internal_panic", "attempts_unknown": true}) - t.Fail() - } - }() - if err := fidelityRun(bufio.NewReaderSize(os.Stdin, 64<<10), encoder); err != nil { - _ = encoder.Encode(map[string]any{"type": "summary", "status": "harness_failed", "error_class": err.Error()}) - t.Fail() - } -} - -func fidelityRun(input *bufio.Reader, output *json.Encoder) error { - var boot fidelityBootstrap - if err := fidelityReadJSON(input, &boot); err != nil { - return errors.New("invalid_bootstrap") - } - if err := fidelityValidateBootstrap(boot); err != nil { - return err - } - cfg, err := config.LoadForBootstrap() - if err != nil { - return errors.New("production_config_load_failed") - } - // No network clients other than HTTPUpstream are constructed. In particular - // database/Redis credentials loaded by the production config stay unused. - remaining := fidelityMaxDuration - time.Duration(boot.Ledger.ElapsedMS)*time.Millisecond - ctx, cancel := context.WithTimeout(context.Background(), remaining) - defer cancel() - h := &fidelityHarness{boot: boot, input: input, output: output, started: time.Now(), ctx: ctx} - h.sourceAccountSHA = fidelityHashJSON(&h.boot.Source.Account) - u := &fidelityBudgetUpstream{h: h, inner: repository.NewHTTPUpstream(cfg), attempts: boot.Ledger.Attempts, used: map[int]bool{}} - for _, slot := range boot.Ledger.ConsumedSlots { - u.used[slot] = true - } - h.upstream = u - h.gateway, err = service.ReasoningFidelityGatewayForTest(cfg, u, boot.Source.SystemPrompts, boot.Source.Settings) - if err != nil { - return errors.New("unsupported_source_policy") - } - // Resolve the exact production endpoint/header builder without sending. - probe := fidelityRequestBody("gpt-5.6-sol", "xhigh", fidelitySinglePrompt, false) - probe, err = fidelityPrepareIngressBody(probe, boot.Source) - if err != nil { - return errors.New("source_group_policy_rejected") - } - probeContext, _ := h.newContext(ctx, probe, "xhigh") - req, err := service.ReasoningFidelityDirectRequestForTest(probeContext.Request.Context(), h.gateway, probeContext, &h.boot.Source.Account, probe) - if err != nil { - return errors.New("source_endpoint_validation_failed") - } - if req.URL == nil || req.URL.Scheme != "https" || req.URL.User != nil || req.URL.RawQuery != "" { - return errors.New("source_endpoint_not_https_responses") - } - u.endpoint = req.URL.String() - if !strings.HasSuffix(req.URL.Path, "/responses") { - return errors.New("source_endpoint_not_responses") - } - u.authorizationSHA = fidelityHash([]byte(req.Header.Get("Authorization"))) - u.proxyURL = "" - if h.boot.Source.Account.Proxy != nil { - u.proxyURL = h.boot.Source.Account.Proxy.URL() - } - _ = req.Body.Close() - _ = output.Encode(map[string]any{"type": "ready", "phase": boot.Phase, "source_sha256": boot.Source.Fingerprint, "endpoint_sha256": fidelityHash([]byte(u.endpoint)), "attempts": u.attempts}) - if os.Getenv("SUB2API_REASONING_FIDELITY_VALIDATE_ONLY") == "1" { - return output.Encode(map[string]any{"type": "summary", "phase": boot.Phase, "status": "validated", "attempts": u.attempts}) - } - if boot.Phase == "before" { - h.runBefore() - } else { - h.runAfter() - } - status := "completed" - if h.stopped != "" { - status = h.stopped - } - return output.Encode(map[string]any{"type": "summary", "phase": boot.Phase, "status": status, "attempts": u.attempts, "blocked_retries": u.totalBlocked, "phase_duration_ms": time.Since(h.started).Milliseconds(), "results": len(h.results), "source_sha256": boot.Source.Fingerprint}) -} - -func fidelityValidateBootstrap(b fidelityBootstrap) error { - if b.SchemaVersion != 1 || (b.Phase != "before" && b.Phase != "after") || b.ConfigMode != "production_env" || !regexp.MustCompile(`^[A-Za-z0-9_-]{1,80}$`).MatchString(b.RunID) { - return errors.New("invalid_bootstrap_contract") - } - if !regexp.MustCompile(`^[a-f0-9]{64}$`).MatchString(b.Source.Fingerprint) { - return errors.New("invalid_source_fingerprint") - } - a := &b.Source.Account - if a.ID <= 0 || a.Name != "白嫖666" || a.Type != service.AccountTypeAPIKey || a.Platform != service.PlatformOpenAI || a.Credentials == nil || a.GetOpenAIProtocolAPIKey() == "" || a.Status != service.StatusActive || !a.Schedulable || a.Concurrency < 1 { - return errors.New("invalid_fixed_account") - } - if a.ParentAccountID != nil || !a.SupportsOpenAIEndpointCapability(service.OpenAIEndpointCapabilityResponses) { - return errors.New("unsupported_fixed_account") - } - if b.Source.Group.ID <= 0 || b.Source.Group.Platform != service.PlatformOpenAI || b.Source.Group.Status != service.StatusActive { - return errors.New("invalid_fixed_group") - } - member := false - for _, groupID := range a.GroupIDs { - if groupID == b.Source.Group.ID { - member = true - } - } - if !member { - return errors.New("fixed_account_group_mismatch") - } - if (a.ProxyID == nil) != (a.Proxy == nil) || (a.Proxy != nil && (*a.ProxyID != a.Proxy.ID || !a.Proxy.IsActive() || a.Proxy.IsExpired(time.Now()))) { - return errors.New("invalid_fixed_proxy") - } - if b.Source.Settings == nil || b.Source.FastPolicy == nil || len(b.Source.ChannelModels) != 2 || b.Source.ChannelModels["gpt-5.6-sol"] == "" || b.Source.ChannelModels["gpt-6-astra"] == "" { - return errors.New("missing_frozen_policy") - } - if b.Ledger.Attempts < 0 || b.Ledger.Attempts > fidelityMaxAttempts || b.Ledger.ElapsedMS < 0 || b.Ledger.ElapsedMS >= fidelityMaxDuration.Milliseconds() || len(b.Ledger.ConsumedSlots) != b.Ledger.Attempts { - return errors.New("invalid_budget_ledger") - } - used := map[int]bool{} - for _, slot := range b.Ledger.ConsumedSlots { - if slot < 1 || slot > fidelityMaxAttempts || used[slot] { - return errors.New("invalid_budget_slots") - } - used[slot] = true - } - return nil -} - -func fidelityReadJSON(r *bufio.Reader, dst any) error { - var buf bytes.Buffer - for { - line, more, err := r.ReadLine() - if err != nil { - return errors.New("broker_input_unavailable") - } - if buf.Len()+len(line) > fidelityMaxBody { - return errors.New("broker_input_limit") - } - buf.Write(line) - if !more { - break - } - } - dec := json.NewDecoder(&buf) - if err := dec.Decode(dst); err != nil { - return errors.New("broker_input_invalid") - } - var trailing any - if dec.Decode(&trailing) != io.EOF { - return errors.New("broker_input_trailing") - } - return nil -} - -func (h *fidelityHarness) newContext(ctx context.Context, body []byte, effort string) (*gin.Context, *httptest.ResponseRecorder) { - r := httptest.NewRecorder() - c, _ := gin.CreateTestContext(r) - c.Request = httptest.NewRequest(http.MethodPost, "/v1/responses", bytes.NewReader(body)).WithContext(ctx) - c.Request.Header.Set("Content-Type", "application/json") - c.Request.Header.Set("Accept", "text/event-stream") - c.Request.Header.Set("User-Agent", "sub2api-reasoning-fidelity-diagnostic/1") - c.Request.Header.Set("X-Client-Request-Id", fidelityNewID()) - service.ReasoningFidelityContextForTest(ctx, c, &h.boot.Source.Group, h.boot.Source.FastPolicy, h.boot.Source.UserID, effort) - return c, r -} - -func (h *fidelityHarness) runBefore() { - for modelIndex, model := range []string{"gpt-5.6-sol", "gpt-6-astra"} { - effort := []string{"xhigh", "max"}[modelIndex] - base := modelIndex*6 + 1 - single := fidelityRequestBody(model, effort, fidelitySinglePrompt, false) - h.runSlot(fidelitySlot{base, model, effort, "direct", "single"}, single) - h.runSlot(fidelitySlot{base + 1, model, effort, "gateway", "single"}, single) - h.runTools(fidelitySlot{base + 2, model, effort, "direct", "tool_first"}) - h.runTools(fidelitySlot{base + 4, model, effort, "gateway", "tool_first"}) - } -} - -func (h *fidelityHarness) runAfter() { - type pair struct { - direct, gateway fidelityExchange - model, effort string - } - pairs := make([]pair, 0, 2) - for modelIndex, model := range []string{"gpt-5.6-sol", "gpt-6-astra"} { - effort := []string{"xhigh", "max"}[modelIndex] - base := 13 + modelIndex*4 - single := fidelityRequestBody(model, effort, fidelitySinglePrompt, false) - d := h.runSlot(fidelitySlot{base, model, effort, "direct", "single"}, single) - g := h.runSlot(fidelitySlot{base + 1, model, effort, "gateway", "single"}, single) - h.runTools(fidelitySlot{base + 2, model, effort, "gateway", "tool_first"}) - pairs = append(pairs, pair{d, g, model, effort}) - } - for i, p := range pairs { - eligible := fidelityPilotPairEligible(p.direct, p.gateway) - for armIndex, arm := range []string{"direct", "gateway"} { - slot := fidelitySlot{21 + i*2 + armIndex, p.model, p.effort, arm, "pilot"} - if !eligible { - h.skip(slot, "pilot_ineligible") - continue - } - base := p.direct - if arm == "gateway" { - base = p.gateway - } - body, ok := fidelityPilotBody(base) - if !ok { - h.skip(slot, "pilot_state_unavailable") - continue - } - h.runSlot(slot, body) - } - } -} - -func (h *fidelityHarness) runTools(firstSlot fidelitySlot) { - body := fidelityRequestBody(firstSlot.Model, firstSlot.Effort, fidelityOrdersPrompt, true) - first := h.runSlot(firstSlot, body) - secondSlot := firstSlot - secondSlot.ID++ - secondSlot.Fixture = "tool_final" - next, ok := fidelityToolContinuation(body, first.Response) - if !ok { - h.skip(secondSlot, "tool_state_unavailable") - return - } - h.runSlot(secondSlot, next) -} - -func (h *fidelityHarness) skip(slot fidelitySlot, status string) fidelityExchange { - r := fidelityResult{Type: "result", Slot: slot.ID, Phase: h.boot.Phase, Model: slot.Model, Effort: slot.Effort, Arm: slot.Arm, Fixture: slot.Fixture, Status: status, Attempts: h.upstream.attempts, FixtureSHA: fidelityFixtureHash(slot.Fixture)} - h.results = append(h.results, r) - _ = h.output.Encode(r) - return fidelityExchange{Result: r} -} - -func (h *fidelityHarness) runSlot(slot fidelitySlot, rawBody []byte) fidelityExchange { - if h.stopped != "" { - return h.skip(slot, "batch_stopped") - } - if h.upstream.used[slot.ID] { - return h.skip(slot, "already_consumed") - } - if h.ctx.Err() != nil { - h.stopped = "batch_time_limit" - return h.skip(slot, "batch_time_limit") - } - body, policyErr := fidelityPrepareIngressBody(rawBody, h.boot.Source) - if policyErr != nil { - return h.skip(slot, "group_policy_rejected") - } - ctx, cancel := context.WithTimeout(h.ctx, fidelityRequestTimeout) - defer cancel() - c, recorder := h.newContext(ctx, body, slot.Effort) - ctx = c.Request.Context() - h.upstream.activeSlot = slot.ID - h.upstream.activeContext = ctx - h.upstream.sentBody = nil - h.upstream.rawResponse = nil - h.upstream.responseContentType = "" - h.upstream.responseStatus = 0 - h.upstream.blocked = 0 - h.upstream.lastError = "" - attemptsBefore := h.upstream.attempts - start := time.Now() - var response fidelityResponse - var callErr error - if slot.Arm == "gateway" { - _, callErr = h.gateway.Forward(ctx, c, &h.boot.Source.Account, body) - response = fidelityParseResponse(recorder.Body.Bytes(), recorder.Header().Get("Content-Type"), recorder.Code) - } else { - var req *http.Request - req, callErr = service.ReasoningFidelityDirectRequestForTest(ctx, h.gateway, c, &h.boot.Source.Account, body) - if callErr == nil { - var resp *http.Response - resp, callErr = h.upstream.Do(req, h.upstream.proxyURL, h.boot.Source.Account.ID, h.boot.Source.Account.Concurrency) - if resp != nil { - data, readErr := io.ReadAll(io.LimitReader(resp.Body, fidelityMaxBody+1)) - _ = resp.Body.Close() - if readErr != nil { - callErr = readErr - } - response = fidelityParseResponse(data, resp.Header.Get("Content-Type"), resp.StatusCode) - } - } - } - upstreamResponse := fidelityParseResponse(h.upstream.rawResponse, h.upstream.responseContentType, h.upstream.responseStatus) - if h.upstream.responseStatus == 401 || h.upstream.responseStatus == 403 || upstreamResponse.ErrorClass == "auth" { - h.stopped = "authentication_stopped" - } - if h.upstream.responseStatus == 402 || h.upstream.responseStatus == 429 || upstreamResponse.ErrorClass == "quota" || upstreamResponse.ErrorClass == "rate_limit" { - h.stopped = "quota_or_rate_limit_stopped" - } - if h.upstream.lastError == "source_changed" || strings.HasPrefix(h.upstream.lastError, "broker_") || h.upstream.lastError == "invalid_broker_grant" { - h.stopped = "source_verification_stopped" - } - r := fidelityResult{Type: "result", Slot: slot.ID, Phase: h.boot.Phase, Model: slot.Model, Effort: slot.Effort, Arm: slot.Arm, Fixture: slot.Fixture, Status: response.Status, ErrorClass: response.ErrorClass, HTTPStatus: h.upstream.responseStatus, Attempted: h.upstream.attempts > attemptsBefore, Attempts: h.upstream.attempts, BlockedRetries: h.upstream.blocked, DurationMS: time.Since(start).Milliseconds(), FixtureSHA: fidelityFixtureHash(slot.Fixture), Usage: upstreamResponse.Usage, EncryptedReasoningComplete: response.ReasoningComplete} - if callErr != nil { - if r.Status == "" || r.Status == "completed" { - r.Status = "request_failed" - } - if ctx.Err() != nil { - r.ErrorClass = "request_timeout" - } else if h.upstream.lastError != "" { - r.ErrorClass = h.upstream.lastError - } else { - r.ErrorClass = "forward_error" - } - } - if h.upstream.blocked > 0 { - r.Status = "retry_blocked" - } - if r.Status == "" { - r.Status = "no_response" - } - if response.Text != "" { - r.AnswerSHA = fidelityHash([]byte(response.Text)) - } - if response.ID != "" { - r.ResponseIDSHA = fidelityHash([]byte(response.ID)) - } - r.RequestIDSHA = fidelityHash([]byte(c.Request.Header.Get("X-Client-Request-Id"))) - if len(h.upstream.sentBody) > 0 { - r.SentBodySHA = fidelityHash(h.upstream.sentBody) - fidelityObserveRequest(&r, body, h.upstream.sentBody, response.Model) - } - if len(response.Output) > 0 { - r.ResponseOutputSHA = fidelityHashJSON(response.Output) - } - r.RawOutputPreserved = response.Status == "completed" && upstreamResponse.Status == "completed" && len(response.Output) > 0 && fidelityHashJSON(response.Output) == fidelityHashJSON(upstreamResponse.Output) - if r.Usage != nil && r.Usage.ReasoningTokens != nil { - n := *r.Usage.ReasoningTokens - r.Candidate518 = n >= 516 && (n+2)%518 == 0 - } - if r.Status == "completed" && !response.HasError && !response.HasIncomplete && !response.HasRefusal && slot.Fixture != "tool_first" { - correct := fidelityScore(slot.Fixture, response.Text) - r.Correct = &correct - } - h.results = append(h.results, r) - _ = h.output.Encode(r) - return fidelityExchange{Response: response, Result: r, RequestBody: append([]byte(nil), rawBody...), SentBody: append([]byte(nil), h.upstream.sentBody...)} -} - -func fidelityRequestBody(model, effort, prompt string, tool bool) []byte { - v := map[string]any{"model": model, "reasoning": map[string]any{"effort": effort}, "stream": true, "store": false, "include": []string{"reasoning.encrypted_content"}, "instructions": "Use only the supplied task and tool data. Return the requested JSON, without extra prose.", "input": []any{map[string]any{"role": "user", "content": []any{map[string]any{"type": "input_text", "text": prompt}}}}} - if tool { - v["tools"] = []any{map[string]any{"type": "function", "name": "load_orders", "description": "Read the fixed test orders. It is read-only and has no side effects.", "parameters": map[string]any{"type": "object", "properties": map[string]any{}, "required": []string{}, "additionalProperties": false}, "strict": true}} - v["tool_choice"] = map[string]any{"type": "function", "name": "load_orders"} - } - b, _ := json.Marshal(v) - return b -} - -func fidelityChannelMap(body []byte, mapping map[string]string) []byte { - var v map[string]json.RawMessage - if json.Unmarshal(body, &v) != nil { - return body - } - var model string - _ = json.Unmarshal(v["model"], &model) - if mapped := mapping[model]; mapped != "" { - v["model"], _ = json.Marshal(mapped) - } - b, _ := json.Marshal(v) - return b -} - -func fidelityPrepareIngressBody(body []byte, source fidelitySource) ([]byte, error) { - // Match the native handler: client effort policy precedes channel mapping, - // while Forward applies account mapping and endpoint adaptation afterward. - group := source.Group - updated, _, err := service.ApplyOpenAIReasoningEffortPolicy(body, group.MaxReasoningEffort, group.ReasoningEffortMappings, group.MaxReasoningEffortOverLimit) - if err != nil { - return nil, err - } - return fidelityChannelMap(updated, source.ChannelModels), nil -} - -func fidelityToolContinuation(body []byte, r fidelityResponse) ([]byte, bool) { - if r.Status != "completed" || r.HasError || r.HasIncomplete || r.HasRefusal { - return nil, false - } - callID := "" - for _, raw := range r.Output { - var item struct{ Type, CallID, Name, Arguments string } - var fields map[string]json.RawMessage - if json.Unmarshal(raw, &fields) != nil { - return nil, false - } - _ = json.Unmarshal(fields["type"], &item.Type) - if item.Type != "function_call" { - continue - } - _ = json.Unmarshal(fields["call_id"], &item.CallID) - _ = json.Unmarshal(fields["name"], &item.Name) - _ = json.Unmarshal(fields["arguments"], &item.Arguments) - var args map[string]any - if callID != "" || item.CallID == "" || item.Name != "load_orders" || json.Unmarshal([]byte(item.Arguments), &args) != nil || len(args) != 0 { - return nil, false - } - callID = item.CallID - } - if callID == "" { - return nil, false - } - var v map[string]json.RawMessage - if json.Unmarshal(body, &v) != nil { - return nil, false - } - var input []json.RawMessage - if json.Unmarshal(v["input"], &input) != nil { - return nil, false - } - input = append(input, r.Output...) - toolOutput, _ := json.Marshal(map[string]any{"type": "function_call_output", "call_id": callID, "output": fidelityOrdersResult}) - input = append(input, toolOutput) - v["input"], _ = json.Marshal(input) - v["tool_choice"] = json.RawMessage(`"none"`) - b, err := json.Marshal(v) - return b, err == nil -} - -func fidelityPilotPairEligible(a, b fidelityExchange) bool { - for _, e := range []fidelityExchange{a, b} { - if e.Result.Status != "completed" || e.Response.Status != "completed" || !e.Response.ReasoningComplete || e.Response.HasTool || e.Response.HasError || e.Response.HasIncomplete || e.Response.HasRefusal || !e.Result.ResponseModelMatchesSent { - return false - } - var req map[string]json.RawMessage - if json.Unmarshal(e.RequestBody, &req) != nil { - return false - } - for _, key := range []string{"previous_response_id", "conversation", "context_management"} { - if _, present := req[key]; present { - return false - } - } - for _, item := range e.Response.Output { - var v map[string]any - _ = json.Unmarshal(item, &v) - if v["type"] == "compaction" { - return false - } - } - } - if a.Result.SentModelSHA != b.Result.SentModelSHA || a.Result.SentEffortSHA != b.Result.SentEffortSHA { - return false - } - return (a.Result.Correct != nil && !*a.Result.Correct) || (b.Result.Correct != nil && !*b.Result.Correct) || a.Result.Candidate518 || b.Result.Candidate518 -} - -func fidelityPilotBody(e fidelityExchange) ([]byte, bool) { - var v map[string]json.RawMessage - if json.Unmarshal(e.RequestBody, &v) != nil { - return nil, false - } - var input []json.RawMessage - if json.Unmarshal(v["input"], &input) != nil || len(e.Response.RawReasoning) == 0 { - return nil, false - } - input = append(input, e.Response.RawReasoning...) - commentary, _ := json.Marshal(map[string]any{"type": "message", "role": "assistant", "phase": "commentary", "content": []any{map[string]any{"type": "output_text", "text": "I will continue checking the constraints and verify the calculation before providing the final answer."}}}) - input = append(input, commentary) - v["input"], _ = json.Marshal(input) - b, err := json.Marshal(v) - return b, err == nil -} - -func fidelityObserveRequest(r *fidelityResult, before, after []byte, responseModel string) { - var a, b map[string]json.RawMessage - if json.Unmarshal(before, &a) != nil || json.Unmarshal(after, &b) != nil { - return - } - r.InputPreserved = fidelityJSONEqual(a["input"], b["input"]) - r.ReasoningConfigPreserved = fidelityJSONEqual(a["reasoning"], b["reasoning"]) - r.ModelMatchesRequest = fidelityJSONEqual(a["model"], b["model"]) - r.SentInputSHA = fidelityHash(b["input"]) - var sentModel string - _ = json.Unmarshal(b["model"], &sentModel) - r.SentModelSHA = fidelityHash([]byte(sentModel)) - r.ResponseModelMatchesSent = sentModel != "" && responseModel == sentModel - var reasoning map[string]json.RawMessage - _ = json.Unmarshal(b["reasoning"], &reasoning) - r.SentEffortSHA = fidelityHash(reasoning["effort"]) - r.EncryptedInputBefore, r.PhaseItemsBefore = fidelityInputCounts(a["input"]) - r.EncryptedInputSent, r.PhaseItemsSent = fidelityInputCounts(b["input"]) -} - -func fidelityInputCounts(raw []byte) (encrypted, phase int) { - var items []map[string]json.RawMessage - if json.Unmarshal(raw, &items) != nil { - return - } - for _, item := range items { - var text string - if json.Unmarshal(item["encrypted_content"], &text) == nil && text != "" { - encrypted++ - } - if _, ok := item["phase"]; ok { - phase++ - } - } - return -} - -func fidelityJSONEqual(a, b []byte) bool { - var av, bv any - return json.Unmarshal(a, &av) == nil && json.Unmarshal(b, &bv) == nil && reflect.DeepEqual(av, bv) -} -func fidelityHash(b []byte) string { sum := sha256.Sum256(b); return hex.EncodeToString(sum[:]) } -func fidelityHashJSON(v any) string { b, _ := json.Marshal(v); return fidelityHash(b) } -func fidelityNewID() string { - var b [16]byte - if _, err := rand.Read(b[:]); err != nil { - panic("request_id_entropy_unavailable") - } - return hex.EncodeToString(b[:]) -} -func fidelityFixtureHash(kind string) string { - if strings.HasPrefix(kind, "tool_") { - return fidelityHash([]byte(fidelityOrdersPrompt + "\n" + fidelityOrdersResult)) - } - return fidelityHash([]byte(fidelitySinglePrompt)) -} - -// This boundary is reached by real Forward and by direct requests. A grant is -// required exactly once per preallocated slot; Forward retries cannot consume a -// second upstream attempt. GetBody=nil additionally disables Go POST replay. -type fidelityBudgetUpstream struct { - mu sync.Mutex - h *fidelityHarness - inner service.HTTPUpstream - activeSlot, attempts, blocked, totalBlocked int - activeContext context.Context - used map[int]bool - endpoint, authorizationSHA, proxyURL string - sentBody, rawResponse []byte - responseContentType string - responseStatus int - lastError string -} - -func (u *fidelityBudgetUpstream) Do(req *http.Request, proxyURL string, accountID int64, concurrency int) (*http.Response, error) { - return u.send(req, proxyURL, accountID, concurrency, nil) -} -func (u *fidelityBudgetUpstream) DoWithTLS(req *http.Request, proxyURL string, accountID int64, concurrency int, profile *tlsfingerprint.Profile) (*http.Response, error) { - return u.send(req, proxyURL, accountID, concurrency, profile) -} - -func (u *fidelityBudgetUpstream) send(req *http.Request, proxyURL string, accountID int64, concurrency int, profile *tlsfingerprint.Profile) (*http.Response, error) { - u.mu.Lock() - defer u.mu.Unlock() - fail := func(class string) (*http.Response, error) { u.lastError = class; return nil, errors.New(class) } - if u.activeSlot < 1 || u.activeSlot > 24 || u.used[u.activeSlot] { - u.blocked++ - u.totalBlocked++ - return fail("slot_retry_blocked") - } - if u.attempts >= fidelityMaxAttempts { - return fail("attempt_budget_exhausted") - } - if req == nil || req.URL == nil || req.Method != http.MethodPost || req.URL.String() != u.endpoint || accountID != u.h.boot.Source.Account.ID || concurrency != u.h.boot.Source.Account.Concurrency || proxyURL != u.proxyURL || fidelityHash([]byte(req.Header.Get("Authorization"))) != u.authorizationSHA || fidelityHashJSON(&u.h.boot.Source.Account) != u.h.sourceAccountSHA { - return fail("source_changed") - } - slotContext := u.activeContext - if slotContext == nil { - slotContext = u.h.ctx - } - if req.Context().Err() != nil || slotContext.Err() != nil || u.h.ctx.Err() != nil { - return fail("request_timeout") - } - data, err := io.ReadAll(io.LimitReader(req.Body, fidelityMaxBody+1)) - if err != nil || len(data) > fidelityMaxBody { - return fail("request_body_unavailable") - } - _ = req.Body.Close() - if err = u.h.output.Encode(map[string]any{"type": "before_send", "slot": u.activeSlot, "expected_fingerprint": u.h.boot.Source.Fingerprint}); err != nil { - return fail("broker_unavailable") - } - var grant fidelityGrant - // A blocked broker cannot hold an upstream call past its request deadline. - grantResult := make(chan error, 1) - go func() { grantResult <- fidelityReadJSON(u.h.input, &grant) }() - select { - case err = <-grantResult: - case <-slotContext.Done(): - return fail("broker_timeout") - } - if err != nil { - return fail("broker_unavailable") - } - if grant.Type != "send_granted" { - return fail("broker_denied") - } - if grant.Slot != u.activeSlot || grant.Fingerprint != u.h.boot.Source.Fingerprint || grant.Attempt != u.attempts+1 || grant.Attempt > 24 || grant.ElapsedMS < u.h.boot.Ledger.ElapsedMS || grant.ElapsedMS >= fidelityMaxDuration.Milliseconds() { - return fail("invalid_broker_grant") - } - u.used[u.activeSlot] = true - u.attempts = grant.Attempt - u.sentBody = append([]byte(nil), data...) - // Forward deliberately detaches client cancellation so normal clients can - // finish usage accounting. A diagnostic is different: the fixed 180-second - // slot deadline is a hard experiment bound, not a client-disconnect hint. - // Retain all production transport/context values, reattaching only this - // test-owned deadline and cancellation at the final outbound boundary. - sendContext := req.Context() - var cancelDeadline context.CancelFunc = func() {} - if deadline, ok := slotContext.Deadline(); ok { - sendContext, cancelDeadline = context.WithDeadline(sendContext, deadline) - } - sendContext, cancelSend := context.WithCancel(sendContext) - stopCancellation := context.AfterFunc(slotContext, cancelSend) - cleanup := func() { stopCancellation(); cancelSend(); cancelDeadline() } - req = req.Clone(service.WithHTTPUpstreamRedirectsDisabled(sendContext)) - req.Body = io.NopCloser(bytes.NewReader(data)) - req.GetBody = nil - var resp *http.Response - if profile == nil { - resp, err = u.inner.Do(req, proxyURL, accountID, concurrency) - } else { - resp, err = u.inner.DoWithTLS(req, proxyURL, accountID, concurrency, profile) - } - if err != nil { - cleanup() - u.lastError = "transport_error" - return nil, errors.New("transport_error") - } - if resp == nil || resp.Body == nil { - cleanup() - return fail("empty_upstream_response") - } - u.responseStatus = resp.StatusCode - u.responseContentType = resp.Header.Get("Content-Type") - resp.Body = &fidelityCaptureBody{ReadCloser: resp.Body, output: &u.rawResponse, remaining: fidelityMaxBody, cleanup: cleanup} - return resp, nil -} - -type fidelityCaptureBody struct { - io.ReadCloser - output *[]byte - remaining int - cleanup func() - closeOnce sync.Once -} - -func (r *fidelityCaptureBody) Close() error { - err := r.ReadCloser.Close() - r.closeOnce.Do(func() { - if r.cleanup != nil { - r.cleanup() - } - }) - return err -} - -func (r *fidelityCaptureBody) Read(p []byte) (int, error) { - n, err := r.ReadCloser.Read(p) - if n > 0 { - take := n - if take > r.remaining { - take = r.remaining - } - *r.output = append(*r.output, p[:take]...) - r.remaining -= take - if take < n { - return n, errors.New("response_body_limit") - } - } - return n, err -} diff --git a/backend/internal/service/reasoning_fidelity_parse_test.go b/backend/internal/service/reasoning_fidelity_parse_test.go deleted file mode 100644 index 33a73441b117..000000000000 --- a/backend/internal/service/reasoning_fidelity_parse_test.go +++ /dev/null @@ -1,923 +0,0 @@ -//go:build reasoning_fidelity - -package service_test - -import ( - "bufio" - "bytes" - "encoding/json" - "errors" - "fmt" - "io" - "sort" - "strings" - "testing" -) - -const fidelityMaxResponseBytes = 8 << 20 - -// These fixtures contain the problem and output contract, never the answer. -const fidelitySinglePrompt = `Arrange the six distinct letters A, B, C, D, E, F in a line, using each letter exactly once. A and B must both occur before D. B must occur before E. C and D must both occur before F. E must not occur immediately before F. Count every valid arrangement, and determine the lexicographically first and last valid arrangements using A < B < C < D < E < F. Return only one JSON object with exactly these fields: "count" (integer), "first" (six-letter string), "last" (six-letter string). Do not include prose or the reasoning process.` - -const fidelityOrdersPrompt = `Use the read-only load_orders tool exactly once to obtain the available orders before solving this task. Select at most three distinct orders, with total hours no greater than 11. Orders A and D are mutually exclusive. Order E may be selected only if order B is also selected. Maximize total value; break ties by minimizing total hours, then by the lexicographic order of the selected IDs sorted ascending. Remember these constraints when the tool result arrives; do not ask the tool for an answer or further data. After receiving its result, return only one JSON object with exactly these fields: "selected" (array of order IDs sorted ascending), "hours" (integer), "value" (integer). Do not include prose or the reasoning process.` - -const fidelityOrdersResult = `{"orders":[{"id":"A","hours":4,"value":13},{"id":"B","hours":3,"value":8},{"id":"C","hours":5,"value":16},{"id":"D","hours":6,"value":20},{"id":"E","hours":4,"value":15},{"id":"F","hours":2,"value":6}]}` - -type fidelityUsage struct { - InputTokens *int64 `json:"input_tokens"` - OutputTokens *int64 `json:"output_tokens"` - ReasoningTokens *int64 `json:"reasoning_tokens"` - CachedTokens *int64 `json:"cached_tokens"` - TotalTokens *int64 `json:"total_tokens"` -} - -type fidelityResponse struct { - Status string - Model string - ID string - Output []json.RawMessage - Usage *fidelityUsage - Text string - HasError bool - HasIncomplete bool - HasRefusal bool - HasTool bool - ReasoningComplete bool - RawReasoning []json.RawMessage - ErrorClass string -} - -// fidelityParseResponse is deliberately not a forgiving client renderer. It -// consumes one bounded response, requires explicit terminal evidence, and never -// makes a partial response look completed. Raw output is used for replay, not a -// narrower decoded/re-encoded representation that could discard unknown fields. -func fidelityParseResponse(body []byte, contentType string, httpStatus int) fidelityResponse { - result := fidelityResponse{Status: "unknown"} - if len(body) > fidelityMaxResponseBytes { - result.HasError, result.ErrorClass, result.Status = true, "body_limit", "failed" - return result - } - trimmed := bytes.TrimSpace(body) - terminal := false - if strings.Contains(strings.ToLower(contentType), "text/event-stream") || - bytes.HasPrefix(trimmed, []byte("data:")) || bytes.HasPrefix(trimmed, []byte("event:")) || - bytes.HasPrefix(trimmed, []byte(":")) { - result, terminal = fidelityParseSSE(body) - } else { - parsed, _, err := fidelityParseDocument(trimmed) - if err != nil { - result.HasError, result.ErrorClass = true, "parse" - } else { - result = parsed - terminal = result.Status == "completed" || result.Status == "failed" || result.Status == "incomplete" || result.Status == "cancelled" - } - } - - // Status codes are transport evidence, not claims about a provider's balance. - // In particular, a generic 429 is a rate limit, not proof of exhausted credit. - if httpStatus < 200 || httpStatus >= 300 { - result.HasError = true - switch httpStatus { - case 401, 403: - result.ErrorClass = "auth" - case 402: - if result.ErrorClass != "quota" { - result.ErrorClass = "payment_required" - } - case 429: - if result.ErrorClass != "quota" && result.ErrorClass != "auth" { - result.ErrorClass = "rate_limit" - } - default: - if result.ErrorClass == "" { - result.ErrorClass = "http" - } - } - } - if !terminal && !result.HasError { - result.HasIncomplete, result.ErrorClass = true, "eof" - } - fidelityInspectOutput(&result) - if result.HasError { - result.Status = "failed" - result.ReasoningComplete = false - } else if result.HasIncomplete || !terminal { - result.Status = "incomplete" - result.ReasoningComplete = false - } - return result -} - -func fidelityParseDocument(data []byte) (fidelityResponse, bool, error) { - result := fidelityResponse{Status: "unknown"} - object, err := fidelityJSONObject(data) - if err != nil { - return result, false, err - } - for name, target := range map[string]*string{"id": &result.ID, "model": &result.Model, "status": &result.Status} { - if raw, ok := object[name]; ok { - if bytes.Equal(raw, []byte("null")) || json.Unmarshal(raw, target) != nil { - return result, false, errors.New("invalid response scalar") - } - } - } - output, outputPresent := object["output"] - if outputPresent { - if bytes.Equal(bytes.TrimSpace(output), []byte("null")) || json.Unmarshal(output, &result.Output) != nil { - return result, false, errors.New("invalid response output") - } - } - if raw := object["usage"]; fidelityNonNull(raw) { - result.Usage, err = fidelityParseUsage(raw) - if err != nil { - return result, outputPresent, err - } - } - if raw := object["error"]; fidelityNonNull(raw) { - result.HasError = true - result.ErrorClass = fidelityStructuredErrorClass(raw) - } - // Some providers return a bare structured error instead of {error:{...}}. - if raw := object["type"]; len(raw) != 0 { - var kind string - if json.Unmarshal(raw, &kind) == nil && (kind == "error" || strings.HasSuffix(kind, "_error")) { - result.HasError = true - result.ErrorClass = fidelityStructuredErrorClass(data) - } - } - if fidelityNonNull(object["incomplete_details"]) || result.Status == "incomplete" || result.Status == "cancelled" { - result.HasIncomplete = true - } - if result.Status == "failed" { - result.HasError = true - if result.ErrorClass == "" { - result.ErrorClass = "upstream" - } - } - return result, outputPresent, nil -} - -func fidelityParseUsage(raw json.RawMessage) (*fidelityUsage, error) { - object, err := fidelityJSONObject(raw) - if err != nil { - return nil, err - } - usage := &fidelityUsage{} - for name, target := range map[string]**int64{ - "input_tokens": &usage.InputTokens, "output_tokens": &usage.OutputTokens, "total_tokens": &usage.TotalTokens, - } { - if err := fidelityReadTokenCount(object[name], target); err != nil { - return nil, err - } - } - for name, field := range map[string]struct { - key string - target **int64 - }{ - "input_tokens_details": {"cached_tokens", &usage.CachedTokens}, - "output_tokens_details": {"reasoning_tokens", &usage.ReasoningTokens}, - } { - if !fidelityNonNull(object[name]) { - continue - } - details, err := fidelityJSONObject(object[name]) - if err != nil { - return nil, err - } - if err := fidelityReadTokenCount(details[field.key], field.target); err != nil { - return nil, err - } - } - return usage, nil -} - -func fidelityReadTokenCount(raw json.RawMessage, target **int64) error { - if !fidelityNonNull(raw) { - return nil - } - var count int64 - if json.Unmarshal(raw, &count) != nil || count < 0 { - return errors.New("invalid token count") - } - *target = &count - return nil -} - -func fidelityParseSSE(body []byte) (fidelityResponse, bool) { - result := fidelityResponse{Status: "unknown"} - items := make(map[int]json.RawMessage) - announced := make(map[int]json.RawMessage) - terminal, finalOutputPresent, doneMarker := false, false, false - eventName := "" - var data []byte - hadData := false - setError := func(class string) { - result.HasError = true - if result.ErrorClass == "" || class == "auth" || class == "quota" { - result.ErrorClass = class - } - } - apply := func() { - if !hadData { - return - } - payload := bytes.TrimSuffix(data, []byte{'\n'}) - if bytes.Equal(bytes.TrimSpace(payload), []byte("[DONE]")) { - doneMarker = true - return - } - if doneMarker { - setError("parse") - return - } - object, err := fidelityJSONObject(payload) - if err != nil { - setError("parse") - return - } - var kind string - if raw, ok := object["type"]; ok { - if json.Unmarshal(raw, &kind) != nil { - setError("parse") - return - } - } - if kind == "" { - kind = eventName - } else if eventName != "" && eventName != "message" && eventName != kind { - setError("parse") - return - } - if kind == "error" || kind == "response.error" { - errRaw := object["error"] - if !fidelityNonNull(errRaw) { - errRaw = payload - } - setError(fidelityStructuredErrorClass(errRaw)) - return - } - if fidelityNonNull(object["error"]) { - setError(fidelityStructuredErrorClass(object["error"])) - } - if terminal { - // No response-mutating event may add content after its terminal - // document. Only SSE comments and the [DONE] trailer are allowed. - setError("parse") - return - } - if kind == "response.refusal.delta" || kind == "response.refusal.done" { - result.HasRefusal = true - } - if kind == "response.output_item.done" || kind == "response.output_item.added" { - var index int - if !fidelityNonNull(object["output_index"]) || json.Unmarshal(object["output_index"], &index) != nil || index < 0 || index > fidelityMaxResponseBytes { - setError("parse") - return - } - item := object["item"] - if _, err := fidelityJSONObject(item); err != nil { - setError("parse") - return - } - observed := fidelityResponse{Status: "completed", Output: []json.RawMessage{item}} - fidelityInspectOutput(&observed) - result.HasTool = result.HasTool || observed.HasTool - result.HasRefusal = result.HasRefusal || observed.HasRefusal - if observed.HasError { - setError(observed.ErrorClass) - } - if kind == "response.output_item.added" { - if previous, exists := announced[index]; exists && !fidelitySameItemIdentity(previous, item) { - setError("parse") - return - } - if previous, exists := items[index]; exists && !fidelitySameItemIdentity(previous, item) { - setError("parse") - return - } - announced[index] = item - return - } - result.HasIncomplete = result.HasIncomplete || observed.HasIncomplete - if previous, exists := announced[index]; exists && !fidelitySameItemIdentity(previous, item) { - setError("parse") - return - } - if previous, exists := items[index]; exists && !bytes.Equal(previous, item) { - setError("parse") - return - } - items[index] = item - return - } - if kind != "response.completed" && kind != "response.done" && kind != "response.failed" && kind != "response.incomplete" { - return - } - parsed, outputPresent, err := fidelityParseDocument(object["response"]) - if err != nil { - setError("parse") - return - } - if kind == "response.completed" && parsed.Status != "completed" || - kind == "response.failed" && parsed.Status != "failed" || - kind == "response.incomplete" && parsed.Status != "incomplete" { - setError("parse") - return - } - if parsed.Status != "completed" && parsed.Status != "failed" && parsed.Status != "incomplete" && parsed.Status != "cancelled" { - setError("parse") - return - } - terminal = true - result.Status, result.Model, result.ID = parsed.Status, parsed.Model, parsed.ID - result.Output, result.Usage, finalOutputPresent = parsed.Output, parsed.Usage, outputPresent - result.HasIncomplete = result.HasIncomplete || parsed.HasIncomplete - if parsed.HasError { - setError(parsed.ErrorClass) - } - } - - scanner := bufio.NewScanner(bytes.NewReader(body)) - scanner.Buffer(make([]byte, 4096), fidelityMaxResponseBytes+1) - scanner.Split(fidelitySSELine) - for scanner.Scan() { - line := scanner.Bytes() - if len(line) == 0 { - apply() - data, eventName, hadData = data[:0], "", false - continue - } - if line[0] == ':' { - continue - } - field, value, found := bytes.Cut(line, []byte{':'}) - if !found { - value = nil - } - value = bytes.TrimPrefix(value, []byte{' '}) - switch string(field) { - case "event": - eventName = string(value) - case "data": - data = append(data, value...) - data = append(data, '\n') - hadData = true - } - } - if scanner.Err() != nil { - setError("parse") - } - if hadData { - // A pending event at EOF was never terminated by its SSE blank line. - result.HasIncomplete = true - if result.ErrorClass == "" { - result.ErrorClass = "eof" - } - } - for index, item := range announced { - var finished json.RawMessage - if finalOutputPresent { - if index < len(result.Output) { - finished = result.Output[index] - } - } else { - finished = items[index] - } - if len(finished) == 0 { - result.HasIncomplete = true - if result.ErrorClass == "" { - result.ErrorClass = "eof" - } - } else if !fidelitySameItemIdentity(item, finished) { - setError("parse") - } - } - if !finalOutputPresent && len(items) > 0 { - indexes := make([]int, 0, len(items)) - for index := range items { - indexes = append(indexes, index) - } - sort.Ints(indexes) - for expected, index := range indexes { - if expected != index { - setError("parse") - result.Output = nil - break - } - result.Output = append(result.Output, items[index]) - } - } - return result, terminal -} - -// Type and an already-issued ID cannot change at the same output index. The -// terminal representation may otherwise legitimately enrich an added item. -func fidelitySameItemIdentity(before, after json.RawMessage) bool { - left, err := fidelityJSONObject(before) - if err != nil { - return false - } - right, err := fidelityJSONObject(after) - if err != nil { - return false - } - for _, key := range []string{"type", "id", "call_id"} { - var expected, actual string - if fidelityNonNull(left[key]) && json.Unmarshal(left[key], &expected) != nil { - return false - } - if expected == "" { - continue - } - if json.Unmarshal(right[key], &actual) != nil || expected != actual { - return false - } - } - return true -} - -// The SSE line grammar permits LF, CRLF, and CR. Keep event boundaries rather -// than guessing that EOF means the current event was successfully completed. -func fidelitySSELine(data []byte, atEOF bool) (int, []byte, error) { - for i, b := range data { - if b == '\n' { - return i + 1, data[:i], nil - } - if b == '\r' { - if i+1 == len(data) && !atEOF { - return 0, nil, nil - } - advance := i + 1 - if i+1 < len(data) && data[i+1] == '\n' { - advance++ - } - return advance, data[:i], nil - } - } - if atEOF && len(data) != 0 { - return len(data), data, nil - } - return 0, nil, nil -} - -func fidelityInspectOutput(result *fidelityResponse) { - var finalText, unphasedText strings.Builder - hasFinalPhase, hasReasoning, allReasoningComplete := false, false, true - setParseError := func() { - result.HasError = true - if result.ErrorClass != "auth" && result.ErrorClass != "quota" && result.ErrorClass != "rate_limit" { - result.ErrorClass = "parse" - } - } - for _, raw := range result.Output { - object, err := fidelityJSONObject(raw) - if err != nil { - setParseError() - continue - } - var kind, status string - if json.Unmarshal(object["type"], &kind) != nil || kind == "" { - setParseError() - continue - } - if rawStatus := object["status"]; fidelityNonNull(rawStatus) { - if json.Unmarshal(rawStatus, &status) != nil { - setParseError() - continue - } - if status == "incomplete" || status == "in_progress" || status == "failed" || status == "cancelled" { - result.HasIncomplete = true - } - } - if kind == "reasoning" { - hasReasoning = true - result.RawReasoning = append(result.RawReasoning, raw) - var encrypted string - if json.Unmarshal(object["encrypted_content"], &encrypted) != nil || encrypted == "" || (status != "" && status != "completed") { - allReasoningComplete = false - } - continue - } - if strings.HasSuffix(kind, "_call") || strings.HasSuffix(kind, "_call_output") || - kind == "mcp_approval_request" || kind == "mcp_list_tools" { - result.HasTool = true - } - if kind == "refusal" { - result.HasRefusal = true - } - if kind != "message" { - continue - } - var message struct { - Role string `json:"role"` - Phase string `json:"phase"` - Content []json.RawMessage `json:"content"` - } - if json.Unmarshal(raw, &message) != nil { - setParseError() - continue - } - if message.Role == "assistant" && message.Phase == "final_answer" { - hasFinalPhase = true - } - for _, rawPart := range message.Content { - part, err := fidelityJSONObject(rawPart) - var partType, partText string - if err != nil || json.Unmarshal(part["type"], &partType) != nil { - setParseError() - continue - } - if partType == "refusal" { - result.HasRefusal = true - } - if message.Role != "assistant" || partType != "output_text" { - continue - } - if !fidelityNonNull(part["text"]) || json.Unmarshal(part["text"], &partText) != nil { - setParseError() - continue - } - switch message.Phase { - case "final_answer": - finalText.WriteString(partText) - case "": - unphasedText.WriteString(partText) - } - } - } - if hasFinalPhase { - result.Text = finalText.String() - } else { - result.Text = unphasedText.String() - } - result.ReasoningComplete = result.Status == "completed" && !result.HasError && !result.HasIncomplete && hasReasoning && allReasoningComplete -} - -func fidelityStructuredErrorClass(raw []byte) string { - object, err := fidelityJSONObject(raw) - if err != nil { - return "upstream" - } - classification := "upstream" - for _, key := range []string{"code", "type"} { - var code string - // Numeric codes and message prose are deliberately not classifications. - if json.Unmarshal(object[key], &code) != nil { - continue - } - switch code { - case "authentication_error", "invalid_api_key", "unauthorized", "permission_denied", "access_denied", "forbidden", "invalid_authentication", "account_deactivated": - return "auth" - case "insufficient_quota", "quota_exceeded", "budget_exceeded", "billing_hard_limit_reached", "billing_not_active", "usage_limit_reached": - classification = "quota" - case "rate_limit_exceeded", "rate_limit_error", "rate_limit_reached", "too_many_requests": - if classification != "quota" { - classification = "rate_limit" - } - } - } - return classification -} - -func fidelityNonNull(raw []byte) bool { - return len(raw) != 0 && !bytes.Equal(bytes.TrimSpace(raw), []byte("null")) -} - -// Reject duplicate object keys and trailing JSON values for fields whose -// semantics the harness consumes. Unknown values remain untouched RawMessages. -func fidelityJSONObject(data []byte) (map[string]json.RawMessage, error) { - decoder := json.NewDecoder(bytes.NewReader(data)) - token, err := decoder.Token() - if err != nil || token != json.Delim('{') { - return nil, errors.New("expected JSON object") - } - object := make(map[string]json.RawMessage) - for decoder.More() { - token, err := decoder.Token() - if err != nil { - return nil, err - } - key, ok := token.(string) - if !ok { - return nil, errors.New("invalid object key") - } - if _, duplicate := object[key]; duplicate { - return nil, errors.New("duplicate object key") - } - var raw json.RawMessage - if err := decoder.Decode(&raw); err != nil { - return nil, err - } - object[key] = raw - } - if _, err := decoder.Token(); err != nil { - return nil, err - } - if _, err := decoder.Token(); err != io.EOF { - return nil, errors.New("trailing JSON data") - } - return object, nil -} - -func fidelityScore(fixture string, text string) bool { - text = strings.TrimSpace(text) - if strings.HasPrefix(text, "```") { - lineEnd := strings.IndexByte(text, '\n') - if lineEnd == -1 { - return false - } - language := strings.TrimSpace(text[3:lineEnd]) - if language != "" && language != "json" { - return false - } - rest := strings.TrimSpace(text[lineEnd+1:]) - lastLine := strings.LastIndexByte(rest, '\n') - if lastLine == -1 || strings.TrimSpace(rest[lastLine+1:]) != "```" { - return false - } - text = strings.TrimSpace(rest[:lastLine]) - } - object, err := fidelityJSONObject([]byte(text)) - if err != nil || len(object) != 3 { - return false - } - switch fixture { - case "single", "pilot": - var count int - var first, last string - return json.Unmarshal(object["count"], &count) == nil && count == 25 && - json.Unmarshal(object["first"], &first) == nil && first == "ABCDFE" && - json.Unmarshal(object["last"], &last) == nil && last == "CBEADF" - case "tool", "tool_final": - var selected []string - var hours, value int - return json.Unmarshal(object["selected"], &selected) == nil && len(selected) == 3 && - selected[0] == "A" && selected[1] == "B" && selected[2] == "E" && - json.Unmarshal(object["hours"], &hours) == nil && hours == 11 && - json.Unmarshal(object["value"], &value) == nil && value == 36 - default: - return false - } -} - -func TestReasoningFidelityFixtures(t *testing.T) { - t.Run("independent permutation truth", func(t *testing.T) { - letters := []byte("ABCDEF") - total := 0 - var valid []string - var enumerate func(int) - enumerate = func(index int) { - if index == len(letters) { - total++ - positions := make(map[byte]int, len(letters)) - for position, letter := range letters { - positions[letter] = position - } - if positions['A'] < positions['D'] && positions['B'] < positions['D'] && - positions['B'] < positions['E'] && positions['C'] < positions['F'] && - positions['D'] < positions['F'] && positions['E']+1 != positions['F'] { - valid = append(valid, string(letters)) - } - return - } - for i := index; i < len(letters); i++ { - letters[index], letters[i] = letters[i], letters[index] - enumerate(index + 1) - letters[index], letters[i] = letters[i], letters[index] - } - } - enumerate(0) - sort.Strings(valid) - if total != 720 || len(valid) != 25 || valid[0] != "ABCDFE" || valid[len(valid)-1] != "CBEADF" { - t.Fatal("independent permutation enumeration disagrees with fixed truth") - } - }) - t.Run("independent order truth", func(t *testing.T) { - var catalog struct { - Orders []struct { - ID string `json:"id"` - Hours int `json:"hours"` - Value int `json:"value"` - } `json:"orders"` - } - if err := json.Unmarshal([]byte(fidelityOrdersResult), &catalog); err != nil || len(catalog.Orders) != 6 { - t.Fatal("invalid order fixture") - } - total, bestHours, bestValue, bestIDs := 0, 0, -1, "" - for mask := 0; mask < 1< 3 || hours > 11 || strings.Contains(joined, "A") && strings.Contains(joined, "D") || - strings.Contains(joined, "E") && !strings.Contains(joined, "B") { - continue - } - if value > bestValue || value == bestValue && (hours < bestHours || hours == bestHours && joined < bestIDs) { - bestIDs, bestHours, bestValue = joined, hours, value - } - } - if total != 64 || bestIDs != "ABE" || bestHours != 11 || bestValue != 36 { - t.Fatal("independent subset enumeration disagrees with fixed truth") - } - }) - t.Run("strict scoring", func(t *testing.T) { - goodSingle := `{"count":25,"first":"ABCDFE","last":"CBEADF"}` - goodTool := `{"selected":["A","B","E"],"hours":11,"value":36}` - for _, text := range []string{goodSingle, "```json\n" + goodSingle + "\n```", "```\n" + goodSingle + "\n```"} { - if !fidelityScore("single", text) { - t.Fatal("valid single score rejected") - } - } - if !fidelityScore("tool", goodTool) || !fidelityScore("tool_final", goodTool) || !fidelityScore("pilot", goodSingle) { - t.Fatal("valid tool score rejected") - } - for _, text := range []string{ - `25 ABCDFE CBEADF`, `{"count":25}`, goodSingle + `{}`, "answer: " + goodSingle, - `{"count":24,"count":25,"first":"ABCDFE","last":"CBEADF"}`, - `{"count":25,"first":"ABCDFE","last":"CBEADF","extra":true}`, - `{"count":25.0,"first":"ABCDFE","last":"CBEADF"}`, - "```python\n" + goodSingle + "\n```", "```json\n" + goodSingle, - } { - if fidelityScore("single", text) { - t.Fatal("partial or ambiguous single score accepted") - } - } - for _, text := range []string{`{"selected":["A","B","E"]}`, `{"selected":["E","B","A"],"hours":11,"value":36}`, `{"selected":["A","B","E"],"hours":11,"value":35}`} { - if fidelityScore("tool", text) { - t.Fatal("invalid tool score accepted") - } - } - }) - t.Run("json raw output and usage", func(t *testing.T) { - reasoning := `{ "type":"reasoning", "status":"completed", "encrypted_content":"opaque-test-only", "summary":[], "future":{"preserve":true} }` - answer := `{"type":"message","role":"assistant","phase":"final_answer","content":[{"type":"output_text","text":"{\"count\":25,\"first\":\"ABCDFE\",\"last\":\"CBEADF\"}"}]}` - body := `{"id":"resp_test","model":"test-model","status":"completed","output":[` + reasoning + `,` + answer + `],"usage":{"input_tokens":7,"output_tokens":9,"total_tokens":16,"input_tokens_details":{"cached_tokens":0},"output_tokens_details":{"reasoning_tokens":5}}}` - got := fidelityParseResponse([]byte(body), "application/json", 200) - if got.Status != "completed" || got.HasError || got.HasIncomplete || !got.ReasoningComplete || got.ID != "resp_test" || got.Model != "test-model" || !fidelityScore("single", got.Text) { - t.Fatal("completed JSON response lost meaning") - } - if len(got.Output) != 2 || string(got.Output[0]) != reasoning || len(got.RawReasoning) != 1 || string(got.RawReasoning[0]) != reasoning { - t.Fatal("raw output was changed") - } - if got.Usage == nil || got.Usage.InputTokens == nil || *got.Usage.InputTokens != 7 || got.Usage.OutputTokens == nil || *got.Usage.OutputTokens != 9 || - got.Usage.CachedTokens == nil || *got.Usage.CachedTokens != 0 || got.Usage.ReasoningTokens == nil || *got.Usage.ReasoningTokens != 5 || got.Usage.TotalTokens == nil || *got.Usage.TotalTokens != 16 { - t.Fatal("usage fields not preserved") - } - }) - t.Run("usage unknown is not zero", func(t *testing.T) { - for _, suffix := range []string{"", `,"usage":null`} { - got := fidelityParseResponse([]byte(`{"status":"completed","output":[]`+suffix+`}`), "application/json", 200) - if got.Usage != nil { - t.Fatal("absent usage synthesized") - } - } - got := fidelityParseResponse([]byte(`{"status":"completed","output":[],"usage":{"input_tokens":0,"output_tokens_details":{},"input_tokens_details":null}}`), "application/json", 200) - if got.Usage == nil || got.Usage.InputTokens == nil || *got.Usage.InputTokens != 0 || got.Usage.OutputTokens != nil || got.Usage.ReasoningTokens != nil || got.Usage.CachedTokens != nil || got.Usage.TotalTokens != nil { - t.Fatal("unknown usage confused with measured zero") - } - }) - t.Run("sse final raw or ordered fallback", func(t *testing.T) { - first := `{"type":"reasoning","encrypted_content":"opaque-test-only","summary":[]}` - second := `{"type":"message","role":"assistant","phase":"final_answer","content":[{"type":"output_text","text":"answer"}],"future":7}` - prefix := "event: response.output_item.done\ndata: {\"type\":\"response.output_item.done\",\"output_index\":1,\"item\":" + second + "}\n\n" + - "data: {\"type\":\"response.output_item.done\",\"output_index\":0,\"item\":" + first + "}\n\n" - for _, newline := range []string{"\n", "\r\n", "\r"} { - body := prefix + "event: response.completed\ndata: {\"type\":\"response.completed\",\ndata: \"response\":{\"status\":\"completed\",\"id\":\"resp_test\"}}\n\ndata: [DONE]\n\n" - got := fidelityParseResponse([]byte(strings.ReplaceAll(body, "\n", newline)), "text/event-stream", 200) - if got.Status != "completed" || got.HasError || got.HasIncomplete || got.Text != "answer" || !got.ReasoningComplete || len(got.Output) != 2 || string(got.Output[0]) != first || string(got.Output[1]) != second { - t.Fatal("SSE indexed fallback lost raw ordered output") - } - } - body := prefix + "data: {\"type\":\"response.completed\",\"response\":{\"status\":\"completed\",\"output\":[]}}\n\n" - got := fidelityParseResponse([]byte(body), "text/event-stream", 200) - if got.Status != "completed" || len(got.Output) != 0 || got.ReasoningComplete { - t.Fatal("explicit final output was overridden by older events") - } - }) - t.Run("sse announced items must finish", func(t *testing.T) { - added := "data: {\"type\":\"response.output_item.added\",\"output_index\":0,\"item\":{\"type\":\"reasoning\",\"id\":\"rs_test\",\"status\":\"in_progress\",\"summary\":[]}}\n\n" - done := "data: {\"type\":\"response.output_item.done\",\"output_index\":0,\"item\":{\"type\":\"reasoning\",\"id\":\"rs_test\",\"status\":\"completed\",\"encrypted_content\":\"opaque-test-only\",\"summary\":[]}}\n\n" - terminal := "data: {\"type\":\"response.completed\",\"response\":{\"status\":\"completed\"}}\n\n" - got := fidelityParseResponse([]byte(added+done+terminal), "text/event-stream", 200) - if got.Status != "completed" || got.HasIncomplete || got.HasError || !got.ReasoningComplete { - t.Fatal("completed announced item was not accepted") - } - unfinishedTool := "data: {\"type\":\"response.output_item.added\",\"output_index\":1,\"item\":{\"type\":\"function_call\",\"status\":\"in_progress\",\"call_id\":\"call_test\",\"name\":\"load_orders\",\"arguments\":\"\"}}\n\n" - got = fidelityParseResponse([]byte(added+done+unfinishedTool+terminal), "text/event-stream", 200) - if got.Status == "completed" || !got.HasIncomplete || !got.HasTool || got.ReasoningComplete { - t.Fatal("unfinished announced tool silently omitted") - } - for _, body := range []string{terminal + done, added + terminal, added + strings.ReplaceAll(done, "rs_test", "rs_other") + terminal} { - got := fidelityParseResponse([]byte(body), "text/event-stream", 200) - if got.Status == "completed" || got.ReasoningComplete { - t.Fatal("post-terminal or mismatched fallback item accepted") - } - } - refusal := "data: {\"type\":\"response.refusal.delta\",\"delta\":\"test-only\"}\n\n" - got = fidelityParseResponse([]byte(refusal+terminal), "text/event-stream", 200) - if !got.HasRefusal { - t.Fatal("observed refusal lost at terminal") - } - }) - t.Run("strict terminal evidence", func(t *testing.T) { - completed := "data: {\"type\":\"response.completed\",\"response\":{\"status\":\"completed\",\"output\":[]}}\n\n" - cases := []struct { - body string - contentType string - }{ - {"", "application/json"}, {`{"status":"in_progress","output":[]}`, "application/json"}, - {`{"status":"completed","output":[],"error":{"code":"test_error"}}`, "application/json"}, - {`{"status":"completed","output":[],"incomplete_details":{"reason":"max_output_tokens"}}`, "application/json"}, - {`{"status":"failed","output":[]}`, "application/json"}, {`{"status":"incomplete","output":[]}`, "application/json"}, - {`{"status":"completed","output":[]} {}`, "application/json"}, - {`{"status":"failed","status":"completed","output":[]}`, "application/json"}, - {"data: [DONE]\n\n", "text/event-stream"}, - {strings.TrimRight(completed, "\n"), "text/event-stream"}, - {"data: {\"type\":\"response.completed\",\"response\":{\"status\":\"in_progress\"}}\n\n", "text/event-stream"}, - {"data: {\"type\":\"error\",\"code\":\"server_error\"}\n\n" + completed, "text/event-stream"}, - {completed + "data: {\"type\":\"error\",\"code\":\"server_error\"}\n\n", "text/event-stream"}, - {completed + completed, "text/event-stream"}, - {"data: {\"type\":\"response.output_item.done\",\"output_index\":1,\"item\":{\"type\":\"reasoning\"}}\n\ndata: {\"type\":\"response.completed\",\"response\":{\"status\":\"completed\"}}\n\n", "text/event-stream"}, - {completed + "data: {", "text/event-stream"}, - } - for index, tc := range cases { - got := fidelityParseResponse([]byte(tc.body), tc.contentType, 200) - if got.Status == "completed" || got.ReasoningComplete || !got.HasError && !got.HasIncomplete { - t.Fatalf("invalid response %d was treated as completed", index) - } - } - }) - t.Run("structured errors only", func(t *testing.T) { - cases := []struct { - status int - body string - class string - }{ - {401, `{"error":{"message":"do not record"}}`, "auth"}, - {403, `not JSON`, "auth"}, {402, `not JSON`, "payment_required"}, - {429, `{"error":{"message":"insufficient_quota budget_exceeded"}}`, "rate_limit"}, - {429, `{"error":{"type":"budget_exceeded","code":"429"}}`, "quota"}, - {200, `{"error":{"code":"insufficient_quota"}}`, "quota"}, - {200, `{"error":{"code":429,"message":"quota_exceeded"}}`, "upstream"}, - {200, `{"error":{"code":"invalid_api_key"}}`, "auth"}, - {401, `{"status":"completed","output":[null]}`, "auth"}, - {200, `{"type":"error","code":"rate_limit_exceeded"}`, "rate_limit"}, - {200, `{"error":{"code":"server_error","message":"invalid_api_key"}}`, "upstream"}, - } - for index, tc := range cases { - got := fidelityParseResponse([]byte(tc.body), "application/json", tc.status) - if !got.HasError || got.Status == "completed" || got.ErrorClass != tc.class { - t.Fatalf("error classification mismatch in case %d", index) - } - } - }) - t.Run("phase refusal tool and incomplete reasoning", func(t *testing.T) { - message := func(phase, text string) string { - encoded, _ := json.Marshal(text) - return fmt.Sprintf(`{"type":"message","role":"assistant","phase":%q,"content":[{"type":"output_text","text":%s}]}`, phase, encoded) - } - answer := `{"count":25,"first":"ABCDFE","last":"CBEADF"}` - output := message("commentary", "not the answer") + "," + message("", "old answer") + "," + message("final_answer", answer) - got := fidelityParseResponse([]byte(`{"status":"completed","output":[`+output+`]}`), "application/json", 200) - if !fidelityScore("single", got.Text) || len(got.Output) != 3 { - t.Fatal("phase selection polluted scoring or dropped replay output") - } - output = message("commentary", "not the answer") + "," + message("", answer) - got = fidelityParseResponse([]byte(`{"status":"completed","output":[`+output+`]}`), "application/json", 200) - if !fidelityScore("single", got.Text) { - t.Fatal("commentary polluted unphased final answer") - } - got = fidelityParseResponse([]byte(`{"status":"completed","output":[{"type":"reasoning","status":"in_progress","encrypted_content":"test-only"},{"type":"function_call","call_id":"call_test","name":"load_orders","arguments":"{}"},{"type":"message","role":"assistant","content":[{"type":"refusal","refusal":"test-only"}]}]}`), "application/json", 200) - if got.Status == "completed" || got.ReasoningComplete || !got.HasIncomplete || !got.HasTool || !got.HasRefusal { - t.Fatal("unsafe continuation eligibility") - } - for _, part := range []string{ - `{"type":"refusal","type":"output_text","text":"{\"count\":25,\"first\":\"ABCDFE\",\"last\":\"CBEADF\"}"}`, - `{"type":"output_text","text":"wrong","text":"{\"count\":25,\"first\":\"ABCDFE\",\"last\":\"CBEADF\"}"}`, - } { - got := fidelityParseResponse([]byte(`{"status":"completed","output":[{"type":"message","role":"assistant","content":[`+part+`]}]}`), "application/json", 200) - if !got.HasError || got.Status == "completed" || fidelityScore("single", got.Text) { - t.Fatal("ambiguous content object accepted") - } - } - }) - t.Run("bounded input and invalid usage", func(t *testing.T) { - got := fidelityParseResponse(bytes.Repeat([]byte{' '}, fidelityMaxResponseBytes+1), "application/json", 200) - if !got.HasError || got.ErrorClass != "body_limit" || got.Status == "completed" { - t.Fatal("response body limit ignored") - } - for _, value := range []string{`-1`, `1.2`, `"9"`, `9223372036854775808`} { - got := fidelityParseResponse([]byte(`{"status":"completed","output":[],"usage":{"input_tokens":`+value+`}}`), "application/json", 200) - if !got.HasError || got.Status == "completed" { - t.Fatal("invalid usage accepted") - } - } - }) -} diff --git a/backend/internal/service/reasoning_fidelity_support_test.go b/backend/internal/service/reasoning_fidelity_support_test.go deleted file mode 100644 index 1d44c109fce8..000000000000 --- a/backend/internal/service/reasoning_fidelity_support_test.go +++ /dev/null @@ -1,125 +0,0 @@ -//go:build reasoning_fidelity - -package service - -import ( - "context" - "errors" - "net/http" - - "github.com/Wei-Shaw/sub2api/internal/config" - "github.com/Wei-Shaw/sub2api/internal/pkg/ctxkey" - "github.com/gin-gonic/gin" - "github.com/tidwall/gjson" -) - -// ReasoningFidelityGatewayForTest exposes only in-memory dependency wiring to -// the external test package. This file is excluded from application builds. -// No repository that can write account, billing, or session state is attached. -func ReasoningFidelityGatewayForTest(cfg *config.Config, upstream HTTPUpstream, prompts *SystemPromptConfig, settings map[string]string) (*OpenAIGatewayService, error) { - svc := NewOpenAIGatewayService(nil, nil, nil, nil, nil, nil, nil, cfg, - nil, nil, nil, nil, nil, upstream, nil, nil, nil, nil, nil, nil, nil, nil) - // Runtime settings use the same getters against an immutable, read-only - // snapshot. Missing rows retain their real defaults, not invented values. - frozen := &reasoningFidelitySettingStore{values: make(map[string]string, len(settings))} - for key, value := range settings { - frozen.values[key] = value - } - svc.settingService = NewSettingService(frozen, cfg) - recovery, err := ProvideReasoningRecoveryService(frozen) - if err != nil { - return nil, errors.New("invalid_frozen_reasoning_recovery") - } - svc.SetReasoningRecoveryService(recovery) - if prompts != nil { - normalized, err := normalizeSystemPromptConfig(*prompts) - if err != nil { - return nil, errors.New("invalid_frozen_prompt") - } - policy := &SystemPromptService{} - policy.publish(normalized) - svc.SetSystemPromptService(policy) - } - return svc, nil -} - -type reasoningFidelitySettingStore struct{ values map[string]string } - -func (r *reasoningFidelitySettingStore) Get(_ context.Context, key string) (*Setting, error) { - value, ok := r.values[key] - if !ok { - return nil, ErrSettingNotFound - } - return &Setting{Key: key, Value: value}, nil -} -func (r *reasoningFidelitySettingStore) GetValue(_ context.Context, key string) (string, error) { - value, ok := r.values[key] - if !ok { - return "", ErrSettingNotFound - } - return value, nil -} -func (r *reasoningFidelitySettingStore) GetMultiple(_ context.Context, keys []string) (map[string]string, error) { - result := map[string]string{} - for _, key := range keys { - if value, ok := r.values[key]; ok { - result[key] = value - } - } - return result, nil -} -func (r *reasoningFidelitySettingStore) GetAll(context.Context) (map[string]string, error) { - result := map[string]string{} - for key, value := range r.values { - result[key] = value - } - return result, nil -} -func (*reasoningFidelitySettingStore) Set(context.Context, string, string) error { - return errors.New("diagnostic_read_only") -} -func (*reasoningFidelitySettingStore) SetMultiple(context.Context, map[string]string) error { - return errors.New("diagnostic_read_only") -} -func (*reasoningFidelitySettingStore) Delete(context.Context, string) error { - return errors.New("diagnostic_read_only") -} - -// ReasoningFidelityContextForTest reproduces the trusted group policy context -// without authentication, scheduling, or a production API key. -func ReasoningFidelityContextForTest(ctx context.Context, c *gin.Context, group *Group, policy *OpenAIFastPolicySettings, userID int64, effort string) context.Context { - if policy == nil { - policy = DefaultOpenAIFastPolicySettings() - } - ctx = context.WithValue(ctx, ctxkey.Group, group) - ctx = context.WithValue(ctx, ctxkey.UserID, userID) - ctx = withOpenAIFastPolicyContext(ctx, policy) - ctx = WithRequestedReasoningEffort(ctx, effort) - ctx = WithOpenAIReasoningEffortPolicy(ctx, group.MaxReasoningEffort, group.ReasoningEffortMappings, group.MaxReasoningEffortOverLimit) - c.Request = c.Request.WithContext(ctx) - c.Set("api_key", &APIKey{ID: -1, UserID: userID, GroupID: &group.ID, Group: group}) - SetOpenAIClientTransport(c, OpenAIClientTransportHTTP) - return ctx -} - -// ReasoningFidelityDirectRequestForTest bypasses Forward's protocol/body -// transformations while using the same admin policy, endpoint construction, -// authentication and configured header overrides as the gateway arm. -func ReasoningFidelityDirectRequestForTest(ctx context.Context, svc *OpenAIGatewayService, c *gin.Context, account *Account, body []byte) (*http.Request, error) { - model := gjson.GetBytes(body, "model").String() - body = ReplaceModelInBody(body, account.GetMappedModel(model)) - var err error - // The native handler applies group effort policy before channel/account - // mapping. The common harness ingress already performs it exactly once for - // both arms; repeating it here could apply a chained mapping twice. - body, err = svc.applyOpenAIFastPolicyToBody(ctx, account, gjson.GetBytes(body, "model").String(), body) - if err != nil { - return nil, err - } - // buildUpstreamRequest applies the same final system prompt as Forward. - token, _, err := svc.GetAccessToken(ctx, account) - if err != nil { - return nil, err - } - return svc.buildUpstreamRequest(ctx, c, account, body, token, true, gjson.GetBytes(body, "prompt_cache_key").String(), false) -} From ba095ac8bf16dbf39893ef899d1a51c7c8ed2433 Mon Sep 17 00:00:00 2001 From: Codex Date: Sun, 4 Oct 2026 13:16:45 +0800 Subject: [PATCH 2/4] docs(reasoning): name every release that still holds the fidelity diagnostic The entry for the deleted reasoning fidelity diagnostic dated the deletion "after v0.2.13-codexrip.1, whose tree still holds it". That names the wrong boundary. v0.2.13-codexrip.2 (bc67df343, the commit this branch is cut from) and v0.2.13-codexrip.3 (c29da309e, main since #237) hold the four files as well, so a reader would take .2 for the first release without them. The sentence now names the three releases whose trees hold the diagnostic and gives the boundary by the merge instead of by a release number: the first release tagged after this change merged is the first without it. That stays true if another release is tagged before the merge. Checked: git ls-tree of the three tags (four files each), git ls-remote for the tags and main, git diff --check. Co-Authored-By: Claude Opus 5.5 --- .downstream/upstream-review-v0.2.13.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.downstream/upstream-review-v0.2.13.md b/.downstream/upstream-review-v0.2.13.md index adebbf00109c..9bb976fc6ae3 100644 --- a/.downstream/upstream-review-v0.2.13.md +++ b/.downstream/upstream-review-v0.2.13.md @@ -46,7 +46,7 @@ Every official commit is adopted with its official behaviour. Where upstream and - The rejected-cipher memory runs on one long-lived Redis client of its own, built with the gateway cache from the shared options: every read and write ends at the deadline of the operation's 100 ms budget, a refused dial fails the operation at once, and application cleanup closes the client before the shared one. - The stop reason `policy_unavailable` is gone on both sides: nothing produces it since recovery became a direct call, the continuation diagnostic no longer accepts it, and the Ops error detail has no text for it. The text for `disabled` there names the switch, the account type and the request path instead of a per-account scope. - The dated live recovery diagnostic is deleted (owner decision of 2026-10-04). It was the fork side of one sealed study of Chat reasoning replay and signature recovery (run `reasoning-replay-recovery-20260907`): a broker outside this repository ran one pinned binary once and passed only when the Chat replay cases completed, and those cases no longer exist. Deleted here: the live test, its offline control test and its support file, and the second command of `.github/scripts/test_reasoning_recovery.sh`. The broker scripts and their command entry outside this repository are deleted with it; the study's ledger on the server is not touched. - - The reasoning fidelity diagnostic of the same date is deleted as well (owner decision of 2026-10-04), after `v0.2.13-codexrip.1`, whose tree still holds it. It was the fork side of a second sealed study (run `reasoning-fidelity-20260907`), driven by a broker outside this repository: four test files of `service` under the `reasoning_fidelity` build tag, namely the live test, its two offline tests (the control test, and the response parser with its fixtures) and the support file with the frozen gateway helper the recovery diagnostic had shared. For this release that helper was made to load the recovery switch from its frozen settings snapshot, and once the second command of the gate was gone no CI step compiled the four files. Nothing outside them used what they declared, and no file carries the build tag now. The broker scripts outside this repository are deleted with it; the study's run directory and ledger on the server are not touched. The continuation-repair live test still refuses to start while `SUB2API_REASONING_FIDELITY_LIVE` is set; it belongs to a sealed study of its own and is unchanged. + - The reasoning fidelity diagnostic of the same date is deleted as well (owner decision of 2026-10-04), by a later change of its own: the trees of `v0.2.13-codexrip.1`, `v0.2.13-codexrip.2` and `v0.2.13-codexrip.3` still hold it, and the first release tagged after that change merged is the first without it. It was the fork side of a second sealed study (run `reasoning-fidelity-20260907`), driven by a broker outside this repository: four test files of `service` under the `reasoning_fidelity` build tag, namely the live test, its two offline tests (the control test, and the response parser with its fixtures) and the support file with the frozen gateway helper the recovery diagnostic had shared. For this release that helper was made to load the recovery switch from its frozen settings snapshot, and once the second command of the gate was gone no CI step compiled the four files. Nothing outside them used what they declared, and no file carries the build tag now. The broker scripts outside this repository are deleted with it; the study's run directory and ledger on the server are not touched. The continuation-repair live test still refuses to start while `SUB2API_REASONING_FIDELITY_LIVE` is set; it belongs to a sealed study of its own and is unchanged. - Console theme: the page joins the selectors of the 系统提示词 rules through its own static `data-ui="reasoning-recovery"` hook (`pages/settings-ops-public.css`, and the narrow-screen heading rule of `pages/user.css`); it has no rule of its own. - **Console theme for the new upstream UI.** Behaviour is upstream's; only styling was added under `frontend/src/styles/console`: the eleventh platform segment of the account dialog takes the `sky` identity glyph (`controls.css` 7.1a); the priority control sits as the value of the stacked priority line with label-toned steppers (`pages/accounts.css` 5, reached through the component's own static `data-testid` attributes); the mode toggle of `RechargeBonusTierEditor` carries the static `data-ui="seg-box"` hook. No `:has()`, no runtime-bound hook. - One existing rule also reaches the new UI without a change: the `RechargeBonusTierEditor` card has the same header and `p-4` body as the payment provider card beside it, so the positional body rule of `pages/settings-ops-public.css` 2.2 gives it the settings card padding. The comment of that rule now names both cards. From 1e02164c29410fa935afe74b7f4e1ac3e8b7b4a0 Mon Sep 17 00:00:00 2001 From: Codex Date: Sun, 4 Oct 2026 16:33:06 +0800 Subject: [PATCH 3/4] docs(reasoning): state the release boundary of the fidelity diagnostic without a list Releases keep being tagged while this change waits; the sentence no longer enumerates them. Co-Authored-By: Claude Opus 5.5 --- .downstream/upstream-review-v0.2.13.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.downstream/upstream-review-v0.2.13.md b/.downstream/upstream-review-v0.2.13.md index 9bb976fc6ae3..709d36025c14 100644 --- a/.downstream/upstream-review-v0.2.13.md +++ b/.downstream/upstream-review-v0.2.13.md @@ -46,7 +46,7 @@ Every official commit is adopted with its official behaviour. Where upstream and - The rejected-cipher memory runs on one long-lived Redis client of its own, built with the gateway cache from the shared options: every read and write ends at the deadline of the operation's 100 ms budget, a refused dial fails the operation at once, and application cleanup closes the client before the shared one. - The stop reason `policy_unavailable` is gone on both sides: nothing produces it since recovery became a direct call, the continuation diagnostic no longer accepts it, and the Ops error detail has no text for it. The text for `disabled` there names the switch, the account type and the request path instead of a per-account scope. - The dated live recovery diagnostic is deleted (owner decision of 2026-10-04). It was the fork side of one sealed study of Chat reasoning replay and signature recovery (run `reasoning-replay-recovery-20260907`): a broker outside this repository ran one pinned binary once and passed only when the Chat replay cases completed, and those cases no longer exist. Deleted here: the live test, its offline control test and its support file, and the second command of `.github/scripts/test_reasoning_recovery.sh`. The broker scripts and their command entry outside this repository are deleted with it; the study's ledger on the server is not touched. - - The reasoning fidelity diagnostic of the same date is deleted as well (owner decision of 2026-10-04), by a later change of its own: the trees of `v0.2.13-codexrip.1`, `v0.2.13-codexrip.2` and `v0.2.13-codexrip.3` still hold it, and the first release tagged after that change merged is the first without it. It was the fork side of a second sealed study (run `reasoning-fidelity-20260907`), driven by a broker outside this repository: four test files of `service` under the `reasoning_fidelity` build tag, namely the live test, its two offline tests (the control test, and the response parser with its fixtures) and the support file with the frozen gateway helper the recovery diagnostic had shared. For this release that helper was made to load the recovery switch from its frozen settings snapshot, and once the second command of the gate was gone no CI step compiled the four files. Nothing outside them used what they declared, and no file carries the build tag now. The broker scripts outside this repository are deleted with it; the study's run directory and ledger on the server are not touched. The continuation-repair live test still refuses to start while `SUB2API_REASONING_FIDELITY_LIVE` is set; it belongs to a sealed study of its own and is unchanged. + - The reasoning fidelity diagnostic of the same date is deleted as well (owner decision of 2026-10-04), by a later change of its own: every release tagged before that change merged still holds it, `v0.2.13-codexrip.1` included, and the first release tagged after it is the first without it. It was the fork side of a second sealed study (run `reasoning-fidelity-20260907`), driven by a broker outside this repository: four test files of `service` under the `reasoning_fidelity` build tag, namely the live test, its two offline tests (the control test, and the response parser with its fixtures) and the support file with the frozen gateway helper the recovery diagnostic had shared. For this release that helper was made to load the recovery switch from its frozen settings snapshot, and once the second command of the gate was gone no CI step compiled the four files. Nothing outside them used what they declared, and no file carries the build tag now. The broker scripts outside this repository are deleted with it; the study's run directory and ledger on the server are not touched. The continuation-repair live test still refuses to start while `SUB2API_REASONING_FIDELITY_LIVE` is set; it belongs to a sealed study of its own and is unchanged. - Console theme: the page joins the selectors of the 系统提示词 rules through its own static `data-ui="reasoning-recovery"` hook (`pages/settings-ops-public.css`, and the narrow-screen heading rule of `pages/user.css`); it has no rule of its own. - **Console theme for the new upstream UI.** Behaviour is upstream's; only styling was added under `frontend/src/styles/console`: the eleventh platform segment of the account dialog takes the `sky` identity glyph (`controls.css` 7.1a); the priority control sits as the value of the stacked priority line with label-toned steppers (`pages/accounts.css` 5, reached through the component's own static `data-testid` attributes); the mode toggle of `RechargeBonusTierEditor` carries the static `data-ui="seg-box"` hook. No `:has()`, no runtime-bound hook. - One existing rule also reaches the new UI without a change: the `RechargeBonusTierEditor` card has the same header and `p-4` body as the payment provider card beside it, so the positional body rule of `pages/settings-ops-public.css` 2.2 gives it the settings card padding. The comment of that rule now names both cards. From 4a74cbd245afb9f03a3eed815795d624d526fa87 Mon Sep 17 00:00:00 2001 From: Codex Date: Sun, 4 Oct 2026 16:53:22 +0800 Subject: [PATCH 4/4] chore(responses): delete the dated continuation-repair diagnostic Owner decision of 2026-10-04, like the reasoning fidelity diagnostic this branch already deletes. The diagnostic was the fork side of one sealed study (run responses-continuation-repair-20260909, in two rounds): two test files of internal/handler under the continuation_repair_live build tag, namely the live test with its three offline tests and the support file with the fixture that built the OpenAI gateway handler with its gateway and Ops services over in-memory stores from a frozen snapshot. No CI step ever built with that tag. Nothing outside the two files used what they declared, every production function they called has other callers, and no file carries the build tag now, so no production code changes. The live test was also the last code that named an environment switch of the dated studies: besides its own, it refused to start while the switch of the fidelity study or of the namespace round-trip study was set. The fork side of that study had left the tree with #206. .downstream/upstream-review-v0.2.13.md: the fidelity entry no longer says that the continuation-repair live test keeps its refusal, a new entry says what this change deletes and what stays (the Ops continuation diagnostic and the ordinary unit tests), and the note on the pinned upstream-risk.json counts the two paths. The history documents of earlier versions and upstream-risk.json still list them. Checked: go build -p 2 ./..., go vet -p 2 -tags unit ./internal/handler, test_upstream_automation_contract.py (16 tests), git diff --check, and a search of the tree for the build tag, the deleted names and the environment switches. No test was run: the two files were the only ones compiled under the tag. Co-Authored-By: Claude Opus 5.5 --- .downstream/upstream-review-v0.2.13.md | 5 +- .../continuation_repair_live_support_test.go | 470 ------ .../handler/continuation_repair_live_test.go | 1483 ----------------- 3 files changed, 3 insertions(+), 1955 deletions(-) delete mode 100644 backend/internal/handler/continuation_repair_live_support_test.go delete mode 100644 backend/internal/handler/continuation_repair_live_test.go diff --git a/.downstream/upstream-review-v0.2.13.md b/.downstream/upstream-review-v0.2.13.md index 709d36025c14..8a1501ae1fcf 100644 --- a/.downstream/upstream-review-v0.2.13.md +++ b/.downstream/upstream-review-v0.2.13.md @@ -46,7 +46,8 @@ Every official commit is adopted with its official behaviour. Where upstream and - The rejected-cipher memory runs on one long-lived Redis client of its own, built with the gateway cache from the shared options: every read and write ends at the deadline of the operation's 100 ms budget, a refused dial fails the operation at once, and application cleanup closes the client before the shared one. - The stop reason `policy_unavailable` is gone on both sides: nothing produces it since recovery became a direct call, the continuation diagnostic no longer accepts it, and the Ops error detail has no text for it. The text for `disabled` there names the switch, the account type and the request path instead of a per-account scope. - The dated live recovery diagnostic is deleted (owner decision of 2026-10-04). It was the fork side of one sealed study of Chat reasoning replay and signature recovery (run `reasoning-replay-recovery-20260907`): a broker outside this repository ran one pinned binary once and passed only when the Chat replay cases completed, and those cases no longer exist. Deleted here: the live test, its offline control test and its support file, and the second command of `.github/scripts/test_reasoning_recovery.sh`. The broker scripts and their command entry outside this repository are deleted with it; the study's ledger on the server is not touched. - - The reasoning fidelity diagnostic of the same date is deleted as well (owner decision of 2026-10-04), by a later change of its own: every release tagged before that change merged still holds it, `v0.2.13-codexrip.1` included, and the first release tagged after it is the first without it. It was the fork side of a second sealed study (run `reasoning-fidelity-20260907`), driven by a broker outside this repository: four test files of `service` under the `reasoning_fidelity` build tag, namely the live test, its two offline tests (the control test, and the response parser with its fixtures) and the support file with the frozen gateway helper the recovery diagnostic had shared. For this release that helper was made to load the recovery switch from its frozen settings snapshot, and once the second command of the gate was gone no CI step compiled the four files. Nothing outside them used what they declared, and no file carries the build tag now. The broker scripts outside this repository are deleted with it; the study's run directory and ledger on the server are not touched. The continuation-repair live test still refuses to start while `SUB2API_REASONING_FIDELITY_LIVE` is set; it belongs to a sealed study of its own and is unchanged. + - The reasoning fidelity diagnostic of the same date is deleted as well (owner decision of 2026-10-04), by a later change: every release tagged before that change merged still holds it, `v0.2.13-codexrip.1` included, and the first release tagged after it is the first without it. It was the fork side of a second sealed study (run `reasoning-fidelity-20260907`), driven by a broker outside this repository: four test files of `service` under the `reasoning_fidelity` build tag, namely the live test, its two offline tests (the control test, and the response parser with its fixtures) and the support file with the frozen gateway helper the recovery diagnostic had shared. For this release that helper was made to load the recovery switch from its frozen settings snapshot, and once the second command of the gate was gone no CI step compiled the four files. Nothing outside them used what they declared, and no file carries the build tag now. The broker scripts outside this repository are deleted with it; the study's run directory and ledger on the server are not touched. + - The continuation-repair diagnostic of 2026-09-09 is deleted by the same change (owner decision of 2026-10-04), so the same boundary holds for it. It was the fork side of a sealed study of its own (run `responses-continuation-repair-20260909`, in two rounds), driven by a broker outside this repository: two test files of `handler` under the `continuation_repair_live` build tag, namely the live test with its three offline tests and the support file with the fixture that built the OpenAI gateway handler with its gateway and Ops services over in-memory stores from a frozen snapshot. No CI step ever built with that tag. Nothing outside the two files used what they declared, every production function they called has other callers, and no file carries the build tag now. The live test was also the last code that named an environment switch of the dated studies: besides its own, it refused to start while the switch of the fidelity study or of the namespace round-trip study of 2026-09-08 was set. The fork side of that study, four test files of `service`, had left the tree with #206. The Ops continuation diagnostic and the unit tests of continuation, recovery and the namespace round trip are production code and ordinary tests; they were not part of the studies and are not changed. The broker scripts of both studies outside this repository are deleted with it; their run directories and ledgers on the server are not touched. - Console theme: the page joins the selectors of the 系统提示词 rules through its own static `data-ui="reasoning-recovery"` hook (`pages/settings-ops-public.css`, and the narrow-screen heading rule of `pages/user.css`); it has no rule of its own. - **Console theme for the new upstream UI.** Behaviour is upstream's; only styling was added under `frontend/src/styles/console`: the eleventh platform segment of the account dialog takes the `sky` identity glyph (`controls.css` 7.1a); the priority control sits as the value of the stacked priority line with label-toned steppers (`pages/accounts.css` 5, reached through the component's own static `data-testid` attributes); the mode toggle of `RechargeBonusTierEditor` carries the static `data-ui="seg-box"` hook. No `:has()`, no runtime-bound hook. - One existing rule also reaches the new UI without a change: the `RechargeBonusTierEditor` card has the same header and `p-4` body as the payment provider card beside it, so the positional body rule of `pages/settings-ops-public.css` 2.2 gives it the settings card padding. The comment of that rule now names both cards. @@ -141,7 +142,7 @@ Every official commit is adopted with its official behaviour. Where upstream and - **Frontend.** `vue-tsc --noEmit` passes. ESLint passes on the 48 changed frontend files. Vitest (`--no-cache`) passes for 13 files with 279 tests: the eleven specs the follow-ups changed (`BulkEditAccountModal`, `CreateAccountModal`, `EditAccountModal`, `AccountActionMenu.spark_shadow`, `AccountBulkActionsBar`, `AppSidebar`, `AppSidebar.simple-mode`, `useModelContextCapacities`, `AccountsView.selectionScope`, `ReasoningRecoveryView`, `groupsProfitControl`), locale key completeness, and `CodexRuntime.native`, which mounts the continuation diagnostics. These ran against a local dependency tree that holds axios 1.18.1 and dompurify 3.3.1, while the lockfile pins 1.20.0 and 3.4.14. - **推理恢复 on both sides.** The page's client and its spec use `GET` and `PUT` on `/admin/reasoning-recovery` under the `/api/v1` base with `{"enabled": }` and read the answer through the envelope the API client unwraps. The handler test pins the path `/api/v1/admin/reasoning-recovery`, the envelope `{"code":0,"message":"success","data":{"enabled": …}}` and the 400 for any other body; the route test pins administrator authentication without step-up, and the page uses none. - **Workflow references.** Every spec and script `.github/workflows/downstream-verify.yml` names exists. Its `Reasoning recovery contracts` step runs the renamed gate script, and its vitest list names `ReasoningRecoveryView.spec.ts` in place of the two deleted reasoning specs. The name filters of its Go steps select what they selected before, less the deleted tests (the retired settings test of the refusal runtime and the three scheduler cache projection tests of the per-account options); the two renamed Alpha Search tests still match. The critical list of the root `Makefile` no longer names the Cindy spec that #206 deleted. `test_upstream_automation_contract.py` (16 tests), `test_upstream_risk.py` (4) and `test_release_preflight.py` (12) pass. -- `.downstream/upstream-risk.json` is unchanged. It is computed from the two upstream tags and the downstream commit of the integration (`291d25e7f`: 157 upstream paths, 65 overlapping, 31 critical, 1420 downstream paths), so it also lists files the follow-ups delete and the four files of the reasoning fidelity diagnostic, deleted after the release. The gate that recomputes it (`.github/workflows/upstream-risk-gate.yml`) runs for pull requests from `sync/upstream-*` branches and on a manual dispatch for a pull request number. By reading, it recomputes from the tags and the downstream commit the manifest records, not from the head of the pull request, so a run dispatched for the follow-ups reproduces this manifest, deleted files included, and then requires the `upstream-reviewed` label because the class is `review_required`. +- `.downstream/upstream-risk.json` is unchanged. It is computed from the two upstream tags and the downstream commit of the integration (`291d25e7f`: 157 upstream paths, 65 overlapping, 31 critical, 1420 downstream paths), so it also lists files the follow-ups delete and the six files of the two diagnostics deleted after the release (four of the reasoning fidelity diagnostic, two of the continuation-repair diagnostic). The gate that recomputes it (`.github/workflows/upstream-risk-gate.yml`) runs for pull requests from `sync/upstream-*` branches and on a manual dispatch for a pull request number. By reading, it recomputes from the tags and the downstream commit the manifest records, not from the head of the pull request, so a run dispatched for the follow-ups reproduces this manifest, deleted files included, and then requires the `upstream-reviewed` label because the class is `review_required`. - **Changed after the runs above.** Five changes followed the runs above: the comment headers of 266 and 267 now cite the previous release (`v0.2.11-codexrip.9`) and its line numbers, with no statement changed, so the migration tests applied the same SQL; `TestOpenAIReasoningRecoveryChatToolRoundKeepsNoReasoningState` lost its last assertion, a search of the gin context for a key only the deleted replay set; the zh label of the recovery line in the Ops continuation diagnostics is 推理恢复, the name the reason text of the same panel already used; the live recovery diagnostic and the second command of the gate were deleted; and the frozen gateway helper of the reasoning fidelity diagnostic was made to load the recovery switch from its frozen snapshot. The zh texts of the 推理恢复 page are the ones the owner approved in the preview of 2026-10-03. Run on the final tree: `go build ./...`; `go vet` on `service` with the `unit` tag; `.github/scripts/test_reasoning_recovery.sh`, which passes with its one remaining command; `test_upstream_automation_contract.py`; `vue-tsc --noEmit`; ESLint on the two changed frontend files; and vitest (`--no-cache`) for six files with 20 tests (`ReasoningRecoveryView`, locale key completeness, locale message compile, locale key collision, `CodexRuntime.native`, `OpsErrorDetailModal`). The fifth change was covered on that tree by `go vet` on `service` with the diagnostic's build tag and by the diagnostic's two offline tests; the diagnostic was deleted after the release (see Integration decisions), and that change and both checks went with it. - **Checked for the integration before it merged as #232 and not repeated here:** the build for `GOOS=linux GOARCH=amd64`; `go vet` plain, with the `e2e` tag and on the packages the follow-ups do not touch; `go generate ./ent`; the TypeSafe, System One, recharge bonus, security fix and platform list tests; the vitest specs of the new upstream UI. The previews the owner saw on 2026-10-03 (the new upstream UI under the console theme, the profit control section, 推理恢复, the TypeSafe dialogs) were captured from the separate branches against a mock API; the captures are not part of the repository. - **Not run:** full suites, race tests, any other integration test, the production migration path against a database in the state of `v0.2.11-codexrip.9` (read only, see Migrations), a browser preview of the assembled tree, an owner preview of two zh texts of the Ops continuation diagnostics written after 2026-10-03 (the label 推理恢复 and the reason shown for `disabled`), real model or TypeSafe calls, deploy. Source integration does not claim a successful deployment. diff --git a/backend/internal/handler/continuation_repair_live_support_test.go b/backend/internal/handler/continuation_repair_live_support_test.go deleted file mode 100644 index 27dd440f6321..000000000000 --- a/backend/internal/handler/continuation_repair_live_support_test.go +++ /dev/null @@ -1,470 +0,0 @@ -//go:build continuation_repair_live - -package handler - -import ( - "context" - "encoding/json" - "errors" - "reflect" - "strings" - "sync" - "sync/atomic" - "time" - - "github.com/Wei-Shaw/sub2api/internal/config" - "github.com/Wei-Shaw/sub2api/internal/pkg/ctxkey" - middleware2 "github.com/Wei-Shaw/sub2api/internal/server/middleware" - "github.com/Wei-Shaw/sub2api/internal/service" - "github.com/gin-gonic/gin" -) - -// This fixture exercises the production handler, forwarder and Ops parser, but -// every persistence dependency belongs to this process. Its usage rows prove -// request attribution only: the synthetic balance and projected channel are not -// a production-pricing or live-concurrency acceptance test. -type continuationRepairFixture struct { - handler *OpenAIGatewayHandler - ops *service.OpsService - gateway *service.OpenAIGatewayService - billing *service.BillingCacheService - account *continuationRepairAccountStore - usage *continuationRepairUsageStore - key *service.APIKey -} - -func continuationRepairBuildFixture(ctx context.Context, cfg *config.Config, source continuationRepairSource, upstream service.HTTPUpstream) (*continuationRepairFixture, error) { - if cfg == nil || upstream == nil || source.Account.ID != 16050 || source.Group.ID != 35 || source.UserID != 920000016050 || source.FastPolicy == nil { - return nil, errors.New("invalid_fixture_source") - } - // Never switch an enabled source policy off to make this narrow test proceed. - if source.SystemPrompts != nil && source.SystemPrompts.Enabled { - return nil, errors.New("unsupported_source_policy") - } - encoded, err := json.Marshal(source) - if err != nil { - return nil, errors.New("invalid_fixture_source") - } - var frozen continuationRepairSource - if json.Unmarshal(encoded, &frozen) != nil { - return nil, errors.New("invalid_fixture_source") - } - settings := &continuationRepairSettingStore{values: frozen.Settings} - if settings.values == nil { - settings.values = make(map[string]string) - } - fastJSON, err := json.Marshal(frozen.FastPolicy) - if err != nil { - return nil, errors.New("invalid_fast_policy") - } - // FastPolicy is captured separately by the broker. If its backing setting - // was included too, both sources must agree before any model request. - if raw, ok := settings.values[service.SettingKeyOpenAIFastPolicySettings]; ok { - var existing service.OpenAIFastPolicySettings - if strings.TrimSpace(raw) == "" { - existing = *service.DefaultOpenAIFastPolicySettings() - } else if json.Unmarshal([]byte(raw), &existing) != nil { - return nil, errors.New("invalid_fast_policy") - } - if !reflect.DeepEqual(existing, *frozen.FastPolicy) { - return nil, errors.New("fast_policy_snapshot_mismatch") - } - } - settings.values[service.SettingKeyOpenAIFastPolicySettings] = string(fastJSON) - settingService := service.NewSettingService(settings, cfg) - account := &continuationRepairAccountStore{account: frozen.Account, groupID: frozen.Group.ID, temp: make(map[int64]*service.TempUnschedState)} - groups := &continuationRepairGroupStore{group: frozen.Group} - usage := &continuationRepairUsageStore{} - user := &service.User{ID: frozen.UserID, Status: service.StatusActive, Balance: 1000000, Concurrency: 1} - users := &continuationRepairUserStore{user: user} - key := &service.APIKey{ID: 920000016051, UserID: user.ID, User: user, GroupID: &groups.group.ID, Group: &groups.group, Status: service.StatusActive} - billing := service.NewBillingCacheService(nil, users, nil, nil, nil, nil, cfg, nil) - concurrency := service.NewConcurrencyService(&concurrencyCacheMock{ - acquireUserSlotFn: func(context.Context, int64, int, string) (bool, error) { return true, nil }, - acquireAccountSlotFn: func(context.Context, int64, int, string) (bool, error) { return true, nil }, - }) - channels := service.NewChannelService(&continuationRepairChannelStore{group: groups.group, models: frozen.ChannelModels, - imageBridge: frozen.ChannelImageBridge}, groups, nil, nil, nil) - rateLimits := service.NewRateLimitService(account, usage, cfg, nil, account) - rateLimits.SetSettingService(settingService) - rateLimits.SetOpenAIAPIKeyHealthCache(account) - cache := &continuationRepairGatewayCache{sessions: make(map[continuationRepairSessionKey]int64), reasoning: make(map[string]string)} - gateway := service.NewOpenAIGatewayService(account, usage, &continuationRepairBillingStore{users: users, applied: make(map[string]string)}, users, - nil, nil, cache, cfg, nil, concurrency, service.NewBillingService(cfg, nil), rateLimits, billing, upstream, - service.NewDeferredService(account, nil, time.Minute), nil, nil, nil, channels, nil, settingService, nil) - ops := service.NewOpsService(nil, settings, cfg, account, users, concurrency, nil, gateway, nil, nil, nil) - keys := service.NewAPIKeyService(nil, users, groups, nil, nil, nil, cfg) - h := NewOpenAIGatewayHandler(gateway, concurrency, billing, keys, nil, nil, nil, ops, cfg) - return &continuationRepairFixture{handler: h, ops: ops, gateway: gateway, billing: billing, account: account, usage: usage, key: key}, nil -} - -func (f *continuationRepairFixture) decorateContext(c *gin.Context) { - c.Set(string(middleware2.ContextKeyAPIKey), f.key) - c.Set(string(middleware2.ContextKeyUser), middleware2.AuthSubject{UserID: f.key.UserID, Concurrency: f.key.User.Concurrency}) - ctx := context.WithValue(c.Request.Context(), ctxkey.Group, f.key.Group) - ctx = context.WithValue(ctx, ctxkey.UserID, f.key.UserID) - if id := strings.TrimSpace(c.GetHeader("X-Request-ID")); id != "" { - ctx = context.WithValue(ctx, ctxkey.RequestID, id) - c.Set("request_id", id) - } - c.Request = c.Request.WithContext(ctx) - service.SetOpenAIClientTransport(c, service.OpenAIClientTransportHTTP) -} - -func (f *continuationRepairFixture) usageSnapshot() []service.UsageLog { - f.usage.mu.Lock() - defer f.usage.mu.Unlock() - return append([]service.UsageLog(nil), f.usage.rows...) -} - -func (f *continuationRepairFixture) healthChanges() int { return int(f.account.health.Load()) } - -func (f *continuationRepairFixture) close() { - f.billing.Stop() - f.gateway.CloseOpenAIWSPool() -} - -type continuationRepairSettingStore struct{ values map[string]string } - -func (s *continuationRepairSettingStore) Get(ctx context.Context, key string) (*service.Setting, error) { - v, err := s.GetValue(ctx, key) - if err != nil { - return nil, err - } - return &service.Setting{Key: key, Value: v}, nil -} -func (s *continuationRepairSettingStore) GetValue(_ context.Context, key string) (string, error) { - if v, ok := s.values[key]; ok { - return v, nil - } - return "", service.ErrSettingNotFound -} -func (s *continuationRepairSettingStore) GetMultiple(_ context.Context, keys []string) (map[string]string, error) { - out := make(map[string]string) - for _, key := range keys { - if value, ok := s.values[key]; ok { - out[key] = value - } - } - return out, nil -} -func (s *continuationRepairSettingStore) GetAll(context.Context) (map[string]string, error) { - out := make(map[string]string, len(s.values)) - for k, v := range s.values { - out[k] = v - } - return out, nil -} -func (*continuationRepairSettingStore) Set(context.Context, string, string) error { - return errors.New("diagnostic_read_only") -} -func (*continuationRepairSettingStore) SetMultiple(context.Context, map[string]string) error { - return errors.New("diagnostic_read_only") -} -func (*continuationRepairSettingStore) Delete(context.Context, string) error { - return errors.New("diagnostic_read_only") -} - -type continuationRepairGroupStore struct { - service.GroupRepository - group service.Group -} - -func (s *continuationRepairGroupStore) GetByID(_ context.Context, id int64) (*service.Group, error) { - if id != s.group.ID { - return nil, service.ErrGroupNotFound - } - out := s.group - return &out, nil -} -func (s *continuationRepairGroupStore) GetByIDLite(ctx context.Context, id int64) (*service.Group, error) { - return s.GetByID(ctx, id) -} - -// The broker freezes resolved channel model names, not a billing contract. -// Keeping a separate channel projection exercises normal mapping exactly once -// without changing any account mapping or claiming a production channel ID. -type continuationRepairChannelStore struct { - service.ChannelRepository - group service.Group - models map[string]string - imageBridge *bool -} - -func (s *continuationRepairChannelStore) ListAll(context.Context) ([]service.Channel, error) { - channel := service.Channel{ID: -1, Name: "continuation-diagnostic-projection", Status: service.StatusActive, - BillingModelSource: service.BillingModelSourceRequested, GroupIDs: []int64{s.group.ID}, - ModelMapping: map[string]map[string]string{s.group.Platform: s.models}} - if s.imageBridge != nil { - channel.FeaturesConfig = map[string]any{"codex_image_generation_bridge": map[string]any{s.group.Platform: *s.imageBridge}} - } - return []service.Channel{channel}, nil -} -func (s *continuationRepairChannelStore) GetGroupPlatforms(context.Context, []int64) (map[int64]string, error) { - return map[int64]string{s.group.ID: s.group.Platform}, nil -} - -type continuationRepairUserStore struct { - service.UserRepository - mu sync.Mutex - user *service.User -} - -func (s *continuationRepairUserStore) GetByID(_ context.Context, id int64) (*service.User, error) { - s.mu.Lock() - defer s.mu.Unlock() - if id != s.user.ID { - return nil, errors.New("diagnostic_unknown_user") - } - u := *s.user - return &u, nil -} - -type continuationRepairBillingStore struct { - service.UsageBillingRepository - mu sync.Mutex - users *continuationRepairUserStore - applied map[string]string -} - -func (s *continuationRepairBillingStore) Apply(_ context.Context, cmd *service.UsageBillingCommand) (*service.UsageBillingApplyResult, error) { - s.mu.Lock() - defer s.mu.Unlock() - if cmd == nil || cmd.UserID != s.users.user.ID || cmd.APIKeyID != 920000016051 || cmd.AccountID != 16050 { - return nil, errors.New("diagnostic_billing_scope_mismatch") - } - cmd.Normalize() - if prior, ok := s.applied[cmd.RequestID]; ok { - if prior != cmd.RequestFingerprint { - return nil, service.ErrUsageBillingRequestConflict - } - return &service.UsageBillingApplyResult{Applied: false}, nil - } - s.applied[cmd.RequestID] = cmd.RequestFingerprint - s.users.mu.Lock() - s.users.user.Balance -= cmd.BalanceCost - balance := s.users.user.Balance - s.users.mu.Unlock() - return &service.UsageBillingApplyResult{Applied: true, OwnerAccountID: cmd.AccountID, NewBalance: &balance}, nil -} - -type continuationRepairUsageStore struct { - service.UsageLogRepository - mu sync.Mutex - rows []service.UsageLog -} - -func (s *continuationRepairUsageStore) Create(_ context.Context, row *service.UsageLog) (bool, error) { - s.mu.Lock() - defer s.mu.Unlock() - if row == nil || row.AccountID != 16050 || row.UserID != 920000016050 || row.APIKeyID != 920000016051 { - return false, errors.New("diagnostic_usage_scope_mismatch") - } - for _, old := range s.rows { - if old.RequestID == row.RequestID && old.APIKeyID == row.APIKeyID { - return false, nil - } - } - s.rows = append(s.rows, *row) - return true, nil -} - -type continuationRepairAccountStore struct { - service.AccountRepository - account service.Account - groupID int64 - health atomic.Int64 - mu sync.Mutex - temp map[int64]*service.TempUnschedState - fails int64 -} - -func (s *continuationRepairAccountStore) GetByID(_ context.Context, id int64) (*service.Account, error) { - if id != s.account.ID { - return nil, service.ErrAccountNotFound - } - // Forward may enrich its local account view. Do not let that mutate the - // source snapshot or the next request's immutable scheduling input. - raw, err := json.Marshal(s.account) - if err != nil { - return nil, errors.New("invalid_frozen_account") - } - var out service.Account - if json.Unmarshal(raw, &out) != nil { - return nil, errors.New("invalid_frozen_account") - } - return &out, nil -} -func (s *continuationRepairAccountStore) GetByIDs(ctx context.Context, ids []int64) ([]*service.Account, error) { - var rows []*service.Account - for _, id := range ids { - if id == s.account.ID { - row, err := s.GetByID(ctx, id) - if err != nil { - return nil, err - } - rows = append(rows, row) - } - } - return rows, nil -} -func (s *continuationRepairAccountStore) ListByGroup(ctx context.Context, id int64) ([]service.Account, error) { - if id != s.groupID { - return nil, nil - } - return s.ListByPlatform(ctx, s.account.Platform) -} -func (s *continuationRepairAccountStore) ListByPlatform(ctx context.Context, platform string) ([]service.Account, error) { - if platform != s.account.Platform { - return nil, nil - } - row, err := s.GetByID(ctx, s.account.ID) - if err != nil { - return nil, err - } - return []service.Account{*row}, nil -} -func (s *continuationRepairAccountStore) ListSchedulableByPlatform(ctx context.Context, platform string) ([]service.Account, error) { - if !s.account.IsSchedulable() { - return nil, nil - } - return s.ListByPlatform(ctx, platform) -} -func (s *continuationRepairAccountStore) ListSchedulableByGroupIDAndPlatform(ctx context.Context, id int64, platform string) ([]service.Account, error) { - if id != s.groupID { - return nil, nil - } - return s.ListSchedulableByPlatform(ctx, platform) -} -func (s *continuationRepairAccountStore) ListSchedulableUngroupedByPlatform(context.Context, string) ([]service.Account, error) { - return nil, nil -} -func (s *continuationRepairAccountStore) ListModelAvailabilityCandidates(ctx context.Context, groupID *int64, platforms []string, includeGrouped bool) ([]service.Account, error) { - if (groupID == nil && !includeGrouped) || (groupID != nil && *groupID != s.groupID) || !s.account.IsActive() || !s.account.Schedulable { - return nil, nil - } - for _, p := range platforms { - if p == s.account.Platform { - return s.ListByPlatform(ctx, p) - } - } - return nil, nil -} -func (s *continuationRepairAccountStore) SetError(context.Context, int64, string) error { - s.health.Add(1) - return nil -} -func (s *continuationRepairAccountStore) SetSchedulable(context.Context, int64, bool) error { - s.health.Add(1) - return nil -} -func (s *continuationRepairAccountStore) SetRateLimited(context.Context, int64, time.Time) error { - s.health.Add(1) - return nil -} -func (s *continuationRepairAccountStore) SetModelRateLimit(context.Context, int64, string, time.Time, ...string) error { - s.health.Add(1) - return nil -} -func (s *continuationRepairAccountStore) SetOverloaded(context.Context, int64, time.Time) error { - s.health.Add(1) - return nil -} -func (s *continuationRepairAccountStore) SetTempUnschedulable(context.Context, int64, time.Time, string) error { - s.health.Add(1) - return nil -} -func (s *continuationRepairAccountStore) UpdateExtra(context.Context, int64, map[string]any) error { - // Quota/fingerprint telemetry is intentionally not persisted by a fixture. - return nil -} -func (s *continuationRepairAccountStore) UpdateSessionWindow(context.Context, int64, *time.Time, *time.Time, string) error { - return nil -} -func (s *continuationRepairAccountStore) GetTempUnsched(_ context.Context, id int64) (*service.TempUnschedState, error) { - s.mu.Lock() - defer s.mu.Unlock() - return s.temp[id], nil -} -func (s *continuationRepairAccountStore) SetTempUnsched(_ context.Context, id int64, state *service.TempUnschedState) error { - s.health.Add(1) - s.mu.Lock() - defer s.mu.Unlock() - if state != nil { - cp := *state - s.temp[id] = &cp - } - return nil -} -func (s *continuationRepairAccountStore) DeleteTempUnsched(_ context.Context, id int64) error { - s.mu.Lock() - defer s.mu.Unlock() - delete(s.temp, id) - return nil -} -func (s *continuationRepairAccountStore) RecordOpenAIAPIKeyHealthFailure(_ context.Context, _ int64, _ int, threshold int) (int64, bool, error) { - s.health.Add(1) - s.mu.Lock() - defer s.mu.Unlock() - s.fails++ - return s.fails, s.fails >= int64(threshold), nil -} - -type continuationRepairSessionKey struct { - groupID int64 - hash string -} -type continuationRepairGatewayCache struct { - service.GatewayCache - mu sync.Mutex - sessions map[continuationRepairSessionKey]int64 - reasoning map[string]string -} - -func (s *continuationRepairGatewayCache) GetSessionAccountID(_ context.Context, id int64, hash string) (int64, error) { - s.mu.Lock() - defer s.mu.Unlock() - if account, ok := s.sessions[continuationRepairSessionKey{id, hash}]; ok { - return account, nil - } - return 0, service.ErrStickySessionNotFound -} -func (s *continuationRepairGatewayCache) SetSessionAccountID(_ context.Context, groupID int64, hash string, accountID int64, _ time.Duration) error { - s.mu.Lock() - defer s.mu.Unlock() - s.sessions[continuationRepairSessionKey{groupID, hash}] = accountID - return nil -} -func (*continuationRepairGatewayCache) RefreshSessionTTL(context.Context, int64, string, time.Duration) error { - return nil -} -func (s *continuationRepairGatewayCache) DeleteSessionAccountID(_ context.Context, groupID int64, hash string) error { - s.mu.Lock() - defer s.mu.Unlock() - delete(s.sessions, continuationRepairSessionKey{groupID, hash}) - return nil -} -func (s *continuationRepairGatewayCache) DeleteSessionAccountIDIfMatches(_ context.Context, groupID int64, hash string, accountID int64) (bool, error) { - s.mu.Lock() - defer s.mu.Unlock() - key := continuationRepairSessionKey{groupID, hash} - if s.sessions[key] != accountID { - return false, nil - } - delete(s.sessions, key) - return true, nil -} -func (s *continuationRepairGatewayCache) SetReasoningContent(_ context.Context, id, content string, _ time.Duration) error { - s.mu.Lock() - defer s.mu.Unlock() - s.reasoning[id] = content - return nil -} -func (s *continuationRepairGatewayCache) GetReasoningContent(_ context.Context, id string) (string, error) { - s.mu.Lock() - defer s.mu.Unlock() - if value, ok := s.reasoning[id]; ok { - return value, nil - } - return "", service.ErrReasoningContentNotFound -} diff --git a/backend/internal/handler/continuation_repair_live_test.go b/backend/internal/handler/continuation_repair_live_test.go deleted file mode 100644 index b7244767594a..000000000000 --- a/backend/internal/handler/continuation_repair_live_test.go +++ /dev/null @@ -1,1483 +0,0 @@ -//go:build continuation_repair_live - -package handler - -import ( - "bufio" - "bytes" - "context" - "crypto/rand" - "crypto/sha256" - "encoding/hex" - "encoding/json" - "errors" - "flag" - "io" - "log" - "log/slog" - "net/http" - "net/http/httptest" - "net/url" - "os" - "reflect" - "regexp" - "strconv" - "strings" - "sync" - "testing" - "time" - - "github.com/Wei-Shaw/sub2api/internal/config" - "github.com/Wei-Shaw/sub2api/internal/pkg/logger" - "github.com/Wei-Shaw/sub2api/internal/pkg/tlsfingerprint" - "github.com/Wei-Shaw/sub2api/internal/repository" - "github.com/Wei-Shaw/sub2api/internal/service" - "github.com/gin-gonic/gin" -) - -const ( - continuationRepairRunID = "responses-continuation-repair-20260909" - continuationRepairMaxRequests = 4 - continuationRepairMaxAttempts = 6 - continuationRepairMaxBody = 8 << 20 - continuationRepairDuration = 15 * time.Minute - continuationRepairTurnTimeout = 180 * time.Second - continuationRepairTool = "continuation_probe_value" - continuationRepairToolOutput = `{"value":"ALPHA"}` - continuationRepairFinal = "CONTINUATION_OK" -) - -type continuationRepairSource struct { - Account service.Account `json:"account"` - Group service.Group `json:"group"` - FastPolicy *service.OpenAIFastPolicySettings `json:"fast_policy"` - SystemPrompts *service.SystemPromptConfig `json:"system_prompts"` - Settings map[string]string `json:"settings"` - ChannelModels map[string]string `json:"channel_models"` - ChannelImageBridge *bool `json:"channel_image_bridge"` - Fingerprint string `json:"fingerprint"` - UserID int64 `json:"user_id"` -} - -type continuationRepairBootstrap struct { - SchemaVersion int `json:"schema_version"` - Mode string `json:"mode"` - ConfigMode string `json:"config_mode"` - RunID string `json:"run_id"` - SourceSHA string `json:"source_sha"` - ManifestSHA string `json:"manifest_sha256"` - Source continuationRepairSource `json:"source"` -} - -type continuationRepairGrant struct { - Type string `json:"type"` - Scenario string `json:"scenario"` - Turn int `json:"turn"` - Request int `json:"request"` - Slot int `json:"slot"` - Attempt int `json:"attempt"` - AttemptInTurn int `json:"attempt_in_turn"` - Fingerprint string `json:"fingerprint"` - ElapsedMS int64 `json:"elapsed_ms"` -} - -// Every field below is a broker-allowlisted enum, count, boolean, or digest. -// Credential material, request/response bodies, tool IDs, and upstream error -// messages remain in memory and never enter stdout or the persistent ledger. -type continuationRepairIdentity struct { - Scenario string `json:"scenario"` - Turn int `json:"turn"` - Request int `json:"request"` - Model string `json:"model"` - AccountID int64 `json:"account_id"` - WireModel string `json:"wire_model"` -} - -type continuationRepairAttempt struct { - continuationRepairIdentity - Type string `json:"type"` - Slot int `json:"slot"` - AttemptInTurn int `json:"attempt_in_turn"` - UpstreamHTTPStatus int `json:"upstream_http_status"` - SignatureRejection bool `json:"signature_rejection"` - SemanticOutput bool `json:"semantic_output"` - Uncertain bool `json:"uncertain"` - ErrorClass string `json:"error_class"` -} - -type continuationRepairTurnResult struct { - continuationRepairIdentity - Type string `json:"type"` - Status string `json:"status"` - Completed bool `json:"completed"` - Terminal string `json:"terminal"` - Attempts int `json:"attempts"` - ClientHTTPStatus int `json:"client_http_status"` - OpsStatus *int `json:"ops_status"` - ToolCallReceived bool `json:"tool_call_received"` - ToolContractValid bool `json:"tool_contract_valid"` - HistoryPreserved bool `json:"history_preserved"` - HeartbeatNormalized bool `json:"heartbeat_normalized"` - AccountHealthUnchanged bool `json:"account_health_unchanged"` - BillingRows int `json:"billing_rows"` - OpsRows int `json:"ops_rows"` - ErrorClass string `json:"error_class"` - Usage map[string]int64 `json:"usage,omitempty"` - RequestIDSHA string `json:"request_id_sha256,omitempty"` - ResponseIDSHA string `json:"response_id_sha256,omitempty"` -} - -type continuationRepairResponse struct { - Terminal, Status, Model, ID, Text, ErrorClass string - Output []json.RawMessage - Usage map[string]int64 - Completed, Semantic, Signature, Invalid bool -} - -type continuationRepairHarness struct { - boot continuationRepairBootstrap - input *bufio.Reader - output *json.Encoder - ctx context.Context - fixture *continuationRepairFixture - upstream *continuationRepairUpstream - requests int - finished map[string]bool - stop string - sourceHash string -} - -// Normal application builds and CI exclude this file. Even a tagged binary -// must receive one exact test selection, the explicit live gate, sealed source -// stdin, and one fsynced broker grant before each individual upstream POST. -func TestContinuationRepairLive(t *testing.T) { - if os.Getenv("SUB2API_CONTINUATION_REPAIR_LIVE") != "1" { - t.Skip("explicit sealed runner opt-in required") - } - selection := flag.Lookup("test.run") - if selection == nil || selection.Value.String() != "^TestContinuationRepairLive$" || os.Getenv("SUB2API_REASONING_FIDELITY_LIVE") != "" || os.Getenv("SUB2API_NAMESPACE_ROUNDTRIP_LIVE") != "" { - t.Fatal("invalid_live_test_selection") - } - if !regexp.MustCompile(`^/proc/self/fd/[0-9]+$`).MatchString(os.Getenv("CONFIG_FILE")) { - t.Fatal("sealed_production_config_required") - } - protocol := os.Stdout - null, err := os.OpenFile(os.DevNull, os.O_WRONLY, 0) - if err != nil { - t.Fatal("diagnostic_output_initialization_failed") - } - os.Stdout, os.Stderr = null, null - gin.SetMode(gin.ReleaseMode) - gin.DefaultWriter, gin.DefaultErrorWriter = io.Discard, io.Discard - _ = logger.Init(logger.InitOptions{Level: "fatal", Output: logger.OutputOptions{ToStdout: true}}) - logger.SetSink(nil) - log.SetOutput(io.Discard) - slog.SetDefault(slog.New(slog.NewTextHandler(io.Discard, nil))) - out := json.NewEncoder(protocol) - fallback := func(class string) { - _ = out.Encode(map[string]any{"type": "summary", "status": "failed", "requests": 0, "attempts": 0, - "astra_completed": false, "sol_completed": false, "blocked_retries": 0, "error_class": class}) - t.Fail() - } - defer func() { - if recover() != nil { - fallback("handler_failed") - } - }() - if err := continuationRepairRun(bufio.NewReaderSize(os.Stdin, 64<<10), out); err != nil { - class := "handler_failed" - var wireError continuationRepairWireError - if errors.As(err, &wireError) { - class = string(wireError) - } - fallback(class) - } -} - -func continuationRepairRun(in *bufio.Reader, out *json.Encoder) error { - var boot continuationRepairBootstrap - if continuationRepairReadJSON(in, &boot) != nil || continuationRepairValidateSource(boot) != nil { - return errors.New("source_changed") - } - cfg, err := config.LoadForBootstrap() - if err != nil { - return errors.New("source_changed") - } - ctx, cancel := context.WithTimeout(context.Background(), continuationRepairDuration) - defer cancel() - h := &continuationRepairHarness{boot: boot, input: in, output: out, ctx: ctx, finished: make(map[string]bool), sourceHash: continuationRepairHashJSON(boot.Source)} - endpoint, err := continuationRepairEndpoint(&boot.Source.Account) - if err != nil { - return err - } - if os.Getenv("SUB2API_CONTINUATION_REPAIR_VALIDATE_ONLY") == "1" { - if err := continuationRepairPreflight(ctx, cfg, boot.Source); err != nil { - return err - } - return out.Encode(map[string]any{"type": "summary", "status": "validated", "requests": 0, "attempts": 0, - "astra_completed": false, "sol_completed": false, "blocked_retries": 0}) - } - u := &continuationRepairUpstream{h: h, inner: repository.NewHTTPUpstream(cfg), endpoint: endpoint, - authorizationHash: continuationRepairHash([]byte("Bearer " + boot.Source.Account.GetOpenAIProtocolAPIKey()))} - if boot.Source.Account.Proxy != nil { - u.proxy = boot.Source.Account.Proxy.URL() - } - h.upstream = u - h.fixture, err = continuationRepairBuildFixture(ctx, cfg, boot.Source, u) - if err != nil { - return errors.New("source_changed") - } - defer h.fixture.close() - // Use the real Ops capture/parser/sanitization queue, without starting a DB - // writer. The complete fixture owns this fresh, single-test process. - opsErrorLogOnce.Do(func() {}) - opsErrorLogMu.Lock() - opsErrorLogQueue = make(chan opsErrorLogJob, 32) - opsErrorLogMu.Unlock() - if err := out.Encode(map[string]any{"type": "ready", "run_id": boot.RunID, "source_sha": boot.SourceSHA, - "source_sha256": boot.Source.Fingerprint, "endpoint_sha256": continuationRepairHash([]byte(endpoint)), "attempts": 0}); err != nil { - return errors.New("broker_unavailable") - } - for _, scenario := range []string{"astra", "sol"} { - if h.stop != "" || ctx.Err() != nil { - break - } - firstBody := continuationRepairInitialBody(continuationRepairModel(scenario)) - first, result := h.runTurn(scenario, 1, firstBody) - if result.Status != "completed" { - continue - } - secondBody, err := continuationRepairContinuation(firstBody, first, scenario) - if err != nil { - h.stop = "history_invalid" - break - } - _, result = h.runTurn(scenario, 2, secondBody) - h.finished[scenario] = result.Status == "completed" - } - status := "failed" - if h.finished["astra"] && h.finished["sol"] && h.stop == "" { - status = "completed" - } - return out.Encode(h.summary(status)) -} - -func (h *continuationRepairHarness) summary(status string) map[string]any { - result := map[string]any{"type": "summary", "status": status, "requests": h.requests, "attempts": h.upstream.attempts, - "astra_completed": h.finished["astra"], "sol_completed": h.finished["sol"], "blocked_retries": h.upstream.blocked} - if h.stop != "" { - result["error_class"] = h.stop - } - return result -} - -func continuationRepairValidateSource(b continuationRepairBootstrap) error { - if b.SchemaVersion != 1 || b.Mode != "continuation_repair" || b.ConfigMode != "production_env" || b.RunID != continuationRepairRunID || - !regexp.MustCompile(`^[a-f0-9]{40}$`).MatchString(b.SourceSHA) || b.SourceSHA != os.Getenv("CONTINUATION_REPAIR_SOURCE_SHA") || - !regexp.MustCompile(`^[a-f0-9]{64}$`).MatchString(b.ManifestSHA) || !regexp.MustCompile(`^[a-f0-9]{64}$`).MatchString(b.Source.Fingerprint) { - return errors.New("source_changed") - } - a, group := &b.Source.Account, &b.Source.Group - if a.ID != 16050 || a.Name != "白嫖-dmxapi" || a.Platform != service.PlatformOpenAI || a.Type != service.AccountTypeAPIKey || - a.ParentAccountID != nil || a.GetOpenAIProtocolAPIKey() == "" || a.Status != service.StatusActive || !a.Schedulable || a.Concurrency < 1 || - group.ID != 35 || group.Platform != service.PlatformOpenAI || group.Status != service.StatusActive || b.Source.UserID != 920000016050 || - !a.SupportsOpenAIEndpointCapability(service.OpenAIEndpointCapabilityResponses) { - return errors.New("source_changed") - } - member := false - for _, id := range a.GroupIDs { - member = member || id == group.ID - } - if !member || (a.ProxyID == nil) != (a.Proxy == nil) || (a.Proxy != nil && (*a.ProxyID != a.Proxy.ID || !a.Proxy.IsActive() || a.Proxy.IsExpired(time.Now()))) { - return errors.New("source_changed") - } - // Live preflight established that the production prompt is disabled. A - // changed runtime is a new source, not permission to toggle it for a test. - if (b.Source.SystemPrompts != nil && b.Source.SystemPrompts.Enabled) || b.Source.FastPolicy == nil || b.Source.Settings == nil || len(b.Source.ChannelModels) != 2 { - return errors.New("source_changed") - } - for _, scenario := range []string{"astra", "sol"} { - model := continuationRepairModel(scenario) - mapped := b.Source.ChannelModels[model] - if mapped == "" || !continuationRepairSafeWireModel(a.GetMappedModel(mapped)) { - return errors.New("model_mismatch") - } - } - return nil -} - -func continuationRepairEndpoint(account *service.Account) (string, error) { - base := strings.TrimRight(strings.TrimSpace(account.GetOpenAIBaseURL()), "/") - if base == "" { - base = "https://api.openai.com" - } - parsed, err := url.Parse(base) - if err != nil || parsed.Scheme != "https" || parsed.Host == "" || parsed.User != nil || parsed.RawQuery != "" || parsed.Fragment != "" { - return "", errors.New("source_changed") - } - switch parsed.Path { - case "": - parsed.Path = "/v1/responses" - case "/v1": - parsed.Path += "/responses" - case "/v1/responses": - default: - return "", errors.New("source_changed") - } - return parsed.String(), nil -} - -func continuationRepairModel(scenario string) string { - if scenario == "astra" { - return "gpt-6-astra" - } - return "gpt-5.6-sol" -} - -func continuationRepairSafeWireModel(model string) bool { - return model == "gpt-6-astra" || model == "gpt-6-astra-ssvip" || model == "gpt-5.6-sol" -} - -func continuationRepairInitialBody(model string) []byte { - key := make([]byte, 16) - if _, err := rand.Read(key); err != nil { - return nil - } - body, _ := json.Marshal(map[string]any{ - "model": model, "stream": true, "store": false, "reasoning": map[string]any{"effort": "low"}, - "max_output_tokens": 4096, "include": []string{"reasoning.encrypted_content"}, "prompt_cache_key": hex.EncodeToString(key), - "instructions": "This is an isolated continuation protocol check. The only tool is a read-only constant value. Follow its requested call and then the later heartbeat instructions. Do not use any other tool.", - "input": []any{map[string]any{"role": "user", "content": "Call continuation_probe_value exactly once with an empty JSON object. Do not answer yet."}}, - "tools": []any{map[string]any{"type": "function", "name": continuationRepairTool, "description": "Return a fixed constant, without file, shell, network or business actions.", - "parameters": map[string]any{"type": "object", "properties": map[string]any{}, "required": []string{}, "additionalProperties": false}, "strict": true}}, - "tool_choice": map[string]any{"type": "function", "name": continuationRepairTool}, "parallel_tool_calls": false, - }) - return body -} - -func continuationRepairContinuation(initial []byte, response continuationRepairResponse, scenario string) ([]byte, error) { - call, valid := continuationRepairToolCall(response) - if !valid { - return nil, errors.New("tool_contract_invalid") - } - var fields map[string]json.RawMessage - var input []json.RawMessage - if json.Unmarshal(initial, &fields) != nil || json.Unmarshal(fields["input"], &input) != nil { - return nil, errors.New("history_invalid") - } - // Preserve every original response.output item, including namespace:null, - // encrypted reasoning, phase, IDs and fields unknown to this test binary. - input = append(input, response.Output...) - var callID string - _ = json.Unmarshal(call["call_id"], &callID) - // The original call (including its namespace) is replayed above. A new - // matching result is identified by call_id; do not invent extra namespace - // metadata on a field shape the normal gateway deliberately normalizes. - toolOutput := map[string]any{"type": "function_call_output", "call_id": callID, "output": continuationRepairToolOutput} - raw, _ := json.Marshal(toolOutput) - input = append(input, raw) - for i := 0; i < 2; i++ { - heartbeat := "continuation-repair-" + scenario + "" + time.Now().UTC().Format(time.RFC3339Nano) + - "Read the preceding local tool result. If its value is ALPHA, return exactly CONTINUATION_OK. Do not call another tool." - item := map[string]any{"type": "function_call_output", "namespace": "codex_app", "name": "automation_update", "output": heartbeat} - if i == 1 { - item["call_id"] = "" - } - raw, _ = json.Marshal(item) - input = append(input, raw) - } - fields["input"], _ = json.Marshal(input) - fields["tool_choice"] = json.RawMessage(`"none"`) - return json.Marshal(fields) -} - -func continuationRepairToolCall(response continuationRepairResponse) (map[string]json.RawMessage, bool) { - if !response.Completed { - return nil, false - } - var found map[string]json.RawMessage - for _, raw := range response.Output { - item, err := continuationRepairObject(raw) - if err != nil { - return nil, false - } - var kind string - _ = json.Unmarshal(item["type"], &kind) - if kind != "function_call" { - if strings.HasSuffix(kind, "_call") || strings.HasSuffix(kind, "_call_output") || strings.HasPrefix(kind, "mcp_") { - return nil, false - } - continue - } - var name, id, args string - if found != nil || json.Unmarshal(item["name"], &name) != nil || name != continuationRepairTool || - json.Unmarshal(item["call_id"], &id) != nil || strings.TrimSpace(id) == "" || json.Unmarshal(item["arguments"], &args) != nil { - return nil, false - } - arguments, err := continuationRepairObject([]byte(args)) - if err != nil || len(arguments) != 0 { - return nil, false - } - found = item - } - return found, found != nil -} - -func (h *continuationRepairHarness) runTurn(scenario string, turn int, body []byte) (continuationRepairResponse, continuationRepairTurnResult) { - request := turn - if scenario == "sol" { - request += 2 - } - identity := continuationRepairIdentity{Scenario: scenario, Turn: turn, Request: request, Model: continuationRepairModel(scenario)} - result := continuationRepairTurnResult{continuationRepairIdentity: identity, Type: "turn_result", Status: "failed", Terminal: "none"} - if len(body) == 0 || h.requests >= continuationRepairMaxRequests || h.ctx.Err() != nil { - h.stop = "request_budget_exhausted" - return continuationRepairResponse{}, result - } - if h.output.Encode(map[string]any{"type": "before_turn", "scenario": scenario, "turn": turn, "request": request, "expected_fingerprint": h.boot.Source.Fingerprint}) != nil { - h.stop = "broker_unavailable" - return continuationRepairResponse{}, result - } - var grant continuationRepairGrant - if h.readGrant(h.ctx, &grant) != nil || grant.Type != "turn_granted" || grant.Scenario != scenario || grant.Turn != turn || grant.Request != request || grant.Fingerprint != h.boot.Source.Fingerprint { - h.stop = "broker_denied" - return continuationRepairResponse{}, result - } - h.requests++ - ctx, cancel := context.WithTimeout(h.ctx, continuationRepairTurnTimeout) - defer cancel() - u := h.upstream - u.beginTurn(ctx, identity, body) - beforeUsage, beforeHealth := len(h.fixture.usageSnapshot()), h.fixture.healthChanges() - router := gin.New() - var handlerContext *gin.Context - router.Use(func(c *gin.Context) { - h.fixture.decorateContext(c) - handlerContext = c - c.Next() - }) - router.Use(OpsErrorLoggerMiddleware(h.fixture.ops)) - router.POST("/v1/responses", h.fixture.handler.Responses) - req := httptest.NewRequest(http.MethodPost, "/v1/responses", bytes.NewReader(body)).WithContext(ctx) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Accept", "text/event-stream") - req.Header.Set("User-Agent", "codex_cli_rs/0.149.0 (continuation-repair-isolated-check)") - recorder := httptest.NewRecorder() - router.ServeHTTP(recorder, req) - u.finishOutstanding() - parsed := continuationRepairParseResponse(recorder.Body.Bytes(), recorder.Header().Get("Content-Type"), recorder.Code) - result.Completed, result.Terminal, result.ClientHTTPStatus = parsed.Completed, parsed.Terminal, recorder.Code - result.Attempts, result.HistoryPreserved, result.HeartbeatNormalized = u.turnAttempts, u.historyPreserved, u.heartbeatNormalized - result.AccountHealthUnchanged = beforeHealth == h.fixture.healthChanges() - if handlerContext != nil { - if id, ok := handlerContext.Get(opsAccountIDKey); ok && id == int64(16050) { - result.AccountID = 16050 - } - } - if u.turnAttempts > 0 { - result.WireModel = u.identity.WireModel - } - _, result.ToolCallReceived = continuationRepairToolCall(parsed) - result.ToolContractValid = result.ToolCallReceived - if turn == 2 { - result.ToolContractValid = parsed.Completed && !continuationRepairHasTool(parsed.Output) && strings.TrimSpace(parsed.Text) == continuationRepairFinal - } - usage := h.fixture.usageSnapshot() - result.BillingRows = len(usage) - beforeUsage - attributed := result.BillingRows == 1 - if attributed { - row := usage[len(usage)-1] - actualModel := row.Model - if row.UpstreamModel != nil { - actualModel = *row.UpstreamModel - } - attributed = row.AccountID == 16050 && row.UserID == h.boot.Source.UserID && row.GroupID != nil && *row.GroupID == 35 && actualModel == result.WireModel - if row.RequestID != "" { - result.RequestIDSHA = continuationRepairHash([]byte(row.RequestID)) - } - } - for _, entry := range continuationRepairDrainOps() { - result.OpsRows++ - status := entry.StatusCode - result.OpsStatus = &status - } - if parsed.ID != "" { - result.ResponseIDSHA = continuationRepairHash([]byte(parsed.ID)) - } - result.Usage = parsed.Usage - switch { - case u.lastError != "": - result.ErrorClass = u.lastError - case u.turnAttempts == 0: - result.Status, result.ErrorClass = "not_covered", "handler_failed" - case parsed.Invalid || u.uncertain: - result.Status, result.ErrorClass = "uncertain", "body_incomplete" - case !parsed.Completed: - result.ErrorClass = parsed.ErrorClass - if result.ErrorClass == "" { - result.ErrorClass = "response_not_completed" - } - case result.AccountID != 16050 || result.WireModel == "": - result.Status, result.ErrorClass = "not_covered", "model_mismatch" - case !u.responseModelMatches: - result.Status, result.ErrorClass = "not_covered", "model_mismatch" - case !continuationRepairJSONEqual(continuationRepairMarshal(u.completedOutput), continuationRepairMarshal(parsed.Output)): - result.ErrorClass = "history_invalid" - case !result.ToolContractValid: - result.Status, result.ErrorClass = "not_covered", "tool_contract_invalid" - case !result.HistoryPreserved: - result.ErrorClass = "history_invalid" - case turn == 2 && !result.HeartbeatNormalized: - result.ErrorClass = "heartbeat_not_normalized" - case !result.AccountHealthUnchanged: - result.ErrorClass = "account_health_changed" - case !attributed: - result.ErrorClass = "billing_attribution_missing" - default: - result.Status = "completed" - } - if h.output.Encode(result) != nil { - h.stop = "broker_unavailable" - } - return parsed, result -} - -func continuationRepairDrainOps() []*service.OpsInsertErrorLogInput { - var entries []*service.OpsInsertErrorLogInput - for { - select { - case job := <-opsErrorLogQueue: - opsErrorLogQueueLen.Add(-1) - opsErrorLogQueueBytes.Add(-job.queuedBytes) - entries = append(entries, job.entry) - default: - return entries - } - } -} - -func (h *continuationRepairHarness) readGrant(ctx context.Context, grant *continuationRepairGrant) error { - finished := make(chan error, 1) - go func() { finished <- continuationRepairReadJSON(h.input, grant) }() - select { - case err := <-finished: - return err - case <-ctx.Done(): - // No subsequent read is permitted after a timed-out protocol exchange. - h.stop = "broker_unavailable" - return errors.New("broker_unavailable") - } -} - -type continuationRepairUpstream struct { - mu sync.Mutex - h *continuationRepairHarness - inner service.HTTPUpstream - identity continuationRepairIdentity - active context.Context - endpoint, authorizationHash, proxy string - body, firstSent []byte - attempts, turnAttempts, blocked int - last *continuationRepairAttempt - outstanding *continuationRepairCapture - historyPreserved, heartbeatNormalized bool - responseModelMatches bool - completedOutput []json.RawMessage - uncertain bool - lastError string -} - -func (u *continuationRepairUpstream) beginTurn(ctx context.Context, identity continuationRepairIdentity, body []byte) { - u.mu.Lock() - defer u.mu.Unlock() - u.identity, u.active, u.body = identity, ctx, bytes.Clone(body) - u.identity.WireModel = u.h.boot.Source.Account.GetMappedModel(u.h.boot.Source.ChannelModels[identity.Model]) - u.turnAttempts, u.last, u.outstanding, u.firstSent, u.lastError = 0, nil, nil, nil, "" - u.historyPreserved, u.heartbeatNormalized, u.uncertain = false, identity.Turn == 1, false - u.responseModelMatches, u.completedOutput = false, nil -} - -func (u *continuationRepairUpstream) Do(req *http.Request, proxy string, accountID int64, concurrency int) (*http.Response, error) { - return u.send(req, proxy, accountID, concurrency, nil) -} - -func (u *continuationRepairUpstream) DoWithTLS(req *http.Request, proxy string, accountID int64, concurrency int, profile *tlsfingerprint.Profile) (*http.Response, error) { - return u.send(req, proxy, accountID, concurrency, profile) -} - -func (u *continuationRepairUpstream) send(req *http.Request, proxy string, accountID int64, concurrency int, profile *tlsfingerprint.Profile) (*http.Response, error) { - u.mu.Lock() - defer u.mu.Unlock() - fail := func(class string) (*http.Response, error) { u.lastError = class; return nil, errors.New(class) } - ordinal := u.turnAttempts + 1 - if u.identity.Turn < 1 || ordinal > u.identity.Turn || (ordinal == 2 && (u.last == nil || !u.last.SignatureRejection || u.last.SemanticOutput || u.last.Uncertain)) { - u.blocked++ - return fail("attempt_budget_exhausted") - } - if u.attempts >= continuationRepairMaxAttempts || u.h.stop != "" { - return fail("attempt_budget_exhausted") - } - if req == nil || req.URL == nil || req.Method != http.MethodPost || req.URL.String() != u.endpoint || - accountID != 16050 || concurrency != u.h.boot.Source.Account.Concurrency || proxy != u.proxy || - continuationRepairHash([]byte(req.Header.Get("Authorization"))) != u.authorizationHash || continuationRepairHashJSON(u.h.boot.Source) != u.h.sourceHash { - u.h.stop = "source_changed" - return fail("source_changed") - } - if u.active == nil || u.active.Err() != nil || req.Context().Err() != nil || req.Body == nil { - return fail("body_incomplete") - } - body, err := io.ReadAll(io.LimitReader(req.Body, continuationRepairMaxBody+1)) - _ = req.Body.Close() - if err != nil || len(body) > continuationRepairMaxBody { - return fail("body_incomplete") - } - if err := u.validateWire(body, ordinal); err != nil { - return fail(err.Error()) - } - cause := "initial" - if ordinal == 2 { - cause = "signature_recovery" - } - slot := u.attempts + 1 - if u.h.output.Encode(map[string]any{"type": "before_send", "scenario": u.identity.Scenario, "turn": u.identity.Turn, "request": u.identity.Request, - "slot": slot, "attempt_in_turn": ordinal, "cause": cause, "expected_fingerprint": u.h.boot.Source.Fingerprint}) != nil { - u.h.stop = "broker_unavailable" - return fail("broker_unavailable") - } - var grant continuationRepairGrant - if u.h.readGrant(u.active, &grant) != nil || grant.Type != "send_granted" || grant.Scenario != u.identity.Scenario || grant.Turn != u.identity.Turn || - grant.Request != u.identity.Request || grant.Slot != slot || grant.Attempt != slot || grant.AttemptInTurn != ordinal || grant.Fingerprint != u.h.boot.Source.Fingerprint || - grant.ElapsedMS < 0 || grant.ElapsedMS >= continuationRepairDuration.Milliseconds() { - u.h.stop = "broker_denied" - return fail("broker_denied") - } - u.attempts, u.turnAttempts = slot, ordinal - if ordinal == 1 { - u.firstSent = bytes.Clone(body) - } - identity := u.identity - identity.AccountID = accountID - result := continuationRepairAttempt{continuationRepairIdentity: identity, Type: "attempt_result", Slot: slot, AttemptInTurn: ordinal} - // Keep transport policy context, but restore the bounded client deadline - // and cancellation that ordinary usage-draining forwarding may detach. - ctx := req.Context() - cancelDeadline := func() {} - if deadline, ok := u.active.Deadline(); ok { - ctx, cancelDeadline = context.WithDeadline(ctx, deadline) - } - ctx, cancelSend := context.WithCancel(ctx) - stop := context.AfterFunc(u.active, cancelSend) - cleanup := func() { stop(); cancelSend(); cancelDeadline() } - req = req.Clone(service.WithHTTPUpstreamRedirectsDisabled(ctx)) - req.Body, req.GetBody = io.NopCloser(bytes.NewReader(body)), nil - var response *http.Response - if profile == nil { - response, err = u.inner.Do(req, proxy, accountID, concurrency) - } else { - response, err = u.inner.DoWithTLS(req, proxy, accountID, concurrency, profile) - } - if err != nil || response == nil || response.Body == nil { - cleanup() - result.Uncertain, result.ErrorClass = true, "transport_error" - u.recordLocked(result) - return fail("transport_error") - } - result.UpstreamHTTPStatus = response.StatusCode - contentType := response.Header.Get("Content-Type") - capture := &continuationRepairCapture{ReadCloser: response.Body, cleanup: cleanup, finish: func(raw []byte, incomplete, eof bool) { - parsed := continuationRepairParseResponse(raw, contentType, response.StatusCode) - result.SignatureRejection, result.SemanticOutput = parsed.Signature, parsed.Semantic - result.Uncertain = incomplete || parsed.Invalid || parsed.Terminal == "none" || (!eof && parsed.Terminal == "error") - result.ErrorClass = parsed.ErrorClass - if result.Uncertain { - result.ErrorClass = "body_incomplete" - } - u.mu.Lock() - defer u.mu.Unlock() - if parsed.Completed { - u.responseModelMatches = parsed.Model == identity.WireModel || parsed.Model == identity.Model - u.completedOutput = parsed.Output - } - u.recordLocked(result) - }} - u.outstanding, response.Body = capture, capture - return response, nil -} - -func (u *continuationRepairUpstream) recordLocked(result continuationRepairAttempt) { - u.last = &result - u.uncertain = u.uncertain || result.Uncertain - if u.h.output.Encode(result) != nil { - u.h.stop = "broker_unavailable" - } -} - -func (u *continuationRepairUpstream) finishOutstanding() { - u.mu.Lock() - capture := u.outstanding - u.mu.Unlock() - if capture != nil { - _ = capture.Close() - } -} - -type continuationRepairWireError string - -func (e continuationRepairWireError) Error() string { return string(e) } - -func (u *continuationRepairUpstream) validateWire(body []byte, ordinal int) error { - sent, err := continuationRepairObject(body) - if err != nil { - return continuationRepairWireError("wire_json_mismatch") - } - var model string - var stream, store bool - if json.Unmarshal(sent["model"], &model) != nil || model != u.identity.WireModel { - return continuationRepairWireError("wire_model_mismatch") - } - if json.Unmarshal(sent["stream"], &stream) != nil || !stream { - return continuationRepairWireError("wire_stream_mismatch") - } - if json.Unmarshal(sent["store"], &store) != nil || store { - return continuationRepairWireError("wire_store_mismatch") - } - if ordinal == 2 { - if !continuationRepairOnlyCipherRemoval(u.firstSent, body) { - return continuationRepairWireError("wire_recovery_mismatch") - } - return nil - } - before, err := continuationRepairObject(u.body) - if err != nil || !continuationRepairJSONEqual(before["instructions"], sent["instructions"]) { - return continuationRepairWireError("wire_instructions_mismatch") - } - var expected []json.RawMessage - if json.Unmarshal(before["input"], &expected) != nil { - return continuationRepairWireError("wire_input_mismatch") - } - if u.identity.Turn == 2 { - if len(expected) < 4 { - return continuationRepairWireError("wire_input_mismatch") - } - for i := len(expected) - 2; i < len(expected); i++ { - item, err := continuationRepairObject(expected[i]) - var text string - if err != nil || json.Unmarshal(item["output"], &text) != nil { - return continuationRepairWireError("wire_input_mismatch") - } - expected[i], _ = json.Marshal(map[string]any{"type": "message", "role": "user", "content": []any{map[string]any{"type": "input_text", "text": text}}}) - } - } - normalized, _ := json.Marshal(expected) - u.historyPreserved = continuationRepairJSONEqual(normalized, sent["input"]) - u.heartbeatNormalized = u.identity.Turn == 1 || u.historyPreserved - if !u.historyPreserved { - return continuationRepairWireError("wire_input_mismatch") - } - return nil -} - -// A recovery grant cannot be used by a same-account compatibility retry. Its -// only permitted wire change is omission of already-present reasoning -// encrypted_content fields; every other input and request field must match. -func continuationRepairOnlyCipherRemoval(before, after []byte) bool { - a, err := continuationRepairObject(before) - if err != nil { - return false - } - b, err := continuationRepairObject(after) - if err != nil { - return false - } - var ai, bi []json.RawMessage - if json.Unmarshal(a["input"], &ai) != nil || json.Unmarshal(b["input"], &bi) != nil || len(ai) != len(bi) { - return false - } - delete(a, "input") - delete(b, "input") - aRaw, _ := json.Marshal(a) - bRaw, _ := json.Marshal(b) - if !continuationRepairJSONEqual(aRaw, bRaw) { - return false - } - removed := 0 - for i := range ai { - if continuationRepairJSONEqual(ai[i], bi[i]) { - continue - } - oldItem, oldErr := continuationRepairObject(ai[i]) - newItem, newErr := continuationRepairObject(bi[i]) - var kind, cipher string - _, remaining := newItem["encrypted_content"] - if oldErr != nil || newErr != nil || remaining || json.Unmarshal(oldItem["type"], &kind) != nil || kind != "reasoning" || - json.Unmarshal(oldItem["encrypted_content"], &cipher) != nil || cipher == "" { - return false - } - delete(oldItem, "encrypted_content") - raw, _ := json.Marshal(oldItem) - if !continuationRepairJSONEqual(raw, bi[i]) { - return false - } - removed++ - } - return removed > 0 -} - -type continuationRepairCapture struct { - io.ReadCloser - readMu sync.Mutex - mu sync.Mutex - raw []byte - incomplete bool - eof bool - closing bool - once sync.Once - closeOnce sync.Once - cleanup func() - finish func([]byte, bool, bool) -} - -func (c *continuationRepairCapture) Read(p []byte) (int, error) { - // The production timeout/keepalive scanner may read on another goroutine. - // Preserve byte order without holding the state lock across a network read. - c.readMu.Lock() - defer c.readMu.Unlock() - c.mu.Lock() - closing := c.closing - c.mu.Unlock() - if closing { - return 0, io.ErrClosedPipe - } - n, err := c.ReadCloser.Read(p) - c.mu.Lock() - if n > 0 { - if len(c.raw)+n > continuationRepairMaxBody { - c.incomplete = true - err = errors.New("body_incomplete") - } else { - c.raw = append(c.raw, p[:n]...) - } - } - if err == io.EOF { - c.eof = true - } else if err != nil && !c.closing { - c.incomplete = true - } - finish := err != nil && !c.closing - c.mu.Unlock() - if finish { - c.complete() - } - return n, err -} - -func (c *continuationRepairCapture) complete() { - c.once.Do(func() { - c.mu.Lock() - raw, incomplete, eof := bytes.Clone(c.raw), c.incomplete, c.eof - c.mu.Unlock() - c.finish(raw, incomplete, eof) - }) -} - -func (c *continuationRepairCapture) Close() error { - var result error - c.closeOnce.Do(func() { - c.mu.Lock() - c.closing = true - c.mu.Unlock() - // Never start a second reader to drain a half-open SSE tail. Cancel and - // close first to unblock an existing scanner, then freeze all its bytes. - // The observer accepts an explicit complete terminal without requiring - // TCP EOF; a provisional bare error without EOF remains uncertain. - c.cleanup() - result = c.ReadCloser.Close() - c.readMu.Lock() - defer c.readMu.Unlock() - c.complete() - }) - return result -} - -func continuationRepairParseResponse(raw []byte, contentType string, status int) continuationRepairResponse { - r := continuationRepairResponse{Terminal: "none"} - if len(raw) == 0 || len(raw) > continuationRepairMaxBody { - r.Invalid, r.ErrorClass = true, "invalid_response" - return r - } - framed := strings.Contains(strings.ToLower(contentType), "text/event-stream") || bytes.HasPrefix(bytes.TrimSpace(raw), []byte("data:")) || bytes.HasPrefix(bytes.TrimSpace(raw), []byte("event:")) || bytes.HasPrefix(bytes.TrimSpace(raw), []byte(":")) - if !framed { - object, err := continuationRepairObject(raw) - if err != nil { - r.Invalid = true - } else { - r = continuationRepairDocument(object) - if status >= 400 { - r.Terminal = "error" - r.Signature = continuationRepairSignature(object, status) - } - } - } else { - var data []byte - event := "" - done := false - provisionalError, provisionalSignature := false, false - apply := func() { - if len(data) == 0 { - return - } - payload := bytes.TrimSuffix(data, []byte{'\n'}) - if bytes.Equal(bytes.TrimSpace(payload), []byte("[DONE]")) { - done = true - return - } - object, err := continuationRepairObject(payload) - if err != nil || done { - r.Invalid = true - return - } - var kind string - _ = json.Unmarshal(object["type"], &kind) - if kind == "" { - kind = event - } else if event != "" && event != "message" && event != kind { - r.Invalid = true - } - if r.Terminal != "none" { - r.Invalid = true - } - switch kind { - case "response.created", "response.queued", "response.in_progress": - // Explicit non-semantic lifecycle events may precede recovery. - case "response.completed", "response.failed", "response.incomplete", "response.done": - response, err := continuationRepairObject(object["response"]) - if err != nil { - r.Invalid = true - return - } - parsed := continuationRepairDocument(response) - if kind == "response.done" { - kind = "response." + parsed.Status - } - if kind != "response.completed" && kind != "response.failed" && kind != "response.incomplete" { - r.Invalid = true - return - } - if kind != "response."+parsed.Status { - r.Invalid = true - } - parsed.Terminal, parsed.Semantic, parsed.Invalid = kind, parsed.Semantic || r.Semantic, parsed.Invalid || r.Invalid - parsed.Signature = kind == "response.failed" && continuationRepairSignature(object, status) - r = parsed - case "error", "response.error": - // A bare error is provisional until EOF; some upstreams then - // provide an authoritative completed/failed response document. - provisionalError, provisionalSignature = true, continuationRepairSignature(object, status) - default: - // Treat unknown events conservatively as semantic. A payload - // must not buy a retry just because the harness cannot name it. - r.Semantic = true - } - } - scanner := bufio.NewScanner(bytes.NewReader(bytes.ReplaceAll(bytes.ReplaceAll(raw, []byte("\r\n"), []byte("\n")), []byte("\r"), []byte("\n")))) - scanner.Buffer(make([]byte, 4096), continuationRepairMaxBody+1) - for scanner.Scan() { - line := scanner.Bytes() - if len(line) == 0 { - apply() - data, event = nil, "" - continue - } - if line[0] == ':' { - continue - } - field, value, _ := bytes.Cut(line, []byte{':'}) - value = bytes.TrimPrefix(value, []byte{' '}) - switch string(field) { - case "data": - data = append(append(data, value...), '\n') - case "event": - event = string(value) - } - } - if scanner.Err() != nil || len(data) != 0 { - r.Invalid = true - } - if r.Terminal == "none" && provisionalError { - r.Terminal, r.Signature, r.ErrorClass = "error", provisionalSignature, "upstream_error" - } - } - if status < 200 || status >= 300 { - r.Completed = false - if status == http.StatusBadRequest { - r.ErrorClass = "request_rejected" - } else { - r.ErrorClass = "upstream_error" - } - } - if r.Signature { - r.ErrorClass = "signature_rejection" - } - if r.Invalid { - r.Completed, r.Signature, r.ErrorClass = false, false, "invalid_response" - } - if r.Terminal == "none" { - r.Completed = false - } - return r -} - -func continuationRepairDocument(object map[string]json.RawMessage) continuationRepairResponse { - r := continuationRepairResponse{Terminal: "none"} - for name, target := range map[string]*string{"status": &r.Status, "model": &r.Model, "id": &r.ID} { - if raw, present := object[name]; present && json.Unmarshal(raw, target) != nil { - r.Invalid = true - } - } - if raw, present := object["output"]; present { - if json.Unmarshal(raw, &r.Output) != nil || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { - r.Invalid = true - } - } - r.Completed = r.Status == "completed" - if r.Completed { - r.Terminal = "response.completed" - if _, present := object["output"]; !present { - r.Invalid = true - } - } else if r.Status == "failed" || r.Status == "incomplete" { - r.Terminal, r.ErrorClass = "response."+r.Status, "upstream_error" - } - for _, field := range []string{"error", "incomplete_details"} { - if value := bytes.TrimSpace(object[field]); len(value) > 0 && !bytes.Equal(value, []byte("null")) { - r.Completed, r.ErrorClass = false, "upstream_error" - } - } - for _, raw := range r.Output { - item, err := continuationRepairObject(raw) - if err != nil { - r.Invalid = true - continue - } - r.Semantic = true - var kind string - _ = json.Unmarshal(item["type"], &kind) - var parts []map[string]json.RawMessage - if content, present := item["content"]; present && kind == "message" { - if json.Unmarshal(content, &parts) != nil { - r.Invalid = true - } - for _, part := range parts { - var kind, text string - _ = json.Unmarshal(part["type"], &kind) - _ = json.Unmarshal(part["text"], &text) - if kind == "output_text" { - r.Text += text - } - if kind == "refusal" { - r.Completed, r.ErrorClass = false, "upstream_error" - } - } - } - } - if raw := object["usage"]; len(raw) > 0 && !bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { - usage, err := continuationRepairObject(raw) - if err != nil { - r.Invalid = true - } else { - r.Usage = make(map[string]int64) - for _, name := range []string{"input_tokens", "output_tokens", "total_tokens"} { - if value, present := usage[name]; present { - var count int64 - if json.Unmarshal(value, &count) != nil || count < 0 || count > 1_000_000_000 { - r.Invalid = true - } else { - r.Usage[name] = count - } - } - } - } - } - return r -} - -func continuationRepairSignature(object map[string]json.RawMessage, status int) bool { - protected := func(status int) bool { - return status == 401 || status == 402 || status == 403 || status == 407 || status == 429 - } - if protected(status) { - return false - } - var kind, state string - _ = json.Unmarshal(object["type"], &kind) - _ = json.Unmarshal(object["status"], &state) - if kind != "" && kind != "error" && kind != "response.failed" && kind != "response.done" || state != "" && state != "failed" { - return false - } - errorObject, _ := continuationRepairObject(object["error"]) - response, _ := continuationRepairObject(object["response"]) - if response != nil { - var responseStatus string - _ = json.Unmarshal(response["status"], &responseStatus) - if kind != "response.failed" && responseStatus != "failed" { - return false - } - errorObject, _ = continuationRepairObject(response["error"]) - } else if errorObject == nil && kind == "error" { - errorObject = object - } - var code string - if json.Unmarshal(errorObject["code"], &code) != nil || (code != "invalid_encrypted_content" && code != "thinking_signature_invalid") { - return false - } - for _, candidate := range []map[string]json.RawMessage{object, errorObject} { - for _, field := range []string{"status_code", "status"} { - var number int - if json.Unmarshal(candidate[field], &number) == nil && protected(number) { - return false - } - var text string - if json.Unmarshal(candidate[field], &text) == nil && (text == "401" || text == "402" || text == "403" || text == "407" || text == "429") { - return false - } - } - } - return true -} - -func continuationRepairHasTool(output []json.RawMessage) bool { - for _, raw := range output { - object, err := continuationRepairObject(raw) - if err != nil { - return true - } - var kind string - _ = json.Unmarshal(object["type"], &kind) - if strings.HasSuffix(kind, "_call") || strings.HasSuffix(kind, "_call_output") || strings.HasPrefix(kind, "mcp_") { - return true - } - } - return false -} - -func continuationRepairReadJSON(reader *bufio.Reader, target any) error { - line, err := reader.ReadBytes('\n') - if err != nil || len(line) > continuationRepairMaxBody || !hasUniqueJSONMembers(line) { - return errors.New("broker_unavailable") - } - decoder := json.NewDecoder(bytes.NewReader(line)) - decoder.DisallowUnknownFields() - if err := decoder.Decode(target); err != nil { - return errors.New("broker_unavailable") - } - var trailing any - if decoder.Decode(&trailing) != io.EOF { - return errors.New("broker_unavailable") - } - return nil -} - -func continuationRepairObject(raw []byte) (map[string]json.RawMessage, error) { - if !hasUniqueJSONMembers(raw) { - return nil, errors.New("invalid_response") - } - var object map[string]json.RawMessage - if json.Unmarshal(raw, &object) != nil || object == nil { - return nil, errors.New("invalid_response") - } - return object, nil -} - -func continuationRepairJSONEqual(left, right []byte) bool { - decode := func(raw []byte) (any, bool) { - decoder := json.NewDecoder(bytes.NewReader(raw)) - decoder.UseNumber() - var value, trailing any - if decoder.Decode(&value) != nil || decoder.Decode(&trailing) != io.EOF { - return nil, false - } - return value, true - } - a, okA := decode(left) - b, okB := decode(right) - return okA && okB && reflect.DeepEqual(a, b) -} - -func continuationRepairHash(raw []byte) string { - sum := sha256.Sum256(raw) - return hex.EncodeToString(sum[:]) -} - -func continuationRepairHashJSON(value any) string { - raw, _ := json.Marshal(value) - return continuationRepairHash(raw) -} - -func continuationRepairMarshal(value any) []byte { - raw, _ := json.Marshal(value) - return raw -} - -// This is the sole offline harness smoke test. Its upstream has no network -// client, and its broker grants are an in-memory serial transcript. It proves -// the complete fixture can route, normalize, attribute billing and parse Ops -// before a sealed live run spends any of its four approved requests. -func TestContinuationRepairOffline(t *testing.T) { - gin.SetMode(gin.TestMode) - ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) - defer cancel() - t.Run("capture_close_unblocks_existing_reader", func(t *testing.T) { - body := &continuationRepairOfflineBlockingBody{started: make(chan struct{}), closed: make(chan struct{})} - finished := make(chan bool, 2) - capture := &continuationRepairCapture{ReadCloser: body, cleanup: func() {}, finish: func(_ []byte, incomplete, eof bool) { finished <- incomplete || eof }} - go func() { _, _ = capture.Read(make([]byte, 1)) }() - select { - case <-body.started: - case <-ctx.Done(): - t.Fatal("offline capture reader did not start") - } - closed := make(chan struct{}) - go func() { _ = capture.Close(); _ = capture.Close(); close(closed) }() - select { - case <-closed: - case <-ctx.Done(): - t.Fatal("offline capture close did not unblock its reader") - } - if len(finished) != 1 || <-finished { - t.Fatal("local close must report one frozen capture without inventing EOF") - } - }) - if err := continuationRepairPreflight(ctx, &config.Config{RunMode: config.RunModeStandard}, continuationRepairOfflineSource()); err != nil { - t.Fatal(err) - } -} - -func continuationRepairOfflineSource() continuationRepairSource { - return continuationRepairSource{ - Account: service.Account{ID: 16050, Name: "白嫖-dmxapi", Platform: service.PlatformOpenAI, Type: service.AccountTypeAPIKey, - Status: service.StatusActive, Schedulable: true, Concurrency: 1, GroupIDs: []int64{35}, - Credentials: map[string]any{"api_key": "offline-only", "base_url": "https://continuation.example.test", - "model_mapping": map[string]any{"gpt-6-astra": "gpt-6-astra-ssvip", "gpt-5.6-sol": "gpt-5.6-sol"}}, - Extra: map[string]any{"use_responses_api": true}}, - Group: service.Group{ID: 35, Platform: service.PlatformOpenAI, Status: service.StatusActive, RateMultiplier: 1}, - FastPolicy: service.DefaultOpenAIFastPolicySettings(), Settings: map[string]string{}, - ChannelModels: map[string]string{"gpt-6-astra": "gpt-6-astra", "gpt-5.6-sol": "gpt-5.6-sol"}, - Fingerprint: strings.Repeat("a", 64), UserID: 920000016050, - } -} - -// Exercise frozen production policy through the complete handler using only -// in-memory grants and synthetic responses. This object cannot open a socket; -// none of its requests, usage rows or counters belong to the live broker. -func continuationRepairPreflight(ctx context.Context, cfg *config.Config, source continuationRepairSource) error { - ctx, cancel := context.WithTimeout(ctx, 15*time.Second) - defer cancel() - endpoint, err := continuationRepairEndpoint(&source.Account) - if err != nil { - return err - } - var grants, protocol bytes.Buffer - encoder := json.NewEncoder(&grants) - for i, scenario := range []string{"astra", "sol"} { - for turn := 1; turn <= 2; turn++ { - request := i*2 + turn - _ = encoder.Encode(map[string]any{"type": "turn_granted", "scenario": scenario, "turn": turn, "request": request, "fingerprint": source.Fingerprint}) - _ = encoder.Encode(map[string]any{"type": "send_granted", "scenario": scenario, "turn": turn, "request": request, "slot": request, - "attempt": request, "attempt_in_turn": 1, "fingerprint": source.Fingerprint, "elapsed_ms": 0}) - } - } - h := &continuationRepairHarness{boot: continuationRepairBootstrap{Source: source}, input: bufio.NewReader(&grants), output: json.NewEncoder(&protocol), - ctx: ctx, finished: make(map[string]bool), sourceHash: continuationRepairHashJSON(source)} - fake := &continuationRepairOfflineUpstream{endpoint: endpoint} - h.upstream = &continuationRepairUpstream{h: h, inner: fake, endpoint: endpoint, - authorizationHash: continuationRepairHash([]byte("Bearer " + source.Account.GetOpenAIProtocolAPIKey()))} - if source.Account.Proxy != nil { - h.upstream.proxy = source.Account.Proxy.URL() - } - h.fixture, err = continuationRepairBuildFixture(ctx, cfg, source, h.upstream) - if err != nil { - return errors.New("handler_failed") - } - defer h.fixture.close() - opsErrorLogOnce.Do(func() {}) - opsErrorLogMu.Lock() - opsErrorLogQueue = make(chan opsErrorLogJob, 32) - opsErrorLogMu.Unlock() - for _, scenario := range []string{"astra", "sol"} { - initial := continuationRepairInitialBody(continuationRepairModel(scenario)) - response, result := h.runTurn(scenario, 1, initial) - if result.Status != "completed" { - return continuationRepairPreflightError(result.ErrorClass) - } - body, err := continuationRepairContinuation(initial, response, scenario) - if err != nil { - return errors.New("handler_failed") - } - _, result = h.runTurn(scenario, 2, body) - if result.Status != "completed" || !result.HeartbeatNormalized || !result.HistoryPreserved || result.BillingRows != 1 { - return continuationRepairPreflightError(result.ErrorClass) - } - } - if h.requests != 4 || h.upstream.attempts != 4 || fake.calls != 4 || h.upstream.blocked != 0 || grants.Len() != 0 || h.stop != "" { - return errors.New("handler_failed") - } - counts := make(map[string]int) - decoder := json.NewDecoder(&protocol) - for decoder.More() { - var event map[string]json.RawMessage - if decoder.Decode(&event) != nil { - return errors.New("handler_failed") - } - var kind string - _ = json.Unmarshal(event["type"], &kind) - counts[kind]++ - } - for _, kind := range []string{"before_turn", "before_send", "attempt_result", "turn_result"} { - if counts[kind] != 4 { - return errors.New("handler_failed") - } - } - return nil -} - -func TestContinuationRepairPreflightFrozenPolicy(t *testing.T) { - for _, override := range []bool{false, true} { - name := "global_bridge_rejected_before_send" - if override { - name = "channel_disable_overrides_global" - } - t.Run(name, func(t *testing.T) { - source := continuationRepairOfflineSource() - source.Group.AllowImageGeneration = true - if override { - disabled := false - source.ChannelImageBridge = &disabled - } - cfg := &config.Config{RunMode: config.RunModeStandard, Gateway: config.GatewayConfig{CodexImageGenerationBridgeEnabled: true}} - beforeSource, beforeConfig := continuationRepairHashJSON(source), continuationRepairHashJSON(cfg) - err := continuationRepairPreflight(context.Background(), cfg, source) - if override { - if err != nil { - t.Fatal(err) - } - } else { - var wireError continuationRepairWireError - if !errors.As(err, &wireError) || wireError != "wire_instructions_mismatch" { - t.Fatalf("unexpected preflight classification: %v", err) - } - } - if continuationRepairHashJSON(source) != beforeSource || continuationRepairHashJSON(cfg) != beforeConfig { - t.Fatal("preflight changed frozen source or configuration") - } - }) - } -} - -func TestContinuationRepairWireMismatchClassification(t *testing.T) { - initial := continuationRepairInitialBody("gpt-6-astra") - t.Run("invalid_json", func(t *testing.T) { - u := &continuationRepairUpstream{body: initial, identity: continuationRepairIdentity{Turn: 1, WireModel: "gpt-6-astra"}} - if err := u.validateWire([]byte(`{"model":"gpt-6-astra","model":"gpt-6-astra"}`), 1); err == nil || err.Error() != "wire_json_mismatch" { - t.Fatalf("unexpected JSON classification: %v", err) - } - }) - for _, test := range []struct{ name, field, value, class string }{ - {"model", "model", `"gpt-5.6-sol"`, "wire_model_mismatch"}, - {"stream", "stream", "false", "wire_stream_mismatch"}, - {"store", "store", "true", "wire_store_mismatch"}, - {"instructions", "instructions", `"different"`, "wire_instructions_mismatch"}, - {"unknown_input_field", "input", `[{"role":"user","content":"Call continuation_probe_value exactly once with an empty JSON object. Do not answer yet.","unknown":9007199254740993}]`, "wire_input_mismatch"}, - } { - t.Run(test.name, func(t *testing.T) { - fields, _ := continuationRepairObject(initial) - fields[test.field] = json.RawMessage(test.value) - u := &continuationRepairUpstream{body: initial, identity: continuationRepairIdentity{Turn: 1, WireModel: "gpt-6-astra"}} - err := u.validateWire(continuationRepairMarshal(fields), 1) - if err == nil || err.Error() != test.class { - t.Fatalf("unexpected wire classification: %v", err) - } - }) - } - first := []byte(`{"model":"gpt-6-astra","stream":true,"store":false,"input":[{"type":"reasoning","encrypted_content":"opaque","unknown":9007199254740993}],"unknown":{"large":9007199254740993}}`) - u := &continuationRepairUpstream{firstSent: first, identity: continuationRepairIdentity{Turn: 2, WireModel: "gpt-6-astra"}} - allowed := bytes.Replace(first, []byte(`"encrypted_content":"opaque",`), nil, 1) - if err := u.validateWire(allowed, 2); err != nil { - t.Fatal(err) - } - for _, changed := range [][]byte{ - bytes.Replace(first, []byte("opaque"), []byte("changed"), 1), - bytes.Replace(allowed, []byte("9007199254740993"), []byte("9007199254740992"), 1), - bytes.ReplaceAll(allowed, []byte(`"unknown"`), []byte(`"changed"`)), - } { - if err := u.validateWire(changed, 2); err == nil || err.Error() != "wire_recovery_mismatch" { - t.Fatalf("recovery accepted a change beyond ciphertext omission: %v", err) - } - } -} - -func continuationRepairPreflightError(class string) error { - switch class { - case "wire_json_mismatch", "wire_model_mismatch", "wire_stream_mismatch", "wire_store_mismatch", - "wire_instructions_mismatch", "wire_input_mismatch", "wire_recovery_mismatch": - return continuationRepairWireError(class) - default: - return errors.New("handler_failed") - } -} - -type continuationRepairOfflineUpstream struct { - calls int - endpoint string -} - -func (u *continuationRepairOfflineUpstream) Do(req *http.Request, _ string, accountID int64, _ int) (*http.Response, error) { - if req == nil || req.URL == nil || req.URL.String() != u.endpoint || accountID != 16050 || req.GetBody != nil { - return nil, errors.New("offline_transport_contract") - } - body, err := io.ReadAll(req.Body) - if err != nil { - return nil, err - } - _ = req.Body.Close() - fields, err := continuationRepairObject(body) - if err != nil { - return nil, err - } - var model string - _ = json.Unmarshal(fields["model"], &model) - u.calls++ - var output []json.RawMessage - if u.calls%2 == 1 { - output = []json.RawMessage{ - json.RawMessage(`{"type":"reasoning","id":"rs_offline","summary":[],"encrypted_content":"offline-opaque"}`), - json.RawMessage(`{"type":"function_call","id":"fc_offline","call_id":"call_offline","name":"continuation_probe_value","arguments":"{}","status":"completed","namespace":null,"unknown_vendor_field":{"large_integer":9007199254740993}}`), - } - } else { - output = []json.RawMessage{json.RawMessage(`{"type":"message","id":"msg_offline","role":"assistant","status":"completed","content":[{"type":"output_text","text":"CONTINUATION_OK","annotations":[]}]}`)} - } - payload := continuationRepairMarshal(map[string]any{"type": "response.completed", "response": map[string]any{ - "id": "resp_offline_" + strconv.Itoa(u.calls), "status": "completed", "model": model, "output": output, - "usage": map[string]int{"input_tokens": 4, "output_tokens": 2, "total_tokens": 6}}}) - return &http.Response{StatusCode: http.StatusOK, Header: http.Header{"Content-Type": []string{"text/event-stream"}, "X-Request-Id": []string{"offline-" + strconv.Itoa(u.calls)}}, - Body: io.NopCloser(bytes.NewReader(append(append([]byte("event: response.completed\ndata: "), payload...), '\n', '\n')))}, nil -} - -func (u *continuationRepairOfflineUpstream) DoWithTLS(req *http.Request, proxy string, accountID int64, concurrency int, _ *tlsfingerprint.Profile) (*http.Response, error) { - return u.Do(req, proxy, accountID, concurrency) -} - -type continuationRepairOfflineBlockingBody struct { - started, closed chan struct{} - start, stop sync.Once -} - -func (b *continuationRepairOfflineBlockingBody) Read([]byte) (int, error) { - b.start.Do(func() { close(b.started) }) - <-b.closed - return 0, io.ErrClosedPipe -} - -func (b *continuationRepairOfflineBlockingBody) Close() error { - b.stop.Do(func() { close(b.closed) }) - return nil -}