Commit Graph

127 Commits

Author SHA1 Message Date
Jason dbb5bd1537 feat(codex): xAI (Grok) OAuth managed provider with native Responses compat
Add a managed "xAI (Grok) OAuth" Codex provider that routes through the
local proxy to api.x.ai via the shared Grok CLI OAuth identity, plus the
native-Responses compatibility layer that makes Codex 0.142+ traffic work
against xAI's strict upstream serde parser.

Provider:
- codex.rs: recognize the xai_oauth placeholder in extract_auth, hard-pin
  the base URL to api.x.ai and the tool profile to native Responses
- forwarder.rs: treat xAI OAuth auth failures as non-retryable
- presets + ProviderForm/CodexFormFields: managed OAuth preset that hides
  the api key/endpoint fields and derives the provider type across apps

Native Responses compatibility (gated on is_xai_oauth, so no other
provider is affected):
- transform_codex_responses_namespace: flatten Codex's private
  namespace/plugin tool declarations into top-level function tools on the
  request; restore the flat function_call names back to {name, namespace}
  on the response (streaming and non-streaming) so the client matches its
  own namespaced tool registry
- transform_codex_responses_xai_sanitize: strip the OpenAI-backend-private
  fields xAI rejects (external_web_access, prompt_cache_retention,
  safety_identifier, the additional_tools carrier, tool_search, ...) with
  deterministic removals that keep the prompt-cache prefix stable
- wire both into the native passthrough after the request transform;
  response restore runs in a dedicated handler so the generic passthrough
  hot path is untouched

Ports the proven approach of sub2api's Grok Responses gateway. Verified
with a 4-round codex -> xAI OAuth workload: all tasks green, zero upstream
errors.
2026-07-21 16:39:34 +08:00
Jason c9ac6efd69 fix(proxy): add stable usage keys and idempotent raw-response logging
Derive request ids from upstream envelope ids (Codex/OpenAI top-level
id, Gemini responseId, Claude message id with non-empty filtering)
scoped as session:{app_type}:{provider_id}:{id} for non-Claude sources;
Claude keeps bare session:{id} to preserve session-log convergence.

The logger now queries and conditionally writes under a single
connection guard: identical semantic replays return without writing or
notifying, session_log rows may be upgraded by proxy, and same-id
different-semantic responses land on a deterministic SHA-256 collision
fallback key instead of being overwritten. Fixes the random-UUID
INSERT OR REPLACE duplication behind #5496.
2026-07-21 16:39:34 +08:00
Jason a35209a6e7 feat(xai): add Grok OAuth device-flow backend and proxy routing
Add an xAI OAuth manager using the OAuth 2.0 Device Authorization Grant
with endpoints resolved from xAI's OIDC discovery document. All HTTP goes
through the app-managed proxy client.

- Managed provider kind xai_oauth: forced openai_responses wire format,
  pinned api.x.ai base URL, bearer injection gated to the xAI origin,
  tokens registered for log redaction, single-auth-key takeover policy.
- Token cache cannot bypass account state: cache hits re-validate account
  usability, refresh commits run under the mutation lock with a
  refresh-token CAS check, and pending logins are re-checked before an
  account is persisted.
- Refresh classification: 401/403 with any body and 400 with a non-JSON
  body mark the account for re-auth; 429/5xx stay transient.
- Shared auth_* commands dispatch to xAI with guard types mirroring the
  Copilot/Codex branches.
2026-07-21 16:39:33 +08:00
Komi 08710d51fc fix(proxy): default Codex tool parameters to object schema (#5315)
Normalize function tool parameters on the Codex Responses -> Chat Completions bridge: default null/missing parameters (direct and nested function forms) to {"type":"object","properties":{}}, coerce explicit type:null, and add a root type for top-level oneOf schemas so strict OpenAI-compatible upstreams (DeepSeek etc.) no longer reject built-in Codex tools like codex_app__automation_update. Also hardens the Codex -> Anthropic tool path with the same object-type guarantee and adds regression coverage for all reported shapes.
2026-07-21 10:13:00 +08:00
ka79376046 613fef70bc fix(codex-chat): attach reasoning forward for responses bridge (#5508)
* fix(codex-chat): attach reasoning forward for responses bridge

* fix(codex-chat): preserve tail reasoning after embedded reasoning

* docs(codex-chat): align trailing reasoning comments
2026-07-19 11:39:29 +08:00
Thefool 1c0ee0c58a feat(grokbuild): add first-class Grok Build support (#5453)
* feat(grokbuild): add backend integration

* feat(grokbuild): add provider and management UI

* test(grokbuild): cover configuration and integrations

* fix(grokbuild): address backend review feedback

* fix(grokbuild): complete UI review feedback

* feat(grokbuild): add CLI lifecycle management

* fix(grokbuild): align provider icon fallback

* test(grokbuild): cover provider icon persistence
2026-07-17 15:50:50 +08:00
SaladDay 6d316c0bda fix(codex): preserve streamed tool call identity and order (#5310)
Keep non-empty call IDs across continuation deltas and release parallel tool calls in Chat index order when identity fields arrive late. Preserve valid sparse and later calls during finalization.
2026-07-14 20:01:10 +08:00
Ryan2128 9ca1a41f58 fix: normalize function parameters type to "object" for strict OpenAI-compatible providers (#4706)
* fix: normalize function parameters type to "object" for strict OpenAI-compatible providers

Some Responses tools carry parameters with `type: null` (e.g.
codex_app__automation_update), causing HTTP 400 from strict
OpenAI-compatible providers like DeepSeek that require
`{"type": "object", "properties": {...}}`.

This adds normalize_function_parameters() to ensure the type
field is always "object" in both branches of
responses_function_tool_to_chat_tool.

Closes #4705

* style: fix cargo fmt issues

* fix: handle null function parameters
2026-07-14 19:23:41 +08:00
Jason 6eb217b242 revert(proxy): drop the 1-hour cache TTL option and TTL-bucketed write accounting
The forced 1-hour cache_control TTL (schema v14) was a mistake. Injected
breakpoints return to Anthropic's standard 5-minute TTL, caller-owned
markers are preserved verbatim instead of having their TTLs rewritten,
and the 5m/1h cache-write buckets are removed from usage parsing and
pricing (back to the single aggregate cache-creation rate). The cache
TTL selector is removed from the rectifier settings panel along with its
i18n keys in all four locales.

SCHEMA_VERSION returns to 13: the unreleased cache_creation_1h_tokens
column and the v13->v14 migration are removed. The feature never shipped
in a release and the introducing commit was never pushed, so no
databases were stamped v14 outside this machine (local DB verified at
user_version 13).
2026-07-13 17:55:33 +08:00
Jason af58740bcd fix(proxy): align Codex OAuth client identity
ChatGPT's Codex backend routes model availability by the originator and version header pair. Requests identifying as cc-switch without a version were assigned to a cohort where gpt-5.6-luna resolved to an unavailable internal engine, resulting in a misleading 404 Model not found response.

Identify takeover requests as codex_cli_rs and send version 0.144.1, satisfying luna's minimal_client_version requirement of 0.144.0. A direct HTTP A/B test confirmed the existing headers returned 404 while the aligned identity completed successfully, so no WebSocket transport workaround is required.
2026-07-13 16:48:22 +08:00
Jason 51d6c458ff feat(codex): route native ChatGPT sessions through proxy takeover
Allow the built-in Codex official provider to participate in takeover mode while preserving Codex's native OAuth or API-key credentials instead of persisting them into provider records.

Project official routing into a dedicated TOML provider, normalize inline tables, clean stale managed placeholders, and fail closed when the live configuration cannot be transformed safely.

Validate forwarded authorization, make official 401/403 responses non-retryable, avoid circuit-breaker pollution, and share the first-party ChatGPT endpoint across the Codex and Claude adapters.
2026-07-13 16:48:21 +08:00
Jason 650905af2c fix(proxy): harden Responses and Anthropic protocol bridges
Fail closed on HTTP 2xx failure envelopes and pre-output SSE failures so semantic upstream errors can trigger failover instead of becoming empty successful replies.

Finalize incomplete and truncated streams explicitly, handle clean EOF and whole JSON responses, and keep tool-call stop reasons and terminal event ordering consistent.

Preserve structured tool results, URL images, documents, system roles, and signed thinking across both conversion directions. Drop incomplete historical tool calls safely and classify malformed completed arguments as non-retryable client requests.

Keep Codex-to-Anthropic prompt caching enabled by default while honoring the dedicated cache-injection switch.
2026-07-13 16:48:21 +08:00
Jason a078b4b207 feat(proxy): session-based prompt_cache_key routing for Codex Chat bridge
Codex always sends prompt_cache_key in its Responses requests, but the
Responses -> Chat Completions conversion dropped it, breaking session
cache affinity on upstreams that route by key (e.g. Kimi Coding).

- Re-inject prompt_cache_key after conversion in the forwarder: an
  explicit client key wins, otherwise a client-provided session ID;
  generated per-request UUIDs are never sent upstream.
- Provider-aware gating: "auto" enables only known-compatible upstreams
  (api.openai.com, api.kimi.com/coding) because strict gateways reject
  unknown fields with HTTP 400 (e.g. Fireworks); an advanced
  Auto/Enabled/Disabled override is available on the Codex form in all
  four locales.
- Kimi For Coding preset opts in explicitly.
2026-07-13 16:48:21 +08:00
Jason 27ce0a519c fix(proxy): harden Responses reasoning and tool-call conversion
Round-trip encrypted Responses reasoning items through bridge-owned Anthropic thinking blocks, discard orphaned reasoning-only history, and consume the official reasoning text event vocabulary.

Track concurrent streaming items by stable IDs and output indexes, reuse a dedicated fallback block for keyless legacy reasoning, recover tool arguments from done events, and close blocks in protocol order.

Normalize empty or incomplete non-streaming tool arguments, reject malformed completed calls, and persist upstream usage before returning terminal conversion errors.
2026-07-13 16:48:21 +08:00
Jason f991726ff0 fix(usage): account for cache-write tokens across schema versions
Parse cache_write_tokens from OpenAI usage details and preserve cache creation data across Chat, Responses, and Anthropic conversion paths.

Add explicit input-token semantics to request logs and rollups so legacy rows subtract cache reads only while new total-inclusive rows subtract both cache reads and writes. Migrate v12 databases, normalize rollups to fresh input, and cover historical backfill behavior with regression tests.
2026-07-13 16:48:21 +08:00
Komi ded0b63a8e fix: handle missing provider keys and tool schema types (#5069)
* fix: handle missing api keys and tool schema types

* fix: preserve nested tool schemas

---------

Co-authored-by: Jason <farion1231@gmail.com>
2026-07-11 16:57:46 +08:00
Yeeyzy 99e11e0851 feat(codex): support native Anthropic Messages protocol as upstream (#5071)
* feat(codex): support native Anthropic Messages protocol as upstream

Allow gateways that only expose the native Anthropic Messages protocol
(/v1/messages) to be used by Codex: the local proxy performs bidirectional
request/response/streaming conversion between Responses and Anthropic.

Backend:
- Add two conversion modules: transform_codex_anthropic / streaming_codex_anthropic
- codex.rs: add routing detection and auth: ANTHROPIC_AUTH_TOKEN→Bearer (default),
  ANTHROPIC_API_KEY→x-api-key, mutually exclusive
- handlers.rs: add handle_codex_anthropic_to_responses_transform
- forwarder.rs: support optional Claude Code client fingerprint impersonation
  (User-Agent / anthropic-beta / x-app / system prompt first-line injection)
  and /responses→/v1/messages rewriting
- codex_config: the anthropic format reuses the NativeResponses profile to strip
  custom tools
- ProviderMeta: add impersonateClaudeCode

Frontend:
- CodexApiFormat: add "anthropic"; the form adds auth field selection and an
  impersonation toggle
- Add en/ja/zh/zh-TW copy

Robustness:
- Downgrade when tool history / forced tool_choice conflicts with extended
  thinking, avoiding upstream 400s
- Emit cache_creation_input_tokens in usage and use saturating_add to guard
  against overflow
- Append a unique suffix to non-streaming/streaming output-item ids to avoid
  multi-segment text/thinking overwriting each other

* fix(codex): harden Anthropic bridge against empty text blocks and truncated streams

- Drop empty/whitespace-only text content blocks when rebuilding Anthropic
  messages from Responses history; Anthropic 400s on empty text blocks (e.g. an
  empty assistant text emitted alongside a tool_use), which broke follow-up and
  tool-result requests. Also drop messages left without content.
- Do not report a truncated Anthropic stream as completed: when the SSE
  connection ends before message_stop with no stop_reason, emit an incomplete
  response if partial output exists, or a failed (stream_truncated) response
  otherwise, mirroring the chat converter's EOF handling.

* fix(codex): resolve Anthropic-bridge review findings

Blocking:
1. Defer stripping the [1m] long-context marker until after catalog matching and model write-back, re-stripping it on the final Codex→Anthropic body and setting a flag to emit the context-1m-2025-08-07 beta header, so the marker is no longer lost or overridden by the default model.
2. Gate Anthropic thinking on the trailing turn only (via trailing_turn_allows_thinking) instead of scanning full history, so a Codex session that resends history each round no longer permanently loses thinking after the first tool call.
3. Inject 5m ephemeral cache_control on the Codex→Anthropic body by reusing cache_injector (handling the system string→array conversion), so system/tools/history are cached instead of re-sent at full price every round.
4. Add a shared base_url_is_full_endpoint helper (normalizing whitespace/query/fragment/trailing slash) used by both the Anthropic and Chat paths, so a base URL already ending in /v1/messages is treated as a full endpoint instead of double-appending to /v1/messages/v1/messages.
5. Align the catalog tool-profile predicate with the routing predicate so resolve_codex_catalog_tool_profile returns the Anthropic profile whenever the request converts to Anthropic, preventing freeform tools like apply_patch from being silently filtered by a ProxyChat catalog.
6. Explicitly disable native web_search for the Anthropic profile (including the no-catalog branch) via set_codex_native_web_search_field, so Codex no longer treats it as available while the transform silently drops it.
7. Only forward tool_choice when tools survive filtering, dropping it otherwise, to avoid a non-retryable 400 from upstream when tool_choice is sent with no tools.
8. Lower the fallback default to max_tokens=8192 (only when max_output_tokens is omitted) and clamp the thinking budget to max_tokens/2, disabling thinking below the 1024 floor, to avoid hard 400s on low-output-ceiling models/gateways.

Minor:
9. Centralize the Codex/OpenAI fingerprint-header denylist in is_codex_client_fingerprint_header so impersonating Claude Code uniformly drops originator/session_id/conversation_id/chatgpt-account-id/x-client-request-id/openai-* and the x-stainless-*/x-codex-* prefixes.
10. Retain content_block_start.input as start_input and fall back to it at block close when no input_json_delta arrived, so a gateway that carries the full tool input on the start event no longer yields empty tool arguments.
11. Extract a shared codex_responses_sse module as the single Responses SSE envelope builder that both the chat and anthropic streaming emitters delegate to, with byte-for-byte-unchanged wire output, so future event-format fixes touch one place instead of two.

* fix(codex): add per-provider max_output_tokens override for Codex→Anthropic path

Codex does not forward model_max_output_tokens in the request body,
causing the proxy to fall back to a conservative 8192 default. This
truncates long or thinking-heavy responses (stop_reason=max_tokens).

- Add maxOutputTokens field to ProviderMeta (Rust + TypeScript)
- Inject the value into the Anthropic request body before transform,
  taking precedence over request-supplied and default values
- Add numeric input in Codex form fields (Anthropic format only)
- Add i18n entries for label, placeholder, and hint (en/zh/zh-TW/ja)
- Include roundtrip and omission unit tests for the new field

* fix(codex): harden Anthropic protocol bridge

* fix(codex): address Anthropic bridge review

* fix(codex): preserve flattened Anthropic inputs

* fix(codex): harden Anthropic recovery paths

* fix(ci): satisfy Rust clippy

---------

Co-authored-by: Jason <farion1231@gmail.com>
2026-07-10 23:06:30 +08:00
zhaoyuan 524b9d9825 fix(proxy): 修复 Copilot/Codex OAuth 模块绕过全局代理导致 Claude 模型 400 错误 (#4583)
* fix(copilot): 修复 GitHub Copilot 出站请求不走全局代理导致 model not supported

* fix(proxy): 修复全局代理客户端配置未被应用到 Copilot/Codex OAuth 模块

两个关键问题:

1. CopilotAuthManager 在构造时写死 Client::new(),使得拉取 /models 列表、
   换取 token 等认证流程无视全局代理配置(global_proxy_url),直连目标服务。
   结果:直连时 /models 返回 0 个 Claude 模型 → live resolution 失效 →
   模型 ID 无法正确归一化/匹配 → Copilot 上游返回 400 model_not_supported。

2. CodexOAuthManager 也有完全相同的问题,导致 Codex OAuth 认证请求绕过代理。

改动:删除两个模块的自持 http_client 字段,改为每次请求时从全局客户端现取
(crate::proxy::http_client::get())。这样:
- 遵循全局代理 URL 配置
- 支持运行时热更新代理设置
- 符合代码库设计意图(http_client.rs 注释明确说"所有 HTTP 请求应使用此模块")

修复覆盖范围:
- copilot_auth.rs: 7 处调用(token 获取、/models 拉取、model vendor 判断)
- codex_oauth_auth.rs: 4 处调用(device code、OAuth token 刷新)

Fixes #2016 #2931
2026-06-25 16:05:23 +08:00
Jason 92930461b7 style(proxy): apply rustfmt wrapping to legacy function_call branch
CI cargo fmt --check failed on transform_codex_chat.rs (the wrapping
introduced upstream did not match rustfmt 1.95.0). Apply cargo fmt so the
chat_legacy_function_call_to_response_item call uses the expected layout.
2026-06-22 10:59:09 +08:00
Huei Feng 455556380b Chat API: skip tool calls with missing function names (#4159)
* Chat API: skip tool calls with missing function names

Some providers send empty or absent function names in streaming
tool call deltas. Previously these produced invalid output items.

- Don't overwrite accumulated state.name with empty deltas
- Skip tool calls that never received a valid name (instead of
  falling back to 'unknown_tool')
- Apply the same defensive guard in finalize_tools and the
  non-streaming path

* Address review: defer empty-name skip to finalization

Require both call_id and name before triggering should_add,
instead of skipping eagerly when name is absent in the first
delta.  This handles providers that send id before name,
as suggested in the Codex review.

* Guard legacy function_call against empty name

Return Option<Value> from chat_legacy_function_call_to_response_item,
returning None when function_call.name is missing or empty. This covers
the legacy message.function_call path that the original guard missed.

* Remove unreachable unknown_tool fallback

---------

Co-authored-by: Jason <farion1231@gmail.com>
2026-06-19 23:38:25 +08:00
Jason b724f5dde9 revert(proxy): drop Anthropic system-message hoisting (#3775)
The unrelated Windows test fixes from #3775 are kept; tool-thinking-history normalization is unchanged.

Refs #4297
2026-06-16 22:39:27 +08:00
chen-985211 36b557b2e6 fix(codex): restore cached tool call fields (#4160)
* fix(codex): restore cached tool call fields

* refactor(codex): merge duplicate enrich loops in chat history

enrich_call_item_from_cache copied the fill-if-empty loop for
reasoning_content/reasoning. The two loops are identical and key
order is irrelevant, so fold both key sets into a single loop.

Pure refactor, no behavior change; codex_chat_history tests pass.

---------

Co-authored-by: Jason <farion1231@gmail.com>
2026-06-16 10:59:16 +08:00
Mask 3e38889ccc feat(proxy): strip effort params when thinking:disabled for DeepSeek endpoint (#4239)
DeepSeek's Anthropic-compatible endpoint rejects requests where
thinking.type=disabled coexists with effort parameters, returning
HTTP 400. This breaks Claude Code 2.1.166+ sub-agents (Workflow/Dynamic
Workflow), which hardcode thinking:disabled.

Rather than overriding thinking:disabled, remove the conflicting effort
parameters (output_config.effort / reasoning_effort) to respect Claude
Code's intent — sub-agents don't need to display reasoning.

Fixes: https://github.com/deepseek-ai/DeepSeek-V3/issues/1397

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 10:23:16 +08:00
Jason a5903d8600 feat(health-check): replace real-LLM probe with HTTP reachability check
The provider panel health check sent a real streaming model request, which many third-party providers block (401/403/WAF), causing false negatives while only stable official endpoints passed. Replace it with a lightweight reachability probe: GET the provider base_url and treat any HTTP response (200/4xx/5xx) as reachable; only DNS/connect/TLS/timeout count as failure. Latency is the probe's TTFB.

Backend (services/stream_check.rs): rewrite ~2200 -> ~350 lines, dropping real-request building, format conversion, auth and API-path resolution while keeping per-app base_url extraction. Defaults: 8s timeout, 1 retry, 1500ms degraded threshold.

Failover invariant: the reachability check must never reset the circuit breaker (reachable != usable; a 403 host is reachable but broken for real traffic). Remove the resetCircuitBreaker call from useStreamCheck; failover failure detection stays driven solely by real proxy traffic (forwarder/circuit_breaker untouched). useResetCircuitBreaker is kept dormant for a future manual-recovery entry.

Open the check to all providers: drop the official/copilot/codex-oauth/third-party gating and the 'sends a real request' confirm dialog. For official providers whose base_url is intentionally empty, fall back to the endpoint the client actually uses (Claude -> api.anthropic.com, Codex -> chatgpt.com/backend-api/codex, Gemini -> generativelanguage). Non-official providers with a missing base_url still error to avoid a false green light. Claude Desktop Official is native 1P mode (talks to claude.ai, cc-switch not in the request path, no reliable endpoint) so its button stays hidden.

Slim StreamCheckConfig and per-provider testConfig to timeout/threshold/retries (drop test model + prompt); sync zh/en/ja/zh-TW. Retain the now-unused anthropic_to_openai/anthropic_to_gemini transform utilities and their test suites.
2026-06-14 21:21:45 +08:00
Jason cb01593f7d fix(proxy): exclude cache_read and cache_creation from input on Claude←OpenAI paths
Builds on #2774 (which fixed cache_read for the streaming openai_chat path).
Two gaps remained, both double-counting cache tokens when a Claude client
meters as app_type="claude" (input_includes_cache_read=false):

1. cache_read was still added to input on the non-streaming openai_chat path
   (transform.rs openai_to_anthropic) and the whole openai_responses family
   (transform_responses.rs build_anthropic_usage_from_responses, covering the
   non-streaming call site and both streaming_responses call sites).

2. cache_creation was never subtracted on any converted path, including the
   streaming openai_chat path #2774 had already touched. Claude billing treats
   cache_creation as a separate bucket, so an inclusive upstream carrying a
   direct cache_creation_input_tokens field billed it twice.

All four metering points now compute:
  input = prompt_tokens - cache_read - cache_creation
restoring the invariant input + cache_read + cache_creation == prompt_tokens.
Pure OpenAI upstreams are unaffected (no cache_creation concept/field).

Tests: update direct-cache assertions (40->20), add a streaming conservation
regression test, and pin prompt<cache underflow (saturating clamp to 0) for all
three metering functions. cargo test 1573 pass, clippy clean.

Note: fix is forward-only; historical rows are not recomputed (cost is frozen at
log time and app_type="claude" mixes native + converted rows).
2026-06-11 08:29:29 +08:00
Jason 36a103bbe4 fix(proxy): correct usage accounting on format-conversion paths
Audited all proxy format-conversion paths (Chat<->Message, Chat<->Response,
Gemini<->Message) for usage/cache metering. Five issues found and fixed.
The dedup mechanism (request_id PK, proxy/session source isolation) is
untouched, so no double-counting is introduced.

- A (Claude + openai_chat, streaming): inject stream_options.include_usage
  so OpenAI-compatible upstreams emit usage in the SSE tail. Without it the
  converted Anthropic message_delta was all-zero and the whole request's
  input/output/cache was dropped. Same root cause as the already-fixed
  Codex Chat path; the injection is extracted into a shared helper
  (transform::inject_openai_stream_include_usage) reused by both paths.

- C (Claude + gemini_native): subtract cachedContentTokenCount from
  input_tokens in build_anthropic_usage so input becomes fresh input
  (Anthropic semantics). Previously the cache-hit tokens were billed twice
  because this path meters as app_type="claude" (input_includes_cache_read
  = false) while Gemini's promptTokenCount includes the cache.

- D (Codex + openai_chat, streaming): gate log_usage on
  has_billable_tokens() to skip the synthetic all-zero usage the converter
  emits when a non-compliant upstream omits usage, preventing empty-row
  request-count inflation.

- P2 (from_claude_stream_events): use has_billable_tokens() for the return
  gate instead of input>0||output>0, so a fully-cached streamed request
  (cache_read>0, input==output==0) is still recorded. Affects all
  Claude-streaming paths, not just Gemini.

- P3 (Codex Chat->Responses, non-streaming): apply the same
  has_billable_tokens() filter the streaming branch got, since the
  synthesized all-zero usage makes from_codex_response return Some and
  bypass the `if let Some` guard.

Add TokenUsage::has_billable_tokens() as the unified predicate. New tests
cover include_usage injection, gemini input subtraction, the gate itself,
cache-only stream recording, and synthetic all-zero codex usage.
Full lib suite: 1569 passed.
2026-06-11 08:29:29 +08:00
LaoYueHanNi edc597ab23 修复 Completions转Anthropic时不记录实际返回模型、Input token记录错误问题 (#2774)
* fix(proxy): 修复completions转claude格式流式响应未记录实际命中模型

* style: cargo fmt fix

* fix(proxy): 修复completions转claude格式时input与cache_read重复计费

* fix(proxy): 修复完全缓存命中时input_tokens计算错误

* test: 更新input_tokens期望值匹配去重逻辑
2026-06-09 20:30:05 +08:00
cc10143 4f5250fc4d fix(proxy): strip cache_control from OpenAI format conversion (#3841)
* fix(proxy): strip cache_control from OpenAI format conversion (#3805)

- Remove cache_control passthrough from system messages, text blocks,
  and tools to prevent 400 errors on strict OpenAI-compatible endpoints
- Always simplify single text block content to plain string format
- Fixes two format conversion bugs reported in issue #3805

* fix(proxy): apply cargo fmt to fix CI formatting check
2026-06-08 12:38:39 +08:00
Jason f59fab6c24 feat(proxy): map input_file and input_audio content parts to chat
Convert Responses input_file (requiring file_id or file_data, never file_url which Chat file parts do not support) and input_audio parts into their Chat Completions equivalents, and handle top-level input_* items that previously fell through and were dropped, clearing stale pending reasoning for non-assistant messages.
2026-06-07 20:56:35 +08:00
Jason 6940a4b208 fix(proxy): distinguish truncated chat streams from normal completion
Replace the unconditional finalize at chat-to-responses stream end with a three-way guard: complete normally when finish_reason or [DONE] arrived, emit an incomplete response when substantive output exists without a finish_reason, and emit a failed (stream_truncated) event for empty truncation instead of masking it as completed. Also propagate late-arriving reasoning_content onto still-active tool-call items.
2026-06-07 20:56:35 +08:00
Jason ea6123adf7 fix(proxy): cache reasoning across turns for custom_tool_call and tool_search_call
Generalize the cross-turn reasoning cache in codex chat history from function_call only to the full tool-call triad (function_call, custom_tool_call, tool_search_call) and their *_output counterparts, so apply_patch and tool-search calls keep their reasoning_content when restored via previous_response_id.
2026-06-07 20:56:35 +08:00
Dearli666 3cd9a0dec5 fix(proxy): 规范化 Anthropic system 消息 (#3775) 2026-06-05 22:50:49 +08:00
oasis ea95f39adf fix(codex): drop tool_choice when tools is empty in Responses→Chat conversion (#3640)
Strict OpenAI-compatible upstreams (vLLM, enterprise gateways) reject
requests that carry tool_choice or parallel_tool_calls without a
non-empty tools array, returning 503/400 with:
  "When using `tool_choice`, `tools` must be set."

The Responses→Chat Completions converter unconditionally forwarded
tool_choice and parallel_tool_calls even when tools ended up absent
or empty after conversion. This commit adds a guard that removes both
fields when the resulting tools array is missing or empty.

Added 9 regression tests covering:
- tool_choice dropped when tools absent
- tool_choice dropped when tools is empty array
- parallel_tool_calls dropped when tools absent
- tool_choice dropped when all tools filtered (e.g., missing name)
- tool_choice preserved when tools present (auto + function type)
- clean output when neither tool_choice nor tools present
- tool_choice 'none' dropped when no tools
- tool_search_output providing tools keeps tool_choice

Refs #3557

Co-authored-by: yueqi.guo <guo_yueqi@qq.com>
2026-06-04 15:20:04 +08:00
Cherilyn Buren 33eafbad51 fix(copilot): 调高无限空白检测阈值 20 → 500 (#2647)
* fix(copilot): raise infinite-whitespace threshold from 20 to 500

The previous threshold of 20 falsely aborted legitimate tool calls whose
arguments contain indented code (write_file / edit_file with 4-8 levels
of indentation in Python / YAML / Rust / Markdown easily exceed 20
consecutive whitespace chars, especially when newlines are counted).

The real infinite-whitespace bug emits hundreds to thousands of
consecutive whitespace characters, so 500 keeps the safety net while
drastically reducing false positives.

Refs #2646

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs: clarify whitespace threshold comment

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Jason <farion1231@gmail.com>
2026-06-04 11:29:25 +08:00
Cao Xin c2337d6857 fix(codex): preserve custom tool metadata in chat routing (#3644)
* fix(codex): preserve custom tool metadata in chat routing

* fix(codex): reduce custom tool metadata description overhead

* fix(codex): stabilize custom tool metadata descriptions

* refactor(codex): remove unreachable custom tool description fallback
2026-06-04 08:33:53 +08:00
Yeeyzy b4f262c7bd fix(codex): always include output_tokens_details.reasoning_tokens in … (#3514)
* fix(codex): always include output_tokens_details.reasoning_tokens in chat→responses transform

Codex CLI strictly requires reasoning_tokens in the response.completed
usage object. Custom providers using /chat/completions often omit
completion_tokens_details, causing repeated parse failures and retries.

* fix(codex): guard non-object completion_tokens_details before insertion

---------

Co-authored-by: yeeyzy <yeeyzy@yangzhiying05@gmail.com>
2026-06-03 14:10:28 +08:00
Jason d66030bee6 Stream Codex custom tools with native input events
Custom (freeform) Codex tools are bridged through Chat Completions as
JSON `{"input": "..."}` functions, but the Chat->Responses stream still
re-emitted them via `response.function_call_arguments.*`, leaking the
JSON wrapper and using event types the Codex client does not route for
freeform tools.

Emit `response.custom_tool_call_input.delta`/`.done` (with the unwrapped
input text) for custom tool calls instead, suppressing the intermediate
function-argument deltas since a partial `{"input":` fragment cannot be
safely unwrapped mid-stream. Custom tool call items now use a `ctc_`
item-id prefix (matching the input events' item_id) consistently across
the streaming and non-streaming paths via a shared
response_tool_call_item_id_from_chat_name helper.

Covered by new streaming and non-streaming custom-tool tests.
2026-06-01 17:01:00 +08:00
Jason 5968336364 Restore Codex tool plugins over Chat Completions third-party proxy
When proxying Codex Responses requests through the Chat Completions
format (api_format=openai_chat), the request transform only forwarded
plain `function` tools and silently dropped `tool_search`, `namespace`
(MCP), and `custom` tools, so third-party APIs never saw the official
Codex plugins and could not call them.

Introduce CodexToolContext, built from the original Responses request,
which flattens all four tool kinds into Chat functions and keeps a
chat_name -> CodexToolSpec map. The response path (streaming and
non-streaming) looks names up in this map to restore the original
tool_search_call / custom_tool_call / namespaced function_call items
instead of reparsing the flattened name. Long namespaced names are
truncated with a sha256 suffix to fit the 64-char Chat tool-name limit.

Covered by new round-trip tests for all four tool kinds across both
streaming and non-streaming paths.
2026-06-01 16:29:23 +08:00
Siskon afa09e127e fix(usage): resolve per-app credentials for native balance/coding-plan queries (#3355)
* fix(usage): resolve per-app credentials for native balance/coding-plan queries

The native usage-query paths (balance + coding_plan) in
`query_provider_usage_inner` read credentials only from `env.ANTHROPIC_*`.
That matches Claude providers, but Codex stores its key in
`auth.OPENAI_API_KEY` with the base URL inside a TOML `config` string,
Hermes/OpenClaw flatten them at the top level, and OpenCode nests them under
`options`. So the card "refresh usage" / auto-query returned empty
credentials and failed ("查询失败") for those apps, even though the
config-page "Test" button worked (the frontend extracts per-app correctly).

Introduce a single per-app resolver `Provider::resolve_usage_credentials`
that mirrors the frontend `getProviderCredentials`, and route both native
branches through it. Add `extract_codex_base_url` to `codex_config` as the
canonical Codex TOML base-URL parser and make the proxy adapter delegate to
it (removing a duplicate copy). Align the frontend `getProviderCredentials`
to cover OpenCode and Claude Desktop and to use the same OpenRouter/Google
key fallbacks as the backend.

Fixes #3158
Fixes #3100
Fixes #2625

* refactor(usage): explicit AppType arms + frontend trailing-slash trim

Address two review nits on the per-app credential resolver:

- provider.rs: replace the catch-all `_` arm in resolve_usage_credentials with an explicit `AppType::Claude | AppType::ClaudeDesktop` arm, so a new AppType variant fails to compile here instead of silently defaulting to the Anthropic env shape.
- UsageScriptModal.tsx: normalize getProviderCredentials baseUrl through a single trailing-slash trim so the frontend 'Test' path matches the backend resolver (trim_end_matches), keeping front/back truly in lockstep.

No behavior change for well-formed configs; tests/typecheck/fmt/clippy clean.

* fix(usage): skip empty primary credential fields in fallback chain

`obj.get(key)` returns `Some` for a present-but-empty field, so the
`.or_else()` fallback chains for the Claude/ClaudeDesktop and Gemini api-key
lookups only skipped *absent* keys, not empty ones. Presets seed fields like
`ANTHROPIC_AUTH_TOKEN` as present-but-empty placeholders, so a provider whose
real key lives in a fallback field (`ANTHROPIC_API_KEY` / `OPENROUTER_API_KEY`
/ `GOOGLE_API_KEY`, or `GOOGLE_API_KEY` for Gemini) resolved to an empty key on
the native balance/coding-plan path — while the frontend `a || b` (which skips
empty strings) still found it, reproducing the same Test-works / refresh-fails
divergence this PR removes.

Add a `first_non_empty` helper that skips present-but-empty values, matching
the frontend `||` semantics, and use it for both fallback chains. Tests cover
empty primary + populated fallback for Claude and Gemini.
2026-06-01 08:43:40 +08:00
Haoyu Wang e02a2763c2 fix: add kimi/moonshot to Anthropic tool thinking history normalizer (#3377)
* fix: add kimi/moonshot to Anthropic tool thinking history normalizer

Kimi's Anthropic-compatible endpoint requires reasoning_content in
assistant tool call messages when thinking is enabled, same as DeepSeek.
The is_reasoning_content_compatible_identifier already included kimi/moonshot
but is_anthropic_tool_thinking_history_identifier was missing them, causing
400 errors on multi-turn conversations with tool calls.

Closes #3351

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor: unify reasoning vendor hints into a single SSOT list

Replace the two separately-maintained vendor identifier lists with one
shared REASONING_VENDOR_HINTS constant and a single
is_reasoning_vendor_identifier predicate. The Anthropic
tool-thinking-history matcher and the openai_chat reasoning_content
matcher previously kept independent lists, which is exactly how
kimi/moonshot ended up in one but not the other (#3351). A single source
of truth keeps the two from drifting apart again.

Also add a Kimi regression test for the Anthropic tool-history
normalization path.

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Jason <farion1231@gmail.com>
2026-05-31 20:10:02 +08:00
Jason 0877b9e35d Upgrade default Claude Opus model to 4.8
Bump the default Opus route/model from claude-opus-4-7 to claude-opus-4-8
across provider presets (claude, claudeDesktop, hermes, openclaw, opencode,
universal), i18n locales (zh/en/ja/zh-TW), pricing seed data, and the
user-manual docs.

- Add claude-opus-4-8 pricing row ($5/$25/$0.50/$6.25); keep the 4-7 row
  for historical usage stats (seeded via INSERT OR IGNORE).
- Claude Desktop proxy: accept bidirectional opus 4-7 <-> 4-8 route alias
  during rollout so previously saved routes keep resolving.
- thinking_optimizer: route opus-4-8 through adaptive thinking and normalize
  dotted model ids (also fixes dotted 4-6/4-7 falling back to legacy).
- usage_stats: normalize Bedrock/Vertex/aggregator opus-4-8 ids to base
  pricing.

Also merge role:"system" messages into the Gemini systemInstruction in the
Anthropic->Gemini transform.
2026-05-29 22:52:25 +08:00
Q3yp 554e3b4875 fix(proxy): normalize DeepSeek Anthropic tool thinking history (#3203)
Fixes #3200
2026-05-29 22:51:12 +08:00
zhizhuowq 9c2add9a09 fix(proxy): 修复 Claude 兼容模式下流式响应因空 tool_calls 数组导致 block 状态重置 (#2915)
问题:OpenAI 兼容 Provider(如 MiniMax)在发送 reasoning_content 流时,
      每个 chunk 包含 "tool_calls": [] 空数组,导致每个字符被当作独立 block 处理

根因:streaming.rs:343 的 if let Some(tool_calls) 匹配空数组进入处理分支,
      发送 content_block_stop 并重置 current_non_tool_block_type 为 None

修改:在 tool_calls 处理前添加 if !tool_calls.is_empty() 判断

示例输入:
```jsonl
{"delta": {"content": null, "reasoning_content": "用户", "role": "assistant", "tool_calls": []}}
{"delta": {"content": null, "reasoning_content": "用", "role": "assistant", "tool_calls": []}}
{"delta": {"content": null, "reasoning_content": "中文", "role": "assistant", "tool_calls": []}}
...
```
示例错误输出:
```text
event: content_block_start → index 0, type thinking
event: content_block_delta → thinking "用户"
event: content_block_stop → index 0
event: content_block_start → index 1, type thinking
event: content_block_delta → thinking "用"
event: content_block_stop → index 1
event: content_block_start → index 2, type thinking
event: content_block_delta → thinking "中文"
event: content_block_stop → index 2
...
```

Co-authored-by: WangQiang <wangqiang75@huawei.com>
2026-05-27 10:49:41 +08:00
zhangyapu1 707a5593e5 feat: add MiMo reasoning_content support for Claude Code proxy (#2990)
* feat: add MiMo reasoning_content support for Claude Code proxy

MiMo requires reasoning_content to be passed back in multi-turn
conversations with tool calls. Add "mimo" and "xiaomimimo" to the
preserve_reasoning_content whitelist so thinking blocks are converted
to reasoning_content when proxying Claude Code → MiMo.

Also handle redacted_thinking blocks (encrypted by Claude Code across
turns) by injecting a placeholder to prevent MiMo 400 errors.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: support MiMo reasoning histories

- Fix the PR CI clippy failure for redacted thinking handling.
- Preserve MiMo reasoning content on the OpenAI Chat proxy path.
- Normalize Claude Desktop Anthropic thinking history for MiMo local routes.
- Reject unsupported Claude Desktop direct model remaps.

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Jason <farion1231@gmail.com>
2026-05-26 22:55:36 +08:00
Jason 95f2dd4126 feat(codex): preserve OAuth login state during third-party provider switching
Codex provider switches now only write config.toml for third-party providers,
injecting the API key as experimental_bearer_token. The user's auth.json
(ChatGPT OAuth tokens) is preserved. Official providers with login material
still write auth.json normally. Backfill restores bearer tokens into stored
provider auth.OPENAI_API_KEY to maintain canonical shape.
2026-05-25 22:20:34 +08:00
Jason c12d20efd0 refactor(proxy): replace panic-prone unwrap/expect with safe patterns
Harden the proxy module against panics by removing optimistic
unwrap()/expect() calls in favor of pattern matching and graceful
fallbacks:

- copilot_optimizer/cache_injector: bind Value::Array/String directly
  instead of is_array()+as_array().unwrap(); use is_none_or and in-place
  string mutation
- hyper_client: gate the raw-write path with if-let + filter instead of
  has_cases + unwrap()
- gemini_shadow: recover poisoned RwLock via into_inner() rather than
  panicking, with warn logging
- streaming_codex_chat: replace expect("tool state exists") with
  let-else (return/continue)
- merge_tool_results: early-return when messages is absent
- sse: fall back to from_utf8_lossy on the UTF-8 boundary slice

No behavior change on the happy path; all proxy tests pass.
2026-05-25 22:20:34 +08:00
Jason f9db9913a0 fix: coerce empty tool-call arguments to {} in Codex Chat transform
No-argument tool calls produced empty argument strings that passed
through both the streaming response path and the request transform
verbatim. Strict OpenAI-compatible upstreams (e.g. Minimax) reject
`arguments: ""` with a 400 "invalid function arguments json string",
while lenient ones (OpenAI, Kimi) silently treat it as an empty object.

Add canonicalize_tool_arguments / canonicalize_tool_arguments_str helpers
that coerce empty/whitespace arguments to "{}" and apply them at the four
tool-call argument sites (streaming finalize + three request/response
transform paths). Leave function_call_output content untouched, where an
empty string is valid.
2026-05-25 22:20:34 +08:00
Jason 44d9aabbf3 feat: adaptive reasoning detection for Codex Chat providers
Auto-detect each Chat-routed Codex provider's reasoning interface from
its name, base URL, and model, then inject the matching thinking
parameter without manual configuration:

- Platform-first inference (OpenRouter, SiliconFlow) overrides model
  rules, since the same model exposes different reasoning controls
  depending on the hosting platform.
- Effort tiers are forwarded only to providers that support them
  (DeepSeek, OpenRouter, and StepFun's step-3.5-flash-2603); on/off-only
  providers (Kimi, GLM, Qwen, MiniMax, MiMo, SiliconFlow) drop the level
  instead of sending a field the upstream rejects.
- OpenRouter uses the native reasoning:{effort} object, clamps max to
  xhigh (its enum has no max), and forwards an explicit effort:"none" so
  reasoning can be turned off.
- StepFun falls back to inference so per-model effort support is honored
  (the static preset would have forced effort on step-3.5-flash too).

Includes the Codex provider-form reasoning controls, i18n strings
(zh/en/ja), and response-side reasoning extraction.
2026-05-25 22:20:33 +08:00
Jason 5048ed632d fix: inject stream_options.include_usage for Codex Chat streaming
Responses-to-Chat conversion did not set stream_options.include_usage,
so OpenAI-compatible upstreams (kimi/MiniMax, etc.) omitted the trailing
usage chunk in streaming mode. This caused token/cost/cache stats to be
recorded as zero for third-party streaming requests routed through the
Codex Chat path.

Inject include_usage when the request is streaming, merging into any
client-provided stream_options instead of overwriting it. Add tests for
the streaming, non-streaming, and merge cases.
2026-05-25 22:20:33 +08:00
Jason b710c6549e fix: backfill placeholder reasoning_content for bare tool-call messages
Thinking models like kimi/Moonshot and DeepSeek reject any assistant
message carrying tool_calls without a non-empty reasoning_content. When
cross-turn history recovery misses (proxy restart drops the in-memory
cache, ambiguous call_id, or a turn produced no reasoning upstream), the
converted assistant message has none and the whole request fails with
'reasoning_content is missing in assistant tool call message'.

Add a final-stage backfill that runs after all input items are
processed, so genuine trailing reasoning items still attach first and a
placeholder is only injected when none remains. Mirrors the placeholder
behavior in transform::anthropic_to_openai_with_reasoning_content.
2026-05-25 22:20:33 +08:00