`cli-chat-proxy.grok.com` turns away requests that do not name a
supported client version — the failure mode #3952 was opened for. The
pin has sat at 0.2.93 since 2026-07-17, while https://x.ai/cli/stable
and `npm view @xai-official/grok version` both report 0.2.114.
The version was written out in three places, which is how they came to
drift:
- `repository/http_upstream.go` (OAuth traffic through the CLI proxy)
- `pkg/xai/billing.go` (billing and quota probes)
- `service/openai_gateway_grok.go` (gateway request headers)
`xai.CLIClientVersion` is now the single source, and the other two build
their own client identity from it, so the next bump is one line.
`internal/pkg/xai` is a leaf package both layers already import, so this
adds no cycle. `CLIUserAgent` derives from the same constant.
Checked against the real 0.2.114 binary rather than the version feed
alone: the `x-grok-client-version` header name, the `xai-grok-cli`
token-auth value, and the version string all match what the CLI sends.
The pinned value is also the floor an operator override must clear, so
some fixtures carried a meaning relative to it rather than a fixed
number. Those were moved, not search-and-replaced:
- the "valid override" case now uses 0.2.115-alpha.1; at 0.2.95-alpha.1
it would fall below the new floor, be dropped, and stop testing
acceptance at all
- the prerelease-at-the-minimum case tracks the pin to 0.2.114-beta.1,
or it becomes a copy of the below-minimum case
- three of the five malformed-semver entries sat below the new floor and
would have been turned away for being old, not for being malformed
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Resolve const-block conflict in openai_gateway_grok_cache.go:
keep #4590's client tool cache constants, drop grokFreeRolling24hTokenLimit
(moved to pkg/xai as IsGrokFreeRolling24hTokenLimit with legacy 2M support).
Round-trip Grok encrypted reasoning via Anthropic thinking.signature and
seed cache identity from Claude Code session IDs, so multi-turn /v1/messages
traffic can hit xAI prompt cache more like CLIProxyAPI. Also strip invalid
encrypted_content and retry on the Messages path when decrypt fails after
account switches.
Use *string for AnthropicResponse.StopReason so streaming message_start
serializes as JSON null instead of "", matching official Anthropic wire
format and avoiding strict client turn-finalization failures.
The Anthropic→Responses streaming converter omits two things the OpenAI
Responses wire format requires, which breaks clients that reconstruct the
response from the event stream (rather than just reading deltas).
1. response.content_part.added is never emitted.
A message item is opened with content: [], and the OpenAI SDK's
accumulating stream helper (client.responses.stream) only appends a
content part when it sees content_part.added. Without it, the next
output_text.delta indexes output.content[content_index] and raises:
File "openai/lib/streaming/responses/_responses.py", line 352,
in accumulate_event
content = output.content[event.content_index]
IndexError: list index out of range
Raw iteration (responses.create(stream=True)) does not accumulate and is
unaffected, which is why this went unnoticed.
2. response.completed carries Output: []ResponsesOutput{}.
get_final_response() and tracing integrations parse the terminal event's
response directly, so callers see an empty output_text even though the
deltas streamed correctly. This one is invisible when only watching the
stream render.
Also carries the full text on output_text.done / content_part.done (deltas
carry increments only, done events carry the whole part) and fills in
content/arguments/summary on output_item.done, which had the same
empty-payload issue.
Reproduced against a live Anthropic-platform group with the openai Python
SDK 2.46.0; Arize Phoenix's playground hits the same path. Verified before
(IndexError) and after (full text via get_final_response()).
Adds regression tests covering event ordering, done-event payloads, and the
terminal event's output for both text and tool calls.
Behind a reverse proxy (e.g. nginx with X-Real-IP), admin audit logs and
session IP/UA binding always recorded 127.0.0.1 because they hardcoded
the gin trusted_proxies chain, while API key IP restriction already
honored the "trust forwarded client IP" system setting.
- add ip.GetSecurityClientIP(c, trustForwarded) as the single source of
truth for security-sensitive client IP selection; API key auth
middlewares (main + google) refactored onto it with zero behavior change
- SessionBindingContext(cfg) now resolves the client IP via the same
toggle and injects it into the request context; token issuance,
binding enforcement and its mismatch audit record all read the
injected value, so issue/verify can never diverge
- audit log middleware and audit-log clear trace record the same
security client IP (middleware.SecurityClientIP), falling back to the
trusted proxy chain when the injection is absent
- settings UI hint (zh/en) documents the broadened toggle scope and the
one-time re-login after toggling while session binding is enabled
With the toggle off (default) behavior is byte-for-byte unchanged.
Co-Authored-By: Claude <noreply@anthropic.com>