feat(oauth): 账号封禁前置 OAuth 验证、抽取 provider_context、完善账号状态分类

- 新增 verify_oauth_before_account_block:在标记账号封禁前先尝试刷新 token,
  区分 OAuth 过期与真正的账号级封禁,避免误标
- 抽取 provider_context.py 统一解析 provider_type,解决 ORM detached 访问问题
- account_state 新增 workspace_deactivated 分类和 auto-removable 状态集合,
  补充中文验证关键词匹配
- OAuth refresh 成功后仅清除可恢复的 token 错误,不再自动清除账号级 block
- deploy.sh 依赖指纹改用纯 shell 实现,移除对 Python tomllib 的依赖
- 前端 Pool 管理页面新增筛选和批量操作优化
- 补充对应测试用例
This commit is contained in:
fawney19
2026-03-20 16:50:59 +08:00
parent aa83b4a7a7
commit 25d38ae632
44 changed files with 1527 additions and 370 deletions

View File

@@ -1,8 +1,12 @@
from __future__ import annotations
import sys
import types
from types import SimpleNamespace
from typing import Any, cast
import pytest
from src.services.orchestration.error_handler import ErrorHandlerService
@@ -31,11 +35,16 @@ def _build_key() -> SimpleNamespace:
)
def test_mark_oauth_key_blocked_auto_remove_enabled(monkeypatch: Any) -> None:
def test_mark_oauth_key_blocked_auto_remove_enabled_skips_verification_state(
monkeypatch: Any,
) -> None:
db = _FakeDB()
service = ErrorHandlerService(db=cast(Any, db))
key = _build_key()
provider = SimpleNamespace(config={"pool_advanced": {"auto_remove_banned_keys": True}})
provider = SimpleNamespace(
provider_type="codex",
config={"pool_advanced": {"auto_remove_banned_keys": True}},
)
monkeypatch.setattr(
ErrorHandlerService,
@@ -46,8 +55,8 @@ def test_mark_oauth_key_blocked_auto_remove_enabled(monkeypatch: Any) -> None:
service._mark_oauth_key_blocked(cast(Any, key), "req-1", provider=cast(Any, provider))
assert db.commit_count == 1
assert db.deleted == [key]
assert key.is_active is False
assert db.deleted == []
assert key.is_active is True
assert str(key.oauth_invalid_reason).startswith("[ACCOUNT_BLOCK] ")
@@ -61,5 +70,62 @@ def test_mark_oauth_key_blocked_auto_remove_disabled() -> None:
assert db.commit_count == 1
assert db.deleted == []
assert key.is_active is False
assert key.is_active is True
assert str(key.oauth_invalid_reason).startswith("[ACCOUNT_BLOCK] ")
def test_mark_oauth_key_blocked_auto_remove_enabled_for_deactivated_account(
monkeypatch: Any,
) -> None:
db = _FakeDB()
service = ErrorHandlerService(db=cast(Any, db))
key = _build_key()
provider = SimpleNamespace(
provider_type="codex",
config={"pool_advanced": {"auto_remove_banned_keys": True}},
)
monkeypatch.setattr(
ErrorHandlerService,
"_schedule_auto_cleanup_after_delete",
staticmethod(lambda **kwargs: None),
)
service._mark_oauth_key_blocked(
cast(Any, key),
"req-1",
reason="account has been deactivated",
provider=cast(Any, provider),
)
assert db.commit_count == 1
assert db.deleted == [key]
assert key.is_active is True
assert key.oauth_invalid_reason == "[ACCOUNT_BLOCK] account has been deactivated"
@pytest.mark.asyncio
async def test_verify_oauth_before_account_block_skips_when_refresh_marks_token_expired(
monkeypatch: Any,
) -> None:
db = _FakeDB()
service = ErrorHandlerService(db=cast(Any, db))
key = _build_key()
endpoint = SimpleNamespace()
fake_module = types.ModuleType("src.services.provider.auth")
async def _fake_get_provider_auth(*_args: Any, **_kwargs: Any) -> None:
key.oauth_invalid_reason = "[OAUTH_EXPIRED] token expired"
fake_module.get_provider_auth = _fake_get_provider_auth
monkeypatch.setitem(sys.modules, "src.services.provider.auth", fake_module)
should_mark = await service._verify_oauth_before_account_block(
endpoint=cast(Any, endpoint),
key=cast(Any, key),
request_id="req-1",
candidate_reason="Google 要求验证账号",
)
assert should_mark is False

View File

@@ -2,7 +2,10 @@
from __future__ import annotations
from src.services.provider.pool.account_state import resolve_pool_account_state
from src.services.provider.pool.account_state import (
resolve_pool_account_state,
should_auto_remove_account_state,
)
def test_resolve_from_kiro_banned_metadata() -> None:
@@ -41,6 +44,18 @@ def test_resolve_from_structured_oauth_reason_verification() -> None:
assert state.reason == "Google requires verification"
def test_resolve_from_structured_oauth_reason_verification_chinese() -> None:
state = resolve_pool_account_state(
provider_type="codex",
upstream_metadata=None,
oauth_invalid_reason="[ACCOUNT_BLOCK] Google 要求验证账号",
)
assert state.blocked is True
assert state.code == "account_verification"
assert state.label == "需要验证"
assert state.reason == "Google 要求验证账号"
def test_resolve_from_structured_oauth_reason_suspended() -> None:
state = resolve_pool_account_state(
provider_type="codex",
@@ -155,3 +170,25 @@ def test_request_failed_prefix_does_not_block() -> None:
oauth_invalid_reason="[REQUEST_FAILED] Codex 账户访问受限 (403)",
)
assert state.blocked is False
def test_auto_remove_state_excludes_token_expired_and_verification() -> None:
expired = resolve_pool_account_state(
provider_type="codex",
upstream_metadata=None,
oauth_invalid_reason="[OAUTH_EXPIRED] token invalidated",
)
verification = resolve_pool_account_state(
provider_type="codex",
upstream_metadata=None,
oauth_invalid_reason="[ACCOUNT_BLOCK] Google 要求验证账号",
)
disabled = resolve_pool_account_state(
provider_type="codex",
upstream_metadata=None,
oauth_invalid_reason="[ACCOUNT_BLOCK] account has been deactivated",
)
assert should_auto_remove_account_state(expired) is False
assert should_auto_remove_account_state(verification) is False
assert should_auto_remove_account_state(disabled) is True

View File

@@ -25,7 +25,7 @@ def test_select_probe_key_ids_selects_silent_keys_only() -> None:
keys = [
_key("k1"), # never used, should be probed
_key("k2", last_used_at=now - timedelta(minutes=2)), # recently used, skip
_key("k2", last_used_at=now - timedelta(minutes=2)), # recently used,仍可定期探测
_key(
"k3",
upstream_metadata={"codex": {"updated_at": now_ts - (20 * 60)}},
@@ -40,10 +40,10 @@ def test_select_probe_key_ids_selects_silent_keys_only() -> None:
last_probe_timestamps={},
limit=0,
)
assert selected == ["k1", "k3"]
assert selected == ["k1", "k2", "k3"]
def test_select_probe_key_ids_resets_probe_window_after_key_usage() -> None:
def test_select_probe_key_ids_keeps_periodic_probe_even_after_recent_usage() -> None:
now = datetime(2026, 3, 5, 12, 0, 0, tzinfo=timezone.utc)
now_ts = int(now.timestamp())
@@ -55,7 +55,7 @@ def test_select_probe_key_ids_resets_probe_window_after_key_usage() -> None:
)
]
# 上一次主动探测非常早,但 key 刚刚被真实流量使用,应跳过本次探测
# 即使 key 刚刚被真实流量使用,只要上次额度刷新/主动探测已过窗口,仍应继续定期探测
selected = _select_probe_key_ids(
keys=keys, # type: ignore[arg-type]
provider_type="codex",
@@ -64,7 +64,7 @@ def test_select_probe_key_ids_resets_probe_window_after_key_usage() -> None:
last_probe_timestamps={"k1": now_ts - (25 * 60)},
limit=0,
)
assert selected == []
assert selected == ["k1"]
def test_select_probe_key_ids_applies_limit_by_oldest_anchor_first() -> None:
@@ -72,9 +72,9 @@ def test_select_probe_key_ids_applies_limit_by_oldest_anchor_first() -> None:
now_ts = int(now.timestamp())
keys = [
_key("k1", last_used_at=now - timedelta(minutes=60)),
_key("k2", last_used_at=now - timedelta(minutes=50)),
_key("k3", last_used_at=now - timedelta(minutes=40)),
_key("k1", upstream_metadata={"codex": {"updated_at": now_ts - (60 * 60)}}),
_key("k2", upstream_metadata={"codex": {"updated_at": now_ts - (50 * 60)}}),
_key("k3", upstream_metadata={"codex": {"updated_at": now_ts - (40 * 60)}}),
]
selected = _select_probe_key_ids(

View File

@@ -83,6 +83,20 @@ def test_account_state_takes_priority_over_manual_disabled() -> None:
assert summary.reason == "account_forbidden"
def test_workspace_deactivated_uses_specific_reason_code() -> None:
dimensions = evaluate_pool_scheduling_dimensions(
_snapshot(
account_blocked=True,
account_block_label="工作区停用",
account_block_reason="deactivated_workspace",
)
)
summary = summarize_pool_scheduling_dimensions(dimensions)
assert summary.status == "blocked"
assert summary.reason == "workspace_deactivated"
def test_summary_degraded_when_cost_reaches_soft_threshold() -> None:
dimensions = evaluate_pool_scheduling_dimensions(
_snapshot(cost_window_usage=8200, cost_limit=10000, cost_soft_threshold_percent=80)

View File

@@ -47,9 +47,7 @@ class _FakeSessionCtx:
return False
def _install_module(
monkeypatch: pytest.MonkeyPatch, name: str, attrs: dict[str, Any]
) -> None:
def _install_module(monkeypatch: pytest.MonkeyPatch, name: str, attrs: dict[str, Any]) -> None:
fake_module = types.ModuleType(name)
for key, value in attrs.items():
setattr(fake_module, key, value)
@@ -110,9 +108,7 @@ def test_mark_refresh_token_invalid_persists_detached_key(
assert fake_db.committed is True
assert key.oauth_invalid_at is not None
assert row.oauth_invalid_at is not None
assert str(key.oauth_invalid_reason).startswith(
"[REFRESH_FAILED] Token 续期失败 (401)"
)
assert str(key.oauth_invalid_reason).startswith("[REFRESH_FAILED] Token 续期失败 (401)")
assert "refresh_token_reused" in str(row.oauth_invalid_reason)
@@ -153,6 +149,30 @@ def test_persist_refreshed_token_clears_legacy_token_invalidated_account_block(
assert key.oauth_invalid_reason is None
def test_persist_refreshed_token_preserves_true_account_block(
monkeypatch: pytest.MonkeyPatch,
) -> None:
key = SimpleNamespace(
id="key-1",
api_key="old-api",
auth_config="old-config",
oauth_invalid_at=datetime.now(timezone.utc),
oauth_invalid_reason="[ACCOUNT_BLOCK] Google requires verification",
)
monkeypatch.setattr(
module, "object_session", lambda _key: (_ for _ in ()).throw(RuntimeError())
)
monkeypatch.setattr(module.crypto_service, "encrypt", lambda value: f"enc:{value}")
module._persist_refreshed_token(key, "new-token", {"refresh_token": "rt-2"})
assert key.api_key == "enc:new-token"
assert key.auth_config == 'enc:{"refresh_token": "rt-2"}'
assert key.oauth_invalid_at is not None
assert key.oauth_invalid_reason == "[ACCOUNT_BLOCK] Google requires verification"
@pytest.mark.asyncio
async def test_refresh_generic_oauth_token_persists_enriched_account_name(
monkeypatch: pytest.MonkeyPatch,

View File

@@ -1,6 +1,8 @@
from __future__ import annotations
import json
import sys
import types
from types import SimpleNamespace
import pytest
@@ -9,6 +11,32 @@ from src.core.vertex_auth import VertexAuthService
from src.services.provider.auth import get_provider_auth
class _FakeQuery:
def __init__(self, row: object | None) -> None:
self._row = row
def filter(self, *_args: object, **_kwargs: object) -> "_FakeQuery":
return self
def first(self) -> object | None:
return self._row
class _FakeSessionCtx:
def __init__(self, row: object | None) -> None:
self._row = row
def __enter__(self) -> "_FakeSessionCtx":
return self
def __exit__(self, exc_type: object, exc: object, tb: object) -> bool:
_ = exc_type, exc, tb
return False
def query(self, _model: object) -> _FakeQuery:
return _FakeQuery(self._row)
@pytest.mark.asyncio
async def test_get_provider_auth_vertex_service_account_uses_provider_proxy(
monkeypatch: pytest.MonkeyPatch,
@@ -124,3 +152,76 @@ async def test_get_provider_auth_vertex_service_account_prefers_key_proxy(
assert auth is not None
assert captured["proxy_config"] == key_proxy
@pytest.mark.asyncio
async def test_get_provider_auth_vertex_service_account_uses_provider_id_lookup_without_touching_endpoint_provider(
monkeypatch: pytest.MonkeyPatch,
) -> None:
sa_json = {
"client_email": "svc@example.iam.gserviceaccount.com",
"private_key": "-----BEGIN PRIVATE KEY-----\nTEST\n-----END PRIVATE KEY-----\n",
"project_id": "demo-project",
}
provider_proxy = {"node_id": "provider-node", "enabled": True}
class _DetachedEndpoint:
provider_id = "provider-1"
@property
def provider(self) -> object:
raise RuntimeError("detached endpoint provider should not be lazy-loaded")
fake_provider = SimpleNamespace(
id="provider-1", provider_type="vertex_ai", proxy=provider_proxy
)
fake_database = types.ModuleType("src.database")
fake_database.create_session = lambda: _FakeSessionCtx(fake_provider)
fake_models = types.ModuleType("src.models.database")
fake_models.Provider = type("Provider", (), {"id": "id"})
monkeypatch.setitem(sys.modules, "src.database", fake_database)
monkeypatch.setitem(sys.modules, "src.models.database", fake_models)
monkeypatch.setattr(
"src.core.crypto.crypto_service.decrypt",
lambda value: json.dumps(sa_json) if value == "enc_cfg" else "",
)
captured: dict[str, object] = {}
def _fake_build_proxy_client_kwargs(
proxy_config: dict[str, object] | None = None,
*,
timeout: float = 30.0,
**_: object,
) -> dict[str, object]:
captured["proxy_config"] = proxy_config
return {"timeout": timeout}
async def _fake_get_access_token(
self: VertexAuthService,
*,
httpx_client_kwargs: dict[str, object] | None = None,
) -> str:
captured["httpx_client_kwargs"] = httpx_client_kwargs
return "ya29.test-token"
monkeypatch.setattr(
"src.services.proxy_node.resolver.build_proxy_client_kwargs",
_fake_build_proxy_client_kwargs,
)
monkeypatch.setattr(VertexAuthService, "get_access_token", _fake_get_access_token)
key = SimpleNamespace(
auth_type="service_account",
auth_config="enc_cfg",
api_key="enc_key",
provider_id="provider-1",
proxy=None,
)
auth = await get_provider_auth(_DetachedEndpoint(), key) # type: ignore[arg-type]
assert auth is not None
assert captured["proxy_config"] == provider_proxy
assert captured["httpx_client_kwargs"] == {"timeout": 30}

View File

@@ -244,10 +244,10 @@ def test_clear_oauth_invalid_response_invalidates_caches(
result = command_module.clear_oauth_invalid_response(cast(Any, db), key_id="key-1")
assert result["message"] == "已清除 OAuth 失效标记Key 已自动启用"
assert result["message"] == "已清除 OAuth 失效标记"
assert key.oauth_invalid_at is None
assert key.oauth_invalid_reason is None
assert key.is_active is True
assert key.is_active is False
assert db.commit_count == 1
assert cache_calls == [("key", "key-1"), ("models", None)]

View File

@@ -771,10 +771,10 @@ async def test_antigravity_refresher_forbidden_collects_updates_without_commit(
)
assert result["status"] == "forbidden"
assert result["auto_disabled"] is True
assert result["auto_disabled"] is False
assert key.is_active is True
assert key.oauth_invalid_reason is None
assert state_updates["k1"]["is_active"] is False
assert "is_active" not in state_updates["k1"]
assert state_updates["k1"]["oauth_invalid_reason"].startswith("账户访问被禁止")
assert metadata_updates["k1"]["antigravity"]["is_forbidden"] is True
assert db.commit_count == 0
@@ -911,7 +911,7 @@ async def test_kiro_refresher_runtime_401_marks_key_invalid(
assert "401" in result["message"]
assert key.is_active is True
assert key.oauth_invalid_reason is None
assert state_updates["k1"]["is_active"] is False
assert "is_active" not in state_updates["k1"]
assert state_updates["k1"]["oauth_invalid_reason"] == "Kiro Token 无效或已过期"
assert db.commit_count == 0

View File

@@ -414,3 +414,53 @@ async def test_refresh_provider_quota_auto_removes_banned_keys_when_enabled(
assert result["results"][0]["auto_removed"] is True
assert deleted_side_effect_calls == [("p1", ["gpt-4o"])]
assert redis_cleared == [("p1", "k1"), ("p1", "k1")]
@pytest.mark.asyncio
async def test_refresh_provider_quota_does_not_auto_remove_oauth_expired_keys(
monkeypatch: pytest.MonkeyPatch,
) -> None:
provider = SimpleNamespace(
id="p1",
provider_type=ProviderType.CODEX,
endpoints=[SimpleNamespace(api_format="openai:cli", is_active=True)],
config={"pool_advanced": {"auto_remove_banned_keys": True}},
)
key = SimpleNamespace(
id="k1",
name="K1",
provider_id="p1",
allowed_models=None,
upstream_metadata={},
is_active=True,
oauth_invalid_at=None,
oauth_invalid_reason=None,
)
db = _FakeDB(provider=provider, keys=[key])
async def _fake_handler(**kwargs: Any) -> dict[str, Any]:
state_updates = kwargs["state_updates"]
state_updates["k1"] = {
"oauth_invalid_at": "expired-at",
"oauth_invalid_reason": "[OAUTH_EXPIRED] token invalidated",
}
return {"key_id": "k1", "key_name": "K1", "status": "error", "message": "expired"}
monkeypatch.setattr(
quota_service_module,
"_select_refresh_endpoint",
lambda provider, provider_type: provider.endpoints[0],
)
monkeypatch.setattr(
quota_service_module, "_resolve_quota_refresh_handler", lambda _: _fake_handler
)
result = await refresh_provider_quota_for_provider(
db=cast(Any, db),
provider_id="p1",
codex_wham_usage_url="https://example.test/wham/usage",
)
assert result["auto_removed"] == 0
assert db.deleted == []
assert key.oauth_invalid_reason == "[OAUTH_EXPIRED] token invalidated"

View File

@@ -1,5 +1,7 @@
from __future__ import annotations
import sys
import types
from dataclasses import dataclass
from types import SimpleNamespace
@@ -16,6 +18,33 @@ class _DummyEndpoint:
api_format: str
custom_path: str | None = None
provider: object | None = None
provider_id: str | None = None
class _FakeQuery:
def __init__(self, row: object | None) -> None:
self._row = row
def filter(self, *_args: object, **_kwargs: object) -> "_FakeQuery":
return self
def first(self) -> object | None:
return self._row
class _FakeSessionCtx:
def __init__(self, row: object | None) -> None:
self._row = row
def __enter__(self) -> "_FakeSessionCtx":
return self
def __exit__(self, exc_type: object, exc: object, tb: object) -> bool:
_ = exc_type, exc, tb
return False
def query(self, _model: object) -> _FakeQuery:
return _FakeQuery(self._row)
def test_codex_openai_cli_uses_responses_path_without_v1_prefix() -> None:
@@ -56,14 +85,16 @@ def test_codex_openai_cli_uses_compact_suffix_when_context_marked_compact() -> N
api_format="openai:cli",
provider=SimpleNamespace(provider_type="codex"),
)
set_codex_request_context(CodexRequestContext(is_compact=True))
url = build_provider_url(
endpoint, # type: ignore[arg-type]
path_params={"model": "ignored"},
is_stream=False,
)
assert url == "https://chatgpt.com/backend-api/codex/responses/compact"
set_codex_request_context(None)
try:
set_codex_request_context(CodexRequestContext(is_compact=True))
url = build_provider_url(
endpoint, # type: ignore[arg-type]
path_params={"model": "ignored"},
is_stream=False,
)
assert url == "https://chatgpt.com/backend-api/codex/responses/compact"
finally:
set_codex_request_context(None)
def test_codex_openai_compact_uses_compact_path_without_v1_prefix() -> None:
@@ -78,3 +109,34 @@ def test_codex_openai_compact_uses_compact_path_without_v1_prefix() -> None:
is_stream=False,
)
assert url == "https://chatgpt.com/backend-api/codex/responses/compact"
def test_codex_openai_cli_uses_provider_id_lookup_without_touching_endpoint_provider(
monkeypatch,
) -> None:
class _DetachedEndpoint:
base_url = "https://chatgpt.com/backend-api/codex"
api_format = "openai:cli"
custom_path = None
provider_id = "provider-1"
@property
def provider(self) -> object:
raise RuntimeError("detached endpoint provider should not be lazy-loaded")
fake_provider = SimpleNamespace(id="provider-1", provider_type="codex", proxy=None)
fake_database = types.ModuleType("src.database")
fake_database.create_session = lambda: _FakeSessionCtx(fake_provider)
fake_models = types.ModuleType("src.models.database")
fake_models.Provider = type("Provider", (), {"id": "id"})
monkeypatch.setitem(sys.modules, "src.database", fake_database)
monkeypatch.setitem(sys.modules, "src.models.database", fake_models)
url = build_provider_url(
_DetachedEndpoint(), # type: ignore[arg-type]
path_params={"model": "ignored"},
is_stream=True,
)
assert url == "https://chatgpt.com/backend-api/codex/responses"

View File

@@ -192,8 +192,8 @@ def test_resolve_pool_account_state_keeps_codex_metadata_block() -> None:
)
assert state.blocked is True
assert state.code == "account_forbidden"
assert state.label == "访问受限"
assert state.code == "workspace_deactivated"
assert state.label == "工作区停用"
assert state.reason == "deactivated_workspace"

View File

@@ -1,5 +1,7 @@
from __future__ import annotations
import sys
import types
from dataclasses import dataclass
from types import SimpleNamespace
@@ -19,6 +21,33 @@ class _DummyEndpoint:
api_format: str
config: dict | None = None
provider: object | None = None
provider_id: str | None = None
class _FakeQuery:
def __init__(self, row: object | None) -> None:
self._row = row
def filter(self, *_args: object, **_kwargs: object) -> "_FakeQuery":
return self
def first(self) -> object | None:
return self._row
class _FakeSessionCtx:
def __init__(self, row: object | None) -> None:
self._row = row
def __enter__(self) -> "_FakeSessionCtx":
return self
def __exit__(self, exc_type: object, exc: object, tb: object) -> bool:
_ = exc_type, exc, tb
return False
def query(self, _model: object) -> _FakeQuery:
return _FakeQuery(self._row)
def test_get_upstream_stream_policy_defaults_to_auto() -> None:
@@ -59,6 +88,24 @@ def test_get_upstream_stream_policy_codex_compact_forces_non_stream() -> None:
set_codex_request_context(None)
def test_get_upstream_stream_policy_uses_explicit_provider_type_without_touching_endpoint_provider() -> (
None
):
class _DetachedEndpoint:
api_format = "openai:cli"
config = None
@property
def provider(self) -> object:
raise RuntimeError("detached endpoint provider should not be lazy-loaded")
ep = _DetachedEndpoint()
assert (
get_upstream_stream_policy(ep, provider_type="codex") == UpstreamStreamPolicy.FORCE_STREAM
)
def test_get_upstream_stream_policy_codex_openai_compact_defaults_to_auto() -> None:
ep = _DummyEndpoint(
api_format="openai:compact",
@@ -68,6 +115,30 @@ def test_get_upstream_stream_policy_codex_openai_compact_defaults_to_auto() -> N
assert get_upstream_stream_policy(ep) == UpstreamStreamPolicy.AUTO
def test_get_upstream_stream_policy_uses_provider_id_lookup_without_touching_endpoint_provider(
monkeypatch,
) -> None:
class _DetachedEndpoint:
api_format = "openai:cli"
config = None
provider_id = "provider-1"
@property
def provider(self) -> object:
raise RuntimeError("detached endpoint provider should not be lazy-loaded")
fake_provider = SimpleNamespace(id="provider-1", provider_type="codex", proxy=None)
fake_database = types.ModuleType("src.database")
fake_database.create_session = lambda: _FakeSessionCtx(fake_provider)
fake_models = types.ModuleType("src.models.database")
fake_models.Provider = type("Provider", (), {"id": "id"})
monkeypatch.setitem(sys.modules, "src.database", fake_database)
monkeypatch.setitem(sys.modules, "src.models.database", fake_models)
assert get_upstream_stream_policy(_DetachedEndpoint()) == UpstreamStreamPolicy.FORCE_STREAM
def test_enforce_stream_mode_for_upstream_openai_chat_sets_stream_options_usage() -> None:
body = {"stream": False}
out = enforce_stream_mode_for_upstream(