Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 13 additions & 6 deletions app/client_profiles.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@

from .site_routing import domain_for_auth, profile_for_auth, profile_product, profile_region

CLI_VERSION = "2.165.0"
CLI_VERSION = "2.179.3"
WORKBUDDY_VERSION = "5.9.3"
WORKBUDDY_CLI_VERSION = CLI_VERSION
INTL_CLI_VERSION = "2.161.0"
INTL_WORKBUDDY_VERSION = "5.8.4"
INTL_WORKBUDDY_CLI_VERSION = INTL_CLI_VERSION
WORKBUDDY_CLI_VERSION = "2.165.0"
INTL_CLI_VERSION = "2.179.3"
INTL_WORKBUDDY_VERSION = "5.9.4"
INTL_WORKBUDDY_CLI_VERSION = "2.165.0"
CLI_USER_AGENT = f"CLI/{CLI_VERSION} CodeBuddy/{CLI_VERSION}"

SDK_HEADERS = {
Expand Down Expand Up @@ -118,7 +118,14 @@ def _identity_claim_headers(auth: dict, account: dict) -> dict:


def credential_headers(auth: dict, account: dict) -> dict:
return {**SDK_HEADERS, **_auth_headers(auth, account), **_identity_claim_headers(auth, account)}
profile = profile_for_auth(auth)
headers = {**SDK_HEADERS, **_auth_headers(auth, account), **_identity_claim_headers(auth, account)}
if profile in ("cn-cli", "intl-cli"):
headers["x-stainless-package-version"] = "7.39.0"
headers["X-Agent-Runtime"] = "other"
Comment on lines +124 to +125

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium · Profile identity differs across requests

When a CLI or international WorkBuddy profile makes a refresh or account-lookup request, or a CLI profile makes a catalog request, refresh_headers omits the profile overrides, so CLI refresh and account requests use SDK version 6.25 and omit the runtime header; catalog_headers also omits the runtime header for CLI requests. The upstream therefore receives inconsistent client identities across the account flow.

Apply the profile-specific SDK-version and runtime overrides in the refresh and catalog header builders as well as the chat header builder.

Also at docs/advanced.md:140, docs/advanced.zh-CN.md:140.

Prompt for AI agents
In `app/client_profiles.py` at lines 124-125:

**Profile identity differs across requests**

When a CLI or international WorkBuddy profile makes a refresh or account-lookup request, or a CLI profile makes a catalog request, `refresh_headers` omits the profile overrides, so CLI refresh and account requests use SDK version 6.25 and omit the runtime header; `catalog_headers` also omits the runtime header for CLI requests. The upstream therefore receives inconsistent client identities across the account flow.

Apply the profile-specific SDK-version and runtime overrides in the refresh and catalog header builders as well as the chat header builder.

Also at `docs/advanced.md:140`, `docs/advanced.zh-CN.md:140`.

elif profile == "intl-work":
headers["X-Agent-Runtime"] = "local-agent"
return headers


def _cn_work_nonchat_headers(headers: dict, profile: str) -> dict:
Expand Down
5 changes: 4 additions & 1 deletion app/request_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,11 @@ def build_attempt_headers(headers, profile, *, root, message_id, request_id, tra
"traceparent": f"00-{trace_id}-{span}-01", "b3": f"{trace_id}-{span}-1-{parent}",
"X-B3-TraceId": trace_id, "X-B3-SpanId": span, "X-B3-ParentSpanId": parent,
"X-B3-Sampled": "1"}
if profile == "cn-work":
if profile in _WORKBUDDY_PROFILES:
outgoing["X-Agent-Runtime"] = "local-agent"
elif profile in ("cn-cli", "intl-cli"):
outgoing["X-Agent-Runtime"] = "other"
if profile == "cn-work":
if conversation_id:
outgoing["X-Session-ID"] = conversation_id
outgoing["X-Root-Request-ID"] = _stable_uuid7(["root", conversation_id]).replace("-", "")
Expand Down
1 change: 1 addition & 0 deletions app/site_routing.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"intl-cli": INTERNATIONAL_ENDPOINT,
"intl-work": "https://www.workbuddy.ai",
}
WORKBUDDY_CN_BACKUP_ENDPOINT = "https://wb.tencentbuddy.com"
# The paid bge-m3 endpoint is verified on WorkBuddy CN for both domestic products.
EMBEDDING_ENDPOINTS = {"cn-cli": "https://www.workbuddy.cn", "cn-work": "https://www.workbuddy.cn"}
DOMAIN_PROFILES = {
Expand Down
21 changes: 20 additions & 1 deletion app/upstream_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import re
from urllib.parse import urlsplit, urlunsplit

from .site_routing import DOMAIN_PROFILES, profile_region
from .site_routing import DOMAIN_PROFILES, WORKBUDDY_CN_BACKUP_ENDPOINT, profile_region

_SCHEMA_MAPS = frozenset(("properties", "patternProperties", "$defs", "definitions",
"dependentSchemas", "dependencies"))
Expand Down Expand Up @@ -202,3 +202,22 @@ def channel_fallback_url(url: str) -> str | None:
if profile is None or profile_region(profile) != "intl":
return None
return urlunsplit(parts._replace(path="/console/chat/completions"))


def workbuddy_origin_fallback_url(url: str, profile: str) -> str | None:
"""Return the official backup origin for domestic WorkBuddy chat."""
if profile != "cn-work":
return None
try:
parts = urlsplit(url)
except ValueError:
return None
if (parts.scheme != "https" or parts.path != "/v2/chat/completions"
or parts.netloc not in ("www.workbuddy.cn", "copilot.tencent.com")):
return None
return urlunsplit(parts._replace(netloc=urlsplit(WORKBUDDY_CN_BACKUP_ENDPOINT).netloc))


def should_origin_fallback(fallback_url: str | None, status: int) -> bool:
"""Replay only official transient gateway statuses on the backup origin."""
return bool(fallback_url) and status in (408, 502, 503, 504)
96 changes: 63 additions & 33 deletions converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ def apply_system_prompt_mode(body, profile, mode):
from app.admin_auth import SessionStoreError
from app.content_filter import ContentFilterDetector, is_filter_error
from app.upstream_compat import (channel_fallback_url, is_channel_check_error, normalize_tool_schemas,
should_channel_fallback)
should_channel_fallback, should_origin_fallback, workbuddy_origin_fallback_url)
from app.request_limits import ImageLimitError, apply_image_policy
from app.safe_logging import format_log_body, sanitize_log_text
from app import perf_metrics
Expand Down Expand Up @@ -5016,46 +5016,74 @@ def _check_upstream_status(status, raw, cred, model, *, headers=None, from_chann
from_channel_fallback=from_channel_fallback)


def _origin_replay_safe(raw: bytes) -> bool:
"""Preserve filter, channel and model-specific errors before origin replay."""
if is_filter_error(raw):
return False
code, _, _ = _upstream_diagnostic(raw)
return code not in NO_FAILOVER_UPSTREAM_CODES and code not in MODEL_SCOPED_ERROR_CODES


@asynccontextmanager
async def _checked_chat_stream(url, headers, body, *, rid, model_name, cred,
on_channel_body=None):
"""Open a chat stream with one same-account console and role fallback."""
"""Open a chat stream with same-account channel and WorkBuddy origin fallback."""
profile = profile_for_headers(headers)
fallback_url = channel_fallback_url(url)
origin_url = workbuddy_origin_fallback_url(url, profile)
from_channel_fallback = False
channel_body_retried = False

def switch_origin(*, cost_note="", **fields):
nonlocal url, origin_url
target = origin_url
observe_attempt("origin_fallback", profile=profile, upstream_model=body.get("model"), **fields)
_log(f"[{rid}] 上游入口不可用,切换 WorkBuddy 备份源站重试 | {model_name}{cost_note}")
url, origin_url = target, None

while True:
async with _backend_stream(url, headers, body, rid=rid, model_name=model_name) as response:
if response.status_code == 200:
_note_cred_model_ok(cred, body.get("model"))
yield response
return
raw = await read_bounded_error(response)
if should_channel_fallback(fallback_url, response.status_code, raw):
observe_attempt("channel_check_fallback", status_code=response.status_code,
profile=profile_for_headers(headers), upstream_model=body.get("model"))
_log(f"[{rid}] 上游渠道校验,切换 /console 端点重试 | {model_name}")
url, fallback_url, from_channel_fallback = fallback_url, None, True
continue
if (from_channel_fallback and not channel_body_retried
and response.status_code == 400 and is_channel_check_error(raw)):
transformed = system_as_user_body(body)
if transformed != body:
try:
_guard_request_size(transformed)
except HTTPException:
transformed = body
response_started = False
try:
async with _backend_stream(url, headers, body, rid=rid, model_name=model_name) as response:
if response.status_code == 200:
response_started = True
_note_cred_model_ok(cred, body.get("model"))
yield response
return
raw = await read_bounded_error(response)
if should_origin_fallback(origin_url, response.status_code) and _origin_replay_safe(raw):
switch_origin(status_code=response.status_code,
cost_note=_replay_cost_note(response.status_code))
continue
if should_channel_fallback(fallback_url, response.status_code, raw):
observe_attempt("channel_check_fallback", status_code=response.status_code,
profile=profile, upstream_model=body.get("model"))
_log(f"[{rid}] 上游渠道校验,切换 /console 端点重试 | {model_name}")
url, fallback_url, from_channel_fallback = fallback_url, None, True
continue
if (from_channel_fallback and not channel_body_retried
and response.status_code == 400 and is_channel_check_error(raw)):
transformed = system_as_user_body(body)
if transformed != body:
channel_body_retried = True
body = transformed
if on_channel_body is not None:
on_channel_body(transformed)
observe_attempt("channel_system_as_user_retry", status_code=response.status_code,
profile=profile_for_headers(headers), upstream_model=body.get("model"))
_log(f"[{rid}] 渠道拒绝,system 指令降级为 user 后同账号重试 | {model_name}")
continue
_check_upstream_status(response.status_code, raw, cred, body.get("model"),
headers=response.headers, from_channel_fallback=from_channel_fallback)

try:
_guard_request_size(transformed)
except HTTPException:
transformed = body
if transformed != body:
channel_body_retried = True
body = transformed
if on_channel_body is not None:
on_channel_body(transformed)
observe_attempt("channel_system_as_user_retry", status_code=response.status_code,
profile=profile, upstream_model=body.get("model"))
_log(f"[{rid}] 渠道拒绝,system 指令降级为 user 后同账号重试 | {model_name}")
continue
_check_upstream_status(response.status_code, raw, cred, body.get("model"),
headers=response.headers, from_channel_fallback=from_channel_fallback)
except httpx.HTTPError as error:
if response_started or origin_url is None or not _failover_safe(error):
raise
switch_origin(error_code=type(error).__name__, cost_note=_replay_cost_note(error))

def _upstream_failure(error, model_name, t0, rid):
"""Normalize failure logs and payloads before endpoint-specific error wrapping."""
Expand Down Expand Up @@ -5295,6 +5323,8 @@ def _cred_manager(cred):

def _replay_cost_note(error) -> str:
"""Label replays that may duplicate already billed work."""
if isinstance(error, int) and error in POSSIBLY_CHARGED_CODES:
return " | 上游可能已处理该请求"
if isinstance(error, UpstreamHTTPError) and error.status in POSSIBLY_CHARGED_CODES:
return " | 上游可能已处理该请求"
if isinstance(error, WRITE_TIMEOUT_TRANSPORT):
Expand Down
2 changes: 1 addition & 1 deletion docs/advanced.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ Generation responses carry a gateway-generated `X-Request-ID` for correlation wi
In scoped mode, optionally send `X-Codebuddy-Session-ID`, `metadata.conversation_id` / `metadata.conversationId`, or top-level `conversation_id` / `conversationId`. Values must agree; conflicting, non-string, control-character or over-512-UTF-8-byte values return 400. Empty values fall back to a fingerprint of adapted instructions and the first user input, including image references; URLs are not fetched. Without reliable input a temporary session is used. Identical inputs without explicit IDs remain indistinguishable; `user`, `metadata.user_id` and `prompt_cache_key` are not session IDs. Raw hints are neither logged nor forwarded upstream.

Switch back to `legacy` to restore old behavior for new requests; in-flight requests retain their initial mode.
Upstream client identities follow verified official packages: domestic CLI 2.165.0 / WorkBuddy 5.9.3 and international CLI 2.161.0 / WorkBuddy 5.8.4. Domestic WorkBuddy uses session-stable UUIDv7 conversation IDs, per-attempt request/tracing headers and gzip for JSON bodies of at least 1024 bytes; CLI and international WorkBuddy use their existing ID relationships and always gzip chat JSON. Headers are generated locally without reading hardware IDs.
Upstream client identities use CLI 2.179.3 for domestic and international CodeBuddy; domestic WorkBuddy 5.9.3 and international WorkBuddy 5.9.4 keep embedded CLI 2.165.0. CLI profiles send `X-Agent-Runtime: other`; international WorkBuddy sends `local-agent`. Domestic WorkBuddy uses session-stable UUIDv7 conversation IDs, per-attempt request/tracing headers and gzip for JSON bodies of at least 1024 bytes; CLI and international WorkBuddy use their existing ID relationships and always gzip chat JSON. Before any response starts, domestic WorkBuddy retries 408/502/503/504 and connection failures once on `wb.tencentbuddy.com` with the same account and `X-Domain`; filter, channel and model-specific errors never trigger it; 502/504 retries log a possible-billing warning. Headers are generated locally without reading hardware IDs.

## Automation and rewards

Expand Down
2 changes: 1 addition & 1 deletion docs/advanced.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ WebUI 系统设置可配置这两项;环境变量为 `CODEBUDDY2API_UPSTREAM_K
scoped 模式可选传入 `X-Codebuddy-Session-ID`、`metadata.conversation_id` / `metadata.conversationId` 或顶层 `conversation_id` / `conversationId`。多处值须一致;冲突、非字符串、控制字符或超过 512 UTF-8 字节时返回 400。空值回退为协议适配后的指令与首条用户输入指纹,包含图片引用但不抓取 URL;缺少可靠输入时使用临时会话。无显式 ID 的相同输入仍无法区分;`user`、`metadata.user_id`、`prompt_cache_key` 不是会话 ID。原始标识不记录、不转发上游。

设回 `legacy` 即恢复新请求的旧行为,在途请求保留入口模式。
上游客户端身份按已核实的官方包对齐:国内 CLI 2.165.0/WorkBuddy 5.9.3,国际 CLI 2.161.0/WorkBuddy 5.8.4。国内 WorkBuddy 使用会话稳定的 UUIDv7 会话 ID、逐次请求/追踪头,并在 JSON 请求体达到 1024 字节时使用 gzip;CLI 与国际 WorkBuddy 保持原有 ID 关系,聊天 JSON 始终使用 gzip。所有身份头均在本地生成,不读取硬件 ID。
国内、国际 CodeBuddy 均使用 CLI 2.179.3;国内 WorkBuddy 5.9.3 与国际 WorkBuddy 5.9.4 的内嵌 CLI 均保持 2.165.0。CLI 均发送 `X-Agent-Runtime: other`,国际 WorkBuddy 发送 `local-agent`。国内 WorkBuddy 使用会话稳定的 UUIDv7 会话 ID、逐次请求/追踪头,并在 JSON 请求体达到 1024 字节时使用 gzip;CLI 与国际 WorkBuddy 保持原有 ID 关系,聊天 JSON 始终使用 gzip。国内 WorkBuddy 在响应开始前遇到 408/502/503/504 或建连失败时,以同账号和原 `X-Domain` 在 `wb.tencentbuddy.com` 重试一次;内容过滤、渠道和模型特定错误不触发域名回退;502/504 回退会记录可能重复计费警告。所有身份头均在本地生成,不读取硬件 ID。

## 自动化与奖励

Expand Down
8 changes: 7 additions & 1 deletion tests/test_api_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,13 @@ def reject(request):
self.assertEqual(response.headers.get("Retry-After"), "17")
self.assertNotIn("X-Private", response.headers)
self.assertEqual(response.json()["error"]["code"], "quota")
self.assertEqual(len(seen), 1, "A retry hint must not enable replay")
expected_attempts = 2 if profile == "cn-work" and status == 503 else 1
self.assertEqual(len(seen), expected_attempts,
"Only domestic WorkBuddy may replay 503 on its backup origin")
if expected_attempts == 2:
self.assertEqual([request.url.host for request in seen],
["www.workbuddy.cn", "wb.tencentbuddy.com"])
self.assertEqual(len({request.headers["x-user-id"] for request in seen}), 1)
entry = self.fx.entries[profile]
self.assertEqual(entry["fail_until"], 0)
if status == 429:
Expand Down
13 changes: 10 additions & 3 deletions tests/test_identity_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,10 +92,10 @@ def picked_uid(self, name, region="cn"):

def test_official_versions_and_private_data_are_profile_scoped(self):
expected = {
"cn-cli": ("CLI/2.165.0 CodeBuddy/2.165.0", "2.165.0", "false", "2.165.0"),
"intl-cli": ("CLI/2.161.0 CodeBuddy/2.161.0", "2.161.0", "false", "2.161.0"),
"cn-cli": ("CLI/2.179.3 CodeBuddy/2.179.3", "2.179.3", "false", "2.179.3"),
"intl-cli": ("CLI/2.179.3 CodeBuddy/2.179.3", "2.179.3", "false", "2.179.3"),
"cn-work": ("WorkBuddy/5.9.3 WorkBuddy/5.9.3 CLI/2.165.0", "5.9.3", "false", "5.9.3:2.165.0"),
"intl-work": ("WorkBuddy/5.8.4 WorkBuddy AI/5.8.4 CLI/2.161.0", "5.8.4", "true", "5.8.4:2.161.0"),
"intl-work": ("WorkBuddy/5.9.4 WorkBuddy AI/5.9.4 CLI/2.165.0", "5.9.4", "true", "5.9.4:2.165.0"),
}
for profile, (user_agent, ide_version, private_data, revision) in expected.items():
with self.subTest(profile=profile):
Expand All @@ -105,6 +105,13 @@ def test_official_versions_and_private_data_are_profile_scoped(self):
self.assertEqual(headers["X-IDE-Version"], ide_version)
self.assertEqual(headers["X-Private-Data"], private_data)
self.assertEqual(client_profiles.catalog_cache_key(profile), f"{profile}:{revision}")
sdk_version = "7.39.0" if profile in ("cn-cli", "intl-cli") else "6.25.0"
self.assertEqual(headers["x-stainless-package-version"], sdk_version)
runtime = {"cn-cli": "other", "intl-cli": "other", "intl-work": "local-agent"}.get(profile)
if runtime is None:
self.assertNotIn("X-Agent-Runtime", headers)
else:
self.assertEqual(headers["X-Agent-Runtime"], runtime)

def test_private_data_and_identity_headers_follow_the_real_account(self):
for profile, domain in DOMAINS.items():
Expand Down
Loading
Loading