Commit Graph

140 Commits

Author SHA1 Message Date
Luis Pater
d31b15916d feat(executor): support token usage parsing for plugin executors
- Add `ParsePluginExecutorResponseUsage` to extract token usage from non-streaming plugin responses across Claude, Gemini, Interactions, Antigravity, and OpenAI/Codex protocols.
- Add `ObservePluginExecutorStreamUsage` to observe and aggregate token usage across streaming chunks.

Closes: #5340
2026-08-30 14:51:08 +08:00
hkfires
6a489fa84d fix(auth): prefer errors from upstream attempts
Track when executor calls cross an upstream transport boundary and use that
signal to keep model/provider errors from being replaced by later local
preparation, selection, or internal failures.

Mark HTTP, websocket, relay, and usage-tracked transports as upstream
attempts, while avoiding marks for local validation, logging, missing
sessions, and successful websocket handshakes before request send.

Parse relative auth expiry metadata and adjust Antigravity refresh timing.
2026-08-29 12:50:46 +08:00
hkfires
bc918ab276 fix(runtime): log safe home refresh error types 2026-08-29 12:50:45 +08:00
hkfires
e4a8f98913 fix(logging): enhance error diagnostics and logging for home refresh operations 2026-08-29 12:50:45 +08:00
hkfires
9a2201c36a fix(auth): forward Home unauthorized upstream errors
Stop refreshing Home-owned OAuth credentials after upstream 401s.
Preserve marked upstream response bodies for direct responses, usage
records, request logs, and websocket handshake failures.
2026-08-29 12:50:44 +08:00
sususu98
4b2beb3da1 feat(executor): measure effective TTFT with protocol-aware token classification (#5313)
* feat(executor): measure effective TTFT with protocol-aware token classification

- Introduce protocol-aware streaming token classification under internal/runtime/executor/helps
- Implement responses_ttft_helpers.go for OpenAI Responses / Codex WebSocket & SSE streaming
- Filter out container metadata, rate limits, handshake headers, and tool execution outputs
- Implement dual-track firstPacketDuration fallback for abnormal / zero-token stream terminations
- Add TrackHTTPClientRoundTripOnly to bypass false TTFT triggers on first response body reads
- Optimize UsageReporter.ObserveTokenEvent hot path with fast-path RLock short-circuiting (0 B/op)
- Align terminal event completions across SSE, WebSocket streaming, and non-streaming executions
- Add EnsurePublished metric recording fallback for successful terminal turns lacking usage blocks
- Add stub helpers with comprehensive specifications for Chat, Claude, and Gemini protocols

* fix(executor): normalize SSE response.done and terminate incomplete WS streams

* fix(usage): capture first-packet fallback on initial body reads with TrackHTTPClientRoundTripOnly
2026-08-29 01:22:21 +08:00
Luis Pater
95f83a8d96 fix(claude): treat allowed_warning as allowed in unified rate limit checks
- Treat `allowed_warning` status as allowed for shared 5h and 7d rate limit windows.
- Ensure Fable-only rejections with warning-level shared windows remain model-scoped instead of credential-scoped.

Closes: #5275
2026-08-27 17:32:07 +08:00
Luis Pater
4b5f1eab25 feat(plugin): support observing upstream websocket response events
- Introduce `WebSocketResponseObserver` capability and bump plugin ABI schema version to 4.
- Forward upstream WebSocket response frames from Codex and xAI executors to configured observers.
- Wire `WebSocketResponseObserver` across API handlers and plugin host dispatchers.

Closes: #5248
2026-08-27 05:30:19 +08:00
sususu98
ca601db05d feat: observe upstream provider quota signals (#5211)
Codex and Claude already emit credential-level quota watermarks on ordinary
responses. CPA used to drop them. Keep the latest watermark in memory and
return it from the management auth-file API.

Hard rule: this is observation only. It must not change scheduling, cooldown
selection, or auth-file persistence.

Snapshot, not accumulation
- QuotaState now has ObservedAt and a bounded Signals map. MarkResult fills
  them from the response headers already recorded on the request.
- Signals is the current response, not a union of earlier ones. Retry-After
  and "limit reached" only appear on the response that produced them; merging
  across responses would keep an expired value forever.
- A response with no quota header (transport failure, 5xx, unrelated endpoint)
  leaves the previous snapshot in place.
- ObservedAt is the time of the current snapshot. It advances even when the
  values did not change, so a consumer can tell a fresh reading from a stale
  one.
- When two model states merge, keep the newer snapshot. Do not union keys
  captured at different times.

What is observed, and what is not
- One predicate, ProviderSupportsQuotaObservation, decides the provider set.
- Keep Codex and Claude. Drop Kimi, xAI/Grok, Antigravity, and the Gemini
  family (gemini/vertex/aistudio): their ordinary headers are not a reliable
  credential-level remaining quota.
- Count-tokens reuses the credential but is not generation traffic.
  ExecuteCount sets SkipQuotaObservation so those headers cannot replace the
  last generation snapshot. Cooldown and success/failure accounting still run.

Cooldown must not overwrite the last snapshot
- Observation writes only ObservedAt and Signals.
- Cooldown writes only Exceeded, Reason, NextRecoverAt, and BackoffLevel,
  through applyCooldownFields. Never assign a fresh QuotaState{...} over a
  live value: that would zero the snapshot on 429, Cloudflare, credential-
  scope sibling updates, and cooldown clears.
- If a credential-quota cooldown is still active, MarkResult still observes
  an already-present model state. It does not create scheduler state just to
  record a watermark.
- .cds files persist cooldownFieldsOf(Quota) only. Restore keeps the newer
  ObservedAt, so reloading cooldown cannot clobber a newer in-memory snapshot.
- cooldownQuotaEqual still ignores observation fields, so a watermark change
  cannot by itself persist cooldown or move the scheduler.
- The management payload omits every cooldown field, so it cannot be mistaken
  for scheduler state or wired back into scheduling.
- Manual ResetQuota still clears the full QuotaState.

Codex websocket events
- Codex WS reports quota as codex.rate_limits frames, not HTTP headers.
  ParseCodexQuotaEventHeaders turns one event into the same bounded header
  shape, and MergeResponseHeaders folds it into the request-scoped holder.
  additional_rate_limits is accepted as an object (websocket) or an array
  (/wham/usage).
- Parse only through AppendCodexAPIWebsocketResponse. The shared
  AppendAPIWebsocketResponse is also used by xAI, and xAI error frames really
  do carry x-ratelimit-* headers. Parsing every frame as Codex quota would
  forge Codex headers into another provider's request log.
- Also capture code_review_rate_limits.
- A malformed active-limit name drops only that one header, not the window
  watermarks parsed from the same event.
- The type discriminator scans a bounded frame prefix, not every byte of
  every frame.
- HTTP namespaces an extra limit by short name (x-codex-bengalfox-*); WS
  namespaces it by limit name (GPT-5.3-Codex-Spark). The two paths cannot
  emit the same header names. The X-Codex-Additional- prefix marks the WS
  origin, and snapshot replacement keeps the two spellings from piling up.

Hardening
- Reject observed values with control characters. These strings reach the
  plain-text request log, and Limit-Name is upstream-controlled, so CR/LF
  could forge a header line.
- When the header cap is hit, keep plan/credits/primary ahead of
  additional-limit namespaces, then sort names so truncation is deterministic.
- QuotaState.Clone deep-copies Signals and is used by Auth.Clone and
  ModelState.Clone.
- Token stores still serialize credential metadata only, so observation adds
  no auth-file writes.
2026-08-24 17:15:37 +08:00
Luis Pater
42d8e746e5 fix(claude): avoid long global cooldowns for Fable-only 7d_oi rate limits
Closes: #5101
2026-08-21 02:02:46 +08:00
sususu98
85d2faddd1 fix(claude): preserve native subagent and environment headers (#4982) (#5084) 2026-08-20 00:30:41 +08:00
sususu98
8aa6868d0d fix(claude): support setup-tokens and gracefully handle 403 OAuth profile errors (#4983) (#5083) 2026-08-20 00:15:31 +08:00
sususu98
788e9b7928 fix(claude): restore tool_search_tool_result references and recognize advisor/agent server tools (#5044) (#5082)
- Add bidirectional remapping for nested tool_references inside tool_search_tool_result across non-stream, SSE stream, and multi-turn message history.
- Add advisor_ and agent_toolset_ to IsClaudeServerToolType to prevent schema stripping and MCP aliasing on native Anthropic server tools.
- Wrap MCP alias restoration errors in claudeMCPAliasRestoreError with IsRequestScoped() bool to avoid cooling down healthy OAuth credentials.
- Add unit tests for tool_search_tool_result remapping, error variants, server tool recognition, and error scoping.
2026-08-19 22:29:51 +08:00
sususu
0f69f09a70 fix(logging): exclude HTTP 499 and client cancellations from forced error logs 2026-08-19 18:58:47 +08:00
sususu98
62f5a2798c fix(executor): prepend empty user turn for model-first Gemini/Antigravity requests (#4959) (#5048)
* fix(executor): prepend empty user turn for model-first requests targeting Gemini/Antigravity (#4959)

When forwarding sliced conversation histories or tool calls across OpenAI Responses,
OpenAI Chat Completions, Claude Messages, and native Gemini, native Gemini and Antigravity
Gemini endpoints require that conversation contents begin with a user turn.

Normalize leading turns at the executor boundary rather than the translator layer:
- Prepend an empty user turn ({"role":"user","parts":[{"text":""}]}) for Gemini, Gemini Vertex,
  AI Studio, and Antigravity Gemini generation and CountTokens requests if the first turn is 'model'.
- Keep Antigravity Claude requests untouched to avoid adapter 400 errors.
- Ensure normalization runs after payload rules so payload index overrides target the original turns.
- Use no-copy GJSON inspection to keep overhead zero on valid user-first requests.

* fix(executor): inject Antigravity leading user after reasoning replay (#4959)

Replay can insert a model functionCall at contents[0] for sliced
tool-result history. Run the empty-user prepend on the final
requestPayload, after sanitize and prepareAntigravityGeminiReasoningReplayPayload.
2026-08-19 18:56:43 +08:00
sususu98
f1b0431c77 feat(claude): add fingerprint-profile=claude-code-cli for API keys and delegated providers (#5047)
* feat(config): add fingerprint-profile to Claude keys and auth JSON

- Add FingerprintProfile to ClaudeKey configuration struct and normalizer
- Track fingerprint-profile in config diff
- Map fingerprint-profile / fingerprint_profile to auth attributes in file and config synthesizers
- Support fingerprint-profile in Management API PatchClaudeKey and normalization
- Add Claude billing attribution string manipulation utilities in internal/util
- Document fingerprint-profile options in config.example.yaml

* feat(claude): add fingerprint policy and request-local CLI identity

- Centralize Claude fingerprint policy resolution in claude_fingerprint_policy.go
- Support stable Claude CLI identity synthesis (UUIDv5 account_uuid and SHA-256 device_id)
  seeded from API keys or stable OAuth IDs, keeping access tokens isolated
- Warn on unrecognized fingerprint-profile values

* feat(claude): apply CLI fingerprint to Messages and keep API keys caller-owned

- Wire centralized fingerprint policy into Claude and Kimi executors
- Keep first-party Anthropic API keys and delegated providers caller-owned by default
- Apply Claude Code CLI wire profile (betas, metadata, diagnostics, MCP aliases)
  when fingerprint-profile=claude-code-cli is configured
- Strictly align CCH signing with native Claude Code 2.1.220: only first-party
  api.anthropic.com and Vertex sign dynamic CCH; third-party gateways and Kimi
  receive billing header without cch= to avoid prompt cache busting
- Respect caller-owned count_tokens bodies by default while aligning CLI shape on opt-in
- Fall back to CLIProxyAPI/<version> User-Agent when caller sends no UA in caller-owned mode
- Scope custom operator header overrides accurately in caller-owned mode
- Add comprehensive test coverage for policy resolution, gateway opt-in, Kimi, and token counting
2026-08-18 17:29:26 +08:00
Bryan
a8f9814a69 fix(claude): keep Fable-only rate limits model scoped
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
2026-08-17 21:29:51 +08:00
Luis Pater
e0b4956242 fix(openai): ensure Responses usage includes token detail fields
- add shared `EnsureResponsesUsageDetails` helper to patch `usage` objects with:
  - `output_tokens_details.reasoning_tokens = 0`
  - `input_tokens_details.cached_tokens = 0`
  - for both plain JSON and SSE `data:` frames, including multi-line frames
- apply the helper to OpenAI Response format outputs in non-stream and stream paths across executors/plugins so translated payloads consistently include required usage details
- update websocket/completion payload builders to emit default `usage` detail fields for prewarm/finish responses

Closes: #4985
2026-08-15 15:00:19 +08:00
Luis Pater
ac82bedfaf fix(claude): add Anthropic unified rate-limit parsing helpers
- Add new Claude rate-limit helper utilities to detect unified header-based quota rejections (5h/7d), handling case-insensitive headers and missing canonical forms.
- Compute deterministic retry duration from `Retry-After` and header reset timestamps, preferring the longest applicable unified window so cooldowns can be applied consistently.
- Introduce reusable rate-limit classification logic to distinguish header-authoritative rejections from request-scoped errors (e.g. fast-mode entitlement failures) for correct credential/model cooldown behavior.

Closes: #4874
2026-08-15 14:08:16 +08:00
sususu
bdde638c27 fix(claude): pass through caller MCP tools on alias server collision
The word-based virtual server spans only ~2048^2 names, and plausible
real MCP server names such as file_system or web_search are valid BIP-39
word pairs. When a caller's own server matched the derived one, its tools
stopped passing through and entered alias recovery instead: they were
silently restored to an unrelated proxied tool, or failed the request
with a 500 when no semantic suffix matched.

Record untouched caller MCP tool names as identity entries in the reverse
map, skip those entries when collecting virtual servers and recovery
candidates, and forward them unchanged on an exact hit. Recording is
skipped when nothing was aliased, so an untouched request still keeps an
empty reverse map and a no-op restore path.

Also warn instead of silently forwarding an original name when the alias
space is exhausted, report an empty embedded wordlist, trace the
semantic-suffix fallback because it guesses rather than fails, and build
both alias entry points through one shared constructor so the exhaustion
tests cannot drift away from the production path.

Refs #4916
2026-08-14 22:45:09 +08:00
sususu
f6f03e4de9 fix(claude): use BIP-39 words for OAuth MCP tool aliases
Replace high-entropy Base32 alias IDs with request-local BIP-39 English
words so weaker models are less likely to drift tool names. Keep a
two-word virtual server plus one-word tool ID, linearly probe wordlist
space on collision without self-overlap, and fall through to unambiguous
longest semantic-suffix recovery after a successful but wrong parse.

Fixes #4916
2026-08-14 22:45:09 +08:00
Luis Pater
7cf92793c1 fix(gemini): skip zero-token usage placeholders and always finalize stream reporters
- Add `defer reporter.EnsurePublished(ctx)` to Gemini, Gemini Vertex, and AI Studio streaming goroutines so stream reporting is always finalized on exit.
- Update Gemini usage parsing to reject all-zero `usageMetadata` frames, preventing placeholder usage events from being accepted.

Closes: #4964
2026-08-14 22:38:56 +08:00
Luis Pater
b08fe3b492 fix(codex): preserve multi-agent-v2 namespace handling across incremental websocket turns
Closes: #4909
2026-08-12 15:04:17 +08:00
Luis Pater
db143aebac fix(codex): make input ID sanitization collision-resistant and deterministic
- Track normalized ID state (`occupied`/`preserved`) during preprocessing to avoid remapping valid existing IDs.
- Resolve collisions by appending deterministic hash-based suffixes until a free ID is found.
- Share occupancy tracking across shortening and remapping so sanitized IDs remain stable and idempotent.

Closes: #4891
2026-08-12 01:04:22 +08:00
sususu
a8bbbea2b9 fix(claude): recognize and pass through native Haiku helpers
Claude Code issues measured Haiku helper requests that intentionally omit the
claude-code beta and, for the minimal shape, the system field. Treat those
profiles as confirmed native clients so CPA does not cloak or rewrite them.

Accept the native metadata builder optional parent_session_id, require
platform headers only for presence and software baseline rather than exact
equality, derive timeout and version from the same header defaults the emitter
uses, and keep discriminating signals strict: exact beta allowlist, body
shape, lowercase hex CCH, and session binding. Preserve helper transport
headers, omit synthetic stream false on markerless helpers, and gate the
billing and CCH fallback on system presence so a later pipeline-attached system
prompt cannot go upstream unsigned while the measured no-system wire stays
intact.
2026-08-11 19:33:40 +08:00
sususu
516ec3a000 fix(antigravity): harden per-credential transport pooling
Review and live-test follow-ups to the shared upstream transport.

Bound the cache with an LRU that closes idle connections on eviction, so
rotating a credential's proxy or supplying a per-request base transport can
no longer leak pools.

Stop deriving a pool scope from Auth.Label: it is documented as an optional
human readable label for logging and carries no uniqueness guarantee, so two
OAuth identities sharing a label would share one TCP/TLS pool. Prefer a
refresh-token digest, which stays stable across access-token rotation and is
available to refresh requests that run before any access token exists.

Replace a typed-nil *http.Transport taken from the request context. It passes
the interface nil check, so leaving it in place made http.Client fall back to
http.DefaultTransport, which advertises h2 over ALPN and breaks the
HTTP/1.1-only fingerprint.

Only widen pool limits: treat MaxIdleConns == 0 and IdleConnTimeout == 0 as
unlimited, and leave a negative MaxIdleConnsPerHost alone because that is how
an operator disables pooling.

Size the cache for large deployments. An unused entry costs under 1 KB and no
goroutines, whereas evicting a live pool forces a fresh TCP + TLS handshake,
so capacity is not the lever for bounding memory.
2026-08-11 18:33:50 +08:00
sususu
c33a33e14a perf(antigravity): reuse native upstream connections 2026-08-11 18:33:50 +08:00
sususu
5fa66293db fix(antigravity): preserve request plugin hook semantics 2026-08-11 16:07:42 +08:00
sususu
cf8c27fe90 perf(antigravity): translate each upstream request once
Execute, executeClaudeNonStream and ExecuteStream all assign the validated
original payload to the request and then translate both values. Since both
translations saw the same bytes, every Antigravity request paid for a second
full scan of the client payload. On a captured 24MB tool-history request that
second pass cost roughly 0.9s of CPU and 651MB of allocations.

Translate once when both inputs share a backing array and hand the caller an
independent duplicate, because later stages edit the working copy in place.
Payloads that genuinely differ still get two translations.
2026-08-11 16:07:42 +08:00
Luis Pater
5d9b629962 fix(runtime): close per-request uTLS HTTP/2 connections with request context
- Create a fresh uTLS/HTTP-2 connection per request instead of reusing cached connections.
- Pass request context through dialing and TLS handshake to make `RoundTrip` cancelable.
- Wrap response bodies so the underlying HTTP/2 connection is closed when the body is closed, and clean up promptly on request/response failures.

Closes: #4878
2026-08-11 00:58:13 +08:00
Luis Pater
673bac5fc6 fix(codex): normalize custom_tool_call_output IDs with ctco_ prefix during Codex input sanitization 2026-08-09 21:34:01 +08:00
Luis Pater
197f520426 fix(codex): normalize custom_tool_call IDs with ctc_ prefix during Codex input sanitization 2026-08-08 23:25:11 +08:00
Luis Pater
4b3cc55cdc fix(cliproxy): centralize client error status mapping and apply context cancellation/deadline HTTP codes
Closes: #4601
2026-08-08 04:53:34 +08:00
jizhenggang
c30e60a11b Reduce Codex request amplification for large payloads
Reuse immutable request bytes during input inspection and avoid a redundant websocket clone while preserving the required outbound envelope.

Constraint: Preserve request immutability and one final WebSocket request-body allocation
Rejected: In-place JSON mutation | request buffers are shared across translation and execution stages
Confidence: high
Scope-risk: narrow
Directive: Do not retain no-copy gjson results or mutate their backing payload while results are in use
Tested: GOTOOLCHAIN=local go test ./...; targeted 8 MiB allocation benchmarks
Not-tested: Live Codex upstream network traffic
2026-08-07 19:16:06 +08:00
Luis Pater
0a95fa62a1 feat(compat): preserve Claude thinking/tool-call content for is-compat OpenAI compatibility models
- Add `is-compat` support to OpenAI compatibility model config, capabilities, hashing, and example config.
- Propagate `IsCompat` through API-key model resolution and switch OpenAI-compat executor translation to compatibility-aware routing.
- Keep Claude assistant thinking content in compatibility mode while keeping default behavior unchanged when `is-compat` is disabled.

Closes: #4776
2026-08-07 06:26:33 +08:00
Luis Pater
5e25566c24 fix(codex): normalize reasoning and function_call item IDs during input sanitization 2026-08-07 05:52:20 +08:00
Luis Pater
dcee14dd3c feat(compat): preserve compat-mode thinking/signature blocks for API-key models
- Added `is-compat` model metadata plumbing from config through executor and helpers, including hash computation.
- Introduced a compatibility-aware translation path (`TranslateRequestWithAPIKeyModelCompatibility`) and wired it into Claude/Gemini/Codex/Interactions request flows.
- Updated Claude message sanitization/translation behavior to keep empty-thinking compatibility blocks (including signatures) when `is-compat` is enabled, while keeping default behavior unchanged.
2026-08-06 17:19:24 +08:00
Luis Pater
e5ea945ed9 feat(codex): add model-level is-compat flag to rewrite MultiAgentV2 agent_message for Responses-compatible endpoints
Closes: #4801
2026-08-06 04:49:28 +08:00
DefinitelyNotSpammy
533b69e3e0 fix(claude): skip context management when thinking is disabled 2026-08-05 18:25:40 +08:00
Luis Pater
42eef103d6 feat(antigravity): obfuscate sensitive words in system instructions
Closes: #4696 #4723 #4732
2026-08-05 00:27:32 +08:00
sususu
8cf1d46f06 fix(usage): account for Claude thinking tokens 2026-08-03 22:30:00 +08:00
Supra4E8C
0fc028613b chore: exclude test changes from Home fixes 2026-08-03 21:40:06 +08:00
Supra4E8C
a81b9e9ced fix(home): report every unauthorized attempt 2026-08-03 21:40:06 +08:00
Supra4E8C
1df21b14bf fix(usage): update token fingerprint after refresh 2026-08-03 21:39:48 +08:00
Supra4E8C
d952cb4297 fix(home): reject disabled refreshed credentials 2026-08-03 21:39:48 +08:00
Supra4E8C
1e38a3a544 fix: retry Home OAuth requests after unauthorized 2026-08-03 21:39:48 +08:00
sususu
903e41b6dc docs(claude): clarify exact fingerprint baseline 2026-08-03 15:25:17 +08:00
sususu
a5f63909a5 fix(claude): harden request lifecycle
Classify malformed caller metadata and Fast failures as request-scoped, reuse the strict Anthropic origin gate, bound diagnostics and proxy caches, and remove the unrelated translator test change.
2026-08-03 14:47:26 +08:00
sususu
707934917a feat(claude): enable TLS session resumption 2026-08-03 14:47:26 +08:00
sususu
b3ed702e3a fix(claude): harden OAuth identity and native routing 2026-08-03 14:47:26 +08:00