Skip to content

feat(proxy): embed IP risk scoring and sync production line - #593

Open
ifThink404 wants to merge 81 commits into
james-6-23:mainfrom
ifThink404:codex/production-main
Open

feat(proxy): embed IP risk scoring and sync production line#593
ifThink404 wants to merge 81 commits into
james-6-23:mainfrom
ifThink404:codex/production-main

Conversation

@ifThink404

@ifThink404 ifThink404 commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Embed the Scamalytics v3 proxy risk scorer directly into Codex2API; no external 8788 service is required at runtime.
  • Add profile management, masked credential handling, quota/daily limits, cache controls, asynchronous batch jobs, cancellation, latest/history snapshots, and safe docs/tutorial links.
  • Improve the proxy list layout with explicit desktop column widths, a stable wide table, wrapped ISP/ownership and proxy-feature details, and an isolated recommendation/action area so buttons never overlap.
  • Keep score/error/unscored states distinct and preserve the existing proxy test and binding flows.
  • Sync the current production line with the latest official main; remove the stale subscription-upgrade initializer left by the merge while retaining official invite-guide changes.

Verification

  • go test ./... -count=1
  • go vet ./...
  • npm run typecheck
  • npm test (192 passed)
  • npm run build
  • git diff --check

Scope

This PR is opened from codex/production-main so the official branch can review the complete release-line integration together with the embedded proxy scoring feature. No credentials are included in source, logs, or this description.

Summary by CodeRabbit

  • New Features
    • Added Claude Code account management, OAuth, token import/export, model refresh, native Messages routing, usage tracking, and settings.
    • Added Claude support across dashboards, usage, API keys, scheduling, groups, and pricing.
    • Added proxy risk scoring with profiles, scans, filters, results, and history.
    • Added proxy import/export options and provider-aware pricing synchronization.
  • Security
    • Added optional encryption for sensitive credentials stored at rest.
  • Documentation
    • Expanded API, Claude, and proxy risk-scoring documentation.

ifThink404 and others added 30 commits August 26, 2026 12:05
Grok's strict tool deserializer rejects function tools whose parameter
schema root is not a single object ('tool parameter root must be an
object type'), which hard-400s whole conversations when a client bridges
MCP tools with anyOf/oneOf union roots (seen with Codex App's
mcp__codex_app__automation_update).

Merge union roots into one object schema (properties union; required
keeps only keys mandatory in every object branch, allOf keeps the union
per its all-must-hold semantics), collapse type-array roots that include
'object', and degrade anything else to a permissive object that keeps
the description. Compliant object roots and nested unions stay
byte-identical so upstream prefix caching is not disturbed.
Grok/xAI error bodies carry the explanation in a top-level string field
({"code":"...","error":"..."}). The extractor only knew the object form,
so these failures reached clients and usage logs as a bare 'Upstream
returned status 400' and diagnosis required container logs. Adopt the
string form only when 'error' is a JSON string, keeping object-form
handling and the HTML/plain-text fallback unchanged.
fix(grok): normalize non-object tool schema roots; surface string-form upstream errors
Production follow-up: the real Codex App schema declares type:"object"
together with a root-level anyOf, and Grok still rejects it as a union
root. Detect anyOf/oneOf/allOf before the type check, use the root's own
properties/required as the merge base, and stop importing branch-side
required keys whenever a non-object branch (typically null, meaning the
tool may be called with no arguments) was dropped — hardening those keys
would forbid calls the original schema allowed. allOf keeps the union of
required per its all-must-hold semantics.
…zation

Adds Claude Code (Anthropic) OAuth subscription accounts as a fourth upstream
provider alongside codex/grok/antigravity, reusing the existing account pool,
scheduler, refresh, usage-window and proxy infrastructure. All changes are
additive, claude-guarded branches.

Backend:
- auth/claude_oauth.go: OAuth2+PKCE login, code exchange, token refresh,
  profile lookup over a uTLS (Cloudflare-resistant) client
- auth/claude_account.go: UpstreamClaude, IsClaudeOAuth, refreshClaudeAccount
- auth/claude_fingerprint.go: per-account stable Claude Code CLI fingerprint
  (UA / x-app / x-stainless-*) + timezone, persisted in credentials
- proxy/claude_upstream.go: near-passthrough to api.anthropic.com/v1/messages
  (Bearer + anthropic-beta oauth + mandatory Claude Code system block);
  preserves a real client's identity headers, else synthesizes from the account
  fingerprint; NFC + invisible-char request sanitization; reuses native SSE path
- admin/claude_accounts.go: OAuth two-step + token-JSON import endpoints,
  proxy-pool selection, dedup under mergeDuplicateMu
- database: UpstreamChannelClaude channel constant + filter
- cmd/claude_login: standalone non-interactive login/refresh self-test CLI

Frontend:
- new ClaudeAccounts page + provider switcher tab, routing, ChannelLogo, api, i18n
- ProxyPoolSelect shows per-proxy bound-account count / idle, unified across all
  providers (Antigravity adopted the shared picker)

Verified: go build ./..., go vet ./..., Claude + relay/messages/grok regression
tests, frontend tsc + vite build all pass.
Adds opt-in, env-gated (CODEX_CRED_ENCRYPTION_KEY) encryption of sensitive
credential fields (access_token/refresh_token/session_token/api_key/id_token/
agent_private_key/client_secret) in the accounts.credentials JSONB, covering
ALL providers (codex/grok/antigravity/claude).

Design:
- Deterministic AEAD (AES-GCM with an HMAC-derived nonce): same plaintext -> same
  ciphertext, so the scheduler-outbox change-detection triggers and the
  account-list presence checks (which compare credentials->>'access_token')
  keep working unchanged.
- Single read choke point: decodeCredentials decrypts, so GetCredential and all
  map readers see plaintext.
- Encryption applied at every credential store site (UpdateCredentials, SQLite
  per-key json_set, InsertAccount*, CAS/merge paths, migrations).
- Off by default: when the key is unset every function is a no-op, so behavior
  is identical to before (all existing tests pass unchanged).
- Backward compatible: legacy plaintext rows (no enc: prefix) are read as-is and
  transparently re-encrypted on next write. Wrong/lost key fails closed (returns
  ciphertext, never plaintext) so the account is simply re-imported.

Non-sensitive fields (upstream_type/email/plan_type/models) stay plaintext for
SQL filtering. Verified: full database suite (key unset), dedicated crypto unit
tests + a DB round-trip integration test (at-rest ciphertext, plaintext reads),
go vet, auth regression all pass.
Login could fail silently against Anthropic's Cloudflare-fronted OAuth endpoints
when the uTLS (Chrome, forced-HTTP/2) client was blocked or incompatible.

- ClaudeAuth now uses a primary uTLS client with automatic fallback to a standard
  proxy-aware http.Client (ALPN-negotiated h1/h2) on transport error or 403.
- Drop the Connection: close / req.Close axios hint that is meaningless over h2.
- cmd/claude_login prints a diagnosis (Cloudflare block / invalid_grant / network)
  on failure to speed up root-causing.

OAuth constants (client_id, endpoints, scope, redirect_uri) re-verified against
the upstream reference and are current. Build + claude auth tests pass.
The Anthropic->Codex model resolver maps any 'claude*' model to gpt-5.4 (fuzzy
fallback), so a native /v1/messages request for e.g. claude-sonnet-4-5 never
matched the Claude account and returned 503 'No available accounts'.

resolveMessagesRoutingBody now keeps the native model ID when the pool has a
Claude Code OAuth account that can serve it (hasNativeClaudeAccountForModel),
routing to the claude passthrough; otherwise it keeps the existing Codex
translation fallback so Codex-backed /v1/messages users are unaffected.

Verified end-to-end: real streaming inference through the gateway returns a
Claude response. Also clarifies cmd/claude_login paste instructions (single-quote
the callback URL to avoid zsh globbing).
- Expose current Claude models (opus-4-5 / sonnet-4-5 / haiku-4-5) in /v1/models
  per-account (owner=anthropic), only when a Claude account exists, mirroring the
  grok/antigravity account-scoped pattern (supportedModelIDs + scopedModelRecords
  + modelBackingClaude). This also makes resolveAnthropicModel treat them as known
  models and keep native routing; deployments without Claude accounts are
  unaffected (claude-* still falls back to Codex translation).
- DefaultClaudeModelIDsForAccount uses the account Models whitelist or a curated
  current-generation default (all three verified against a live subscription).
- Fix claudeFamilyPricing: Haiku 4.x is $1/$5 (not the legacy claude-3-haiku
  $0.25/$1.25). Opus 4.5 $5/$25, Sonnet 4.5 $3/$15 already correct.

Verified end-to-end: all three models listed in /v1/models and return real
streaming inference; usage cost matches official rates. Antigravity(gemini) and
Grok models were already listed+priced via their family rules.
…g list

ListModelPricing now also surfaces Claude models (claudeChannelModels: union of
each Claude account's visible models) alongside codex/grok/antigravity, and tags
every row with a 'channel' (codex/grok/antigravity/claude) so the pricing UI can
group by provider. Claude rows carry the family default price (sonnet 3/15,
opus-4-5 5/25, haiku-4-5 1/5). Empty when no Claude account exists.
…vity/Claude)

Redesign the pricing page for legibility as models grow across providers:
- Add a provider filter row (ChannelLogo + name + count) shown when more than one
  provider is present; click to isolate a provider.
- Group the list by provider with section headers in the combined view.
- Consume the new per-row 'channel' field from ListModelPricing.
Reuses the existing search, source filter, inline edit and sync flows.

Verified: frontend tsc + vite build clean; /admin/model-pricing serves; the
pricing API returns codex/antigravity/claude groups with correct Claude prices.
…l pricing

Stop hardcoding the Claude model list — derive it from what the account can
actually serve, mirroring how Grok/Antigravity are account-driven.

- auth: FetchModels() calls Anthropic GET /v1/models with the account OAuth token
  (paginated) to discover the account's real available models (opus-5, sonnet-5,
  opus-4-8, dated 4.5 variants, ...), not a fixed list.
- import now fetches + stores them in credentials.models (loads into account.Models,
  which DefaultClaudeModelIDsForAccount already prefers); hardcoded default is only
  a fallback when discovery fails.
- POST /accounts/:id/claude/models refreshes an existing account's models live
  (updates in-memory account.Models immediately; LoadAccountByID no-ops for
  already-loaded accounts).
- billing: modern Opus tier — Opus 4.5+ (incl 4.6/4.7/4.8/5) is $5/$25; only
  legacy Opus 3/4/4.1 stays $15/$75, so new models don't inherit stale high prices.
- official pricing sync: add IncludeClaude (config column + API + poller). Anthropic
  has no parseable price doc, so the Claude source stamps each of the account's real
  models with its family-rule price as 'synced' — dynamic, covers every discovered
  model, no hardcoded price table.

Verified live: account exposes its 10 real models in /v1/models + pricing page;
opus-5 inference works; official claude sync applied 10/10 with correct modern
prices; usage cost matches.
…e model refresh

- Pricing page official-sync card gains an Anthropic/Claude source toggle
  (include_claude), wired through config + one-off sync; stamps every discovered
  Claude model with its family-rule price as 'synced'.
- ClaudeAccounts page gains a 'refresh models' action per account -> POST
  /accounts/:id/claude/models -> re-discovers the account's real available models
  live.
- types/api: OfficialPricingSyncConfig.include_claude; syncOfficialModelPricing /
  updateOfficialPricingSyncConfig carry include_claude; refreshClaudeModels().
- i18n (zh/en/zh-TW): claude.refreshModels / claude.modelsRefreshed.

tsc + vite build clean; verified live (toggle persists, refresh returns the
account's 10 real models).
- Pricing page gains a 'Model catalog' button (badge shows count of new models)
  opening a modal: models grouped by provider, searchable; click a model to jump
  to and highlight its price row.
- Refresh account models from the catalog (POST /accounts/claude/models/refresh
  re-discovers every Claude account's real available models).
- NEW badges: models not seen before (localStorage-tracked, seeded on first load)
  are flagged in both the catalog and the price row; 'mark seen' acknowledges them.
- Backend RefreshAllClaudeModels endpoint; api.refreshAllClaudeModels.

tsc + vite build clean; page serves; refresh-all returned 1 account / 10 models.
Bring the Claude accounts page closer to the Codex/Antigravity pages using data
the paged accounts API already returns for claude:
- clickable status stat chips (all/normal/rate-limited/abnormal/error/disabled/
  locked) from the response summary, driving a status filter;
- scheduling health view (healthy/warm/risky);
- search over email/name/model;
- richer per-account rows: model count, 5h/7d usage bars, rate-limit detail.

tsc + vite build clean; page serves.
Introduce Claude Code OAuth accounts as a new upstream channel with full
account management UI, mirroring the Codex pool-mode experience.

Backend:
- OAuth/PKCE login, token refresh, profile-based plan detection
  (pro/max-5x/max-20x/team), dynamic model discovery via /v1/models
- Passthrough to api.anthropic.com/v1/messages with per-account stable
  fingerprint + Claude Code system prompt injection
- Parse Anthropic unified rate-limit headers -> 5h/7d usage snapshots
  (SyncClaudeUsageState), precise cooldown on 429/rejected
- Per-account fingerprint mode (preserve/force) + timezone; global
  ClaudeCode config (claude_config column): session window / fingerprint
  default / default timezone
- Account-group channel support for claude (NormalizeAccountGroupChannel,
  accountRowGroupChannel, display name) — additive, no impact on other channels
- Model pricing: always surface Grok built-in models; Anthropic family pricing

Frontend:
- ClaudeAccounts pool-mode table: CompactStat summary cards, quota/rate-limit
  analysis panels, full filters (status/plan/auth/group/tag/domain/sort),
  column show/hide, pagination, rich rows (usage bars, request pills, cost,
  health bar), centered data columns, sunburst avatar/channel icon
- Shared components: ProxyField (input + test + pool select with location/
  bound-count badges + selection echo), AccountGroupManagerModal, enhanced
  ProxyPoolSelect
- Edit-account modal (proxy/fingerprint/timezone/concurrency/priority/
  auto-pause), OAuth add flow shows auth URL, manual-proxy save-to-pool prompt
- System Settings: ClaudeCode global config card

EXPERIMENTAL: Claude support is not production-hardened and still needs
validation against real production traffic.
…laude-oauth-provider

# Conflicts:
#	database/postgres.go
#	database/sqlite.go
#	frontend/src/pages/Settings.tsx
…fallbacks

- auth: bound CAS retry (2 attempts) for the in-memory fingerprint write in
  RefreshClaudeFingerprintVersions instead of silently skipping on a
  concurrent-write conflict; there is no reconciliation loop for Claude
  accounts, so a skip would persist as memory/DB divergence until the next
  sync or restart.
- admin: SyncClaudeCLIVersion now returns 502 only when the version fetch
  itself failed; a post-fetch fingerprint-refresh error is surfaced as a
  non-fatal `warning` field on an otherwise-200 response. Adds a
  proxy.SetClaudeVersionSourceURLsForTest seam to exercise this.
- proxy: replace the two remaining hardcoded claude-cli/2.1.220 fallbacks
  (empty-UA guard and defaultClaudeIdentityHeader) with
  auth.EffectiveClaudeCLIVersion() so the UA floor tracks synced versions.
- proxy: move StartClaudeCLIVersionSync's startup local fingerprint refresh
  inside the background task so it no longer blocks the caller goroutine;
  it still runs even when CLAUDE_DISABLE_CLI_VERSION_SYNC is set, only the
  networked sync loop is skipped.
- frontend: only update the displayed synced-version marker when the sync
  actually persisted a bump (result.updated), and surface a warning toast
  when the sync response carries one.

Claude-Session: https://claude.ai/code/session_01R2UHu3k9ZvaACfQY7BciXZ
…res and respect cache_control limit

- drop assistant thinking blocks whose signature is empty or truncated before sending (client session files can persist them without signatures)
- on upstream 400 "Invalid signature in thinking block", strip all thinking blocks and retry once on the same account instead of rotating
- inject the Claude Code system preamble without cache_control when the client already uses 4 cache_control blocks

Claude-Session: https://claude.ai/code/session_01R2UHu3k9ZvaACfQY7BciXZ
…c usage semantics

- parse cache_creation_input_tokens (5m/1h breakdown) from Anthropic usage and persist
  cache_write_5m_tokens / cache_write_1h_tokens on usage_logs (sqlite + postgres)
- convert native Claude usage to total-input semantics (uncached + cache read + cache
  write) so cache reads are no longer clamped to input_tokens and priced away
- CalculateCostBreakdownWithCacheWrites adds cache-write costs from the model pricing
  table (default 1.25x / 2x input); account_billed / user_billed and the Usage page
  breakdown now include cache read and write costs
- inject the Claude Code system preamble with ttl=1h when the client's first
  cache_control block requests 1h, so Anthropic no longer rejects the request
- Usage page shows cache-write tokens and 5m/1h write costs and unit prices

Claude-Session: https://claude.ai/code/session_01R2UHu3k9ZvaACfQY7BciXZ
…o usage logs

The handler copied prompt/cached tokens into the usage log but not the new
cache_write_5m/1h fields, so writes were priced as plain input.

Claude-Session: https://claude.ai/code/session_01R2UHu3k9ZvaACfQY7BciXZ
message_start reports the 5m/1h breakdown while message_delta only reports the
total; applying the "no breakdown = 5m" fallback per event and merging by max
counted the same write twice. Keep only reported breakdowns in the parser and
apply the fallback once when mapping into the usage log.

Claude-Session: https://claude.ai/code/session_01R2UHu3k9ZvaACfQY7BciXZ
- credits_required now removes the model from the account's explicit model
  whitelist (persisted), so the scheduler stops selecting that account for it
- MarkModelCooldownWithBackoff never shortens an active longer cooldown; the
  generic 4s rate-limit cooldown used to overwrite the 30m credits_required one
  and the account was re-selected every few seconds

Claude-Session: https://claude.ai/code/session_01R2UHu3k9ZvaACfQY7BciXZ
Claude Fable / Mythos reject thinking: {type: "disabled"} with 400; clients
such as Claude Code with thinking switched off still send it. Omit the
parameter before sending for those models, and when Anthropic rejects
thinking.type.disabled on any model (e.g. Opus 5 at effort xhigh/max), drop
the thinking parameter and retry once on the same account.

Claude-Session: https://claude.ai/code/session_01R2UHu3k9ZvaACfQY7BciXZ
Anthropic phrases the Opus 5 case as "effort 'max' is not supported when
thinking is disabled"; treat it like thinking.type.disabled and retry once
without the thinking parameter.

Claude-Session: https://claude.ai/code/session_01R2UHu3k9ZvaACfQY7BciXZ
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant