feat(proxy): embed IP risk scoring and sync production line - #593
Open
ifThink404 wants to merge 81 commits into
Open
feat(proxy): embed IP risk scoring and sync production line#593ifThink404 wants to merge 81 commits into
ifThink404 wants to merge 81 commits into
Conversation
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
…nt header changes Claude-Session: https://claude.ai/code/session_01R2UHu3k9ZvaACfQY7BciXZ
…ch for Claude CLI version sync Claude-Session: https://claude.ai/code/session_01R2UHu3k9ZvaACfQY7BciXZ
…or before upstream Claude-Session: https://claude.ai/code/session_01R2UHu3k9ZvaACfQY7BciXZ
…write failure Claude-Session: https://claude.ai/code/session_01R2UHu3k9ZvaACfQY7BciXZ
…against regressions Claude-Session: https://claude.ai/code/session_01R2UHu3k9ZvaACfQY7BciXZ
…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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
main; remove the stale subscription-upgrade initializer left by the merge while retaining official invite-guide changes.Verification
go test ./... -count=1go vet ./...npm run typechecknpm test(192 passed)npm run buildgit diff --checkScope
This PR is opened from
codex/production-mainso 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